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


129

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

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

これにより

[0, 1, 2, 3, 4]

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


2
IMO Array.apply(null, Array(30)).map(Number.call, Number)は、プレーンオブジェクトが配列であるかのように見せかけることを避けるため、読みやすくなっています。
fncomp 2013

10
@fncomp 実際に範囲を作成するためにどちらも使用しないでください。単純なアプローチよりも遅いだけでなく、理解するのも簡単ではありません。ここで構文を理解することは困難です(まあ、実際にはAPIであり、構文ではありません)。これは興味深い質問ですが、ひどい製品コードIMOです。
Benjamin Gruenbaum 2013

はい、だれかがそれを使用することを示唆していませんが、オブジェクトリテラルバージョンに比べて、読みやすいと思っていました。
fncomp 2013

1
なぜ誰もがこれをしたいと思うのかはわかりません。この方法でアレイを作成するのにかかる時間は、ややセクシーではありませんが、はるかに高速な方法で実行できます。jsperf.com
Eric Hodonsky 2013年

回答:


263

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

  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

これでapplyarguments特別な変数をログに記録するだけで、動作を確認するプロセスを簡単にすることができます。

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.3Function.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

ふew、これは前の部分より短くなかったかもしれません。しかし、終了するとケーキができるので、しばらくお待ちください。ただし、次のセクション(短くなるとお約束します)の後で、式の分析を開始できます。忘れてしまった場合、問題は次のように機能することでした。

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

3. 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)

これは、わかりにくいハッキングにあまり依存しないため、より複雑で複雑な部分ではありません。

4. Number入力の扱い

実行Number(something)セクション15.7.1somethingは数値に変換され、それがすべてです。それがどのように行われるかは、特に文字列の場合には少し複雑ですが、興味がある場合のために操作はセクション9.3で定義されています。

5.のゲーム 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]

@Zirak次のことを理解するのを手伝ってくださいahaExclamationMark.apply(null, Array(2)); //2, true。なぜそれが返されない2true、それぞれ?Array(2)ここに引数を1つだけ渡していないのですか?
オタク2013

4
@Geekに渡すapply引数は1つだけですが、その引数は、関数に渡される2つの引数に「分割」されます。最初のapply例でそれをもっと簡単に見ることができます。最初の例でconsole.logは、実際に2つの引数(2つの配列項目)を受け取ったことをconsole.log示し、2番目の例では、配列key=>valueの最初のスロットにマッピングがあることを示しています(回答の最初の部分で説明)。
Zirak 2013

4
ために(いくつかの)要求:、あなたは今音声バージョンを楽しむことができますdl.dropboxusercontent.com/u/24522528/SO-answer.mp3
Zirak

1
ホストオブジェクトであるNodeListをネイティブメソッドに渡すのは機能するlog.apply(null, document.getElementsByTagName('script'));必要がなく、一部のブラウザで[].slice.call(NodeList)は機能しないことに注意してください。また、NodeListを配列に変換しても、それらでは機能しません。
RobG 2013年

2
1つの修正:thisデフォルトWindowは非厳密モードのみです。
ComFreek、2015年

21

免責事項:これは上記のコードの非常に正式な説明です-これはがそれを説明する方法知っている方法です。より簡単な答えについては、上記の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同じであるthis15.3.4.3.2.a.に係るコンテキストのよう
  • 次にnew Arraylengthプロパティ付きのオブジェクトを渡して呼び出されます。.applyこれにより、の次の句のために、そのオブジェクトが重要なすべての配列になります.apply
    • lenを、引数「length」を指定してargArrayの[[Get]]内部メソッドを呼び出した結果とします。
  • このように、.apply0から引数を渡して.length呼び出すので、[[Get]]上の{ length: 5 }値で0~4収率undefined配列コンストラクタはその値である5つの引数で呼び出されるundefined(オブジェクトの宣言されていないプロパティを取得します)。
  • 配列コンストラクターは、0、2、またはそれ以上の引数で呼び出されます。新しく構築された配列の長さプロパティは、仕様に従って引数の数に設定され、値は同じ値に設定されます。
  • したがって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.mapNumber.call配列の各要素でコールバック関数(この場合は)を呼び出し、指定されたthis値を使用します(この場合はthis値を `Numberに設定します)。
  • マップのコールバックの2番目のパラメーター(この場合はNumber.call)はインデックスで、最初のパラメーターはthis値です。
  • つまり、as (配列値)とパラメーターとしてインデックスを使用してNumber呼び出されます。したがって、基本的にはそれぞれをその配列インデックスにマッピングするのと同じです(この場合、呼び出しは型変換を実行するため、この場合、数値から番号に変更されず、インデックスは変更されません)。thisundefinedundefinedNumber

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

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


1
ドキュメントの場合:マップの動作に関する仕様:es5.github.io/#x15.4.4.19、Mozillaには、その仕様に従って機能するサンプルスクリプトがdeveloper.mozilla.org/en-US/docs/Web/JavaScript/にあります。参照/…
Patrick Evans

1
しかし、なぜそれだけで機能し、長さの値として渡されるコンストラクタも呼び出されArray.apply(null, { length: 2 })ないのですか?フィドルArray.apply(null, [2])Array2
Andreas

@Andreas Array.apply(null,[2])は、プリミティブ値を2回含む配列ではなく、長さ2の空の配列Array(2)を作成するようなものです。最初の部分の後のノートで私の最新の編集を参照してください、それが十分に明確であるかどうかを知らせてください、そうでなければ私はそれについて明確にします。undefined
Benjamin Gruenbaum 2013

最初の実行でどのように機能するか理解していません... 2回目の読み取りの後、それは理にかなっています。コンストラクタが新しく作成した配列に挿入する{length: 2}2つの要素を持つ配列を偽造しArrayます。存在しない要素にアクセスする実際の配列undefinedは生成されないため、挿入されます。素敵なトリック:)
アンドレアス

5

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

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

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

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

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

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

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

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

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番目の引数は、実際には結果に影響を与えません。間違っていたら訂正してください。


1
Number.call引数を数値に解析する特別な関数はありません。ただ=== Function.prototype.callです。唯一の二番目の引数として渡される関数thisへ-値はcall、関連している- .map(eval.call, Number).map(String.call, Number)および.map(Function.prototype.call, Number)すべての等価です。
ベルギ2013

0

配列は、「長さ」フィールドといくつかのメソッド(プッシュなど)で構成されるオブジェクトです。したがって、arr in var arr = { length: 5}は基本的に、フィールド0..4が未定義のデフォルト値(つまりarr[0] === undefined、trueを返す)を持つ配列と同じです。
2番目の部分については、mapは名前が示すように、ある配列から新しい配列にマップします。これは、元の配列をトラバースし、各アイテムのマッピング関数を呼び出すことによって行われます。

あとは、マッピング関数の結果がインデックスであることを納得させるだけです。トリックは、最初のパラメーターが「this」コンテキストに設定され、2番目が最初のパラメーターになるという小さな例外を除いて、関数を呼び出す「call」(*)という名前のメソッドを使用することです(以下同様)。偶然にも、マッピング関数が呼び出されたとき、2番目のパラメーターはインデックスです。

最後に重要なことですが、呼び出されるメソッドは数値「クラス」であり、JSで知っているように、「クラス」は単なる関数であり、これ(数値)は最初のパラメーターが値であることを想定しています。

(*)関数のプロトタイプにあります(およびNumberは関数です)。

マシャル


1
間に大きな差があります[undefined, undefined, undefined, …]し、new Array(n)または{length: n}-後者は、まばらな、彼らは何の要素を持っていない、すなわち。これはに非常に関連しているためmap、オッズArray.applyが使用されました。
ベルギ2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.