關注 高級前端進階,回覆「加羣」javascript
加入咱們一塊兒學習,每天進步html
做者:木易楊前端
引言
幾個經常使用數組方法的使用方式已經在【進階 6-1 期】 中介紹過了,今天這篇文章主要看看 ECMA-262 規範中是如何定義這些方法的,而且在看完規範後咱們用 JS 模擬實現下,透過源碼探索一些底層的知識,但願本文對你有所幫助。java
Array.prototype.map
完整的結構是 Array.prototype.map(callbackfn[, thisArg])
,map
函數接收兩個參數,一個是必填項回調函數,另外一個是可選項 callbackfn 函數執行時的 this 值。web
map
方法的主要功能就是把原數組中的每一個元素按順序執行一次 callbackfn
函數,而且把全部返回的結果組合在一塊兒生成一個新的數組,map
方法的返回值就是這個新數組。面試
ECMA-262 規範文檔實現以下。數組
Let O be ? ToObject(this value).微信
Let len be ? LengthOfArrayLike(O).app
If IsCallable(callbackfn) is false, throw a TypeError exception.函數
If thisArg is present, let T be thisArg; else let T be undefined.
Let A be ? ArraySpeciesCreate(O, len).
Let k be 0.
Repeat, while k < len
Let Pk be ! ToString(k).
Let kPresent be ? HasProperty(O, Pk).
If kPresent is true, then
Let kValue be ? Get(O, Pk).
Let mappedValue be ? Call(callbackfn, T, « kValue, k, O »).
Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).
Set k to k + 1.
Return A.
用 JS 來模擬實現,核心邏輯以下:
Array.prototype.map = function(callbackfn, thisArg) {
// 異常處理
if (this == null) {
throw new TypeError("Cannot read property 'map' of null or undefined");
}
// Step 1. 轉成數組對象,有 length 屬性和 K-V 鍵值對
let O = Object(this)
// Step 2. 無符號右移 0 位,左側用 0 填充,結果非負
let len = O.length >>> 0
// Step 3. callbackfn 不是函數時拋出異常
if (typeof callbackfn !== 'function') {
throw new TypeError(callbackfn + ' is not a function')
}
// Step 4.
let T = thisArg
// Step 5.
let A = new Array(len)
// Step 6.
let k = 0
// Step 7.
while(k < len) {
// Step 7.一、7.二、7.3
// 檢查 O 及其原型鏈是否包含屬性 k
if (k in O) {
// Step 7.3.1
let kValue = O[k]
// Step 7.3.2 執行 callbackfn 函數
// 傳入 this, 當前元素 element, 索引 index, 原數組對象 O
let mappedValue = callbackfn.call(T, kValue, k, O)
// Step 7.3.3 返回結果賦值給新生成數組
A[k] = mappedValue
}
// Step 7.4
k++
}
// Step 8. 返回新數組
return A
}
// 代碼親測已經過
看完代碼其實挺簡單,核心就是在一個 while
循環中執行 callbackfn
,並傳入 4 個參數,回調函數具體的執行邏輯這裏並不關心,只須要拿到返回結果並賦值給新數組就行了。
只有 O 及其原型鏈上包含屬性 k 時纔會執行 callbackfn
函數,因此對於稀疏數組 empty 元素或者使用 delete
刪除後的索引則不會被調用。
let arr = [1, , 3, , 5]
console.log(0 in arr) // true
delete arr[0]
console.log(0 in arr) // false
console.log(arr) // [empty × 2, 3, empty, 5]
arr.map(ele => {
console.log(ele) // 3, 5
})
map
並不會修改原數組,不過也不是絕對的,若是你在 callbackfn
中修改了原數組,那仍是會改變。那問題來了,修改後會影響到 map
自身的執行嗎?
答案是會的!不過得區分如下幾種狀況。
原數組新增元素:由於
map
第一次執行時 length 已經肯定了,因此不影響原數組修改元素:傳遞給
callbackfn
的元素是 map 遍歷到它們那一瞬間的值,因此可能受影響修改當前索引以前的元素,不受影響
修改當前索引以後的元素,受影響
原數組刪除元素:被刪除的元素沒法被訪問到,因此可能受影響
刪除當前索引以前的元素,已經訪問過了,因此不受影響
刪除當前索引以後的元素,受影響
簡單看下面幾個例子,在 callbackfn
中不要改變原數組,否則會有意想不到的狀況發生。
// 一、原數組新增元素,不受影響
let arr = [1, 2, 3]
let result = arr.map((ele, index, array) => {
array.push(4);
return ele * 2
})
console.log(result)
// 2, 4, 6
// ----------- 完美分割線 -----------
// 二、原數組修改當前索引以前的元素,不受影響
let arr = [1, 2, 3]
let result = arr.map((ele, index, array) => {
if (index === 1) {
array[0] = 4
}
return ele * 2
})
console.log(result)
// 2, 4, 6
// ----------- 完美分割線 -----------
// 三、原數組修改當前索引以後的元素,受影響
let arr = [1, 2, 3]
let result = arr.map((ele, index, array) => {
if (index === 1) {
array[2] = 4
}
return ele * 2
})
console.log(result)
// 2, 4, 8
最後來講說 this
,源碼中有這麼一段 callbackfn.call(T, kValue, k, O)
,其中 T
就是 thisArg
值,若是沒有設置,那就是 undefined。
根據【進階 3-3 期】 中對於 call 的解讀,傳入 undefined 時,非嚴格模式下指向 Window,嚴格模式下爲 undefined。記住這時候回調函數不能用箭頭函數,由於箭頭函數是沒有本身的 this 的。
// 一、傳入 thisArg 但使用箭頭函數
let name = 'Muyiy'
let obj = {
name: 'Hello',
callback: (ele) => {
return this.name + ele
}
}
let arr = [1, 2, 3]
let result = arr.map(obj.callback, obj);
console.log(result)
// ["1", "2", "3"],此時 this 指向 window
// 那爲啥不是 "Muyiy1" 這樣呢,不急,第 3 步介紹
// ----------- 完美分割線 -----------
// 二、傳入 thisArg,使用普通函數
let name = 'Muyiy'
let obj = {
name: 'Hello',
callback: function (ele) {
return this.name + ele
}
}
let arr = [1, 2, 3]
let result = arr.map(obj.callback, obj);
console.log(result)
// ["Hello1", "Hello2", "Hello3"],完美
// ----------- 完美分割線 -----------
// 三、不傳入 thisArg,name 使用 let 聲明
let name = 'Muyiy'
let obj = {
name: 'Hello',
callback: function (ele) {
return this.name + ele
}
}
let arr = [1, 2, 3]
let result = arr.map(obj.callback);
console.log(result)
// ["1", "2", "3"]
// 爲何呢,由於 let 和 const 聲明的變量不會掛載到 window 上
// ----------- 完美分割線 -----------
// 四、不傳入 thisArg,name 使用 var 聲明
var name = 'Muyiy'
let obj = {
name: 'Hello',
callback: function (ele) {
return this.name + ele
}
}
let arr = [1, 2, 3]
let result = arr.map(obj.callback);
console.log(result)
// ["Muyiy1", "Muyiy2", "Muyiy3"]
// 看看,改爲 var 就行了
// ----------- 完美分割線 -----------
// 五、嚴格模式
'use strict'
var name = 'Muyiy'
let obj = {
name: 'Hello',
callback: function (ele) {
return this.name + ele
}
}
let arr = [1, 2, 3]
let result = arr.map(obj.callback);
console.log(result)
// TypeError: Cannot read property 'name' of undefined
// 由於嚴格模式下 this 指向 undefined
上面這部分實操代碼介紹了 5 種狀況,分別是傳入 thisArg 兩種狀況,非嚴格模式下兩種狀況,以及嚴格模式下一種狀況。這部分的知識在以前的文章中都有介紹過,這裏主要是溫故下。若是這塊知識不熟悉,能夠詳細看個人 博客
Array.prototype.filter
完整的結構是 Array.prototype.filter(callbackfn[, thisArg])
,和 map
是同樣的。
filter
字如其名,它的主要功能就是過濾,callbackfn
執行結果若是是 true 就返回當前元素,false 則不返回,返回的全部元素組合在一塊兒生成新數組,並返回。若是沒有任何元素經過測試,則返回空數組。
因此這部分源碼相比 map
而言,多了一步判斷 callbackfn
的返回值。
ECMA-262 規範文檔實現以下。
Let O be ? ToObject(this value).
Let len be ? LengthOfArrayLike(O).
If IsCallable(callbackfn) is false, throw a TypeError exception.
If thisArg is present, let T be thisArg; else let T be undefined.
Let A be ? ArraySpeciesCreate(O, 0).
Let k be 0.
Let to be 0.
Repeat, while k < len
Let kValue be ? Get(O, Pk).
Let selected be ! ToBoolean(? Call(callbackfn, T, « kValue, k, O »)).
If selected is true, then
Set k to k + 1.
Perform ? CreateDataPropertyOrThrow(A, ! ToString(to), kValue).
Set to to to + 1.
Let Pk be ! ToString(k).
Let kPresent be ? HasProperty(O, Pk).
If kPresent is true, then
Return A.
用 JS 來模擬實現,核心邏輯以下:
Array.prototype.filter = function(callbackfn, thisArg) {
// 異常處理
if (this == null) {
throw new TypeError("Cannot read property 'map' of null or undefined");
}
if (typeof callbackfn !== 'function') {
throw new TypeError(callbackfn + ' is not a function')
}
let O = Object(this), len = O.length >>> 0,
T = thisArg, A = new Array(len), k = 0
// 新增,返回數組的索引
let to = 0
while(k < len) {
if (k in O) {
let kValue = O[k]
// 新增
if (callbackfn.call(T, kValue, k, O)) {
A[to++] = kValue;
}
}
k++
}
// 新增,修改 length,初始值爲 len
A.length = to;
return A
}
// 代碼親測已經過
看懂 map
再看這個實現就簡單多了,改動點在於判斷 callbackfn
返回值,新增索引 to
,這樣主要避免使用 k
時生成空元素,並在返回以前修改 length
值。
這部分源碼仍是挺有意思的,驚喜點在於 A.length = to
,以前還沒用過。
Array.prototype.reduce
reduce
能夠理解爲「歸一」,意爲海納百川,萬劍歸一,完整的結構是 Array.prototype.reduce(callbackfn[, initialValue])
,這裏第二個參數並非 thisArg 了,而是初始值 initialValue
,關於初始值以前有介紹過。
若是沒有提供
initialValue
,那麼第一次調用callback
函數時,accumulator
使用原數組中的第一個元素,currentValue
便是數組中的第二個元素。若是提供了
initialValue
,accumulator
將使用這個初始值,currentValue
使用原數組中的第一個元素。在沒有初始值的空數組上調用
reduce
將報錯。
ECMA-262 規範文檔實現以下。
Let O be ? ToObject(this value).
Let len be ? LengthOfArrayLike(O).
If IsCallable(callbackfn) is false, throw a TypeError exception.
If len is 0 and initialValue is not present, throw a TypeError exception.
Let k be 0.
Let accumulator be undefined.
If initialValue is present, then
Set accumulator to initialValue.
Else,
Let Pk be ! ToString(k).
Set kPresent to ? HasProperty(O, Pk).
If kPresent is true, then
Set k to k + 1.
Set accumulator to ? Get(O, Pk).
Let kPresent be false.
Repeat, while kPresent is false and k < len
If kPresent is false, throw a TypeError exception.
Repeat, while k < len
Let kValue be ? Get(O, Pk).
Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, k, O »).
Let Pk be ! ToString(k).
Let kPresent be ? HasProperty(O, Pk).
If kPresent is true, then
Set k to k + 1.
Return accumulator.
用 JS 來模擬實現,核心邏輯以下:
Array.prototype.reduce = function(callbackfn, initialValue) {
// 異常處理
if (this == null) {
throw new TypeError("Cannot read property 'map' of null or undefined");
}
if (typeof callbackfn !== 'function') {
throw new TypeError(callbackfn + ' is not a function')
}
let O = Object(this)
let len = O.length >>> 0
let k = 0, accumulator
// 新增
if (initialValue) {
accumulator = initialValue
} else {
// Step 4.
if (len === 0) {
throw new TypeError('Reduce of empty array with no initial value');
}
// Step 8.
let kPresent = false
while(!kPresent && (k < len)) {
kPresent = k in O
if (kPresent) {
accumulator = O[k]
}
k++
}
}
while(k < len) {
if (k in O) {
let kValue = O[k]
accumulator = callbackfn.call(undefined, accumulator, kValue, k, O)
}
k++
}
return accumulator
}
// 代碼親測已經過
這部分源碼主要多了對於 initialValue
的處理,有初始值時比較簡單,即 accumulator = initialValue
,kValue = O[0]
。
無初始值處理在 Step 8,循環判斷當 O 及其原型鏈上存在屬性 k 時,accumulator = O[k]
並退出循環,由於 k++
,因此 kValue = O[k++]
。
更多的數組方法有 find
、findIndex
、forEach
等,其源碼實現也是大同小異,無非就是在 callbackfn.call
這部分作些處理,有興趣的能夠看看 TC39 和 MDN 官網,參考部分連接直達。
注意
forEach
的源碼和 map
很相同,在 map 的源碼基礎上作些改造就是啦。
Array.prototype.forEach = function(callbackfn, thisArg) {
// 相同
...
while(k < len) {
if (k in O) {
let kValue = O[k]
// 這部分是 map
// let mappedValue = callbackfn.call(T, kValue, k, O)
// A[k] = mappedValue
// 這部分是 forEach
callbackfn.call(T, kValue, k, O)
}
k++
}
// 返回 undefined
// return undefined
}
能夠看到,不一樣之處在於不處理 callbackfn
執行的結果,也不返回。
特地指出來是由於在此以前看到過一種錯誤的說法,叫作「forEach 會跳過空,可是 map 不跳過」
爲何說 map
不跳過呢,由於原始數組有 empty 元素時,map 返回的結果也有 empty 元素,因此不跳過,可是這種說法並不正確。
let arr = [1, , 3, , 5]
console.log(arr) // [1, empty, 3, empty, 5]
let result = arr.map(ele => {
console.log(ele) // 1, 3, 5
return ele
})
console.log(result) // [1, empty, 3, empty, 5]
看 ele
輸出就會明白 map 也是跳空的,緣由就在於源碼中的 k in O
,這裏是檢查 O 及其原型鏈是否包含屬性 k,因此有的實現中用 hasOwnProperty
也是不正確的。
另外 callbackfn
中不可使用 break 跳出循環,是由於 break 只能跳出循環,而 callbackfn
並非循環體。若是有相似的需求可使用for..of
、for..in
、 some
、every
等。
![](http://static.javashuo.com/static/loading.gif)
熟悉源碼以後不少問題就迎刃而解啦,感謝閱讀。
參考
TC39 Array.prototype.map
TC39 Array.prototype.filter
TC39 Array.prototype.reduce
MDN Array.prototype.map
MDN Array.prototype.filter
MDN Array.prototype.reduce
References
[1]
【進階 6-1 期】: https://www.muyiy.cn/blog/6/6.1.html[2]
【進階 3-3 期】: https://muyiy.cn/blog/3/3.3.html[3]
博客: https://muyiy.cn/blog/[4]
TC39 Array.prototype.map: https://tc39.es/ecma262/#sec-array.prototype.map[5]
TC39 Array.prototype.filter: https://tc39.es/ecma262/#sec-array.prototype.filter[6]
TC39 Array.prototype.reduce: https://tc39.es/ecma262/#sec-array.prototype.reduce[7]
MDN Array.prototype.map: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/map[8]
MDN Array.prototype.filter: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/filter[9]
MDN Array.prototype.reduce: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
❤️ 看完三件事
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)
關注個人官網 https://muyiy.cn,讓咱們成爲長期關係
關注公衆號「高級前端進階」,每週重點攻克一個前端面試重難點,公衆號後臺回覆「資料」免費送給你精心準備的前端進階資料。
本文分享自微信公衆號 - 全棧大佬的修煉之路(gh_7795af32a259)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。