[解決済み] JavaScript で範囲を作成する - 奇妙な構文
質問
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の中でもかなり高度なトピックなので、モロに長文になりそうです。トップから始めます。お静かに!
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)
はどのようなことをするのか、ということです。
15.4.2.2項
には、そのプロセスが記述されています。私たちが気にしないような瑣末なことがたくさんありますが、行間を読むことができれば (あるいは、この件に関しては私を信じてくれても構いませんが、そうしないでください)、基本的にはこれに集約されます。
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(という前提(実際の仕様ではチェックされています)で動作します。
len
は有効なuint32であり、単なる値の数ではない、という前提で動作します)
では、なぜ
Array(5).map(...)
がうまくいかない理由がわかりました。
len
を定義しているわけではなく、配列の中に
key => value
のマッピングを作成するのではなく、単に
length
プロパティを変更するだけです。
さて、これで2つ目の魔法のようなものを見てみましょう。
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
が定義されています。ほとんどがどうでもいいことですが、ここからが面白い部分です。
- 引数 "length"で argArray の内部メソッド [[Get]] を呼び出した結果を len とする。
ということは、基本的には
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);
3. どのように
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)
これは、不明瞭なハックにそれほど依存しないので、より簡単で複雑でない部分でしょう。
4. どのように
Number
は入力を処理します。
すること
Number(something)
(
15.7.1項
) に変換する
something
を数値に変換する、ただそれだけです。その方法は、特に文字列の場合、少し複雑ですが、その演算は
セクション 9.3
で定義されています。
5. のゲーム
Function.prototype.call
call
は
apply
の弟で
セクション15.3.4.4で定義されています。
. 引数の配列を取る代わりに、受け取った引数をそのまま前方に渡します。
複数の
call
を連鎖させると面白くなります。
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]
関連
-
[解決済み] JavaScriptで "use strict "は何をするのか、その根拠は?
-
[解決済み] JavaScriptで文字列が部分文字列を含むかどうかを確認する方法は?
-
[解決済み] あるJavaScriptファイルを他のJavaScriptファイルにインクルードするにはどうすればよいですか?
-
[解決済み] なぜGoogleはJSONレスポンスにwhile(1);を前置するのでしょうか?
-
[解決済み] JavaScriptで複数行の文字列を作成する
-
[解決済み] 私のJavaScriptコードは "No 'Access-Control-Allow-Origin' header is present on requested resource "というエラーを受け取りますが、Postmanはそうならないのはなぜですか?
-
[解決済み】JavaScriptの比較では、どちらの等号演算子(== vs ===)を使うべきですか?
-
[解決済み】オブジェクトからプロパティを削除する(JavaScript)
-
[解決済み] ドット記法の文字列を使用してオブジェクトの子プロパティにアクセスする [重複].
-
[解決済み] Bootstrap モーダル:トグル時に背景がトップにジャンプする
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み] JavaScriptで配列の長さを初期化する方法は?
-
[解決済み] JSのDateからDay名
-
[解決済み] jqueryでdivの要素がオーバーフローしていないかチェックする
-
[解決済み] Chart.jsを使ってドーナツチャートの中にテキストを追加するには?
-
[解決済み] event.targetを使用して、要素の親要素をターゲットにすることができますか?
-
[解決済み] Bootstrap モーダル:トグル時に背景がトップにジャンプする
-
[解決済み] ネストしたオブジェクトのプロパティを動的に設定する
-
[解決済み] JavaScriptでクエリ文字列が存在するかどうかを確認するには?
-
[解決済み] Javascriptのsort()はどのように動作するのですか?
-
[解決済み] react-hooksによるステート更新時の非同期コードの実行