es-discussメーリングリストで次のコードに遭遇しました。
Array.apply(null, { length: 5 }).map(Number.call, Number);
これにより
[0, 1, 2, 3, 4]
なぜこれがコードの結果なのですか?ここで何が起こっているのですか?
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のかなり高度なトピックであるため、これはかなり長くなります。上から始めましょう。シートベルトを締める!
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)
ます。セクション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
これで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]
最後から2番目の例で私の主張を証明するのは簡単です。
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);
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つの未定義の値を含む配列を返すことがわかりました。
次に、式の2番目の部分に移動します。
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
これは、わかりにくいハッキングにあまり依存しないため、より複雑で複雑な部分ではありません。
Number
入力の扱い実行Number(something)
(セクション15.7.1)something
は数値に変換され、それがすべてです。それがどのように行われるかは、特に文字列の場合には少し複雑ですが、興味がある場合のために操作はセクション9.3で定義されています。
Function.prototype.call
call
あるapply
のは、で定義された弟、セクション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']]
// ^ ^
おっと、おっと、おっと...ちょっとバックアップしましょう。何が起きてる?セクション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);
2つの部分で機能します。
var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
最初の部分では、5つの未定義アイテムの配列を作成します。2つ目はその配列を調べ、そのインデックスを取得して、要素インデックスの配列を作成します。
[0, 1, 2, 3, 4]
ahaExclamationMark.apply(null, Array(2)); //2, true
。なぜそれが返されない2
とtrue
、それぞれ?Array(2)
ここに引数を1つだけ渡していないのですか?
apply
引数は1つだけですが、その引数は、関数に渡される2つの引数に「分割」されます。最初のapply
例でそれをもっと簡単に見ることができます。最初の例でconsole.log
は、実際に2つの引数(2つの配列項目)を受け取ったことをconsole.log
示し、2番目の例では、配列key=>value
の最初のスロットにマッピングがあることを示しています(回答の最初の部分で説明)。
log.apply(null, document.getElementsByTagName('script'));
必要がなく、一部のブラウザで[].slice.call(NodeList)
は機能しないことに注意してください。また、NodeListを配列に変換しても、それらでは機能しません。
this
デフォルトWindow
は非厳密モードのみです。
免責事項:これは上記のコードの非常に正式な説明です-これは私がそれを説明する方法を知っている方法です。より簡単な答えについては、上記の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
最初の行では、配列コンストラクタは、関数と呼ばれているとFunction.prototype.apply
。
this
値は、null
配列コンストラクタは(のための重要でないthis
同じであるthis
15.3.4.3.2.a.に係るコンテキストのようnew Array
、length
プロパティ付きのオブジェクトを渡して呼び出されます。.apply
これにより、の次の句のために、そのオブジェクトが重要なすべての配列になります.apply
。
.apply
0から引数を渡して.length
呼び出すので、[[Get]]
上の{ length: 5 }
値で0~4収率undefined
配列コンストラクタはその値である5つの引数で呼び出されるundefined
(オブジェクトの宣言されていないプロパティを取得します)。var arr = Array.apply(null, { length: 5 });
、5つの未定義値のリストを作成します。注:通知の間ここでの違いArray.apply(0,{length: 5})
とArray(5)
、最初の作成5倍プリミティブな値のタイプundefined
、具体的には長さ5の空の配列を作成し、後者のための.map
行動(8.B)S」特異的かつ[[HasProperty]
。
したがって、上記の準拠仕様のコードは次のコードと同じです。
var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
次に、第2部に進みます。
Array.prototype.map
Number.call
配列の各要素でコールバック関数(この場合は)を呼び出し、指定されたthis
値を使用します(この場合はthis
値を `Numberに設定します)。Number.call
)はインデックスで、最初のパラメーターはthis値です。Number
呼び出されます。したがって、基本的にはそれぞれをその配列インデックスにマッピングするのと同じです(この場合、呼び出しは型変換を実行するため、この場合、数値から番号に変更されず、インデックスは変更されません)。this
undefined
undefined
Number
したがって、上記のコードは5つの未定義の値を取り、それぞれを配列内のインデックスにマップします。
これが、結果をコードに取得する理由です。
Array.apply(null,[2])
は、プリミティブ値を2回含む配列ではなく、長さ2の空の配列Array(2)
を作成するようなものです。最初の部分の後のノートで私の最新の編集を参照してください、それが十分に明確であるかどうかを知らせてください、そうでなければ私はそれについて明確にします。undefined
{length: 2}
2つの要素を持つ配列を偽造しArray
ます。存在しない要素にアクセスする実際の配列undefined
は生成されないため、挿入されます。素敵なトリック:)
あなたが言ったように、最初の部分:
var arr = Array.apply(null, { length: 5 });
5つのundefined
値の配列を作成します。
2番目の部分は、map
2つの引数を取り、同じサイズの新しい配列を返す配列の関数を呼び出しています。
map
取る最初の引数は、実際には配列の各要素に適用する関数であり、3つの引数を取り、値を返す関数であることが期待されています。例えば:
function foo(a,b,c){
...
return ...
}
関数fooを最初の引数として渡すと、各要素に対して次のように呼び出されます
map
取る2番目の引数は、最初の引数として渡す関数に渡されます。しかし、それはの場合は、B、Cもないだろうfoo
、それは次のようになりthis
。
2つの例:
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]
もう1つだけ明確にするために:
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つの引数を取り、2番目の引数を数値に解析しようとする関数です(最初の引数で何が行われるかはわかりません)。
map
渡される2番目の引数はインデックスなので、そのインデックスで新しい配列に配置される値はインデックスと等しくなります。baz
上記の例の関数のように。Number.call
インデックスを解析しようとします-当然同じ値を返します。
map
コードで関数に渡した2番目の引数は、実際には結果に影響を与えません。間違っていたら訂正してください。
Number.call
引数を数値に解析する特別な関数はありません。ただ=== Function.prototype.call
です。唯一の二番目の引数として渡される関数this
へ-値はcall
、関連している- .map(eval.call, Number)
、.map(String.call, Number)
および.map(Function.prototype.call, Number)
すべての等価です。
配列は、「長さ」フィールドといくつかのメソッド(プッシュなど)で構成されるオブジェクトです。したがって、arr in var arr = { length: 5}
は基本的に、フィールド0..4が未定義のデフォルト値(つまりarr[0] === undefined
、trueを返す)を持つ配列と同じです。
2番目の部分については、mapは名前が示すように、ある配列から新しい配列にマップします。これは、元の配列をトラバースし、各アイテムのマッピング関数を呼び出すことによって行われます。
あとは、マッピング関数の結果がインデックスであることを納得させるだけです。トリックは、最初のパラメーターが「this」コンテキストに設定され、2番目が最初のパラメーターになるという小さな例外を除いて、関数を呼び出す「call」(*)という名前のメソッドを使用することです(以下同様)。偶然にも、マッピング関数が呼び出されたとき、2番目のパラメーターはインデックスです。
最後に重要なことですが、呼び出されるメソッドは数値「クラス」であり、JSで知っているように、「クラス」は単なる関数であり、これ(数値)は最初のパラメーターが値であることを想定しています。
(*)関数のプロトタイプにあります(およびNumberは関数です)。
マシャル
[undefined, undefined, undefined, …]
し、new Array(n)
または{length: n}
-後者は、まばらな、彼らは何の要素を持っていない、すなわち。これはに非常に関連しているためmap
、オッズArray.apply
が使用されました。
Array.apply(null, Array(30)).map(Number.call, Number)
は、プレーンオブジェクトが配列であるかのように見せかけることを避けるため、読みやすくなっています。