如何手動實現數組的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 行

相關文章
相關標籤/搜索