用JavaScript创建范围 - 奇怪的语法

Ben*_*aum 127 javascript ecmascript-5

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

Array.apply(null, { length: 5 }).map(Number.call, Number);
Run Code Online (Sandbox Code Playgroud)

这产生了

[0, 1, 2, 3, 4]
Run Code Online (Sandbox Code Playgroud)

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

Zir*_*rak 257

理解这个"黑客"需要理解几件事:

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

它们在javascript中是相当高级的主题,所以这将是相当长的.我们将从顶部开始.系好安全带!

1.为什么不Array(5).map呢?

什么是数组,真的吗?包含整数键的常规对象,映射到值.它有其他特殊功能,例如魔法length变量,但在它的核心,它是一个常规key => value地图,就像任何其他对象一样.让我们玩一点阵列,不是吗?

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']
Run Code Online (Sandbox Code Playgroud)

我们得到了数组中项目arr.lengthkey=>value与数组映射数之间的固有差异,这可能与数组不同arr.length.

通过扩展数组arr.length 不会创建任何新的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"]
Run Code Online (Sandbox Code Playgroud)

我故意使用方法调用来进一步证明密钥本身永远不会存在:调用undefined.toUpperCase会引发错误,但事实并非如此.为了证明:

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
Run Code Online (Sandbox Code Playgroud)

现在我们达到了我的观点:Array(N)事情是怎样的.第15.4.2.2节描述了该过程.有一堆我们不关心的mumbo jumbo,但是如果你设法在线之间阅读(或者你可以相信我这个,但不要),它基本归结为:

function Array(len) {
    var ret = [];
    ret.length = len;
    return ret;
}
Run Code Online (Sandbox Code Playgroud)

(根据假设(在实际规范中检查)运行,这len是一个有效的uint32,而不仅仅是任意数量的值)

所以现在你可以看到为什么做Array(5).map(...)不起作用 - 我们没有len在数组上定义项,我们不创建key => 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
Run Code Online (Sandbox Code Playgroud)

现在,我们可以apply通过简单地记录arguments特殊变量来简化查看工作的过程:

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]
Run Code Online (Sandbox Code Playgroud)

在倒数第二个例子中很容易证明我的主张:

function ahaExclamationMark () {
    console.log(arguments.length);
    console.log(arguments.hasOwnProperty(0));
}

ahaExclamationMark.apply(null, Array(2)); //2, true
Run Code Online (Sandbox Code Playgroud)

(是的,双关语).该key => value映射可能不是我们移交到数组中已经存在apply,但它在一定存在arguments变数.这与上一个示例的工作原理相同:我们传递的对象上不存在键,但它们确实存在arguments.

这是为什么?让我们看一下第15.3.4.3节,其中Function.prototype.apply定义了.大多数事情我们不关心,但这里是有趣的部分:

  1. 设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);
};
Run Code Online (Sandbox Code Playgroud)

因此,argArray在这种情况下,我们需要模仿的是具有length属性的对象.现在我们可以看到为什么值是未定义的,但键不是,在arguments:我们创建key=>value映射.

Phew,所以这可能不会比前一部分短.但是当我们完成时会有蛋糕,所以请耐心等待!但是,在接下来的部分(这将是简短的,我保证)之后,我们可以开始剖析表达式.万一你忘了,问题是以下工作如何:

Array.apply(null, { length: 5 }).map(Number.call, Number);
Run Code Online (Sandbox Code Playgroud)

3.如何Array处理多个参数

所以!我们看到了传​​递length参数时会发生什么Array,但是在表达式中,我们传递了几个作为参数的东西(undefined确切地说是一个5的数组).第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]
Run Code Online (Sandbox Code Playgroud)

田田!我们得到一个包含几个未定义值的数组,然后返回这些未定义值的数组.

表达的第一部分

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

Array.apply(null, { length: 5 })
Run Code Online (Sandbox Code Playgroud)

我们看到它返回一个包含5个未定义值的数组,所有键都存在.

现在,到表达式的第二部分:

[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
Run Code Online (Sandbox Code Playgroud)

这将是一个更容易,非复杂的部分,因为它不是那么依赖于模糊的黑客.

4.如何Number对待输入

Number(something)(第15.7.1节)转换something为数字,即全部.它是如何做的有点复杂,特别是在字符串的情况下,但如果你感兴趣的话,操作在9.3节中定义.

5.游戏 Function.prototype.call

callapply兄弟,在第15.3.4.4节中定义.它不是取一个参数数组,而是接受它收到的参数,并将它们传递给它们.

当你将多个链接call在一起时,事情就变得有趣了,把这个怪异的东西调到11:

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

在你掌握正在发生的事情之前,这非常有用.log.call只是一个函数,相当于任何其他函数的call方法,因此,它call本身也有一个方法:

log.call === log.call.call; //true
log.call === Function.call; //true
Run Code Online (Sandbox Code Playgroud)

那怎么call办?它接受一个thisArg和一堆参数,并调用其父函数.我们可以通过apply (再次,非常松散的代码,将无法工作)来定义它:

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

让我们跟踪这是如何下降的:

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}])
Run Code Online (Sandbox Code Playgroud)

后面的部分,或者.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
Run Code Online (Sandbox Code Playgroud)

如果我们自己不提供this论证,则默认为window.记下为我们的回调提供参数的顺序,让我们再一次将它变为11:

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

哇哇哇......让我们稍微回过神来.这里发生了什么?我们可以在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);
}
Run Code Online (Sandbox Code Playgroud)

所以,我们得到这个:

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);
Run Code Online (Sandbox Code Playgroud)

现在我们可以看到它的.map(Number.call, Number)工作原理

Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);
Run Code Online (Sandbox Code Playgroud)

它将i当前索引的转换返回到一个数字.

结论,

表达方式

Array.apply(null, { length: 5 }).map(Number.call, Number);
Run Code Online (Sandbox Code Playgroud)

分为两部分:

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

第一部分创建一个包含5个未定义项的数组.第二个遍历该数组并获取其索引,从而产生一个元素索引数组:

[0, 1, 2, 3, 4]
Run Code Online (Sandbox Code Playgroud)

  • @Geek我们只将一个参数传递给`apply`,但是该参数被"splatted"为传递给函数的两个参数.您可以在第一个`apply`示例中更容易地看到它.第一个`console.log`然后显示我们确实收到了两个参数(两个数组项),第二个`console.log`显示该数组在第一个插槽中有一个`key => value`映射(如在答案的第一部分解释). (4认同)
  • 由于[(某些)请求](http://chat.stackoverflow.com/transcript/17?m=11996892#11996892),您现在可以享受音频版本:https://dl.dropboxusercontent.com/u/ 24522528/SO-answer.mp3 (4认同)
  • 一个更正:`this`在非严格模式下仅默认为'Window`. (2认同)

Ben*_*aum 21

免责声明:这是对上述代码的非常正式的描述 - 这就是如何解释它.对于一个更简单的答案 - 检查Zirak上面的答案.这是你脸上更深入的规范而不是"aha".


这里发生了一些事情.让我们分解一下吧.

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
Run Code Online (Sandbox Code Playgroud)

在第一行中,该阵列构造函数作为函数调用Function.prototype.apply.

  • thisnull对于Array构造函数无关紧要(thisthis根据15.3.4.3.2.a的上下文中的值相同).
  • 然后new Array被称为传递一个带有length属性的对象- 这会导致该对象成为一个数组,.apply因为它的所有重要因素都在于.apply:
    • 设len是使用参数"length"调用argArray的[[Get]]内部方法的结果.
  • 因此,.apply被传递参数从0到.length,由于主叫[[Get]]{ length: 5 }具有值0〜4得到undefined的阵列构造函数被调用与五个参数,其值是undefined(获得的对象的未声明属性).
  • 使用0,2或更多参数调用数组构造函数.新构造的数组的length属性设置为根据规范的参数数量和相同值的值.
  • 因此,var arr = Array.apply(null, { length: 5 });创建一个包含五个未定义值的列表.

注意:注意这里Array.apply(0,{length: 5})和之间的区别Array(5),第一个创建原始值类型的五倍,undefined后者创建一个长度为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
Run Code Online (Sandbox Code Playgroud)

现在进入第二部分.

  • Array.prototype.mapNumber.call在数组的每个元素上调用回调函数(在本例中)并使用指定的this值(在这种情况下将this值设置为"Number").
  • map中回调的第二个参数(在本例中Number.call)是索引,第一个是this值.
  • 这意味着Number使用thisas undefined(数组值)和索引作为参数调用.所以它基本上与将每个映射undefined到其数组索引相同(因为调用Number执行类型转换,在这种情况下,从数字到数字不改变索引).

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

这就是为什么我们将结果传递给我们的代码.


Tal*_*l Z 5

正如你所说,第一部分:

var arr = Array.apply(null, { length: 5 }); 
Run Code Online (Sandbox Code Playgroud)

创建一个包含5个undefined值的数组.

第二部分是调用map数组的函数,它接受2个参数并返回一个相同大小的新数组.

第一个参数map实际上是一个应用于数组中每个元素的函数,它应该是一个函数,它接受3个参数并返回一个值.例如:

function foo(a,b,c){
    ...
    return ...
}
Run Code Online (Sandbox Code Playgroud)

如果我们传递函数foo作为第一个参数,它将为每个元素调用

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

第二个参数map是传递给您作为第一个参数传递的函数.但它不是a,b,也不是c foo,如果是的话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]
Run Code Online (Sandbox Code Playgroud)

另一个只是为了让它更清晰:

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

那么Number.call怎么样?

Number.call 是一个带有2个参数的函数,并尝试将第二个参数解析为一个数字(我不确定它对第一个参数的作用).

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

map在代码中传递给函数的第二个参数实际上对结果没有影响.如果我错了,请纠正我.