在 JavaScript 中创建范围 - 奇怪的语法

Posted

技术标签:

【中文标题】在 JavaScript 中创建范围 - 奇怪的语法【英文标题】:Creating range in JavaScript - strange syntax 【发布时间】:2013-09-27 16:17:37 【问题描述】:

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

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

这会产生

[0, 1, 2, 3, 4]

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

【问题讨论】:

IMO Array.apply(null, Array(30)).map(Number.call, Number) 更容易阅读,因为它避免了将普通对象伪装成数组。 @fncomp 请不要使用任何一个来实际上创建一个范围。它不仅比直接方法慢 - 它几乎不那么容易理解。很难理解这里的语法(实际上是 API 而不是语法),这使得这是一个有趣的问题,但 IMO 的生产代码很糟糕。 是的,不建议任何人使用它,但认为相对于对象字面量版本,它仍然更易于阅读。 我不知道为什么有人会想要这样做。以这种方式创建数组所需的时间本来可以以一种稍微不那么性感但更快的方式完成:jsperf.com/basic-vs-extreme 【参考方案1】:

理解这个“hack”需要理解几件事:

    为什么我们不只是做Array(5).map(...) Function.prototype.apply 如何处理参数 Array 如何处理多个参数 Number 函数如何处理参数 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']

我们得到了数组中的项目数 arr.length 与数组具有的 key=>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"]

我故意使用方法调用来进一步证明密钥本身不存在这一点:调用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

现在我们进入我的观点:Array(N) 是如何做事的。 Section 15.4.2.2 描述了这个过程。有一堆我们不关心的胡言乱语,但如果你设法在字里行间阅读(或者你可以相信我,但不要),它基本上可以归结为:

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

(在假设(在实际规范中检查)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

现在,我们可以通过简单地记录 arguments 特殊变量来简化查看 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]

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

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


ahaExclamationMark.apply(null, Array(2)); //2, true

(是的,双关语)。 key => value 映射可能不存在于我们传递给apply 的数组中,但它肯定存在于arguments 变量中。这与上一个示例有效的原因相同:我们传递的对象上不存在键,但它们确实存在于arguments

这是为什么呢?让我们看一下Section 15.3.4.3,其中定义了Function.prototype.apply。主要是我们不关心的事情,但有趣的部分是:

    设 len 为使用参数“length”调用 argArray 的 [[Get]] 内部方法的结果。

这基本上意味着:argArray.length。然后规范继续对length 项执行简单的for 循环,生成对应值的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 上看到为什么值未定义但键未定义:我们创建了key=&gt;value 映射。

唷,所以这可能不会比上一部分短。但是当我们完成时会有蛋糕,所以请耐心等待!然而,在接下来的部分(我保证会很短)之后,我们可以开始剖析表达式。如果您忘记了,问题是以下如何工作:

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

3。 Array 如何处理多个参数

所以!我们看到了当您将 length 参数传递给 Array 时会发生什么,但在表达式中,我们传递了几个东西作为参数(准确地说是 5 个 undefined 的数组)。 Section 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]

多田!我们得到一个包含几个未定义值的数组,并返回一个包含这些未定义值的数组。

表达式的第一部分

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

Array.apply(null,  length: 5 )

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

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

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

这将是更简单、不复杂的部分,因为它不太依赖于晦涩难懂的技巧。

4。 Number 如何处理输入

执行Number(something) (section 15.7.1) 会将something 转换为数字,仅此而已。它是如何做到的有点令人费解,尤其是在字符串的情况下,但如果您感兴趣,该操作在section 9.3 中定义。

5。 Function.prototype.call的游戏

callapply 的兄弟,在section 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

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

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

call 是做什么的?它接受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。记下参数提供给我们的回调的顺序,让我们再次将其怪异到 11:

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

哇哇哇...让我们后退一点。这里发生了什么?我们可以在section 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 转换为数字。

总之,

表达式

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

分为两部分:

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

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

[0, 1, 2, 3, 4]

【讨论】:

@Zirak 请帮助我理解以下ahaExclamationMark.apply(null, Array(2)); //2, true。为什么它分别返回2true?你不是只传递一个参数,即Array(2) 这里吗? @Geek 我们只将一个参数传递给apply,但该参数被“分解”成两个传递给函数的参数。您可以在第一个 apply 示例中更轻松地看到这一点。然后第一个console.log 表明我们确实收到了两个参数(两个数组项),第二个console.log 表明该数组在第一个槽中有一个key=&gt;value 映射(如第1 部分所述回答)。 由于(some) request,您现在可以享受音频版本:dl.dropboxusercontent.com/u/24522528/SO-answer.mp3 请注意,像log.apply(null, document.getElementsByTagName('script')); 中那样将作为宿主对象的NodeList 传递给本机方法不需要工作,并且在某些浏览器中不起作用,而[].slice.call(NodeList) 可以转成NodeList放入数组中也不起作用。 更正一点:this 仅在非严格模式下默认为Window【参考方案2】:

免责声明:这是对上述代码的非常正式的描述——这就是知道如何解释它的方式。如需更简单的答案 - 请查看上面 Zirak 的精彩答案。这是一个更深入的规范,而不是“啊哈”。


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

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

在第一行,the array constructor is called as a function 和 Function.prototype.apply

this 的值是 null,这对于 Array 构造函数无关紧要(this 与根据 15.3.4.3.2.a 的上下文中的 this 相同。 然后调用new Array 并传递一个具有length 属性的对象 - 这导致该对象成为一个数组,就像.apply 一样重要,因为.apply 中的以下子句: 设 len 为使用参数“length”调用 argArray 的 [[Get]] 内部方法的结果。 因此,.apply 将参数从 0 传递到 .length,因为在 length: 5 上使用值 0 到 4 调用 [[Get]] 会产生 undefined 使用五个参数调用数组构造函数,其值为 @ 987654345@(获取对象的未声明属性)。 数组构造函数is called with 0, 2 or more arguments。 新构造的数组的长度属性根据规范设置为参数的数量,值设置为相同的值。 因此var arr = Array.apply(null, length: 5 ); 创建了一个包含五个未定义值的列表。

注意:注意Array.apply(0,length: 5)Array(5)之间的区别,第一个创建五倍原始值类型undefined,后者创建一个长度为5的空数组。具体来说, because of .map's behavior (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 对数组的每个元素调用回调函数(在本例中为 Number.call)并使用指定的 this 值(在本例中将 this 值设置为 `Number)。 map中回调的第二个参数(本例为Number.call)为索引,第一个为this值。 这意味着Numberthis 作为undefined(数组值)和索引作为参数调用。所以它与将每个undefined 映射到其数组索引基本相同(因为调用Number 执行类型转换,在这种情况下从数字到数字不会改变索引)。

因此,上面的代码采用五个未定义的值并将每个值映射到其在数组中的索引。

这就是我们将结果写入代码的原因。

【讨论】:

对于文档:关于地图如何工作的规范:es5.github.io/#x15.4.4.19,Mozilla 在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… 有一个示例脚本,该脚本根据该规范运行 但是为什么它只适用于Array.apply(null, length: 2 ) 而不是Array.apply(null, [2]),它也会调用Array 构造函数,将2 作为长度值传递? fiddle @Andreas Array.apply(null,[2]) 就像 Array(2) 创建一个长度为 2 的 empty 数组,而 not 是一个包含原始值 @987654369 的数组@两次。在第一部分之后的注释中查看我最近的编辑,让我知道它是否足够清楚,如果不是,我会澄清这一点。 我不明白它在第一次运行时的工作方式......在第二次阅读之后它是有道理的。 length: 2 伪造一个包含两个元素的数组,Array 构造函数将插入新创建的数组中。由于没有真正的数组访问不存在的元素会产生undefined,然后将其插入。好把戏:)【参考方案3】:

数组只是一个包含“长度”字段和一些方法(例如推送)的对象。所以 var arr = length: 5 中的 arr 基本上与字段 0..4 具有未定义的默认值的数组相同(即 arr[0] === undefined 产生 true)。 至于第二部分,map,顾名思义,就是从一个数组映射到一个新数组。它通过遍历原始数组并在每个项目上调用映射函数来实现。

剩下的就是让你相信映射函数的结果就是索引。诀窍是使用名为 'call'(*) 的方法,该方法调用一个函数,但有一个小例外,即第一个参数设置为 'this' 上下文,第二个参数设置为第一个参数(依此类推)。巧合的是,在调用映射函数时,第二个参数是索引。

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

(*) 在 Function 的原型中找到(并且 Number 是一个函数)。

马沙尔

【讨论】:

[undefined, undefined, undefined, …]new Array(n)length: n 之间存在巨大差异 - 后者是 sparse,即它们没有元素。这与map 非常相关,这就是为什么使用奇怪的Array.apply【参考方案4】:

如你所说,第一部分:

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

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

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

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

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

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

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

map 采用的第二个参数被传递给您作为第一个参数传递的函数。但在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 个参数的函数,并尝试将第二个参数解析为一个数字(我不确定它对第一个参数的作用)。

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

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

【讨论】:

Number.call 不是将参数解析为数字的特殊函数。只是=== Function.prototype.call。只有第二个参数,即作为 this 值传递给 call 的函数是相关的 - .map(eval.call, Number).map(String.call, Number).map(Function.prototype.call, Number) 都是等价的。

以上是关于在 JavaScript 中创建范围 - 奇怪的语法的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法在 JavaScript 中创建整数范围? [复制]

在 C# Web 浏览器中创建 javascript 变量全局范围

使用 var 关键字在 Javascript 中创建对象时遇到问题

如何在像 Java 这样的 Javascript 中创建 Bean 类?

为啥 SELECT 在 SQL Server 中创建未提交的事务?

在 SQL 中创建数据库表,奇怪的错误