在JavaScript中创build范围 – 奇怪的语法
我在es-discuss邮件列表中遇到以下代码:
Array.apply(null, { length: 5 }).map(Number.call, Number);
这产生
[0, 1, 2, 3, 4]
为什么这是代码的结果? 这里发生了什么事?
了解这个“黑客”需要理解几件事情:
- 为什么我们不只是做
Array(5).map(...)
-
Function.prototype.apply
如何处理参数 -
Array
如何处理多个参数 -
Number
函数如何处理参数 - 什么
Function.prototype.call
做
他们在javascript中是相当先进的话题,所以这会比较长。 我们将从顶部开始。 系好安全带!
1.为什么不只是Array(5).map
?
什么是数组,真的吗? 包含整数键的常规对象,映射到值。 它还有其他一些特殊的function,例如神奇的length
variables,但是它的核心是一个普通的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
的数组中,但它肯定存在于arguments
variables中。 最后一个例子的工作原理是一样的:键不存在于我们传递的对象上,但它们存在于arguments
。
这是为什么? 我们来看第15.3.4.3节 ,其中定义了Function.prototype.apply
。 大部分我们不关心的东西,但是这里有一个有趣的部分:
- 让len是用参数“length”调用argArray的[[Get]]内部方法的结果。
这基本上意味着: argArray.length
。 规范然后继续做一个简单for
循环length
项目,使相应的值list
( list
是一些内部的巫术,但它基本上是一个数组)。 就非常非常宽松的代码而言:
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
游戏
call
是apply
的兄弟,在第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是一个函数)。
马沙尔