第十六篇 如何实现数组splice方法 ?
目录
splice 可以说是最受欢迎的数组方法之一,api 灵活,使用方便。现在来梳理一下用法:
- splice(position,count) 表示从 position 索引的位置开始,删除 count 个元素
- splice(position,0,ele1,ele2,…) 表示从 position 索引的元素后面插入一系列的元素
- splice(postion,count,ele1,ele2,…) 表示从 position 索引的位置开始,删除 count 个元素,然后再插入一系列的元素
- 返回值为被删除元素组成的数组。
接下来我们实现这个方法。
参照 ecma262 草案的规定,详情请点击。
首先我们梳理一下实现的思路。
初步实现
Array.prototype.splice = function(startIndex, deleteCount, ...addElements) { let argumentsLen = arguments.length; let array = Object(this); let len = array.length; let deleteArr = new Array(deleteCount); // 拷贝删除的元素 sliceDeleteElements(array, startIndex, deleteCount, deleteArr); // 移动删除元素后面的元素 movePostElements(array, startIndex, len, deleteCount, addElements); // 插入新元素 for (let i = 0; i < addElements.length; i++) { array[startIndex + i] = addElements[i]; } array.length = len - deleteCount + addElements.length; return deleteArr; }
先拷贝删除的元素,如下所示:
const sliceDeleteElements = (array, startIndex, deleteCount, deleteArr) => { for (let i = 0; i < deleteCount; i++) { let index = startIndex + i; if (index in array) { let current = array[index]; deleteArr[i] = current; } } };
然后对删除元素后面的元素进行挪动,挪动分为三种情况:
- 添加的元素和删除的元素个数相等
- 添加的元素个数小于删除的元素
- 添加的元素个数大于删除的元素
当两者相等时,
const movePostElements = (array, startIndex, len, deleteCount, addElements) => { if (deleteCount === addElements.length) return; }
当添加的元素个数小于删除的元素时,如图所示:
const movePostElements = (array, startIndex, len, deleteCount, addElements) => { //... // 如果添加的元素和删除的元素个数不相等,则移动后面的元素 if(deleteCount > addElements.length) { // 删除的元素比新增的元素多,那么后面的元素整体向前挪动 // 一共需要挪动 len - startIndex - deleteCount 个元素 for (let i = startIndex + deleteCount; i < len; i++) { let fromIndex = i; // 将要挪动到的目标位置 let toIndex = i - (deleteCount - addElements.length); if (fromIndex in array) { array[toIndex] = array[fromIndex]; } else { delete array[toIndex]; } } // 注意注意!这里我们把后面的元素向前挪,相当于数组长度减小了,需要删除冗余元素 // 目前长度为 len + addElements - deleteCount for (let i = len - 1; i >= len + addElements.length - deleteCount; i --) { delete array[i]; } } };
当添加的元素个数大于删除的元素时, 如图所示:
const movePostElements = (array, startIndex, len, deleteCount, addElements) => { //... if(deleteCount < addElements.length) { // 删除的元素比新增的元素少,那么后面的元素整体向后挪动 // 思考一下: 这里为什么要从后往前遍历?从前往后会产生什么问题? for (let i = len - 1; i >= startIndex + deleteCount; i--) { let fromIndex = i; // 将要挪动到的目标位置 let toIndex = i + (addElements.length - deleteCount); if (fromIndex in array) { array[toIndex] = array[fromIndex]; } else { delete array[toIndex]; } } } };
优化一:参数的边界情况
当用户传来非法的 startIndex 和 deleteCount 或者负索引的时候,需要我们做出特殊的处理。
const computeStartIndex = (startIndex, len) => { // 处理索引负数的情况 if (startIndex < 0) { return startIndex + len > 0 ? startIndex + len: 0; } return startIndex >= len ? len: startIndex; } const computeDeleteCount = (startIndex, len, deleteCount, argumentsLen) => { // 删除数目没有传,默认删除 startIndex 及后面所有的 if (argumentsLen === 1) return len - startIndex; // 删除数目过小 if (deleteCount < 0) return 0; // 删除数目过大 if (deleteCount > len - startIndex) return len - startIndex; return deleteCount; } Array.prototype.splice = function (startIndex, deleteCount, ...addElements) { //,... let deleteArr = new Array(deleteCount); // 下面参数的清洗工作 startIndex = computeStartIndex(startIndex, len); deleteCount = computeDeleteCount(startIndex, len, deleteCount, argumentsLen); // 拷贝删除的元素 sliceDeleteElements(array, startIndex, deleteCount, deleteArr); //... }
优化二:数组为密封对象或冻结对象
什么是密封对象?
密封对象是不可扩展的对象,而且已有成员的[[Configurable]]属性被设置为 false,这意味着不能添加、删除方法和属性。但是属性值是可以修改的。
什么是冻结对象?
冻结对象是最严格的防篡改级别,除了包含密封对象的限制外,还不能修改属性值。
接下来,我们来把这两种情况一一排除。
// 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象 if (Object.isSealed(array) && deleteCount !== addElements.length) { throw new TypeError('the object is a sealed object!') } else if(Object.isFrozen(array) && (deleteCount > 0 || addElements.length > 0)) { throw new TypeError('the object is a frozen object!') }
好了,现在就写了一个比较完整的 splice,如下:
const sliceDeleteElements = (array, startIndex, deleteCount, deleteArr) => { for (let i = 0; i < deleteCount; i++) { let index = startIndex + i; if (index in array) { let current = array[index]; deleteArr[i] = current; } } }; const movePostElements = (array, startIndex, len, deleteCount, addElements) => { // 如果添加的元素和删除的元素个数相等,相当于元素的替换,数组长度不变,被删除元素后面的元素不需要挪动 if (deleteCount === addElements.length) return; // 如果添加的元素和删除的元素个数不相等,则移动后面的元素 else if(deleteCount > addElements.length) { // 删除的元素比新增的元素多,那么后面的元素整体向前挪动 // 一共需要挪动 len - startIndex - deleteCount 个元素 for (let i = startIndex + deleteCount; i < len; i++) { let fromIndex = i; // 将要挪动到的目标位置 let toIndex = i - (deleteCount - addElements.length); if (fromIndex in array) { array[toIndex] = array[fromIndex]; } else { delete array[toIndex]; } } // 注意注意!这里我们把后面的元素向前挪,相当于数组长度减小了,需要删除冗余元素 // 目前长度为 len + addElements - deleteCount for (let i = len - 1; i >= len + addElements.length - deleteCount; i --) { delete array[i]; } } else if(deleteCount < addElements.length) { // 删除的元素比新增的元素少,那么后面的元素整体向后挪动 // 思考一下: 这里为什么要从后往前遍历?从前往后会产生什么问题? for (let i = len - 1; i >= startIndex + deleteCount; i--) { let fromIndex = i; // 将要挪动到的目标位置 let toIndex = i + (addElements.length - deleteCount); if (fromIndex in array) { array[toIndex] = array[fromIndex]; } else { delete array[toIndex]; } } } }; const computeStartIndex = (startIndex, len) => { // 处理索引负数的情况 if (startIndex < 0) { return startIndex + len > 0 ? startIndex + len: 0; } return startIndex >= len ? len: startIndex; } const computeDeleteCount = (startIndex, len, deleteCount, argumentsLen) => { // 删除数目没有传,默认删除 startIndex 及后面所有的 if (argumentsLen === 1) return len - startIndex; // 删除数目过小 if (deleteCount < 0) return 0; // 删除数目过大 if (deleteCount > len - startIndex) return len - startIndex; return deleteCount; } Array.prototype.splice = function(startIndex, deleteCount, ...addElements) { let argumentsLen = arguments.length; let array = Object(this); let len = array.length >>> 0; let deleteArr = new Array(deleteCount); startIndex = computeStartIndex(startIndex, len); deleteCount = computeDeleteCount(startIndex, len, deleteCount, argumentsLen); // 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象 if (Object.isSealed(array) && deleteCount !== addElements.length) { throw new TypeError('the object is a sealed object!') } else if(Object.isFrozen(array) && (deleteCount > 0 || addElements.length > 0)) { throw new TypeError('the object is a frozen object!') } // 拷贝删除的元素 sliceDeleteElements(array, startIndex, deleteCount, deleteArr); // 移动删除元素后面的元素 movePostElements(array, startIndex, len, deleteCount, addElements); // 插入新元素 for (let i = 0; i < addElements.length; i++) { array[startIndex + i] = addElements[i]; } array.length = len - deleteCount + addElements.length; return deleteArr; }
以上代码对照MDN文档中的所有测试用例亲测通过。
最后给大家奉上 V8 源码,供大家检查: V8 数组 splice 源码第 660 行
更多相关文章推荐:
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » 第十六篇 如何实现数组splice方法 ?
码云笔记 » 第十六篇 如何实现数组splice方法 ?