likes
comments
collection
share

Array 实例方法 forEach 的实现

作者站长头像
站长
· 阅读数 4

Array 实例方法 forEach 的实现

在本文中,我们将从 ECMAScript 语言规范角度探讨 JavaScript 中 Array.prototype.forEach() 方法的实现。通过深入分析 ECMAScript 规范文档,我们将揭示 forEach() 方法背后的原理和设计理念。从函数签名到具体行为,我们将逐步解析该方法在规范中的定义,并探讨其与其他数组方法的关联。通过本文,读者将了解到如何准确地实现 forEach() 方法,并理解其在 JavaScript 数组操作中的重要性和应用场景。

Array.prototype.forEach()

ECMAScript® 2025 语言规范中对 Array.prototype.forEach() 的原文描述如下:

23.1.3.15 Array.prototype.forEach ( callbackfn [ , thisArg ] )

NOTE 1

callbackfn should be a function that accepts three arguments. forEach calls callbackfn once for each element present in the array, in ascending order. callbackfn is called only for elements of the array which actually exist; it is not called for missing elements of the array.

If a thisArg parameter is provided, it will be used as the this value for each invocation of callbackfn. If it is not provided, undefined is used instead.

callbackfn is called with three arguments: the value of the element, the index of the element, and the object being traversed.

forEach does not directly mutate the object on which it is called but the object may be mutated by the calls to callbackfn.

The range of elements processed by forEach is set before the first call to callbackfn. Elements which are appended to the array after the call to forEach begins will not be visited by callbackfn. If existing elements of the array are changed, their value as passed to callbackfn will be the value at the time forEach visits them; elements that are deleted after the call to forEach begins and before being visited are not visited.

This method performs the following steps when called:

1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. If IsCallable(callbackfn) is false, throw a TypeError exception.
4. Let k be 0.
5. Repeat, while k < len,
    a. Let Pk be ! ToString(F(k)).
    b. Let kPresent be ? HasProperty(O, Pk).
    c. If kPresent is true, then
        i. Let kValue be ? Get(O, Pk).
        ii. Perform ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »).
    d. Set k to k + 1.
6. Return undefined.

NOTE 2

This method is intentionally generic; it does not require that its this value be an Array. Therefore it can be transferred to other kinds of objects for use as a method.

我们翻译一下上面的描述 NOTE1 和 NOTE2:

23.1.3.15 Array.prototype.forEach ( callbackfn [ , thisArg ] )

注1

callbackfn 应该是一个接受三个参数的函数。forEach 按升序为数组中的每个元素调用 callbackfn 一次。仅对数组中实际存在的元素调用 callbackfn。不为数组中缺少的元素调用它。 如果提供了 thisArg 参数,它将被用作每次调用 callbackfnthis 值。如果没有提供,则使用 undefinedcallbackfn 由三个参数调用:元素的值、元素的索引和要遍历的对象。 forEach 不会直接更改调用它的对象,但可以通过调用 callbackfn 来更改该对象。 forEach 处理的元素范围是在第一次调用 callbackfn 之前设置的。在对 forEach 的调用开始后附加到数组中的元素将不会被 callbackfn 访问。如果数组的现有元素发生了更改,则传递给 callbackfn的值将是 forEach 访问它们时的值。在开始调用 forEach 之后和被访问之前删除的元素不会被访问。

注2

这种方法是有意通用的;它不要求它的这个值是一个数组。因此,它可以被转移到其他类型的对象中用作方法。

对 forEach 方法的注意项了解完了,接下来就是重点实现了。

了解规范步骤

forEach() 方法在调用时执行以下步骤:

1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. If IsCallable(callbackfn) is false, throw a TypeError exception.
4. Let k be 0.
5. Repeat, while k < len,
    a. Let Pk be ! ToString(F(k)).
    b. Let kPresent be ? HasProperty(O, Pk).
    c. If kPresent is true, then
        i. Let kValue be ? Get(O, Pk).
        ii. Perform ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »).
    d. Set k to k + 1.
6. Return undefined.

为了让大家看懂,对上面规范中的一些关键词&符号进行解释:

关键词&符号解释
Let规范中,"Let" 关键字用于声明一个新的变量,并将其绑定到当前执行上下文的作用域中。它通常用于声明在块级作用域内部使用的变量,比如在函数内部或者 {} 包裹的代码块内部。这样声明的变量只在当前作用域内有效,不会造成变量的泄漏或冲突。这个 Let 跟我们用的 let 不一样。它后面通常跟一个变量名, Let O 表示定义一个变量 O。
be规范中,"be" 是一个关键词,用于表示赋值操作。它的作用是将右侧的值赋给左侧声明的变量或标识符。"be" 作用相当于等号 "="。
?在规范中,"?" 符号通常表示一个可能会引发异常的操作。当 "?" 符号出现在某个操作的前面时,意味着该操作可能会失败,并且在失败时会引发一个异常。因此,在解释规范时,需要考虑到可能会出现异常的情况,并做好相应的异常处理。"?" 符号提示实现规范时需要在相应的位置进行异常处理。
!在规范中,"!"  符号通常表示一个抽象操作的调用不应该抛出异常。
« »在规范中,"« »" 符号用于表示参数序列。
ToObject(this value)在规范中,类似于函数调用方式通常表示抽象操作。比如 ToObject(this value) 表示接受一个参数把它转换为 Object 类型。

了解完关键词、符号用、抽象操作之后上面的的规范步骤理解起来就不难了。 下面逐一解释一下规范步骤:

  1. Let O be ? ToObject(this value): 这一行代码将当前方法被调用的对象(即 this 值)转换为一个对象(Object),并将结果存储在变量 O 中。? 表示这是一个可能会抛出异常的操作,如果转换失败,会抛出一个异常。

  2. Let len be ? LengthOfArrayLike(O): 这一行代码获取了对象 O 的长度,并将其存储在变量 len 中。LengthOfArrayLike 是一个内置函数,用于获取类数组对象的长度。同样,? 表示可能会抛出异常。

  3. If IsCallable(callbackfn) is false, throw a TypeError exception: 这一行代码检查传递给 forEach() 方法的回调函数是否是一个可调用的函数。如果不是,则抛出一个 TypeError 异常。

  4. Let k be 0: 这一行代码初始化一个变量 k,用于迭代数组中的索引。

  5. Repeat, while k < len: 这表示一个循环结构,它会在索引 k 小于数组长度 len 的情况下执行。

  6. Let Pk be ! ToString(F(k)): 这一行代码将索引 k 转换为字符串,并将结果存储在变量 Pk 中。

  7. Let kPresent be ? HasProperty(O, Pk): 这一行代码检查对象 O 中是否存在属性 Pk。如果存在,则将变量 kPresent 设置为 true,否则设置为 false。

  8. If kPresent is true, then: 如果属性 Pk 存在,则执行下面的步骤。

    a. Let kValue be ? Get(O, Pk): 获取属性 Pk 对应的值,并将其存储在变量 kValue 中。Get 是一个内置函数,用于获取对象的属性值。

    b. Perform ? Call(callbackfn, thisArg, « kValue, F(k), O »): 调用传递给 forEach() 方法的回调函数,并传入三个参数:当前元素的值 kValue、当前元素的索引 k,以及数组本身 O。Call 是一个内置函数,用于调用函数。

  9. Set k to k + 1: 将索引 k 的值增加 1,以便下一次迭代访问下一个元素。

  10. Return undefined: 返回 undefined,因为 forEach() 方法本身并不返回任何值,它只是对数组进行遍历操作。

实现规范步骤中用到的抽象操作

规范中用到多个抽象操作,这些抽象操作根据它们对应的规范我直接实现了,感兴趣的可以去规范中看这些抽象操作的规范描述。

ToObject(argument) 实现

function ToObject (argument) {
    if (Object.is(argument, undefined) || Object.is(argument, null)) {
        throw TypeError('Array.prototype.myForEach called on null or undefined')
    } // 排除 undefined 和 null

    return Object(argument)
}

LengthOfArrayLike(obj) 实现

function LengthOfArrayLike (obj) {
    const length = Number(obj.length)

    if (Number.isNaN(length) || length <= 0) {
        throw TypeError('Length requires a positive integer')
    } // 保证长度为非负整数

    return Math.floor(length)
}

IsCallable(argument) 实现

function IsCallable (argument) {
    // 如果 argument 不是一个对象,则返回 false
    if (Object.is(typeof argument, 'object') || Object.is(argument, null)) {
        return false
    }

    // 如果 argument 有一个 [[Call]] 内部方法,则返回 true
    if (Object.is(typeof argument, 'function') || Object.is(typeof argument?.call, 'function')) {
        return true
    }
    
    // 否则返回 false
    return false
}

ToString(argument) 实现

function ToString (argument) {
    if (typeof argument === 'string') {
        return argument
    }
    
    if (typeof argument === 'symbol') {
        throw new TypeError('Cannot convert a Symbol to a String')
    }
    
    switch (argument) {
        case undefined:
        return 'undefined'
        case null:
        return 'null'
        case true:
        return 'true'
        case false:
        return 'false'
    }
    
    if (typeof argument === 'number') {
        return Number.prototype.toString.call(argument, 10)
    }
    
    if (typeof argument === 'bigint') {
        return BigInt.prototype.toString.call(argument, 10)
    }

    function ToPrimitive (input, preferredType) {
        if (typeof input === 'object' && input !== null) {
            const valueOf = input.valueOf()
            if (typeof valueOf === 'object' && valueOf !== null) {
            const toString = input.toString()
            if (typeof toString === 'object' && toString !== null) {
                throw new TypeError('Cannot convert object to primitive value')
            }
            return toString
            }
            return valueOf
        }
        
        if (preferredType === 'number') {
            return +input
        }
        
        return '' + input
    }

    if (typeof argument === 'object') {
        const primValue = ToPrimitive(argument, 'string')
        return ToString(primValue)
    }
    
    throw new TypeError('Cannot convert argument to a String')
}

HasProperty(O, P) 实现

function HasProperty (O, P) {
    return O.hasOwnProperty(P) ? O.hasOwnProperty(P) : P in O
}

Get(O, P) 实现

function Get (O, P) {
    return O[P]
}

Call(F, V, argumentsList) 实现

function Call (F, V, argumentsList) {
    if (Object.is(argumentsList, undefined)) {
        argumentsList = []
    }
    
    if (IsCallable(F) === false) {
        throw TypeError('F is not callable')
    }
    
    return F.call(V, ...argumentsList)
}

F(x) 实现

function F(x) {
    const integerX = Math.trunc(x)
    return Math.max(integerX, 0)
}

根据规范步骤实现 forEach()

到这里在规范步骤中用到的所有抽象操作都已经实现,现在只需按规范步骤写出 forEach 代码即可。

Array.prototype.myForEach = function (callbackfn, thisArg) {
    // 1. 将 this 值转换为对象
    const O = ToObject(this)
    // 2. 获取数组长度
    const len = LengthOfArrayLike(O.length)
    
    // 3. 检查回调函数是否可调用
    if (IsCallable(callbackfn) === false) {
        throw TypeError(`${typeof callbackfn} ${Object.is(callbackfn, undefined) ? '' : callbackfn} is not a function`)
    }
    
    // 4. 初始化索引 k 为 0
    let k = 0
    
    // 5. 循环遍历数组
    while (k < len) {
        // a. 获取属性名
        const Pk = ToString(k)
        
        // b. 检查属性是否存在
        const kPresent = HasProperty(O, Pk)

        // c. kPresent 是 true
        if (kPresent === true) {
            // i. 获取属性值
            const kValue = Get(O, Pk)
            // ii. 执行 Call 方法
            Call(callbackfn, thisArg, [kValue, F(k), O])
        }
        
        // d. 增加索引
        k++
    }

    // 6. 返回 undefined
    return undefined
}

测试用例

console.log('forEach 不能遍历异步---------------------------')
const ratings = [5, 4, 5]
let sum = 0

const sumFunction = async (a, b) => a + b

ratings.myForEach(async (rating) => {
    sum = await sumFunction(sum, rating)
});
ratings.forEach(async (rating) => {
    sum = await sumFunction(sum, rating)
})

console.log(sum) 
// 0

console.log('在稀疏数组上使用 forEach ----------------------------')
const arraySparse = [1, 3, , 7]
let numCallbackRuns = 0

arraySparse.myForEach((element) => {
  console.log({ element })
  numCallbackRuns++
})
arraySparse.forEach((element) => {
  console.log({ element })
  numCallbackRuns++
})

console.log({ numCallbackRuns }) 
// 6

console.log('打印出数组的内容------------------------')
const logArrayElements = (element, index) => {
  console.log(`a[${index}] = ${element}`)
}
[2, 5, , 9].myForEach(logArrayElements);
[2, 5, , 9].forEach(logArrayElements)
// a[0] = 2
// a[1] = 5
// a[3] = 9

console.log('使用 thisArgs----------------------------------')
const obj = { name: 'Aimilali' }
const obj1 = { name: 'Aimilali' }
const testArr = [1, 2, 3]
testArr.myForEach(function (value) {
    this.name = this.name + value
}, obj)
testArr.forEach(function (value) {
    this.name = this.name + value
}, obj1)

console.log(obj)
console.log(obj1)
// {name: 'Aimilali123'}

console.log('在迭代期间修改数组-----------------------------')
const words = ["one", "two", "three", "four"]
const words1 = ["one", "two", "three", "four"]
words.myForEach((word) => {
  if (word === "two") {
    words.shift()
  }
})
words1.forEach((word) => {
  if (word === "two") {
    words1.shift()
  }
})

console.log(words) // ['two', 'three', 'four']
console.log(words1) // ['two', 'three', 'four']

console.log('扁平化数组---------------------------')
const flatten = (arr) => {
  const result = []
  arr.myForEach((item) => {
    if (Array.isArray(item)) {
      result.push(...flatten(item))
    } else {
      result.push(item)
    }
  })
  return result
}
const flatten1 = (arr) => {
  const result = []
  arr.forEach((item) => {
    if (Array.isArray(item)) {
      result.push(...flatten(item))
    } else {
      result.push(item)
    }
  })
  return result
}

// 用例
const nested = [1, 2, 3, [4, 5, [6, 7], 8, 9]]
const nested1 = [1, 2, 3, [4, 5, [6, 7], 8, 9]]
console.log(flatten(nested)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(flatten1(nested1)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

console.log('在非对象数组上调用 forEach()--------------------------')
const arrayLike = {
  length: 3,
  0: 2,
  1: 3,
  2: 4,
}
Array.prototype.myForEach.call(arrayLike, (x) => console.log(x))
Array.prototype.forEach.call(arrayLike, (x) => console.log(x))
// 2
// 3
// 4

结语

到这里 Array 实例方法 forEach 实现完成啦。推荐大家去看其他方法实现:

Array 实例方法实现系列

JavaScript 中的 Array 类型提供了一系列强大的实例方法。在这个专栏中,我将深入探讨一些常见的 Array 实例方法,解析它们的实现原理。

如果有错误或者不严谨的地方,请请大家务必给予指正,十分感谢。欢迎大家在评论区中讨论。