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
_を呼び出すとエラーが発生しますが、そうではありませんでした。 thatを証明するには:
_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. を見てみましょう。ここで_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
_マッピングを作成します。
うーん、これは前の部分よりも短くなかったかもしれません。しかし、私たちが終了するとケーキがありますので、我慢してください!ただし、次のセクション(短くなりますが、約束します)の後、式の分析を開始できます。忘れてしまった場合、質問は次のように機能するかどうかでした:
_Array.apply(null, { length: 5 }).map(Number.call, Number);
_
Array
が複数の引数を処理する方法そう! length
引数をArray
に渡すとどうなるかを見ましたが、式ではいくつかのことを引数として渡します(正確には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]
_
多田!いくつかの未定義の値の配列を取得し、これらの未定義の値の配列を返します。
最後に、以下を解読できます。
_Array.apply(null, { length: 5 })
_
キーがすべて存在する5つの未定義値を含む配列を返すことがわかりました。
次に、式の2番目の部分に進みます。
_[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
_
これは、あいまいなハックにそれほど依存していないため、より簡単で複雑な部分になります。
Number
が入力を処理する方法Number(something)
( セクション15.7.1 )を実行すると、something
が数値に変換されます。これがすべてです。それがどのように行われるかは、特に文字列の場合は少し複雑ですが、操作は 9. で定義されています。
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']]
// ^ ^
_
おっおおおおお...ちょっとバックアップしましょう。何が起きてる? 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);
_
2つの部分で機能します。
_var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
_
最初の部分は、5つの未定義アイテムの配列を作成します。 2番目はその配列を調べてそのインデックスを取得し、要素インデックスの配列になります。
_[0, 1, 2, 3, 4]
_
免責事項:これは上記のコードの非常に正式な説明です-これは[〜#〜] i [〜# 〜]それを説明する方法を知っています。より簡単な答えについては、上記の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
最初の行では、 配列コンストラクターは関数として呼び出されます with Function.prototype.apply
。
this
の値はnull
であり、これは配列コンストラクターには関係ありません(this
は15.3.4.3.2.aによるコンテキストと同じthis
です。new Array
が呼び出され、length
プロパティを持つオブジェクトが渡されます。これにより、.apply
:の次の句のために、そのオブジェクトは.apply
にとって重要な配列になります。.apply
は0から.length
に引数を渡します。[[Get]]
で{ length: 5 }
を値0から4で呼び出すとundefined
が生成されるため、配列コンストラクターは値がundefined
の5つの引数で呼び出されますオブジェクトの)。var arr = Array.apply(null, { length: 5 });
は5つの未定義値のリストを作成します。注:Array.apply(0,{length: 5})
とArray(5)
の違いに注意してください。最初の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
次に、2番目の部分に移ります。
Array.prototype.map
は、配列の各要素でコールバック関数(この場合はNumber.call
)を呼び出し、指定されたthis
値を使用します(この場合、this
値を `Number)に設定します。Number.call
)はインデックスで、最初のパラメーターはthis値です。Number
がthis
(配列値)としてundefined
として呼び出され、パラメーターとしてインデックスが呼び出されることを意味します。つまり、各undefined
をその配列インデックスにマッピングすることと基本的に同じです( Number
を呼び出すと、タイプ変換が実行されます。この場合、数値から数値へ、インデックスは変更されません)。したがって、上記のコードは5つの未定義の値を取り、それぞれを配列内のインデックスにマップします。
これが、コードに結果を取得する理由です。
あなたが言ったように、最初の部分:
var arr = Array.apply(null, { length: 5 });
5つのundefined
値の配列を作成します。
2番目の部分は、2つの引数を取り、同じサイズの新しい配列を返す配列のmap
関数を呼び出しています。
map
が取る最初の引数は、実際には配列内の各要素に適用する関数であり、3つの引数を取り、値を返す関数であることが期待されています。例えば:
function foo(a,b,c){
...
return ...
}
関数fooを最初の引数として渡すと、各要素ごとに呼び出されます
map
が取る2番目の引数は、最初の引数として渡す関数に渡されています。しかし、foo
の場合、a、b、cではなく、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]
そしてもう一つはそれをより明確にするために:
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番目の引数は、実際には結果に影響しません。間違っている場合は修正してください。
配列は、単に「長さ」フィールドといくつかのメソッド(プッシュなど)で構成されるオブジェクトです。 var arr = { length: 5}
は基本的に、フィールド0..4が未定義のデフォルト値を持つ配列と同じです(つまり、arr[0] === undefined
はtrueを返します。
2番目の部分については、名前が示すとおり、mapは1つの配列から新しい配列にマッピングします。これは、元の配列を走査し、各アイテムのマッピング関数を呼び出すことにより行われます。
残っているのは、mapping-functionの結果がインデックスであることを納得させることです。トリックは、最初のパラメータが「this」コンテキストに設定され、2番目が最初のパラメータになるという小さな例外を除いて、関数を呼び出す「call」(*)という名前のメソッドを使用することです。偶然にも、マッピング関数が呼び出されると、2番目のパラメーターがインデックスになります。
最後になりますが、呼び出されるメソッドはNumber "Class"であり、JSで知られているように、 "Class"は単なる関数であり、この(Number)は最初のパラメーターが値であることを期待します。
(*)Functionのプロトタイプにあります(およびNumberは関数です)。
マサル