(建議精讀)原生JS靈魂之問(中),檢驗本身是否真的熟悉JavaScript?

筆者最近在對原生JS的知識作系統梳理,由於我以爲JS做爲前端工程師的根本技術,學再多遍都不爲過。打算來作一個系列,一共分三次發,以一系列的問題爲驅動,固然也會有追問和擴展,內容系統且完整,對初中級選手會有很好的提高,高級選手也會獲得複習和鞏固。這是本系列的第二篇。前端

掃了一眼目錄後,也許你可能會說:這些八百年都用不到的東西,我爲何要會?是,我認可真實業務場景中遇到諸如手寫splice、深拷貝的場景並很少,但我要說的是,問這些問題的初衷並非讓你拿到平時去用的,而是檢驗你對JS語言的理解有沒有到達那樣的水準,有一些邊界狀況是否可以考慮到,有沒有基本的計算機素養(好比最基本的排序方法到底理不理解),將來有沒有潛力去設計出更加優秀的產品或者框架。若是你僅僅是想經過一篇文章來解決業務中的臨時問題,那很差意思,請出門左拐,這篇文章確實不適合你。但若是你以爲本身的原生編程能力還有待提升,想讓本身的思惟能力上一個臺階,但願我這篇"嘔心瀝血"整理了1萬六千多字的文章可以讓你有所成長。另外補充一句,本文並不針對面試,但如下任何一篇的內容放在面試中,都是很是驚豔的操做:)node

第七篇: 函數的arguments爲何不是數組?如何轉化成數組?

由於argument是一個對象,只不過它的屬性從0開始排,依次爲0,1,2...最後還有callee和length屬性。咱們也把這樣的對象稱爲類數組。git

常見的類數組還有:es6

    1. 用getElementsByTagName/ClassName()得到的HTMLCollection
    1. 用querySlector得到的nodeList

那這致使不少數組的方法就不能用了,必要時須要咱們將它們轉換成數組,有哪些方法呢?github

1. Array.prototype.slice.call()

function sum(a, b) {
  let args = Array.prototype.slice.call(arguments);
  console.log(args.reduce((sum, cur) => sum + cur));//args能夠調用數組原生的方法啦
}
sum(1, 2);//3
複製代碼

2. Array.from()

function sum(a, b) {
  let args = Array.from(arguments);
  console.log(args.reduce((sum, cur) => sum + cur));//args能夠調用數組原生的方法啦
}
sum(1, 2);//3
複製代碼

這種方法也能夠用來轉換Set和Map哦!面試

3. ES6展開運算符

function sum(a, b) {
  let args = [...arguments];
  console.log(args.reduce((sum, cur) => sum + cur));//args能夠調用數組原生的方法啦
}
sum(1, 2);//3
複製代碼

4. 利用concat+apply

function sum(a, b) {
  let args = Array.prototype.concat.apply([], arguments);//apply方法會把第二個參數展開
  console.log(args.reduce((sum, cur) => sum + cur));//args能夠調用數組原生的方法啦
}
sum(1, 2);//3
複製代碼

固然,最原始的方法就是再建立一個數組,用for循環把類數組的每一個屬性值放在裏面,過於簡單,就不浪費篇幅了。算法

第七篇: forEach中return有效果嗎?如何中斷forEach循環?

在forEach中用return不會返回,函數會繼續執行。編程

let nums = [1, 2, 3];
nums.forEach((item, index) => {
  return;//無效
})
複製代碼

中斷方法:api

  1. 使用try監視代碼塊,在須要中斷的地方拋出異常。數組

  2. 官方推薦方法(替換方法):用every和some替代forEach函數。every在碰到return false的時候,停止循環。some在碰到return ture的時候,停止循環

第八篇: JS判斷數組中是否包含某個值

方法一:array.indexOf

此方法判斷數組中是否存在某個值,若是存在,則返回數組元素的下標,不然返回-1。

var arr=[1,2,3,4];
var index=arr.indexOf(3);
console.log(index);
複製代碼

方法二:array.includes(searcElement[,fromIndex])

此方法判斷數組中是否存在某個值,若是存在返回true,不然返回false

var arr=[1,2,3,4];
if(arr.includes(3))
    console.log("存在");
else
    console.log("不存在");
複製代碼

方法三:array.find(callback[,thisArg])

返回數組中知足條件的第一個元素的值,若是沒有,返回undefined

var arr=[1,2,3,4];
var result = arr.find(item =>{
    return item > 3
});
console.log(result);
複製代碼

方法四:array.findeIndex(callback[,thisArg])

返回數組中知足條件的第一個元素的下標,若是沒有找到,返回-1]

var arr=[1,2,3,4];
var result = arr.findIndex(item =>{
    return item > 3
});
console.log(result);
複製代碼

固然,for循環固然是沒有問題的,這裏討論的是數組方法,就再也不展開了。

第九篇: JS中flat---數組扁平化

對於前端項目開發過程當中,偶爾會出現層疊數據結構的數組,咱們須要將多層級數組轉化爲一級數組(即提取嵌套數組元素最終合併爲一個數組),使其內容合而且展開。那麼該如何去實現呢?

需求:多維數組=>一維數組

let ary = [1, [2, [3, [4, 5]]], 6];// -> [1, 2, 3, 4, 5, 6]
let str = JSON.stringify(ary);
複製代碼

1. 調用ES6中的flat方法

ary = arr.flat(Infinity);
複製代碼

2. replace + split

ary = str.replace(/(\[|\])/g, '').split(',')
複製代碼

3. replace + JSON.parse

str = str.replace(/(\[|\]))/g, '');
str = '[' + str + ']';
ary = JSON.parse(str);
複製代碼

4. 普通遞歸

let result = [];
let fn = function(ary) {
  for(let i = 0; i < ary.length; i++) {
    let item = ary[i];
    if (Array.isArray(ary[i])){
      fn(item);
    } else {
      result.push(item);
    }
  }
}
複製代碼

5. 利用reduce函數迭代

function flatten(ary) {
    return ary.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, []);
}
let ary = [1, 2, [3, 4], [5, [6, 7]]]
console.log(flatten(ary))
複製代碼

6:擴展運算符

//只要有一個元素有數組,那麼循環繼續
while (ary.some(Array.isArray)) {
  ary = [].concat(...ary);
}
複製代碼

這是一個比較實用並且很容易被問到的問題,歡迎你們交流補充。

第十篇: JS數組的高階函數——基礎篇

1.什麼是高階函數

概念很是簡單,以下:

一個函數就能夠接收另外一個函數做爲參數或者返回值爲一個函數,這種函數就稱之爲高階函數。

那對應到數組中有哪些方法呢?

2.數組中的高階函數

1.map

  • 參數:接受兩個參數,一個是回調函數,一個是回調函數的this值(可選)。

其中,回調函數被默認傳入三個值,依次爲當前元素、當前索引、整個數組。

  • 建立一個新數組,其結果是該數組中的每一個元素都調用一個提供的函數後返回的結果

  • 對原來的數組沒有影響

let nums = [1, 2, 3];
let obj = {val: 5};
let newNums = nums.map(function(item,index,array) {
  return item + index + array[index] + this.val; 
  //對第一個元素,1 + 0 + 1 + 5 = 7
  //對第二個元素,2 + 1 + 2 + 5 = 10
  //對第三個元素,3 + 2 + 3 + 5 = 13
}, obj);
console.log(newNums);//[7, 10, 13]
複製代碼

固然,後面的參數都是可選的 ,不用的話能夠省略。

2. reduce

  • 參數: 接收兩個參數,一個爲回調函數,另外一個爲初始值。回調函數中三個默認參數,依次爲積累值、當前值、整個數組。
let nums = [1, 2, 3];
// 多個數的加和
let newNums = nums.reduce(function(preSum,curVal,array) {
  return preSum + curVal; 
}, 0);
console.log(newNums);//6
複製代碼

不傳默認值會怎樣?

不傳默認值會自動以第一個元素爲初始值,而後從第二個元素開始依次累計。

3. filter

參數: 一個函數參數。這個函數接受一個默認參數,就是當前元素。這個做爲參數的函數返回值爲一個布爾類型,決定元素是否保留。

filter方法返回值爲一個新的數組,這個數組裏麪包含參數裏面全部被保留的項。

let nums = [1, 2, 3];
// 保留奇數項
let oddNums = nums.filter(item => item % 2);
console.log(oddNums);
複製代碼

4. sort

參數: 一個用於比較的函數,它有兩個默認參數,分別是表明比較的兩個元素。

舉個例子:

let nums = [2, 3, 1];
//兩個比較的元素分別爲a, b
nums.sort(function(a, b) {
  if(a > b) return 1;
  else if(a < b) return -1;
  else if(a == b) return 0;
})
複製代碼

當比較函數返回值大於0,則 a 在 b 的後面,即a的下標應該比b大。

反之,則 a 在 b 的後面,即 a 的下標比 b 小。

整個過程就完成了一次升序的排列。

固然還有一個須要注意的狀況,就是比較函數不傳的時候,是如何進行排序的?

答案是將數字轉換爲字符串,而後根據字母unicode值進行升序排序,也就是根據字符串的比較規則進行升序排序。

第十一篇: 能不能實現數組map方法 ?

依照 ecma262 草案,實現的map的規範以下:

下面根據草案的規定一步步來模擬實現map函數:

Array.prototype.map = function(callbackFn, thisArg) {
  // 處理數組類型異常
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'map' of null or undefined");
  }
  // 處理回調類型異常
  if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
    throw new TypeError(callbackfn + ' is not a function')
  }
  // 草案中提到要先轉換爲對象
  let O = Object(this);
  let T = thisArg;

  
  let len = O.length >>> 0;
  let A = new Array(len);
  for(let k = 0; k < len; k++) {
    // 還記得原型鏈那一節提到的 in 嗎?in 表示在原型鏈查找
    // 若是用 hasOwnProperty 是有問題的,它只能找私有屬性
    if (k in O) {
      let kValue = O[k];
      // 依次傳入this, 當前項,當前索引,整個數組
      let mappedValue = callbackfn.call(T, KValue, k, O);
      A[k] = mappedValue;
    }
  }
  return A;
}
複製代碼

這裏解釋一下, length >>> 0, 字面意思是指"右移 0 位",但其實是把前面的空位用0填充,這裏的做用是保證len爲數字且爲整數。

舉幾個特例:

null >>> 0  //0

undefined >>> 0  //0

void(0) >>> 0  //0

function a (){};  a >>> 0  //0

[] >>> 0  //0

var a = {}; a >>> 0  //0

123123 >>> 0  //123123

45.2 >>> 0  //45

0 >>> 0  //0

-0 >>> 0  //0

-1 >>> 0  //4294967295

-1212 >>> 0  //4294966084
複製代碼

整體實現起來並沒那麼難,須要注意的就是使用 in 來進行原型鏈查找。同時,若是沒有找到就不處理,能有效處理稀疏數組的狀況。

最後給你們奉上V8源碼,參照源碼檢查一下,其實仍是實現得很完整了。

function ArrayMap(f, receiver) {
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.map");

  // Pull out the length so that modifications to the length in the
  // loop will not affect the looping and side effects are visible.
  var array = TO_OBJECT(this);
  var length = TO_LENGTH(array.length);
  if (!IS_CALLABLE(f)) throw %make_type_error(kCalledNonCallable, f);
  var result = ArraySpeciesCreate(array, length);
  for (var i = 0; i < length; i++) {
    if (i in array) {
      var element = array[i];
      %CreateDataProperty(result, i, %_Call(f, receiver, element, i, array));
    }
  }
  return result;
}
複製代碼

參考:

V8源碼

Array 原型方法源碼實現大揭祕

ecma262草案

第十二篇: 能不能實現數組reduce方法 ?

依照 ecma262 草案,實現的reduce的規範以下:

其中有幾個核心要點:

一、初始值不傳怎麼處理

二、回調函數的參數有哪些,返回值如何處理。

Array.prototype.reduce  = function(callbackfn, initialValue) {
  // 異常處理,和 map 同樣
  // 處理數組類型異常
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'reduce' of null or undefined");
  }
  // 處理回調類型異常
  if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
    throw new TypeError(callbackfn + ' is not a function')
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let k = 0;
  let accumulator = initialValue;
  if (accumulator === undefined) {
    for(; k < len ; k++) {
      // 查找原型鏈
      if (k in O) {
        accumulator = O[k];
        k++;
        break;
      }
    }
  }
  // 表示數組全爲空
  if(k === len && accumulator === undefined) 
    throw new Error('Each element of the array is empty');
  for(;k < len; k++) {
    if (k in O) {
      // 注意,核心!
      accumulator = callbackfn.call(undefined, accumulator, O[k], O);
    }
  }
  return accumulator;
}
複製代碼

實際上是從最後一項開始遍歷,經過原型鏈查找跳過空項。

最後給你們奉上V8源碼,以供你們檢查:

function ArrayReduce(callback, current) {
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.reduce");

  // Pull out the length so that modifications to the length in the
  // loop will not affect the looping and side effects are visible.
  var array = TO_OBJECT(this);
  var length = TO_LENGTH(array.length);
  return InnerArrayReduce(callback, current, array, length,
                          arguments.length);
}

function InnerArrayReduce(callback, current, array, length, argumentsLength) {
  if (!IS_CALLABLE(callback)) {
    throw %make_type_error(kCalledNonCallable, callback);
  }

  var i = 0;
  find_initial: if (argumentsLength < 2) {
    for (; i < length; i++) {
      if (i in array) {
        current = array[i++];
        break find_initial;
      }
    }
    throw %make_type_error(kReduceNoInitial);
  }

  for (; i < length; i++) {
    if (i in array) {
      var element = array[i];
      current = callback(current, element, i, array);
    }
  }
  return current;
}
複製代碼

參考:

V8源碼

ecma262草案

第十三篇: 能不能實現數組 push、pop 方法 ?

參照 ecma262 草案的規定,關於 push 和 pop 的規範以下圖所示:

首先來實現一下 push 方法:

Array.prototype.push = function(...items) {
  let O = Object(this);
  let len = this.length >>> 0;
  let argCount = items.length >>> 0;
  // 2 ** 53 - 1 爲JS能表示的最大正整數
  if (len + argCount > 2 ** 53 - 1) {
    throw new TypeError("The number of array is over the max value restricted!")
  }
  for(let i = 0; i < argCount; i++) {
    O[len + i] = items[i];
  }
  let newLength = len + argCount;
  O.length = newLength;
  return newLength;
}
複製代碼

親測已經過MDN上全部測試用例。MDN連接

而後來實現 pop 方法:

Array.prototype.pop = function() {
  let O = Object(this);
  let len = this.length >>> 0;
  if (len === 0) {
    O.length = 0;
    return undefined;
  }
  len --;
  let value = O[len];
  delete O[len];
  O.length = len;
  return value;
}
複製代碼

親測已經過MDN上全部測試用例。MDN連接

參考連接:

V8數組源碼

ecma262規範草案

MDN文檔

第十四篇: 能不能實現數組filter方法 ?

代碼以下:

Array.prototype.filter = function(callbackfn, thisArg) {
  // 處理數組類型異常
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'filter' of null or undefined");
  }
  // 處理回調類型異常
  if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
    throw new TypeError(callbackfn + ' is not a function')
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let resLen = 0;
  let res = [];
  for(let i = 0; i < len; i++) {
    if (i in O) {
      let element = O[i];
      if (callbackfn.call(thisArg, O[i], i, O)) {
        res[resLen++] = element;
      }
    }
  }
  return res;
}
複製代碼

MDN上全部測試用例親測經過。

參考:

V8數組部分源碼第1025行

MDN中filter文檔

第十五篇: 能不能實現數組splice方法 ?

splice 能夠說是最受歡迎的數組方法之一,api 靈活,使用方便。如今來梳理一下用法:

    1. splice(position, count) 表示從 position 索引的位置開始,刪除count個元素
    1. splice(position, 0, ele1, ele2, ...) 表示從 position 索引的元素後面插入一系列的元素
    1. splice(postion, count, ele1, ele2, ...) 表示從 position 索引的位置開始,刪除 count 個元素,而後再插入一系列的元素
    1. 返回值爲被刪除元素組成的數組

接下來咱們實現這個方法。

參照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;
    }
  }
};
複製代碼

而後對刪除元素後面的元素進行挪動, 挪動分爲三種狀況:

  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];
      }
    }
  }
};
複製代碼

優化一: 參數的邊界狀況

當用戶傳來非法的 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 >>> 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 行

第十六篇: 能不能實現數組sort方法?

估計你們對 JS 數組的sort 方法已經不陌生了,以前也對它的用法作了詳細的總結。那,它的內部是如何來實現的呢?若是說咱們可以進入它的內部去看一看, 理解背後的設計,會使咱們的思惟和素養獲得不錯的提高。

sort 方法在 V8 內部相對與其餘方法而言是一個比較高深的算法,對於不少邊界狀況作了反覆的優化,可是這裏咱們不會直接拿源碼來幹講。咱們會來根據源碼的思路,實現一個 跟引擎性能同樣的排序算法,而且一步步拆解其中的奧祕。

V8 引擎的思路分析

首先大概梳理一下源碼中排序的思路:

設要排序的元素個數是n:

  • 當 n <= 10 時,採用插入排序
  • 當 n > 10 時,採用三路快速排序
    • 10 < n <= 1000, 採用中位數做爲哨兵元素
    • n > 1000, 每隔 200~215 個元素挑出一個元素,放到一個新數組,而後對它排序,找到中間位置的數,以此做爲中位數

在動手以前,我以爲咱們有必要爲何這麼作搞清楚。

第1、爲何元素個數少的時候要採用插入排序?

雖然插入排序理論上說是O(n^2)的算法,快速排序是一個O(nlogn)級別的算法。可是別忘了,這只是理論上的估算,在實際狀況中二者的算法複雜度前面都會有一個係數的, 當 n 足夠小的時候,快速排序nlogn的優點會愈來愈小,假若插入排序O(n^2)前面的係數足夠小,那麼就會超過快排。而事實上正是如此,插入排序通過優化之後對於小數據集的排序會有很是優越的性能,不少時候甚至會超過快排。

所以,對於很小的數據量,應用插入排序是一個很是不錯的選擇。

第2、爲何要花這麼大的力氣選擇哨兵元素?

由於快速排序的性能瓶頸在於遞歸的深度,最壞的狀況是每次的哨兵都是最小元素或者最大元素,那麼進行partition(一邊是小於哨兵的元素,另外一邊是大於哨兵的元素)時,就會有一邊是空的,那麼這麼排下去,遞歸的層數就達到了n, 而每一層的複雜度是O(n),所以快排這時候會退化成O(n^2)級別。

這種狀況是要盡力避免的!若是來避免?

就是讓哨兵元素進可能地處於數組的中間位置,讓最大或者最小的狀況儘量少。這時候,你就能理解 V8 裏面所作的種種優化了。

接下來,咱們來一步步實現的這樣的官方排序算法。

插入排序及優化

最初的插入排序多是這樣寫的:

const insertSort = (arr, start = 0, end) => {
  end = end || arr.length;
  for(let i = start; i < end; i++) {
    let j;
    for(j = i; j > start && arr[j - 1] > arr[j]; j --) {
      let temp = arr[j];
      arr[j] = arr[j - 1];
      arr[j - 1] = temp;
    }
  }
  return;
}
複製代碼

看似能夠正確的完成排序,但實際上交換元素會有至關大的性能消耗,咱們徹底能夠用變量覆蓋的方式來完成,如圖所示:

優化後代碼以下:

const insertSort = (arr, start = 0, end) => {
  end = end || arr.length;
  for(let i = start; i < end; i++) {
    let e = arr[i];
    let j;
    for(j = i; j > start && arr[j - 1] > e; j --)
      arr[j] = arr[j-1];
    arr[j] = e;
  }
  return;
}
複製代碼

接下來正式進入到 sort 方法。

尋找哨兵元素

sort的骨架大體以下:

Array.prototype.sort = (comparefn) => {
  let array = Object(this);
  let length = array.length >>> 0;
  return InnerArraySort(array, length, comparefn);
}

const InnerArraySort = (array, length, comparefn) => {
  // 比較函數未傳入
  if (Object.prototype.toString.call(callbackfn) !== "[object Function]") {
    comparefn = function (x, y) {
      if (x === y) return 0;
      x = x.toString();
      y = y.toString();
      if (x == y) return 0;
      else return x < y ? -1 : 1;
    };
  }
  const insertSort = () => {
    //...
  }
  const getThirdIndex = (a, from, to) => {
    // 元素個數大於1000時尋找哨兵元素
  }
  const quickSort = (a, from, to) => {
    //哨兵位置
    let thirdIndex = 0;
    while(true) {
      if(to - from <= 10) {
        insertSort(a, from, to);
        return;
      }
      if(to - from > 1000) {
        thirdIndex = getThirdIndex(a, from , to);
      }else {
        // 小於1000 直接取中點
        thirdIndex = from + ((to - from) >> 2);
      }
    }
    //下面開始快排
  }
}
複製代碼

咱們先來把求取哨兵位置的代碼實現一下:

const getThirdIndex = (a, from, to) => {
  let tmpArr = [];
  // 遞增量,200~215 之間,由於任何正數和15作與操做,不會超過15,固然是大於0的
  let increment = 200 + ((to - from) & 15);
  let j = 0;
  from += 1;
  to -= 1;
  for (let i = from; i < to; i += increment) {
    tmpArr[j] = [i, a[i]];
    j++;
  }
  // 把臨時數組排序,取中間的值,確保哨兵的值接近平均位置
  tmpArr.sort(function(a, b) {
    return comparefn(a[1], b[1]);
  });
  let thirdIndex = tmpArr[tmpArr.length >> 1][0];
  return thirdIndex;
}
複製代碼

完成快排

接下來咱們來完成快排的具體代碼:

const _sort = (a, b, c) => {
  let arr = [a, b, c];
  insertSort(arr, 0, 3);
  return arr;
}

const quickSort = (a, from, to) => {
  //...
  // 上面咱們拿到了thirdIndex
  // 如今咱們擁有三個元素,from, thirdIndex, to
  // 爲了再次確保 thirdIndex 不是最值,把這三個值排序
  [a[from], a[thirdIndex], a[to - 1]] = _sort(a[from], a[thirdIndex], a[to - 1]);
  // 如今正式把 thirdIndex 做爲哨兵
  let pivot = a[thirdIndex];
  // 正式進入快排
  let lowEnd = from + 1;
  let highStart = to - 1;
  // 如今正式把 thirdIndex 做爲哨兵, 而且lowEnd和thirdIndex交換
  let pivot = a[thirdIndex];
  a[thirdIndex] = a[lowEnd];
  a[lowEnd] = pivot;
  
  // [lowEnd, i)的元素是和pivot相等的
  // [i, highStart) 的元素是須要處理的
  for(let i = lowEnd + 1; i < highStart; i++) {
    let element = a[i];
    let order = comparefn(element, pivot);
    if (order < 0) {
      a[i] = a[lowEnd];
      a[lowEnd] = element;
      lowEnd++;
    } else if(order > 0) {
      do{
        highStart--;
        if(highStart === i) break;
        order = comparefn(a[highStart], pivot);
      }while(order > 0)
      // 如今 a[highStart] <= pivot
      // a[i] > pivot
      // 二者交換
      a[i] = a[highStart];
      a[highStart] = element;
      if(order < 0) {
        // a[i] 和 a[lowEnd] 交換
        element = a[i];
        a[i] = a[lowEnd];
        a[lowEnd] = element;
        lowEnd++;
      }
    }
  }
  // 永遠切分大區間
  if (lowEnd - from > to - highStart) {
    // 繼續切分lowEnd ~ from 這個區間
    to = lowEnd;
    // 單獨處理小區間
    quickSort(a, highStart, to);
  } else if(lowEnd - from <= to - highStart) {
    from = highStart;
    quickSort(a, from, lowEnd);
  }
}
複製代碼

測試結果

測試結果以下:

一萬條數據:

十萬條數據:

一百萬條數據:

一千萬條數據:

結果僅供你們參考,由於不一樣的node版本對於部分細節的實現可能不同,我如今的版本是v10.15。

從結果能夠看到,目前版本的 node 對於有序程度較高的數據是處理的不夠好的,而咱們剛剛實現的排序經過反覆肯定哨兵的位置就能 有效的規避快排在這一場景下的劣勢。

最後給你們完整版的sort代碼:

const sort = (arr, comparefn) => {
  let array = Object(arr);
  let length = array.length >>> 0;
  return InnerArraySort(array, length, comparefn);
}

const InnerArraySort = (array, length, comparefn) => {
  // 比較函數未傳入
  if (Object.prototype.toString.call(comparefn) !== "[object Function]") {
    comparefn = function (x, y) {
      if (x === y) return 0;
      x = x.toString();
      y = y.toString();
      if (x == y) return 0;
      else return x < y ? -1 : 1;
    };
  }
  const insertSort = (arr, start = 0, end) => {
    end = end || arr.length;
    for (let i = start; i < end; i++) {
      let e = arr[i];
      let j;
      for (j = i; j > start && comparefn(arr[j - 1], e) > 0; j--)
        arr[j] = arr[j - 1];
      arr[j] = e;
    }
    return;
  }
  const getThirdIndex = (a, from, to) => {
    let tmpArr = [];
    // 遞增量,200~215 之間,由於任何正數和15作與操做,不會超過15,固然是大於0的
    let increment = 200 + ((to - from) & 15);
    let j = 0;
    from += 1;
    to -= 1;
    for (let i = from; i < to; i += increment) {
      tmpArr[j] = [i, a[i]];
      j++;
    }
    // 把臨時數組排序,取中間的值,確保哨兵的值接近平均位置
    tmpArr.sort(function (a, b) {
      return comparefn(a[1], b[1]);
    });
    let thirdIndex = tmpArr[tmpArr.length >> 1][0];
    return thirdIndex;
  };

  const _sort = (a, b, c) => {
    let arr = [];
    arr.push(a, b, c);
    insertSort(arr, 0, 3);
    return arr;
  }

  const quickSort = (a, from, to) => {
    //哨兵位置
    let thirdIndex = 0;
    while (true) {
      if (to - from <= 10) {
        insertSort(a, from, to);
        return;
      }
      if (to - from > 1000) {
        thirdIndex = getThirdIndex(a, from, to);
      } else {
        // 小於1000 直接取中點
        thirdIndex = from + ((to - from) >> 2);
      }
      let tmpArr = _sort(a[from], a[thirdIndex], a[to - 1]);
      a[from] = tmpArr[0]; a[thirdIndex] = tmpArr[1]; a[to - 1] = tmpArr[2];
      // 如今正式把 thirdIndex 做爲哨兵
      let pivot = a[thirdIndex];
      [a[from], a[thirdIndex]] = [a[thirdIndex], a[from]];
      // 正式進入快排
      let lowEnd = from + 1;
      let highStart = to - 1;
      a[thirdIndex] = a[lowEnd];
      a[lowEnd] = pivot;
      // [lowEnd, i)的元素是和pivot相等的
      // [i, highStart) 的元素是須要處理的
      for (let i = lowEnd + 1; i < highStart; i++) {
        let element = a[i];
        let order = comparefn(element, pivot);
        if (order < 0) {
          a[i] = a[lowEnd];
          a[lowEnd] = element;
          lowEnd++;
        } else if (order > 0) {
          do{
            highStart--;
            if (highStart === i) break;
            order = comparefn(a[highStart], pivot);
          }while (order > 0) ;
          // 如今 a[highStart] <= pivot
          // a[i] > pivot
          // 二者交換
          a[i] = a[highStart];
          a[highStart] = element;
          if (order < 0) {
            // a[i] 和 a[lowEnd] 交換
            element = a[i];
            a[i] = a[lowEnd];
            a[lowEnd] = element;
            lowEnd++;
          }
        }
      }
      // 永遠切分大區間
      if (lowEnd - from > to - highStart) {
        // 單獨處理小區間
        quickSort(a, highStart, to);
        // 繼續切分lowEnd ~ from 這個區間
        to = lowEnd;
      } else if (lowEnd - from <= to - highStart) {
        quickSort(a, from, lowEnd);
        from = highStart;
      }
    }
  }
  quickSort(array, 0, length);
}
複製代碼

參考連接:

V8 sort源碼(點開第997行)

冴羽排序源碼專題

第十七篇: 能不能模擬實現一個new的效果?

new被調用後作了三件事情:

  1. 讓實例能夠訪問到私有屬性
  2. 讓實例能夠訪問構造函數原型(constructor.prototype)所在原型鏈上的屬性
  3. 若是構造函數返回的結果不是引用數據類型
function newOperator(ctor, ...args) {
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    let obj = Object.create(ctor.prototype);
    let res = ctor.apply(obj, args);
    
    let isObject = typeof res === 'object' && typeof res !== null;
    let isFunction = typoof res === 'function';
    return isObect || isFunction ? res : obj;
};
複製代碼

第十八篇: 能不能模擬實現一個 bind 的效果?

實現bind以前,咱們首先要知道它作了哪些事情。

  1. 對於普通函數,綁定this指向

  2. 對於構造函數,要保證原函數的原型對象上的屬性不能丟失

Function.prototype.bind = function (context, ...args) {
    // 異常處理
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    // 保存this的值,它表明調用 bind 的函數
    var self = this;
    var fNOP = function () {};

    var fbound = function () {
        self.apply(this instanceof self ? 
            this : 
            context, args.concat(Array.prototype.slice.call(arguments)));
    }

    fNOP.prototype = this.prototype;
    fbound.prototype = new fNOP();

    return fbound;
}
複製代碼

也能夠這麼用 Object.create 來處理原型:

Function.prototype.bind = function (context, ...args) {
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;

    var fbound = function () {
        self.apply(this instanceof self ? 
            this : 
            context, args.concat(Array.prototype.slice.call(arguments)));
    }

    fbound.prototype = Object.create(self.prototype);

    return fbound;
}
複製代碼

第十八篇: 能不能實現一個 call/apply 函數?

引自冴羽大佬的代碼,能夠說比較完整了。

Function.prototype.call = function (context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}
複製代碼

不過我認爲換成 ES6 的語法會更精煉一些:

Function.prototype.call = function (context, ...args) {
  var context = context || window;
  context.fn = this;

  var result = eval('context.fn(...args)');

  delete context.fn
  return result;
}
複製代碼

相似的,有apply的對應實現:

Function.prototype.apply = function (context, args) {
  let context = context || window;
  context.fn = this;
  let result = eval('context.fn(...args)');

  delete context.fn
  return result;
}
複製代碼

第十九篇: 談談你對JS中this的理解。

其實JS中的this是一個很是簡單的東西,只須要理解它的執行規則就OK。

在這裏不想像其餘博客同樣展現太多的代碼例子弄得天花亂墜, 反而不易理解。

call/apply/bind能夠顯式綁定, 這裏就不說了。

主要這些場隱式綁定的場景討論:

  1. 全局上下文
  2. 直接調用函數
  3. 對象.方法的形式調用
  4. DOM事件綁定(特殊)
  5. new構造函數綁定
  6. 箭頭函數

1. 全局上下文

全局上下文默認this指向window, 嚴格模式下指向undefined。

2. 直接調用函數

好比:

let obj = {
  a: function() {
    console.log(this);
  }
}
let func = obj.a;
func();
複製代碼

這種狀況是直接調用。this至關於全局上下文的狀況。

3. 對象.方法的形式調用

仍是剛剛的例子,我若是這樣寫:

obj.a();
複製代碼

這就是對象.方法的狀況,this指向這個對象

4. DOM事件綁定

onclick和addEventerListener中 this 默認指向綁定事件的元素。

IE比較奇異,使用attachEvent,裏面的this默認指向window。

5. new+構造函數

此時構造函數中的this指向實例對象。

6. 箭頭函數?

箭頭函數沒有this, 所以也不能綁定。裏面的this會指向當前最近的非箭頭函數的this,找不到就是window(嚴格模式是undefined)。好比:

let obj = {
  a: function() {
    let do = () => {
      console.log(this);
    }
    do();
  }
}
obj.a(); // 找到最近的非箭頭函數a,a如今綁定着obj, 所以箭頭函數中的this是obj
複製代碼

優先級: new > call、apply、bind > 對象.方法 > 直接調用。

第二十篇: JS中淺拷貝的手段有哪些?

重要: 什麼是拷貝?

首先來直觀的感覺一下什麼是拷貝。

let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;

console.log(arr);//[100, 2, 3]
複製代碼

這是直接賦值的狀況,不涉及任何拷貝。當改變newArr的時候,因爲是同一個引用,arr指向的值也跟着改變。

如今進行淺拷貝:

let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;

console.log(arr);//[1, 2, 3]
複製代碼

當修改newArr的時候,arr的值並不改變。什麼緣由?由於這裏newArr是arr淺拷貝後的結果,newArr和arr如今引用的已經不是同一塊空間啦!

這就是淺拷貝!

可是這又會帶來一個潛在的問題:

let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;

console.log(arr);//[ 1, 2, { val: 1000 } ]
複製代碼

咦!不是已經不是同一塊空間的引用了嗎?爲何改變了newArr改變了第二個元素的val值,arr也跟着變了。

這就是淺拷貝的限制所在了。它只能拷貝一層對象。若是有對象的嵌套,那麼淺拷貝將無能爲力。但幸運的是,深拷貝就是爲了解決這個問題而生的,它能 解決無限極的對象嵌套問題,實現完全的拷貝。固然,這是咱們下一篇的重點。 如今先讓你們有一個基本的概念。

接下來,咱們來研究一下JS中實現淺拷貝到底有多少種方式?

1. 手動實現

const shallowClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}
複製代碼

2. Object.assign

可是須要注意的是,Object.assgin() 拷貝的是對象的屬性的引用,而不是對象自己。

let obj = { name: 'sy', age: 18 };
const obj2 = Object.assign({}, obj, {name: 'sss'});
console.log(obj2);//{ name: 'sss', age: 18 }
複製代碼

3. concat淺拷貝數組

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]
複製代碼

4. slice淺拷貝

開頭的例子不就說的這個嘛!

5. ...展開運算符

let arr = [1, 2, 3];
let newArr = [...arr];//跟arr.slice()是同樣的效果
複製代碼

第二十一篇: 能不能寫一個完整的深拷貝?

上一篇已經解釋了什麼是深拷貝,如今咱們來一塊兒實現一個完整且專業的深拷貝。

1. 簡易版及問題

JSON.parse(JSON.stringify());
複製代碼

估計這個api能覆蓋大多數的應用場景,沒錯,談到深拷貝,我第一個想到的也是它。可是實際上,對於某些嚴格的場景來講,這個方法是有巨大的坑的。問題以下:

  1. 沒法解決循環引用的問題。舉個例子:
const a = {val:2};
a.target = a;
複製代碼

拷貝a會出現系統棧溢出,由於出現了無限遞歸的狀況。

  1. 沒法拷貝一寫特殊的對象,諸如 RegExp, Date, Set, Map等。
  1. 沒法拷貝函數(劃重點)。

所以這個api先pass掉,咱們從新寫一個深拷貝,簡易版以下:

const deepClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = deepClone(target[prop]);
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}
複製代碼

如今,咱們以剛剛發現的三個問題爲導向,一步步來完善、優化咱們的深拷貝代碼。

2. 解決循環引用

如今問題以下:

let obj = {val : 100};
obj.target = obj;

deepClone(obj);//報錯: RangeError: Maximum call stack size exceeded
複製代碼

這就是循環引用。咱們怎麼來解決這個問題呢?

建立一個Map。記錄下已經拷貝過的對象,若是說已經拷貝過,那直接返回它行了。

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const deepClone = (target, map = new Map()) => {
  if(map.get(target)) 
    return target;


  if (isObject(target)) {
    map.put(target, true);
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = deepClone(target[prop]);
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}
複製代碼

如今來試一試:

const a = {val:2};
a.target = a;
let newA = deepClone(a);
console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } }
複製代碼

好像是沒有問題了, 拷貝也完成了。但仍是有一個潛在的坑, 就是map 上的 key 和 map 構成了強引用關係,這是至關危險的。我給你解釋一下與之相對的弱引用的概念你就明白了:

在計算機程序設計中,弱引用與強引用相對, 是指不能確保其引用的對象不會被垃圾回收器回收的引用。 一個對象若只被弱引用所引用,則被認爲是不可訪問(或弱可訪問)的,並所以可能在任什麼時候刻被回收。 --百度百科

說的有一點繞,我用大白話解釋一下,被弱引用的對象能夠在任什麼時候候被回收,而對於強引用來講,只要這個強引用還在,那麼對象沒法被回收。拿上面的例子說,map 和 a一直是強引用的關係, 在程序結束以前,a 所佔的內存空間一直不會被釋放

怎麼解決這個問題?

很簡單,讓 map 的 key 和 map 構成弱引用便可。ES6給咱們提供了這樣的數據結構,它的名字叫WeakMap,它是一種特殊的Map, 其中的鍵是弱引用的。其鍵必須是對象,而值能夠是任意的。

稍微改造一下便可:

const deepClone = (target, map = new WeakMap()) => {
  //...
}
複製代碼

3. 拷貝特殊對象

可繼續遍歷

對於特殊的對象,咱們使用如下方式來鑑別:

Object.prototype.toString.call(obj);
複製代碼

梳理一下對於可遍歷對象會有什麼結果:

["object Map"]
["object Set"]
["object Array"]
["object Object"]
["object Arguments"]
複製代碼

好,以這些不一樣的字符串爲依據,咱們就能夠成功地鑑別這些對象。

const getType = Object.prototype.toString.call(obj);

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};

const deepClone = (target, map = new Map()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 處理不能遍歷的對象
    return;
  }else {
    // 這波操做至關關鍵,能夠保證對象的原型不丟失!
    let ctor = target.prototype;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.put(target, true);

  if(type === mapTag) {
    //處理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key), deepClone(item));
    })
  }
  
  if(type === setTag) {
    //處理Set
    target.forEach(item => {
      target.add(deepClone(item));
    })
  }

  // 處理數組和對象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop]);
    }
  }
  return cloneTarget;
}
複製代碼

不可遍歷的對象

const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
複製代碼

對於不可遍歷的對象,不一樣的對象有不一樣的處理。

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (target) => {
  // 待會的重點部分
}

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch(tag) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}
複製代碼

4. 拷貝函數

雖然函數也是對象,可是它過於特殊,咱們單獨把它拿出來拆解。

提到函數,在JS種有兩種函數,一種是普通函數,另外一種是箭頭函數。每一個普通函數都是 Function的實例,而箭頭函數不是任何類的實例,每次調用都是不同的引用。那咱們只須要 處理普通函數的狀況,箭頭函數直接返回它自己就行了。

那麼如何來區分二者呢?

答案是: 利用原型。箭頭函數是不存在原型的。

代碼以下:

const handleFunc = (func) => {
  // 箭頭函數直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分別匹配 函數參數 和 函數體
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}
複製代碼

到如今,咱們的深拷貝就實現地比較完善了。不過在測試的過程當中,我也發現了一個小小的bug。

5. 小小的bug

以下所示:

const target = new Boolean(false);
const Ctor = target.constructor;
new Ctor(target); // 結果爲 Boolean {true} 而不是 false。
複製代碼

對於這樣一個bug,咱們能夠對 Boolean 拷貝作最簡單的修改, 調用valueOf: new target.constructor(target.valueOf())。

但實際上,這種寫法是不推薦的。由於在ES6後不推薦使用【new 基本類型()】這 樣的語法,因此es6中的新類型 Symbol 是不能直接 new 的,只能經過 new Object(SymbelType)。

所以咱們接下來統一一下:

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}
複製代碼

6. 完整代碼展現

OK!是時候給你們放出完整版的深拷貝啦:

const getType = obj => Object.prototype.toString.call(obj);

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (func) => {
  // 箭頭函數直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分別匹配 函數參數 和 函數體
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

const deepClone = (target, map = new WeakMap()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 處理不能遍歷的對象
    return handleNotTraverse(target, type);
  }else {
    // 這波操做至關關鍵,能夠保證對象的原型不丟失!
    let ctor = target.constructor;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.set(target, true);

  if(type === mapTag) {
    //處理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key, map), deepClone(item, map));
    })
  }
  
  if(type === setTag) {
    //處理Set
    target.forEach(item => {
      cloneTarget.add(deepClone(item, map));
    })
  }

  // 處理數組和對象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
}
複製代碼

❤️ 看完兩件事

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我兩個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)

  2. 關注公衆號「前端三元同窗」,每日堅持靈魂之問,碰見更好的本身!

相關文章
相關標籤/搜索