1. ホーム
  2. javascript

[解決済み] JavaScript で範囲を作成する - 奇妙な構文

2022-07-08 22:52:03

質問

es-discussメーリングリストにて、以下のようなコードに遭遇しました。

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

これは

[0, 1, 2, 3, 4]

なぜこのようなコード結果になっているのでしょうか?ここで何が起こっているのでしょうか?

どのように解決するのですか?

このハックを理解するには、いくつかの事柄を理解する必要があります。

  1. なぜ私たちは Array(5).map(...)
  2. なぜ Function.prototype.apply は引数を処理する
  3. どのように Array は複数の引数を処理する
  4. どのように Number 関数が引数を処理する方法
  5. 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 が定義されています。ほとんどがどうでもいいことですが、ここからが面白い部分です。

  1. 引数 "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

callapply の弟で セクション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]