Ich habe den folgenden Code in der Mailingliste von es-execute gefunden:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Dies erzeugt
[0, 1, 2, 3, 4]
Warum ist das das Ergebnis des Codes? Was passiert hier?
Ich habe den folgenden Code in der Mailingliste von es-execute gefunden:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Dies erzeugt
[0, 1, 2, 3, 4]
Warum ist das das Ergebnis des Codes? Was passiert hier?
Antworten:
Um diesen "Hack" zu verstehen, müssen Sie verschiedene Dinge verstehen:
Array(5).map(...)
Function.prototype.apply
mit Argumenten um?Array
mit mehreren Argumenten um?Number
Funktion mit Argumenten umgehtFunction.prototype.call
machtEs handelt sich um ziemlich fortgeschrittene Themen in Javascript, daher wird dies mehr als lang sein. Wir werden von oben beginnen. Anschnallen!
Array(5).map
?Was ist eigentlich ein Array? Ein reguläres Objekt, das Ganzzahlschlüssel enthält, die Werten zugeordnet sind. Es hat andere Besonderheiten, zum Beispiel die magische length
Variable, aber im Kern ist es eine reguläre key => value
Karte, genau wie jedes andere Objekt. Lass uns ein bisschen mit Arrays spielen, sollen wir?
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']
Wir kommen zu dem inhärenten Unterschied zwischen der Anzahl der Elemente im Array arr.length
und der Anzahl der key=>value
Zuordnungen im Array, die sich von unterscheiden können arr.length
.
Die Erweiterung des Arrays über arr.length
keine keine neuen erstellen key=>value
Zuordnungen, so ist es nicht , dass das Array undefinierte Werte hat, es verfügt nicht über diese Tasten . Und was passiert, wenn Sie versuchen, auf eine nicht vorhandene Eigenschaft zuzugreifen? Du verstehst undefined
.
Jetzt können wir unsere Köpfe ein wenig heben und sehen, warum Funktionen wie arr.map
diese Eigenschaften nicht überschreiten. Wenn arr[3]
nur undefiniert wäre und der Schlüssel vorhanden wäre, würden alle diese Array-Funktionen wie jeder andere Wert darüber hinweggehen:
//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"]
Ich habe absichtlich einen Methodenaufruf verwendet, um weiter zu beweisen, dass der Schlüssel selbst nie vorhanden war: Das Aufrufen undefined.toUpperCase
hätte einen Fehler ausgelöst, aber dies war nicht der Fall. Um zu beweisen , dass :
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
Und jetzt kommen wir zu meinem Punkt: Wie Array(N)
geht es den Dingen? Abschnitt 15.4.2.2 beschreibt den Prozess. Es gibt eine Menge Hokuspokus, die uns egal sind, aber wenn Sie es schaffen, zwischen den Zeilen zu lesen (oder Sie können mir in dieser Zeile einfach vertrauen, aber nicht), läuft es im Grunde darauf hinaus:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(arbeitet unter der Annahme (die in der tatsächlichen Spezifikation überprüft wird), dass len
es sich um eine gültige uint32 handelt und nicht um eine beliebige Anzahl von Werten)
Jetzt können Sie sehen, warum dies Array(5).map(...)
nicht funktioniert - wir definieren keine len
Elemente im Array, wir erstellen keine key => value
Zuordnungen, wir ändern einfach die length
Eigenschaft.
Nachdem wir das aus dem Weg geräumt haben, schauen wir uns die zweite magische Sache an:
Function.prototype.apply
funktioniertIm apply
Grunde genommen wird ein Array genommen und als Argument eines Funktionsaufrufs abgewickelt. Das bedeutet, dass Folgendes ziemlich gleich ist:
function foo (a, b, c) {
return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3
Jetzt können wir den Prozess der Funktionsweise apply
vereinfachen, indem wir einfach die arguments
spezielle Variable protokollieren :
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]
Es ist einfach, meine Behauptung im vorletzten Beispiel zu beweisen:
function ahaExclamationMark () {
console.log(arguments.length);
console.log(arguments.hasOwnProperty(0));
}
ahaExclamationMark.apply(null, Array(2)); //2, true
(Ja, Wortspiel beabsichtigt). Das key => value
Mapping war möglicherweise nicht in dem Array vorhanden, an das wir übergeben haben apply
, aber es ist sicherlich in der arguments
Variablen vorhanden. Es ist der gleiche Grund, warum das letzte Beispiel funktioniert: Die Schlüssel sind für das Objekt, das wir übergeben, nicht vorhanden, aber sie sind in vorhanden arguments
.
Warum ist das so? Schauen wir uns Abschnitt 15.3.4.3 an , in dem Function.prototype.apply
definiert ist. Meistens Dinge, die uns egal sind, aber hier ist der interessante Teil:
- Sei len das Ergebnis des Aufrufs der internen Methode [[Get]] von argArray mit dem Argument "length".
Was im Grunde bedeutet : argArray.length
. Die Spezifikation führt dann eine einfache for
Schleife über length
Elemente durch und erstellt einen list
der entsprechenden Werte ( list
ist ein interner Voodoo, aber im Grunde ist es ein Array). In Bezug auf sehr, sehr losen Code:
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
In diesem Fall müssen wir also nur ein Objekt mit einer length
Eigenschaft nachahmen . Und jetzt können wir sehen, warum die Werte undefiniert sind, die Schlüssel jedoch nicht. arguments
Wir erstellen die key=>value
Zuordnungen.
Puh, das war vielleicht nicht kürzer als der vorherige Teil. Aber wenn wir fertig sind, wird es Kuchen geben, also sei geduldig! Nach dem folgenden Abschnitt (der, wie ich verspreche, kurz sein wird) können wir jedoch beginnen, den Ausdruck zu zerlegen. Falls Sie es vergessen haben, war die Frage, wie Folgendes funktioniert:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Array
mit mehreren Argumenten um?So! Wir haben gesehen, was passiert, wenn Sie ein length
Argument an übergeben Array
, aber im Ausdruck übergeben wir mehrere Dinge als Argumente (ein Array von 5 undefined
, um genau zu sein). In Abschnitt 15.4.2.1 erfahren Sie , was zu tun ist. Der letzte Absatz ist alles, was uns wichtig ist, und er ist wirklich seltsam formuliert , aber es läuft irgendwie auf Folgendes hinaus:
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]
Tada! Wir erhalten ein Array mit mehreren undefinierten Werten und geben ein Array dieser undefinierten Werte zurück.
Schließlich können wir Folgendes entschlüsseln:
Array.apply(null, { length: 5 })
Wir haben gesehen, dass es ein Array zurückgibt, das 5 undefinierte Werte enthält, wobei alle Schlüssel vorhanden sind.
Nun zum zweiten Teil des Ausdrucks:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
Dies wird der einfachere, nicht verschlungene Teil sein, da er nicht so sehr auf obskuren Hacks beruht.
Number
behandelt man Eingaben?Doing Number(something)
( Abschnitt 15.7.1 ) konvertiert something
in eine Zahl, und das ist alles. Wie das geht, ist etwas kompliziert, besonders bei Strings, aber die Operation wird in Abschnitt 9.3 definiert, falls Sie interessiert sind.
Function.prototype.call
call
ist sein apply
Bruder, definiert in Abschnitt 15.3.4.4 . Anstatt ein Array von Argumenten zu verwenden, werden nur die empfangenen Argumente verwendet und weitergeleitet.
Die Dinge werden interessant, wenn Sie mehr als eine call
aneinander ketten und das Unheimliche auf 11 drehen:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
Dies ist ziemlich wertvoll, bis Sie verstehen, was los ist. log.call
ist nur eine Funktion, die der call
Methode einer anderen Funktion entspricht , und hat als solche auch eine call
Methode für sich:
log.call === log.call.call; //true
log.call === Function.call; //true
Und was macht call
das? Es akzeptiert eine thisArg
Reihe von Argumenten und ruft seine übergeordnete Funktion auf. Wir können es definieren über apply
(wieder sehr lockerer Code, funktioniert nicht):
Function.prototype.call = function (thisArg) {
var args = arguments.slice(1); //I wish that'd work
return this.apply(thisArg, args);
};
Lassen Sie uns verfolgen, wie dies abläuft:
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
von allemEs ist noch nicht vorbei. Mal sehen, was passiert, wenn Sie den meisten Array-Methoden eine Funktion bereitstellen:
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
Wenn wir selbst kein this
Argument angeben, wird standardmäßig verwendet window
. Beachten Sie die Reihenfolge, in der die Argumente für unseren Rückruf bereitgestellt werden, und lassen Sie es uns noch einmal bis 11 verrückt machen:
arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^ ^
Whoa whoa whoa ... lass uns ein bisschen zurücktreten. Was ist denn hier los? Wir können in Abschnitt 15.4.4.18 sehen , wo forEach
Folgendes definiert ist:
var callback = log.call,
thisArg = log;
for (var i = 0; i < arr.length; i += 1) {
callback.call(thisArg, arr[i], i, arr);
}
Also, wir bekommen das:
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);
Jetzt können wir sehen, wie es .map(Number.call, Number)
funktioniert:
Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);
Dies gibt die Umwandlung des i
aktuellen Index in eine Zahl zurück.
Der Ausdruck
Array.apply(null, { length: 5 }).map(Number.call, Number);
Arbeitet in zwei Teilen:
var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
Der erste Teil erstellt ein Array von 5 undefinierten Elementen. Der zweite geht über dieses Array und nimmt seine Indizes, was zu einem Array von Elementindizes führt:
[0, 1, 2, 3, 4]
ahaExclamationMark.apply(null, Array(2)); //2, true
. Warum zurückgeben 2
und true
jeweils? Übergeben Sie nicht nur ein Argument, dh Array(2)
hier?
apply
, aber dieses Argument wird in zwei Argumente "aufgeteilt", die an die Funktion übergeben werden. Das sehen Sie leichter an den ersten apply
Beispielen. Das erste console.log
zeigt dann, dass wir tatsächlich zwei Argumente erhalten haben (die beiden Array-Elemente), und das zweite console.log
zeigt, dass das Array eine key=>value
Zuordnung im 1. Slot hat (wie im 1. Teil der Antwort erläutert).
log.apply(null, document.getElementsByTagName('script'));
nicht erforderlich ist, um in einigen Browsern zu funktionieren, und dass das [].slice.call(NodeList)
Verwandeln einer NodeList in ein Array auch in diesen nicht funktioniert.
this
Standardmäßig nur Window
im nicht strengen Modus.
Haftungsausschluss : Dies ist eine sehr formale Beschreibung des obigen Codes - so kann ich ihn erklären. Für eine einfachere Antwort - überprüfen Sie Ziraks großartige Antwort oben. Dies ist eine detailliertere Spezifikation in Ihrem Gesicht und weniger "Aha".
Hier passieren mehrere Dinge. Lassen Sie es uns ein bisschen aufbrechen.
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
In der ersten Zeile wird der Array-Konstruktor als Funktion mit aufgerufenFunction.prototype.apply
.
this
Wert null
spielt für den Array-Konstruktor keine Rolle ( this
ist der gleiche this
wie im Kontext gemäß 15.3.4.3.2.a.new Array
wird das Übergeben eines Objekts mit einer length
Eigenschaft aufgerufen - das bewirkt, dass dieses Objekt ein Array ist, wie es für alles wichtig ist, .apply
aufgrund der folgenden Klausel in .apply
:
.apply
Argumente von 0 an.length
, seit dem Aufruf [[Get]]
auf{ length: 5 }
mit den Werten 0 bis 4 Ausbeuten undefined
des Array - Konstruktor ist mit fünf Argumenten , dessen Wert aufgerufen ist undefined
(eine nicht deklarierte Eigenschaft eines Objekts erhalten).var arr = Array.apply(null, { length: 5 });
wird eine Liste mit fünf undefinierten Werten erstellt.Hinweis : Beachten Sie hier den Unterschied zwischen Array.apply(0,{length: 5})
und Array(5)
, wobei der erste das Fünffache des primitiven Werttyps erstellt und der zweite undefined
ein leeres Array der Länge 5 erstellt. Insbesondere wegen.map
Werttyps erstellt Verhaltens von (8.b) und der und speziell [[HasProperty]
.
Der obige Code in einer kompatiblen Spezifikation ist also der gleiche wie:
var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Nun zum zweiten Teil.
Array.prototype.map
Ruft die Rückruffunktion (in diesem Fall Number.call
) für jedes Element des Arrays auf und verwendet den angegebenen this
Wert (in diesem Fall wird diethis
Wert auf `Number gesetzt).Number.call
) ist der Index, und der erste ist dieser Wert.Number
mit this
as undefined
(dem Array-Wert) und dem Index als Parameter aufgerufen wird. Es ist also im Grunde dasselbe wie die Zuordnung jedes undefined
Arrays zu seinem Array-Index (da der Aufruf Number
eine Typkonvertierung durchführt, in diesem Fall von Nummer zu Nummer, ohne den Index zu ändern).Daher nimmt der obige Code die fünf undefinierten Werte und ordnet sie jeweils ihrem Index im Array zu.
Deshalb erhalten wir das Ergebnis zu unserem Code.
Array.apply(null,[2])
ist so, Array(2)
dass ein leeres Array der Länge 2 erstellt wird und kein Array, das den primitiven Wert undefined
zweimal enthält. Sehen Sie sich meine letzte Bearbeitung in der Notiz nach dem ersten Teil an. Lassen Sie mich wissen, ob es klar genug ist, und wenn nicht, werde ich das klarstellen.
{length: 2}
fälscht ein Array mit zwei Elementen, die der Array
Konstruktor in das neu erstellte Array einfügen würde. Da es kein echtes Array gibt, das auf die nicht vorhandenen Elemente zugreift, ergibt sich, undefined
was dann eingefügt wird. Schöner Trick :)
Wie Sie sagten, der erste Teil:
var arr = Array.apply(null, { length: 5 });
Erstellt ein Array mit 5 undefined
Werten.
Der zweite Teil ruft die map
Funktion des Arrays auf, die zwei Argumente akzeptiert und ein neues Array derselben Größe zurückgibt.
Das erste Argument, das map
benötigt wird, ist eine Funktion, die auf jedes Element im Array angewendet werden soll. Es wird erwartet, dass es sich um eine Funktion handelt, die drei Argumente akzeptiert und einen Wert zurückgibt. Beispielsweise:
function foo(a,b,c){
...
return ...
}
Wenn wir die Funktion foo als erstes Argument übergeben, wird sie für jedes Element mit aufgerufen
Das zweite Argument map
wird an die Funktion übergeben, die Sie als erstes Argument übergeben. Aber es wäre weder a, b noch c, wenn foo
es so wäre this
.
Zwei Beispiele:
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]
und noch eine, um es klarer zu machen:
function qux(a,b,c){
return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]
Was ist also mit Number.call?
Number.call
ist eine Funktion, die 2 Argumente akzeptiert und versucht, das zweite Argument auf eine Zahl zu analysieren (ich bin nicht sicher, was es mit dem ersten Argument macht).
Da das zweite übergebene Argument map
der Index ist, entspricht der Wert, der an diesem Index in das neue Array eingefügt wird, dem Index. Genau wie die Funktion baz
im obigen Beispiel.Number.call
wird versuchen, den Index zu analysieren - es wird natürlich den gleichen Wert zurückgeben.
Das zweite Argument, das Sie map
in Ihrem Code an die Funktion übergeben haben, wirkt sich nicht auf das Ergebnis aus. Korrigieren Sie mich bitte, wenn ich falsch liege.
Number.call
ist keine spezielle Funktion, die Argumente nach Zahlen analysiert. Es ist einfach === Function.prototype.call
. Nur das zweite Argument, die Funktion , die als übergeben wird , this
um -Wertes call
, ist relevant - .map(eval.call, Number)
, .map(String.call, Number)
und .map(Function.prototype.call, Number)
alle sind gleichwertig.
Ein Array ist einfach ein Objekt, das das Feld 'Länge' und einige Methoden (z. B. Push) umfasst. Arr in var arr = { length: 5}
ist also im Grunde dasselbe wie ein Array, in dem die Felder 0..4 den undefinierten Standardwert haben (dh arr[0] === undefined
true ergeben).
Wie der Name schon sagt, wird im zweiten Teil von einem Array ein neues zugeordnet. Dies geschieht durch Durchlaufen des ursprünglichen Arrays und Aufrufen der Zuordnungsfunktion für jedes Element.
Sie müssen Sie nur noch davon überzeugen, dass das Ergebnis der Mapping-Funktion der Index ist. Der Trick besteht darin, die Methode 'call' (*) zu verwenden, die eine Funktion aufruft, mit der kleinen Ausnahme, dass der erste Parameter als 'this'-Kontext festgelegt ist und der zweite zum ersten Parameter wird (und so weiter). Zufälligerweise ist der zweite Parameter der Index, wenn die Zuordnungsfunktion aufgerufen wird.
Last but not least wird als Methode die Zahl "Klasse" aufgerufen, und wie wir in JS wissen, ist eine "Klasse" einfach eine Funktion, und diese (Zahl) erwartet, dass der erste Parameter der Wert ist.
(*) im Prototyp von Function gefunden (und Number ist eine Funktion).
MASHAL
[undefined, undefined, undefined, …]
und new Array(n)
oder {length: n}
- die letzteren sind spärlich , dh sie haben keine Elemente. Dies ist sehr relevant für map
, und deshalb wurde die ungerade Array.apply
verwendet.
Array.apply(null, Array(30)).map(Number.call, Number)
ist einfacher zu lesen, da nicht vorgetäuscht wird, ein einfaches Objekt sei ein Array.