如何手动实现数组的splice方法 ? (V8源码级别)

这篇文章是下一波大文章《原生JS灵魂之问(中)》的预告,下波文章会参照 V8 源码将数组中一些经常使用的的方法实现一遍,能够说全网独家首发,欢迎关注。git

V8引擎中的数组方法采用JS语言实现,连接也附在了最后,若是对个人代码有质疑,随时对照源码检查。而且测试代码已经附在最后,亲测所有经过了MDN的全部测试用例github

splice 能够说是最受欢迎的数组方法之一,api 灵活,使用方便。如今来梳理一下用法:api

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

接下来咱们实现这个方法。数组

首先咱们梳理一下实现的思路。frontend

初步实现

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;
}
复制代码

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

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;
    }
  }
};
复制代码

而后对删除元素后面的元素进行挪动, 挪动分为三种状况:测试

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

当二者相等时,优化

const movePostElements = (array, startIndex, len, deleteCount, addElements) => {
  if (deleteCount === addElements.length) return;
}
复制代码

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

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];
    }
  } 
};
复制代码

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

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 - 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 - 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;
  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 行

相关文章
相关标签/搜索