Array 实例办法 forEach 的完成

在本文中,咱们将从 ECMAScript 言语标准角度讨论 JavaScript 中 Array.prototype.forEach() 办法的完成。通过深入分析 ECMAScript 标准文档,咱们将揭示 forEach() 办法背后的原理和规划理念。从函数签名到详细行为,咱们将逐步解析该办法在标准中的界说,并讨论其与其他数组办法的关联。通过本文,读者将了解到如何精确地完成 forEach() 办法,并了解其在 JavaScript 数组操作中的重要性和运用场景。

Array.prototype.forEach()

ECMAScript 2025 言语标准中对 Array.prototype.forEach() 的原文描绘如下:

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

NOTE 1

callbackfnshould be a function that accepts three arguments.forEachcallscallbackfnonce for each element present in the array, in ascending order.callbackfnis called only for elements of the array which actually exist; it is not called for missing elements of the array.

If athisArgparameter is provided, it will be used as thethisvalue for each invocation ofcallbackfn. If it is not provided,undefinedis used instead.

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

forEachdoes not directly mutate the object on which it is called but the object may be mutated by the calls tocallbackfn.

The range of elements processed byforEachis set before the first call tocallbackfn. Elements which are appended to the array after the call toforEachbegins will not be visited bycallbackfn. If existing elements of the array are changed, their value as passed tocallbackfnwill be the value at the timeforEachvisits them; elements that are deleted after the call toforEachbegins 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 itsthisvalue be an Array. Therefore it can be transferred to other kinds of objects for use as a method.

咱们翻译一下上面的描绘 NOTE1 和 NOTE2:

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

注1

callbackfn 应该是一个承受三个参数的函数。forEach 按升序为数组中的每个元素调用 callbackfn 一次。仅对数组中实践存在的元素调用 callbackfn。不为数组中缺少的元素调用它。
假如供给了 thisArg 参数,它将被用作每次调用 callbackfnthis 值。假如没有供给,则运用 undefined
callbackfn 由三个参数调用:元素的值、元素的索引和要遍历的目标。
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 实例办法,解析它们的完成原理。

假如有错误或许不严谨的地方,请请咱们必须给予纠正,十分感谢。欢迎咱们在谈论区中讨论。