web-dev-qa-db-ja.com

JavaScriptで範囲を作成する-奇妙な構文

Es-discussメーリングリストで次のコードに遭遇しました。

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

これにより

[0, 1, 2, 3, 4]

なぜこれがコードの結果なのですか?ここで何が起こっていますか?

126

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

  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_を呼び出すとエラーが発生しますが、そうではありませんでした。 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番目の魔法のことを見てみましょう。

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_が定義されています。ほとんど気にしないことですが、ここに興味深い部分があります。

  1. 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);
_

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. で定義されています。

5. _Function.prototype.call_のゲーム

callapplyの兄弟で、 セクション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]
_
255
Zirak

免責事項:これは上記のコードの非常に正式な説明です-これは[〜#〜] 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にとって重要な配列になります。
    • Lenを、引数 "length"でargArrayの[[Get]]内部メソッドを呼び出した結果とします。
  • そのため、.applyは0から.lengthに引数を渡します。[[Get]]{ length: 5 }を値0から4で呼び出すとundefinedが生成されるため、配列コンストラクターは値がundefinedの5つの引数で呼び出されますオブジェクトの)。
  • 配列コンストラクター 、2、またはそれ以上の引数で呼び出されます 。新しく構築された配列の長さプロパティは、仕様に応じた引数の数と同じ値の値に設定されます。
  • したがって、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)に設定します。
  • Mapのコールバックの2番目のパラメーター(この場合はNumber.call)はインデックスで、最初のパラメーターはthis値です。
  • これは、Numberthis(配列値)としてundefinedとして呼び出され、パラメーターとしてインデックスが呼び出されることを意味します。つまり、各undefinedをその配列インデックスにマッピングすることと基本的に同じです( Number を呼び出すと、タイプ変換が実行されます。この場合、数値から数値へ、インデックスは変更されません)。

したがって、上記のコードは5つの未定義の値を取り、それぞれを配列内のインデックスにマップします。

これが、コードに結果を取得する理由です。

20

あなたが言ったように、最初の部分:

var arr = Array.apply(null, { length: 5 }); 

5つのundefined値の配列を作成します。

2番目の部分は、2つの引数を取り、同じサイズの新しい配列を返す配列のmap関数を呼び出しています。

mapが取る最初の引数は、実際には配列内の各要素に適用する関数であり、3つの引数を取り、値を返す関数であることが期待されています。例えば:

function foo(a,b,c){
    ...
    return ...
}

関数fooを最初の引数として渡すと、各要素ごとに呼び出されます

  • 現在の反復要素の値としてのa
  • 現在の反復要素のインデックスとしてb
  • 元の配列全体としてのc

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番目の引数は、実際には結果に影響しません。間違っている場合は修正してください。

5
Tal Z

配列は、単に「長さ」フィールドといくつかのメソッド(プッシュなど)で構成されるオブジェクトです。 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は関数です)。

マサル

0
shex