Как вручную реализовать метод сращивания массива? (уровень исходного кода V8)

JavaScript ECMAScript 6

Эта статья является превью следующей большой статьи "Вопрос души нативного JS (середина)". В следующей статье будут реализованы некоторые часто используемые методы в массиве со ссылкой на исходный код V8. Можно сказать, что вся сеть独家首发, добро пожаловать, чтобы следовать.

Метод массива в движке V8 реализован на языке JS, в конце также прикреплена ссылка Если у вас есть какие-либо сомнения относительно моего кода, пожалуйста, сверьтесь с исходным кодом в любое время. И в конце прикрепил тестовый код, все про тесты пройденыMDN的所有测试用例.

splice, возможно, является одним из самых популярных методов массива с гибким и простым в использовании API. Теперь разберемся с использованием:

    1. splice(position, count) означает удаление элементов count, начиная с позиции, индексированной по позиции
    1. splice(position, 0, ele1, ele2, ...) означает вставку ряда элементов после элемента, индексированного по позиции
    1. splice(postion, count, ele1, ele2, ...) означает, начиная с позиции, индексированной по позиции, удаляя элементы count, а затем вставляя серию элементов
    1. Возвращаемое значение被删除元素состоит из数组.

Далее реализуем этот метод.

Для начала разберемся с идеей реализации.

Первоначальная реализация

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

Затем переместите элемент после удаленного элемента.Перемещение делится на три случая:

  1. Количество добавленных и удаленных элементов равно
  2. Количество добавленных элементов меньше количества удаленных элементов
  3. Количество добавленных элементов больше, чем количество удаленных элементов

Когда двое равны,

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];
      }
    }
  }
};

Оптимизация 1: граничные случаи параметров

Когда пользователь отправляет недопустимый 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);
  //...
}

Оптимизация 2: массив представляет собой запечатанный объект или замороженный объект.

Что такое закрытый объект?

Печать объекта не является расширяемым объектом, и есть члены атрибута [[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!')
}

Что ж, теперь я написал относительно полный сплайс, а именно:

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 660