JavaScript中的[].forEach.call()是什么意思?

162

我正在查看一些代码片段,发现有多个元素通过应用于一个空数组的forEach来调用一个函数来遍历节点列表。

例如,我有如下代码:

[].forEach.call( document.querySelectorAll('a'), function(el) {
   // whatever with the current node
});

但我不理解它是如何工作的。有人能解释一下在forEach前面的空数组的行为以及call是如何工作的吗?

14个回答

242

[]是一个数组。
这个数组完全没有被使用。

它被添加到页面上,因为使用数组可以访问数组原型,例如.forEach

这比输入Array.prototype.forEach.call(...);更快捷。

接下来,forEach是一个以函数作为输入的函数...

[1,2,3].forEach(function (num) { console.log(num); });

...并且对于this中的每个元素(其中this是类似数组的,具有length属性,可以像this[1]一样访问其部分),它会传递三个内容:

  1. 数组中的元素
  2. 元素的索引(第三个元素将传递2
  3. 数组的引用

最后,.call是函数的原型(它是一个在其他函数上调用的函数)。
.call将使用其第一个参数替换正常函数内部的this,作为第一个参数传递给call的任何值(在日常JS中,undefinednull将使用window,如果处于“严格模式”,则将使用传递的任何值)。其余的参数将传递给原始函数。

[1, 2, 3].forEach.call(["a", "b", "c"], function (item, i, arr) {
    console.log(i + ": " + item);
});
// 0: "a"
// 1: "b"
// 2: "c"
因此,你正在创建一种快速调用forEach函数的方法,并将this从空数组更改为所有<a>标记的列表,然后按顺序为每个<a>调用提供的函数。

编辑

逻辑结论/清理

下面有一篇文章的链接,建议我们放弃尝试使用函数式编程,每次都坚持手动、内联循环,因为这个解决方案是hack-ish和难看的。

我认为虽然.forEach不如它的对应项.map(transformer).filter(predicate).reduce(combiner, initialValue)有用,但当你真正想要做的只是修改外部世界(而不是数组),n次,同时访问arr[i]i时,它仍然有其作用。

那么我们如何处理这种差异,因为Motto显然是一个有才华和知识渊博的人,而我想象自己知道我在做什么/我在哪里去(现在和然后... ...其他时候是头脑风暴学习)?

答案实际上非常简单,是Uncle Bob和Sir Crockford都会因为疏忽而面露尴尬的:

清理它

function toArray (arrLike) { // or asArray(), or array(), or *whatever*
  return [].slice.call(arrLike);
}

var checked = toArray(checkboxes).filter(isChecked);
checked.forEach(listValues);

现在,如果你在质疑是否需要亲自去做这个事情,答案很可能是不需要...
现在几乎每个支持高阶特性的库都在做这件事情了。
如果你正在使用lodash、underscore甚至是jQuery,它们都有一种方法来获取一组元素并执行n次操作。
如果你没有使用这样的库,那么请尽管自己动手写。

lib.array = (arrLike, start, end) => [].slice.call(arrLike, start, end);
lib.extend = function (subject) {
  var others = lib.array(arguments, 1);
  return others.reduce(appendKeys, subject);
};

ES6(ES2015)及更高版本的更新

为了让那些想要像使用数组一样使用列表的人们(正如他们应该做的那样)更方便,slice( )/array( )/等辅助方法将成为必需品。对于那些有幸在未来使用ES6+浏览器或者今天可以通过Babel进行"转译"的人来说,你们已经内置了语言特性,这种操作是不必要的。

function countArgs (...allArgs) {
  return allArgs.length;
}

function logArgs (...allArgs) {
  return allArgs.forEach(arg => console.log(arg));
}

function extend (subject, ...others) { /* return ... */ }


var nodeArray = [ ...nodeList1, ...nodeList2 ];

超级干净,非常实用。
查找RestSpread运算符; 在BabelJS网站上尝试它们; 如果您的技术栈有序,请在使用Babel和构建步骤时将其用于生产。


没有充分的理由不能将非数组转换为数组的转换...只是不要在代码中做什么都是复制相同的丑陋行。


.call确实会改变this的值,这就是为什么你要在forEach中使用call的原因。如果forEach看起来像forEach(fn){var i = 0; var len = this.length; for (; i <length; i + = 1) {fn(this [i],i,this); } ,那么通过使用forEach.call(newArrLike)来改变this意味着它将使用该新对象作为this - Norguard
3
.call 不会改变你传入 forEach 的函数内部的 this 值,但是它会改变 forEach 函数内部的 this 值。如果你对于为什么 this 值不能从 forEach 传递到想要调用的函数感到困惑,你应该查找 JavaScript 中 this 的行为。另外,在这里(还有 map/filter/reduce),forEach 还有第二个参数,用来设置函数内部的 this 值:arr.forEach(fn, thisInFn) 或者 [].forEach.call(arrLike, fn, thisInFn);。你也可以直接使用 .bindarr.forEach(fn.bind(thisInFn)); - Norguard
1
@thetrystero 这正是关键。[]只是作为一个数组存在,这样你就可以.call .forEach方法,并将this更改为你要处理的集合。 .call看起来像这样: function (thisArg, a, b, c) { var method = this; method(a, b, c); }除了在method内部设置this等于你传递的thisArg的内部黑魔法。 - Norguard
1
简而言之,Array.from()是什么? - zakius
1
@LukeHutchison 指的是,[].forEach.call(arr, fn) 将运行 arr 的长度,传递到 call 中;而不是在 forEach 开始时使用的数组(如前两句所述,并在此之后详细说明)。初始数组的长度完全无关紧要。当您使用 forEach 函数的第二个参数时,您正在使用索引;与直接将函数传递给 forEach 而不是 call 一样。如果记录第一个参数,您将获得 "a"、"b"、"c",而不是 1、2、3,这是由于 call 如何替换 thisforEach 的工作方式所致。 - Norguard
显示剩余10条评论

64
querySelectorAll 方法 返回一个类似于数组的 NodeList 对象,但它并不是一个真正的数组。 因此,它没有 forEach 方法(该方法是由数组对象通过 Array.prototype 继承而来)。
由于 NodeList 类似于数组,因此数组方法实际上可以在其上运行,因此通过使用 [].forEach.call,您正在在上下文中调用 NodeListArray.prototype.forEach 方法,就好像您能够简单地执行yourNodeList.forEach(/*...*/) 一样。
请注意,空数组字面量只是展开版本的快捷方式,您也可能经常看到这种写法。
Array.prototype.forEach.call(/*...*/);

10
那么,澄清一下,在使用标准数组进行日常应用时,使用 [].forEach.call(['a','b'], cb)['a','b'].forEach(cb) 相比没有优势,只有在尝试迭代没有其自己的原型上具有 forEach 方法的类似数组结构时才有作用?这样说对吗? - Matt Fletcher
2
@MattFletcher:是的,那是正确的。两种方法都可以,但为什么要把事情搞得过于复杂呢?直接在数组本身上调用该方法即可。 - James Allardice
好的,谢谢。也许只是为了街头信誉,在ALDI impress老太太之类的原因,我会坚持使用[].forEach() :) - Matt Fletcher
section.forEach((item)=>{ console.log(item.offsetTop) })``` 代码运行良好。 - daslicht
1
@daslicht 在旧版本的IE浏览器中不支持!(但是,它们支持对数组使用forEach,但不支持NodeList对象) - Djave

21
其他回答已经很好地解释了这段代码,所以我只想提出一个建议。
这是需要简化和明确的良好代码示例。不要每次使用[].forEach.call()Array.prototype.forEach.call(),而应将其制作成一个简单的函数:
function forEach( list, callback ) {
    Array.prototype.forEach.call( list, callback );
}
现在你可以调用这个函数,而不是更加复杂和难以理解的代码:
forEach( document.querySelectorAll('a'), function( el ) {
   // whatever with the current node
});

7
[].forEach.call( document.querySelectorAll('a'), function(el) {
   // whatever with the current node
});

这基本上与以下内容相同:

var arr = document.querySelectorAll('a');
arr.forEach(function(el) {
   // whatever with the current node
});

6

可以使用更好的方式进行编写

Array.prototype.forEach.call( document.querySelectorAll('a'), function(el) {

});

它所做的是document.querySelectorAll('a')返回一个类似于数组的对象,但它不继承自Array类型。 因此,我们使用Array.prototype对象调用forEach方法,并将上下文设置为由document.querySelectorAll('a')返回的值。


3
一个空数组在它的原型中有一个名为forEach的属性,它是一个函数对象。(这个空数组只是获取所有Array对象都具有的forEach函数的简单方法。)函数对象反过来又有一个call属性,它也是一个函数。当你调用一个函数的call函数时,它将带着给定的参数运行该函数。第一个参数成为被调用函数中的this
你可以在这里找到call函数的文档,这里。关于forEach的文档在这里

3

需要更新关于旧问题的信息:

在现代浏览器中,使用document.querySelectorAll("a").foreach()直接循环遍历元素的原因已经越来越少了。

NodeList对象是节点的集合,通常由诸如Node.childNodes之类的属性和document.querySelectorAll()之类的方法返回。

虽然NodeList不是Array,但是可以使用forEach()迭代它。也可以使用Array.from()将其转换为真正的数组。

但是,一些旧的浏览器没有实现NodeList.forEach()或Array.from()。这可以通过使用Array.prototype.forEach()来规避-请参阅本文档的示例。


3
这个页面上有很多有用的信息(参见answer+answer+comment),但我最近和提问者有同样的问题,需要进行一些挖掘才能得到全貌。所以,这里是一个简短的版本:
目标是在类似数组的NodeList上使用Array方法,而该节点列表本身没有这些方法。
早期的模式通过Function.call()占用了数组的方法,并使用了一个数组字面量([])而不是Array.prototype,因为它打字更短。
[].forEach.call(document.querySelectorAll('a'), a => {})

一个较新的模式(ECMAScript 2015之后的)是使用 Array.from()
Array.from(document.querySelectorAll('a')).forEach(a => {})

1

我总是用一个快速而简单的解决方案。我不会去修改原型,这是一种好的实践。当然,有很多方法可以让它更好,但你已经明白了。

const forEach = (array, callback) => {
  if (!array || !array.length || !callback) return
  for (var i = 0; i < array.length; i++) {
    callback(array[i], i);
  }
}

forEach(document.querySelectorAll('.a-class'), (item, index) => {
  console.log(`Item: ${item}, index: ${index}`);
});

1

Just add one line:

NodeList.prototype.forEach = HTMLCollection.prototype.forEach = Array.prototype.forEach;

就这样!

document.querySelectorAll('a').forEach(function(el) {
  // whatever with the current node
});

享受:—)

警告:NodeList是全局类。如果您正在编写公共库,请不要使用此建议。但是,当您在网站或node.js应用程序上工作时,这是增加自我效能的非常方便的方式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接