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 - deleteCountfor (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 - deleteCount)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 - deleteCountfor (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 - deleteCount)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 行
参考:三元博客
