在JavaScript中创build范围 – 奇怪的语法

我在es-discuss邮件列表中遇到以下代码:

Array.apply(null, { length: 5 }).map(Number.call, Number); 

这产生

 [0, 1, 2, 3, 4] 

为什么这是代码的结果? 这里发生了什么事?

了解这个“黑客”需要理解几件事情:

  1. 为什么我们不只是做Array(5).map(...)
  2. Function.prototype.apply如何处理参数
  3. Array如何处理多个参数
  4. Number函数如何处理参数
  5. 什么Function.prototype.call

他们在javascript中是相当先进的话题,所以这会比较长。 我们将从顶部开始。 系好安全带!

1.为什么不只是Array(5).map

什么是数组,真的吗? 包含整数键的常规对象,映射到值。 它还有其他一些特殊的function,例如神奇的lengthvariables,但是它的核心是一个普通的key => value map,就像任何其他的对象一样。 我们来玩一下arrays吧?

 var arr = ['a', 'b', 'c']; arr.hasOwnProperty(0); //true arr[0]; //'a' Object.keys(arr); //['0', '1', '2'] arr.length; //3, implies arr[3] === undefined //we expand the array by 1 item arr.length = 4; arr[3]; //undefined arr.hasOwnProperty(3); //false Object.keys(arr); //['0', '1', '2'] 

我们得到数组中项目数量arr.length和数组key=>value映射的数量之间的内在差异,这可能与arr.length不同。

通过arr.length扩展数组不会创build任何新的key=>value映射,所以不是数组有未定义的值,它没有这些键 。 当你尝试访问一个不存在的属性会发生什么? 你得到undefined

现在我们可以抬起头来,看看为什么像arr.map这样的函数不会遍历这些属性。 如果arr[3]仅仅是未定义的,而且键已经存在,那么所有这些数组函数就会像其他值一样遍历它:

 //just to remind you arr; //['a', 'b', 'c', undefined]; arr.length; //4 arr[4] = 'e'; arr; //['a', 'b', 'c', undefined, 'e']; arr.length; //5 Object.keys(arr); //['0', '1', '2', '4'] arr.map(function (item) { return item.toUpperCase() }); //["A", "B", "C", undefined, "E"] 

我故意使用了一个方法调用来进一步certificate这个键本身从来没有在这里:调用undefined.toUpperCase会产生一个错误,但它没有。 为了certificate:

 arr[5] = undefined; arr; //["a", "b", "c", undefined, "e", undefined] arr.hasOwnProperty(5); //true arr.map(function (item) { return item.toUpperCase() }); //TypeError: Cannot call method 'toUpperCase' of undefined 

现在我们明白了: Array(N)是如何做的。 第15.4.2.2节描述了这个过程。 有一大堆我们不在乎的巨型巨人,但是如果你能够在两者之间进行阅读(或者你可以相信我,但是不要),那么基本上可以归结为:

 function Array(len) { var ret = []; ret.length = len; return ret; } 

(在假设下(在实际规范中检查), len是有效的uint32,而不是任何数量的值)

所以,现在你可以明白为什么Array(5).map(...)不起作用了 – 我们没有在数组中定义len项,我们不创buildkey => value映射,我们只需改变length属性。

现在我们已经做到了,让我们看看第二个神奇的东西:

2. Function.prototype.apply是如何工作的

什么apply基本上是一个数组,并展开作为函数调用的参数。 这意味着以下几乎是相同的:

 function foo (a, b, c) { return a + b + c; } foo(0, 1, 2); //3 foo.apply(null, [0, 1, 2]); //3 

现在,我们可以通过简单地loggingarguments特殊variables来简化如何apply工作的过程:

 function log () { console.log(arguments); } log.apply(null, ['mary', 'had', 'a', 'little', 'lamb']); //["mary", "had", "a", "little", "lamb"] //arguments is a pseudo-array itself, so we can use it as well (function () { log.apply(null, arguments); })('mary', 'had', 'a', 'little', 'lamb'); //["mary", "had", "a", "little", "lamb"] //a NodeList, like the one returned from DOM methods, is also a pseudo-array log.apply(null, document.getElementsByTagName('script')); //[script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script] //carefully look at the following two log.apply(null, Array(5)); //[undefined, undefined, undefined, undefined, undefined] //note that the above are not undefined keys - but the value undefined itself! log.apply(null, {length : 5}); //[undefined, undefined, undefined, undefined, undefined] 

倒数第二个例子很容易certificate我的说法:

 function ahaExclamationMark () { console.log(arguments.length); console.log(arguments.hasOwnProperty(0)); } ahaExclamationMark.apply(null, Array(2)); //2, true 

(是的,双关意图)。 key => value映射可能不存在于我们传递给apply的数组中,但它肯定存在于argumentsvariables中。 最后一个例子的工作原理是一样的:键不存在于我们传递的对象上,但它们存在于arguments

这是为什么? 我们来看第15.3.4.3节 ,其中定义了Function.prototype.apply 。 大部分我们不关心的东西,但是这里有一个有趣的部分:

  1. 让len是用参数“length”调用argArray的[[Get]]内部方法的结果。

这基本上意味着: argArray.length 。 规范然后继续做一个简单for循环length项目,使相应的值listlist是一些内部的巫术,但它基本上是一个数组)。 就非常非常宽松的代码而言:

 Function.prototype.apply = function (thisArg, argArray) { var len = argArray.length, argList = []; for (var i = 0; i < len; i += 1) { argList[i] = argArray[i]; } //yeah... superMagicalFunctionInvocation(this, thisArg, argList); }; 

所以我们在这种情况下需要模仿一个argArray是一个length属性的对象。 现在我们可以看到为什么这些值是未定义的,但是键不是,在arguments :我们创buildkey=>value映射。

唷,所以这可能不会比前一部分短。 但是完成后会有蛋糕,所以要耐心等待! 但是,在下面的章节之后(我承诺),我们可以开始剖析这个expression式。 如果你忘记了,问题是如何工作:

 Array.apply(null, { length: 5 }).map(Number.call, Number); 

3. Array如何处理多个参数

所以! 我们看到了当给Array传递一个length参数时会发生什么,但是在expression式中,我们传递了几个东西作为参数(确切地说是一个5个undefined的数组)。 第15.4.2.1节告诉我们该怎么做。 最后一段对我们来说是重要的,它的措辞非常奇怪,但它可以归结为:

 function Array () { var ret = []; ret.length = arguments.length; for (var i = 0; i < arguments.length; i += 1) { ret[i] = arguments[i]; } return ret; } Array(0, 1, 2); //[0, 1, 2] Array.apply(null, [0, 1, 2]); //[0, 1, 2] Array.apply(null, Array(2)); //[undefined, undefined] Array.apply(null, {length:2}); //[undefined, undefined] 

田田! 我们得到了一些未定义的值,我们返回这些未定义值的数组。

expression的第一部分

最后,我们可以破译以下内容:

 Array.apply(null, { length: 5 }) 

我们看到它返回一个包含5个未定义值的数组,其中键全部存在。

现在,到expression的第二部分:

 [undefined, undefined, undefined, undefined, undefined].map(Number.call, Number) 

这将是更容易,更复杂的部分,因为它并不那么依赖晦涩的黑客。

4. Number如何处理input

Number(something) ( 15.7.1节 )将something转换成数字,就这样了。 这样做有点复杂,尤其是在string的情况下,但是在9.3节中定义了操作,以防感兴趣。

5. Function.prototype.call游戏

callapply的兄弟,在第15.3.4.4节中定义 。 而不是采取一系列的参数,它只是把它收到的参数,并传递给他们。

当你把不止一个call连在一起的时候,事情会变得很有趣,

 function log () { console.log(this, arguments); } log.call.call(log, {a:4}, {a:5}); //{a:4}, [{a:5}] //^---^ ^-----^ // this arguments 

这是相当有价值的,直到你掌握了正在发生的事情。 log.call只是一个函数,相当于任何其他函数的call方法,因此它本身也有一个call方法:

 log.call === log.call.call; //true log.call === Function.call; //true 

那叫什么? 它接受一个thisArg和一堆参数,并调用它的父函数。 我们可以通过apply来定义它(再次,非常宽松的代码,将不起作用):

 Function.prototype.call = function (thisArg) { var args = arguments.slice(1); //I wish that'd work return this.apply(thisArg, args); }; 

让我们来看看这是怎么回事:

 log.call.call(log, {a:4}, {a:5}); this = log.call thisArg = log args = [{a:4}, {a:5}] log.call.apply(log, [{a:4}, {a:5}]) log.call({a:4}, {a:5}) this = log thisArg = {a:4} args = [{a:5}] log.apply({a:4}, [{a:5}]) 

后面的部分,或者.map的全部

还没结束。 让我们来看看在给大多数数组方法提供函数时会发生什么:

 function log () { console.log(this, arguments); } var arr = ['a', 'b', 'c']; arr.forEach(log); //window, ['a', 0, ['a', 'b', 'c']] //window, ['b', 1, ['a', 'b', 'c']] //window, ['c', 2, ['a', 'b', 'c']] //^----^ ^-----------------------^ // this arguments 

如果我们自己不提供this参数,它默认为window 。 记下提供给我们的callback的参数的顺序,让我们再次奇怪它一直到11:

 arr.forEach(log.call, log); //'a', [0, ['a', 'b', 'c']] //'b', [1, ['a', 'b', 'c']] //'b', [2, ['a', 'b', 'c']] // ^ ^ 

哇,哇,让我们回来一点。 这里发生了什么? 我们可以在第15.4.4.18节看到 , forEach被定义在哪里,下面的事情发生了:

 var callback = log.call, thisArg = log; for (var i = 0; i < arr.length; i += 1) { callback.call(thisArg, arr[i], i, arr); } 

所以,我们得到这个:

 log.call.call(log, arr[i], i, arr); //After one `.call`, it cascades to: log.call(arr[i], i, arr); //Further cascading to: log(i, arr); 

现在我们可以看到.map(Number.call, Number)是如何工作的:

 Number.call.call(Number, arr[i], i, arr); Number.call(arr[i], i, arr); Number(i, arr); 

它返回当前索引i的转换为一个数字。

结论是,

expression方式

 Array.apply(null, { length: 5 }).map(Number.call, Number); 

分两部分工作:

 var arr = Array.apply(null, { length: 5 }); //1 arr.map(Number.call, Number); //2 

第一部分创build一个5个未定义项目的数组。 第二个遍历该数组并取其索引,得到一组元素索引:

 [0, 1, 2, 3, 4] 

免责声明 :这是上述代码的非常正式的描述 – 这是如何知道如何解释它。 对于一个更简单的答案 – 检查Zirak上面的伟大答案。 这是一个更深入的规范在你的脸上,less“哈哈”。


这里发生了几件事情。 让我们分解一下。

 var arr = Array.apply(null, { length: 5 }); // Create an array of 5 `undefined` values arr.map(Number.call, Number); // Calculate and return a number based on the index passed 

在第一行中,使用Function.prototype.apply 作为函数调用数组构造 Function.prototype.apply

  • this值是null ,对于Array构造函数无关紧要( this与在15.3.4.3.2.a中的上下文中一样。
  • 然后new Array被调用传递一个具有length属性的对象 – 这会导致该对象成为一个数组,因为在应用程序中使用了以下子句。
    • 让len是用参数“length”调用argArray的[[Get]]内部方法的结果。
  • 因此, .apply将参数从0传递给.length ,因为在值为0到4的{ length: 5 }上调用[[Get]]产生undefined的数组构造函数,该函数被五个参数调用,其值是undefined的物体未申报的财产)。
  • 数组构造函数被调用0,2或更多的参数 。 新构build的数组的长度属性被设置为根据规范的参数数目和值为相同的值。
  • 因此var arr = Array.apply(null, { length: 5 }); 创build五个未定义值的列表。

注意 :注意Array.apply(0,{length: 5})Array(5)之间的区别,第一个创build五次原始值typesundefined ,后者创build一个长度为5的空数组。具体来说, 因为.map的行为(8.b) ,特别是[[HasProperty]

因此,符合规范的上面的代码与以下代码相同:

 var arr = [undefined, undefined, undefined, undefined, undefined]; arr.map(Number.call, Number); // Calculate and return a number based on the index passed 

现在到第二部分。

  • Array.prototype.map在数组的每个元素上调用callback函数(在这种情况下是Number.call ),并使用指定的值(在这种情况下, this值设置为Number)。
  • map中callback的第二个参数(在这个例子中是Number.call )是索引,第一个是这个值。
  • 这意味着Number被调用为undefined (数组值)和索引作为参数。 所以它基本上和每个undefined映射到它的数组索引一样(因为调用Number执行types转换,在这种情况下,从数字到数字不会改变索引)。

因此,上面的代码取五个未定义的值,并将它们映射到数组中的索引。

这就是为什么我们得到结果到我们的代码。

正如你所说,第一部分:

 var arr = Array.apply(null, { length: 5 }); 

创build一个包含5个undefined值的数组。

第二部分是调用带有2个参数的数组map函数,并返回一个相同大小的新数组。

map需要的第一个参数实际上是一个应用在数组中每个元素上的函数,它应该是一个带有3个参数并返回一个值的函数。 例如:

 function foo(a,b,c){ ... return ... } 

如果我们将函数foo作为第一个parameter passing,那么每个元素都会被调用

  • a作为当前迭代元素的值
  • b作为当前迭代元素的索引
  • c作为整个原始数组

map采用的第二个参数被传递给您作为第一个parameter passing的函数。 但是如果是foo ,它不会是a,b,c,那就是this

两个例子:

 function bar(a,b,c){ return this } var arr2 = [3,4,5] var newArr2 = arr2.map(bar, 9); //newArr2 is equal to [9,9,9] function baz(a,b,c){ return b } var newArr3 = arr2.map(baz,9); //newArr3 is equal to [0,1,2] 

另一个只是为了更清楚:

 function qux(a,b,c){ return a } var newArr4 = arr2.map(qux,9); //newArr4 is equal to [3,4,5] 

那么Number.call呢?

Number.call是一个函数,它接受2个参数,并尝试将第二个参数parsing为一个数字(我不确定它是如何处理第一个参数的)。

由于map传递的第二个参数是索引,因此将放置在该索引处的新数组中的值等于索引。 就像上面例子中的函数baz一样。 Number.call将尝试parsing索引 – 它自然会返回相同的值。

您在代码中传递给map函数的第二个参数实际上并不影响结果。 请纠正我,如果我错了。

一个数组只是一个包含'length'字段和一些方法(例如push)的对象。 因此, var arr = { length: 5}与其中字段0..4具有未定义的默认值(即, arr[0] === undefined值为true的数组)基本相同。
至于第二部分,顾名思义,地图从一个数组映射到一个新数组。 它通过遍历原始数组并调用每个项目的映射函数来完成。

剩下的就是说服你,映射函数的结果就是索引。 诀窍是使用名为“call”(*)的方法调用一个函数,第一个参数设置为“this”上下文,第二个参数为第一个参数(依此类推)。 巧合的是,当映射函数被调用时,第二个参数是索引。

最后但并非最不重要的是,被调用的方法是数字“类”,正如我们在JS中所知,“类”只是一个函数,而这个(数字)期望第一个参数是值。

(*)在Function的原型中find(而Number是一个函数)。

马沙尔