了解此“ 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); //truearr[0]; //'a'Object.keys(arr); //['0', '1', '2']arr.length; //3, implies arr[3] === undefined//we expand the array by 1 itemarr.length = 4;arr[3]; //undefinedarr.hasOwnProperty(3); //falseObject.keys(arr); //['0', '1', '2']
我们得到了数组中的项目
arr.length数
key=>value与数组中具有的映射数之间的固有差异,该差异可能不同于
arr.length。
通过扩展数组
arr.length不会 创建任何新的
key=>value映射,因此不是数组具有未定义的值, 也不具有这些键
。当您尝试访问不存在的属性时会发生什么?你懂了
undefined。
现在,我们可以稍微抬起头来,看看为什么像
arr.map这样的函数不会越过这些属性。如果
arr[3]只是未定义,并且键存在,那么所有这些数组函数都将像其他任何值一样遍历它:
//just to remind youarr; //['a', 'b', 'c', undefined];arr.length; //4arr[4] = 'e';arr; //['a', 'b', 'c', undefined, 'e'];arr.length; //5Object.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); //truearr.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在数组上定义项目,我们没有创建
key =>value映射,我们只是更改了
length属性。
现在我们已经解决了这个问题,让我们来看第二个神奇的东西:
2. Function.prototype.apply
工作原理
什么
apply确实基本上是采取一个数组,并展开其作为函数调用的参数。这意味着以下内容几乎相同:
function foo (a, b, c) { return a + b + c;}foo(0, 1, 2); //3foo.apply(null, [0, 1, 2]); //3现在,我们可以
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-arraylog.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 twolog.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。
这是为什么?让我们看一下第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:我们创建了
key=>value映射。
ew,所以这可能不比上一部分短。但是,当我们完成时会有蛋糕,所以请耐心等待!但是,在接下来的部分(我保证会很短)之后,我们可以开始剖析表达式。万一您忘记了,问题是以下工作原理:
Array.apply(null, { length: 5 }).map(Number.call, Number);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]多田!我们得到了几个未定义值的数组,并返回了这些未定义值的数组。
表达式的第一部分
最后,我们可以解密以下内容:
Array.apply(null, { length: 5 })我们看到它返回一个包含5个未定义值的数组,并且所有键都存在。
现在,到表达式的第二部分:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
这将是更简单,更轻松的部分,因为它并不太依赖晦涩的hack。
4.如何Number
对待输入
Doing
Number(something)(第15.7.1节)将转换
something为数字,仅此而已。它的操作方式有些复杂,尤其是在字符串的情况下,但是如果您感兴趣的话,该操作在第9.3节中定义。
5.游戏 Function.prototype.call
call是15.3.4.4节中
apply定义的的兄弟。它不采用参数数组,而只是采用接收到的参数并将它们向前传递。
当您将多个链接
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; //truelog.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']]// ^ ^
哇哇…让我们备份一下。这里发生了什么?我们可以在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 }); //1arr.map(Number.call, Number); //2第一部分创建了一个由5个未定义项组成的数组。第二个遍历该数组并获取其索引,从而生成一个元素索引数组:
[0, 1, 2, 3, 4]



