splice 可以说是最受欢迎的数组方法之一,api 灵活,使用方便。现在来梳理一下用法:

  • splice(position, count) 表示从 position 索引的位置开始,删除count个元素
  • splice(position, 0, ele1, ele2, …) 表示从 position 索引的元素后面插入一系列的元素
  • splice(postion, count, ele1, ele2, …) 表示从 position 索引的位置开始,删除 count 个元素,然后再插入一系列的元素
  • 返回值为被删除元素组成的数组

接下来我们实现这个方法。

参照ecma262草案的规定,详情请点击

首先我们梳理一下实现的思路。
1.jpg

初步实现

  1. Array.prototype.splice = function(startIndex, deleteCount, ...addElements) {
  2. let argumentsLen = arguments.length;
  3. let array = Object(this);
  4. let len = array.length;
  5. let deleteArr = new Array(deleteCount);
  6. // 拷贝删除的元素
  7. sliceDeleteElements(array, startIndex, deleteCount, deleteArr);
  8. // 移动删除元素后面的元素
  9. movePostElements(array, startIndex, len, deleteCount, addElements);
  10. // 插入新元素
  11. for (let i = 0; i < addElements.length; i++) {
  12. array[startIndex + i] = addElements[i];
  13. }
  14. array.length = len - deleteCount + addElements.length;
  15. return deleteArr;
  16. }

先拷贝删除的元素,如下所示:

  1. const sliceDeleteElements = (array, startIndex, deleteCount, deleteArr) => {
  2. for (let i = 0; i < deleteCount; i++) {
  3. let index = startIndex + i;
  4. if (index in array) {
  5. let current = array[index];
  6. deleteArr[i] = current;
  7. }
  8. }
  9. };

然后对删除元素后面的元素进行挪动, 挪动分为三种情况:

  1. 添加的元素和删除的元素个数相等
  2. 添加的元素个数小于删除的元素
  3. 添加的元素个数大于删除的元素

当两者相等时,

  1. const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
  2. if (deleteCount === addElements.length) return;
  3. }

当添加的元素个数小于删除的元素时, 如图所示:
2.jpg

  1. const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
  2. //...
  3. // 如果添加的元素和删除的元素个数不相等,则移动后面的元素
  4. if(deleteCount > addElements.length) {
  5. // 删除的元素比新增的元素多,那么后面的元素整体向前挪动
  6. // 一共需要挪动 len - startIndex - deleteCount 个元素
  7. for (let i = startIndex + deleteCount; i < len; i++) {
  8. let fromIndex = i;
  9. // 将要挪动到的目标位置
  10. let toIndex = i - (deleteCount - addElements.length);
  11. if (fromIndex in array) {
  12. array[toIndex] = array[fromIndex];
  13. } else {
  14. delete array[toIndex];
  15. }
  16. }
  17. // 注意注意!这里我们把后面的元素向前挪,相当于数组长度减小了,需要删除冗余元素
  18. // 目前长度为 len + addElements - deleteCount
  19. for (let i = len - 1; i >= len + addElements.length - deleteCount; i --) {
  20. delete array[i];
  21. }
  22. }
  23. };

当添加的元素个数大于删除的元素时, 如图所示:
3.jpg

  1. const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
  2. //...
  3. if(deleteCount < addElements.length) {
  4. // 删除的元素比新增的元素少,那么后面的元素整体向后挪动
  5. // 思考一下: 这里为什么要从后往前遍历?从前往后会产生什么问题?
  6. for (let i = len - 1; i >= startIndex + deleteCount; i--) {
  7. let fromIndex = i;
  8. // 将要挪动到的目标位置
  9. let toIndex = i + (addElements.length - deleteCount);
  10. if (fromIndex in array) {
  11. array[toIndex] = array[fromIndex];
  12. } else {
  13. delete array[toIndex];
  14. }
  15. }
  16. }
  17. };

优化一: 参数的边界情况

当用户传来非法的 startIndex 和 deleteCount 或者负索引的时候,需要我们做出特殊的处理。

  1. const computeStartIndex = (startIndex, len) => {
  2. // 处理索引负数的情况
  3. if (startIndex < 0) {
  4. return startIndex + len > 0 ? startIndex + len: 0;
  5. }
  6. return startIndex >= len ? len: startIndex;
  7. }
  8. const computeDeleteCount = (startIndex, len, deleteCount, argumentsLen) => {
  9. // 删除数目没有传,默认删除startIndex及后面所有的
  10. if (argumentsLen === 1)
  11. return len - startIndex;
  12. // 删除数目过小
  13. if (deleteCount < 0)
  14. return 0;
  15. // 删除数目过大
  16. if (deleteCount > len - deleteCount)
  17. return len - startIndex;
  18. return deleteCount;
  19. }
  20. Array.prototype.splice = function (startIndex, deleteCount, ...addElements) {
  21. //,...
  22. let deleteArr = new Array(deleteCount);
  23. // 下面参数的清洗工作
  24. startIndex = computeStartIndex(startIndex, len);
  25. deleteCount = computeDeleteCount(startIndex, len, deleteCount, argumentsLen);
  26. // 拷贝删除的元素
  27. sliceDeleteElements(array, startIndex, deleteCount, deleteArr);
  28. //...
  29. }

优化二: 数组为密封对象或冻结对象

什么是密封对象?

密封对象是不可扩展的对象,而且已有成员的[[Configurable]]属性被设置为false,这意味着不能添加、删除方法和属性。但是属性值是可以修改的。

什么是冻结对象?

冻结对象是最严格的防篡改级别,除了包含密封对象的限制外,还不能修改属性值。

接下来,我们来把这两种情况一一排除。

  1. // 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象
  2. if (Object.isSealed(array) && deleteCount !== addElements.length) {
  3. throw new TypeError('the object is a sealed object!')
  4. } else if(Object.isFrozen(array) && (deleteCount > 0 || addElements.length > 0)) {
  5. throw new TypeError('the object is a frozen object!')
  6. }

好了,现在就写了一个比较完整的splice,如下:

  1. const sliceDeleteElements = (array, startIndex, deleteCount, deleteArr) => {
  2. for (let i = 0; i < deleteCount; i++) {
  3. let index = startIndex + i;
  4. if (index in array) {
  5. let current = array[index];
  6. deleteArr[i] = current;
  7. }
  8. }
  9. };
  10. const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
  11. // 如果添加的元素和删除的元素个数相等,相当于元素的替换,数组长度不变,被删除元素后面的元素不需要挪动
  12. if (deleteCount === addElements.length) return;
  13. // 如果添加的元素和删除的元素个数不相等,则移动后面的元素
  14. else if(deleteCount > addElements.length) {
  15. // 删除的元素比新增的元素多,那么后面的元素整体向前挪动
  16. // 一共需要挪动 len - startIndex - deleteCount 个元素
  17. for (let i = startIndex + deleteCount; i < len; i++) {
  18. let fromIndex = i;
  19. // 将要挪动到的目标位置
  20. let toIndex = i - (deleteCount - addElements.length);
  21. if (fromIndex in array) {
  22. array[toIndex] = array[fromIndex];
  23. } else {
  24. delete array[toIndex];
  25. }
  26. }
  27. // 注意注意!这里我们把后面的元素向前挪,相当于数组长度减小了,需要删除冗余元素
  28. // 目前长度为 len + addElements - deleteCount
  29. for (let i = len - 1; i >= len + addElements.length - deleteCount; i --) {
  30. delete array[i];
  31. }
  32. } else if(deleteCount < addElements.length) {
  33. // 删除的元素比新增的元素少,那么后面的元素整体向后挪动
  34. // 思考一下: 这里为什么要从后往前遍历?从前往后会产生什么问题?
  35. for (let i = len - 1; i >= startIndex + deleteCount; i--) {
  36. let fromIndex = i;
  37. // 将要挪动到的目标位置
  38. let toIndex = i + (addElements.length - deleteCount);
  39. if (fromIndex in array) {
  40. array[toIndex] = array[fromIndex];
  41. } else {
  42. delete array[toIndex];
  43. }
  44. }
  45. }
  46. };
  47. const computeStartIndex = (startIndex, len) => {
  48. // 处理索引负数的情况
  49. if (startIndex < 0) {
  50. return startIndex + len > 0 ? startIndex + len: 0;
  51. }
  52. return startIndex >= len ? len: startIndex;
  53. }
  54. const computeDeleteCount = (startIndex, len, deleteCount, argumentsLen) => {
  55. // 删除数目没有传,默认删除startIndex及后面所有的
  56. if (argumentsLen === 1)
  57. return len - startIndex;
  58. // 删除数目过小
  59. if (deleteCount < 0)
  60. return 0;
  61. // 删除数目过大
  62. if (deleteCount > len - deleteCount)
  63. return len - startIndex;
  64. return deleteCount;
  65. }
  66. Array.prototype.splice = function(startIndex, deleteCount, ...addElements) {
  67. let argumentsLen = arguments.length;
  68. let array = Object(this);
  69. let len = array.length >>> 0;
  70. let deleteArr = new Array(deleteCount);
  71. startIndex = computeStartIndex(startIndex, len);
  72. deleteCount = computeDeleteCount(startIndex, len, deleteCount, argumentsLen);
  73. // 判断 sealed 对象和 frozen 对象, 即 密封对象 和 冻结对象
  74. if (Object.isSealed(array) && deleteCount !== addElements.length) {
  75. throw new TypeError('the object is a sealed object!')
  76. } else if(Object.isFrozen(array) && (deleteCount > 0 || addElements.length > 0)) {
  77. throw new TypeError('the object is a frozen object!')
  78. }
  79. // 拷贝删除的元素
  80. sliceDeleteElements(array, startIndex, deleteCount, deleteArr);
  81. // 移动删除元素后面的元素
  82. movePostElements(array, startIndex, len, deleteCount, addElements);
  83. // 插入新元素
  84. for (let i = 0; i < addElements.length; i++) {
  85. array[startIndex + i] = addElements[i];
  86. }
  87. array.length = len - deleteCount + addElements.length;
  88. return deleteArr;
  89. }

以上代码对照MDN文档中的所有测试用例亲测通过。

相关测试代码请前往: 传送门

最后给大家奉上V8源码,供大家检查: V8数组 splice 源码第 660 行

参考:三元博客