几个经常使用数组方法的使用方式已经在【进阶 6-1 期】 中介绍过了,今天这篇文章主要看看 ECMA-262 规范中是如何定义这些方法的,而且在看完规范后咱们用 JS 模拟实现下,透过源码探索一些底层的知识,但愿本文对你有所帮助。html
完整的结构是 Array.prototype.map(callbackfn[, thisArg])
,map
函数接收两个参数,一个是必填项回调函数,另外一个是可选项 callbackfn 函数执行时的 this 值。前端
map
方法的主要功能就是把原数组中的每一个元素按顺序执行一次 callbackfn
函数,而且把全部返回的结果组合在一块儿生成一个新的数组,map
方法的返回值就是这个新数组。面试
ECMA-262 规范文档实现以下。数组
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 个参数,回调函数具体的执行逻辑这里并不关心,只须要拿到返回结果并赋值给新数组就行了。app
只有 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
自身的执行吗?测试
答案是会的!不过得区分如下几种状况。ui
map
第一次执行时 length 已经肯定了,因此不影响callbackfn
的元素是 map 遍历到它们那一瞬间的值,因此可能受影响
简单看下面几个例子,在 callbackfn
中不要改变原数组,否则会有意想不到的状况发生。this
// 一、原数组新增元素,不受影响
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。spa
根据【进阶 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(callbackfn[, thisArg])
,和 map
是同样的。
filter
字如其名,它的主要功能就是过滤,callbackfn
执行结果若是是 true 就返回当前元素,false 则不返回,返回的全部元素组合在一块儿生成新数组,并返回。若是没有任何元素经过测试,则返回空数组。
因此这部分源码相比 map
而言,多了一步判断 callbackfn
的返回值。
ECMA-262 规范文档实现以下。
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
,以前还没用过。
reduce
能够理解为「归一」,意为海纳百川,万剑归一,完整的结构是 Array.prototype.reduce(callbackfn[, initialValue])
,这里第二个参数并非 thisArg 了,而是初始值 initialValue
,关于初始值以前有介绍过。
initialValue
,那么第一次调用 callback
函数时,accumulator
使用原数组中的第一个元素,currentValue
便是数组中的第二个元素。initialValue
,accumulator
将使用这个初始值,currentValue
使用原数组中的第一个元素。reduce
将报错。ECMA-262 规范文档实现以下。
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
等。
熟悉源码以后不少问题就迎刃而解啦,感谢阅读。
若是你以为这篇内容对你挺有启发,我想邀请你帮我三个小忙: