Na liście mailingowej es-omówienia natknąłem się na następujący kod:
Array.apply(null, { length: 5 }).map(Number.call, Number);
To produkuje
[0, 1, 2, 3, 4]
Dlaczego jest to wynikiem działania kodu? Co tu się dzieje?
javascript
ecmascript-5
Benjamin Gruenbaum
źródło
źródło
Array.apply(null, Array(30)).map(Number.call, Number)
jest łatwiejsze do odczytania, ponieważ unika udawania, że zwykły obiekt jest tablicą.Odpowiedzi:
Zrozumienie tego „hackowania” wymaga zrozumienia kilku rzeczy:
Array(5).map(...)
Function.prototype.apply
obsługuje argumentyArray
obsługuje wiele argumentówNumber
funkcja obsługuje argumentyFunction.prototype.call
robiSą to raczej zaawansowane tematy w javascript, więc będzie to więcej niż raczej długie. Zaczniemy od góry. Brać się do rzeczy!
1. Dlaczego nie tylko
Array(5).map
?Czym tak naprawdę jest tablica? Zwykły obiekt zawierający klucze całkowite, które są mapowane na wartości. Ma inne specjalne cechy, na przykład magiczną
length
zmienną, ale w swej istocie jest to zwykłakey => value
mapa, jak każdy inny obiekt. Pobawmy się trochę tablicami, dobrze?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']
Dochodzimy do wewnętrznej różnicy między liczbą elementów w tablicy
arr.length
, a liczbąkey=>value
odwzorowań, które ma tablica, która może być inna niżarr.length
.Rozszerzanie tablicy przez
arr.length
nie tworzy żadnych nowychkey=>value
mapowań, więc nie jest tak, że tablica ma niezdefiniowane wartości, nie ma tych kluczy . A co się dzieje, gdy próbujesz uzyskać dostęp do nieistniejącej nieruchomości? Maszundefined
.Teraz możemy trochę unieść głowy i zobaczyć, dlaczego funkcje takie jak
arr.map
nie obejmują tych właściwości. Gdybyarr[3]
była po prostu niezdefiniowana, a klucz istniał, wszystkie te funkcje tablicowe po prostu przeszłyby przez to jak każda inna wartość://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"]
Celowo użyłem wywołania metody, aby dalej udowodnić, że samego klucza nigdy nie było: wywołanie
undefined.toUpperCase
spowodowałoby błąd, ale tak się nie stało. Aby udowodnić, że :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
A teraz dochodzimy do mojego punktu: jak
Array(N)
się sprawy mają. Sekcja 15.4.2.2 opisuje ten proces. Jest kilka mumbo jumbo, na których nam nie zależy, ale jeśli uda ci się czytać między wierszami (lub możesz mi po prostu zaufać, ale nie rób tego), sprowadza się to w zasadzie do tego:function Array(len) { var ret = []; ret.length = len; return ret; }
(działa przy założeniu (co jest sprawdzane w rzeczywistej specyfikacji), że
len
jest poprawnym uint32, a nie dowolną liczbą wartości)Teraz możesz zobaczyć, dlaczego działanie
Array(5).map(...)
nie zadziałałoby - nie definiujemylen
elementów w tablicy, nie tworzymykey => value
mapowań, po prostu zmieniamylength
właściwość.Skoro mamy to już na uboczu, spójrzmy na drugą magiczną rzecz:
2. Jak
Function.prototype.apply
działaPo
apply
prostu pobiera tablicę i rozwija ją jako argumenty wywołania funkcji. Oznacza to, że następujące są prawie takie same:function foo (a, b, c) { return a + b + c; } foo(0, 1, 2); //3 foo.apply(null, [0, 1, 2]); //3
Teraz możemy ułatwić proces sprawdzania, jak
apply
działa, po prostu rejestrującarguments
specjalną zmienną: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]
Łatwo jest udowodnić moje roszczenie w przedostatnim przykładzie:
function ahaExclamationMark () { console.log(arguments.length); console.log(arguments.hasOwnProperty(0)); } ahaExclamationMark.apply(null, Array(2)); //2, true
(tak, gra słów przeznaczona).
key => value
Mapowanie nie może istnieć w tablicy mijaliśmy na celuapply
, ale na pewno istnieje warguments
zmiennej. Z tego samego powodu działa ostatni przykład: klucze nie istnieją w przekazywanym obiekcie, ale istnieją warguments
.Dlaczego? Spójrzmy na sekcję 15.3.4.3 , gdzie
Function.prototype.apply
jest zdefiniowana. Przeważnie rzeczy, na których nam nie zależy, ale oto interesująca część:Co w zasadzie oznacza:
argArray.length
. Następnie specyfikacja wykonuje prostąfor
pętlę nalength
elementach, tworząc alist
z odpowiednich wartości (list
jest to jakieś wewnętrzne voodoo, ale w zasadzie jest to tablica). Pod względem bardzo, bardzo luźnego kodu: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); };
Więc wszystko, czego potrzebujemy, aby naśladować obiekt
argArray
w tym przypadku, to obiekt zlength
właściwością. Teraz możemy zobaczyć, dlaczego wartości są niezdefiniowane, ale klucze nie są włączonearguments
: Tworzymykey=>value
odwzorowania.Uff, więc to nie mogło być krótsze niż poprzednia część. Ale kiedy skończymy, będzie ciasto, więc bądź cierpliwy! Jednak po następnej sekcji (która będzie krótka, obiecuję) możemy rozpocząć analizę wyrażenia. Jeśli zapomniałeś, pytanie brzmiało, jak działa to:
Array.apply(null, { length: 5 }).map(Number.call, Number);
3. Jak
Array
obsługuje wiele argumentówWięc! Widzieliśmy, co się dzieje, gdy przekazujesz
length
argument doArray
, ale w wyrażeniu przekazujemy kilka rzeczy jako argumenty (undefined
dokładniej tablica 5 ). Sekcja 15.4.2.1 mówi nam, co robić. Ostatni akapit to wszystko, co się dla nas liczy, i jest sformułowany naprawdę dziwnie, ale sprowadza się do: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! Otrzymujemy tablicę kilku niezdefiniowanych wartości i zwracamy tablicę tych niezdefiniowanych wartości.
Pierwsza część wyrażenia
Na koniec możemy odszyfrować:
Array.apply(null, { length: 5 })
Widzieliśmy, że zwraca tablicę zawierającą 5 niezdefiniowanych wartości, z wszystkimi kluczami.
Teraz przejdźmy do drugiej części wyrażenia:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
Będzie to łatwiejsza, nieskomplikowana część, ponieważ nie polega ona tak bardzo na niejasnych hackach.
4. Jak
Number
traktuje dane wejścioweWykonanie
Number(something)
( sekcja 15.7.1 ) konwertujesomething
na liczbę i to wszystko. Sposób, w jaki to robi, jest nieco zawiły, szczególnie w przypadku łańcuchów, ale operacja jest zdefiniowana w sekcji 9.3 na wypadek, gdybyś był zainteresowany.5. Gry
Function.prototype.call
call
jestapply
bratem, określonym w sekcji 15.3.4.4 . Zamiast pobierać tablicę argumentów, po prostu pobiera otrzymane argumenty i przekazuje je dalej.Sprawy stają się interesujące, gdy łączysz więcej niż jeden
call
razem, podkręć dziwne do 11:function log () { console.log(this, arguments); } log.call.call(log, {a:4}, {a:5}); //{a:4}, [{a:5}] //^---^ ^-----^ // this arguments
To jest całkiem warte, dopóki nie zrozumiesz, co się dzieje.
log.call
jest po prostu funkcją, równoważnącall
metodzie dowolnej innej funkcji i jako taka ma równieżcall
samą metodę:log.call === log.call.call; //true log.call === Function.call; //true
A co robi
call
? Przyjmuje wielethisArg
argumentów i wywołuje swoją funkcję nadrzędną. Możemy to zdefiniować za pomocąapply
(znowu bardzo luźny kod, nie zadziała):Function.prototype.call = function (thisArg) { var args = arguments.slice(1); //I wish that'd work return this.apply(thisArg, args); };
Prześledźmy, jak to się dzieje:
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}])
Późniejsza część lub
.map
wszystkoTo jeszcze nie koniec. Zobaczmy, co się stanie, gdy dostarczysz funkcję do większości metod tablicowych:
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
Jeśli sami nie podamy
this
argumentu, domyślniewindow
. Zwróć uwagę na kolejność, w jakiej argumenty są dostarczane do naszego wywołania zwrotnego, i zróbmy to jeszcze raz aż do 11:arr.forEach(log.call, log); //'a', [0, ['a', 'b', 'c']] //'b', [1, ['a', 'b', 'c']] //'b', [2, ['a', 'b', 'c']] // ^ ^
Whoa whoa whoa ... cofnijmy się trochę. Co tu się dzieje? Jak widać w sekcji 15.4.4.18 , gdzie
forEach
jest zdefiniowane, dzieje się co następuje:var callback = log.call, thisArg = log; for (var i = 0; i < arr.length; i += 1) { callback.call(thisArg, arr[i], i, arr); }
Tak więc otrzymujemy:
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);
Teraz możemy zobaczyć, jak
.map(Number.call, Number)
działa:Number.call.call(Number, arr[i], i, arr); Number.call(arr[i], i, arr); Number(i, arr);
Co zwraca transformację
i
bieżącego indeksu na liczbę.Podsumowując,
Ekspresja
Array.apply(null, { length: 5 }).map(Number.call, Number);
Działa w dwóch częściach:
var arr = Array.apply(null, { length: 5 }); //1 arr.map(Number.call, Number); //2
Pierwsza część tworzy tablicę 5 niezdefiniowanych elementów. Druga przechodzi przez tę tablicę i pobiera jej indeksy, co daje tablicę indeksów elementów:
[0, 1, 2, 3, 4]
źródło
ahaExclamationMark.apply(null, Array(2)); //2, true
. Dlaczego wraca2
itrue
odpowiednio? Czy nie podajesz tu tylko jednego argumentu tj.Array(2)
?apply
, ale ten argument jest „rozdzielany” na dwa argumenty przekazywane do funkcji. Łatwiej to widać na pierwszychapply
przykładach. Pierwszaconsole.log
pokazuje, że rzeczywiście otrzymaliśmy dwa argumenty (dwa elementy tablicy), a drugaconsole.log
pokazuje, że tablica makey=>value
odwzorowanie w pierwszym gnieździe (jak wyjaśniono w pierwszej części odpowiedzi).log.apply(null, document.getElementsByTagName('script'));
nie jest wymagane do działania i nie działa w niektórych przeglądarkach, a także[].slice.call(NodeList)
do przekształcenia NodeList w tablicę nie będzie działać w nich.this
domyślnie tylkoWindow
w trybie nieostrym.Disclaimer : To jest bardzo formalny opis powyższym kodzie - to jak ja wiem, jak to wytłumaczyć. Aby uzyskać prostszą odpowiedź - sprawdź świetną odpowiedź Ziraka powyżej. To jest bardziej szczegółowa specyfikacja na twojej twarzy i mniej "aha".
Dzieje się tu kilka rzeczy. Rozbijmy to trochę.
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
W pierwszym wierszu konstruktor tablicy jest wywoływany jako funkcja z
Function.prototype.apply
.this
Wartośćnull
, która nie ma znaczenia dla konstruktora Array (this
jest taka samathis
jak w związku według 15.3.4.3.2.a.new Array
nazywane jest przekazaniem obiektu zlength
właściwością - co powoduje, że obiekt jest tablicą podobną do wszystkiego, do czego ma to znaczenie, z.apply
powodu następującej klauzuli w.apply
:.apply
przechodzi od 0 do argumentów.length
, ponieważ powołanie[[Get]]
na{ length: 5 }
z wartościami od 0 do 4 plonyundefined
konstruktor tablica jest wywoływana z pięciu argumentów, których wartość jestundefined
(uzyskanie własności nierejestrowanej obiektu).var arr = Array.apply(null, { length: 5 });
tworzy listę pięciu niezdefiniowanych wartości.Uwaga : Zwróć uwagę na różnicę między
Array.apply(0,{length: 5})
iArray(5)
, pierwszy tworzy pięciokrotnie prymitywny typ wartości,undefined
a drugi tworzy pustą tablicę o długości 5. W szczególności ze względu na.map
zachowanie (8.b) i konkretnie[[HasProperty]
.Zatem powyższy kod w zgodnej specyfikacji jest taki sam, jak:
var arr = [undefined, undefined, undefined, undefined, undefined]; arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Teraz przejdźmy do drugiej części.
Array.prototype.map
wywołuje funkcję zwrotną (w tym przypadkuNumber.call
) na każdym elemencie tablicy i używa określonejthis
wartości (w tym przypadku ustawiającthis
wartość na `Number).Number.call
) jest indeks, a pierwszym jest ta wartość.Number
jest wywoływana zthis
asundefined
(wartością tablicy) i indeksem jako parametrem. Jest to więc w zasadzie to samo, co mapowanie każdegoundefined
z nich na indeks tablicy (ponieważ wywołanieNumber
wykonuje konwersję typu, w tym przypadku z numeru na numer bez zmiany indeksu).Zatem powyższy kod przyjmuje pięć niezdefiniowanych wartości i odwzorowuje każdą z nich na jej indeks w tablicy.
Dlatego otrzymujemy wynik do naszego kodu.
źródło
Array.apply(null, { length: 2 })
a nie,Array.apply(null, [2])
które również wywoływałybyArray
konstruktor przekazujący2
jako wartość długości? skrzypceArray.apply(null,[2])
jest podobne doArray(2)
tego, że tworzy pustą tablicę o długości 2, a nie tablicę zawierającą wartość pierwotnąundefined
dwa razy. Zobacz moją ostatnią zmianę w notatce po pierwszej części, daj mi znać, czy jest wystarczająco jasna, a jeśli nie, wyjaśnię to.{length: 2}
udaje tablicę z dwoma elementami, któreArray
konstruktor wstawiłby do nowo utworzonej tablicy. Ponieważ nie ma prawdziwej tablicy uzyskującej dostęp do nieobecnych elementów,undefined
które są następnie wstawiane. Niezła sztuczka :)Jak powiedziałeś, pierwsza część:
var arr = Array.apply(null, { length: 5 });
tworzy tablicę 5
undefined
wartości.Druga część to wywołanie
map
funkcji tablicy, która przyjmuje 2 argumenty i zwraca nową tablicę o tym samym rozmiarze.Pierwszym argumentem, który
map
przyjmuje, jest w rzeczywistości funkcja do zastosowania na każdym elemencie tablicy, oczekuje się, że będzie to funkcja, która przyjmuje 3 argumenty i zwraca wartość. Na przykład:function foo(a,b,c){ ... return ... }
jeśli jako pierwszy argument przekażemy funkcję foo, zostanie ona wywołana dla każdego elementu z
Drugi argument, który
map
przyjmuje, jest przekazywany do funkcji, którą przekazujesz jako pierwszy argument. Ale nie byłoby to a, b ani c w przypadkufoo
, byłobythis
.Dwa przykłady:
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]
i jeszcze jeden, żeby było jaśniej:
function qux(a,b,c){ return a } var newArr4 = arr2.map(qux,9); //newArr4 is equal to [3,4,5]
A co z Number.call?
Number.call
jest funkcją, która przyjmuje 2 argumenty i próbuje przeanalizować drugi argument na liczbę (nie jestem pewien, co robi z pierwszym argumentem).Ponieważ drugim
map
przekazywanym argumentem jest indeks, wartość, która zostanie umieszczona w nowej tablicy pod tym indeksem, jest równa indeksowi. Podobnie jak funkcjabaz
w powyższym przykładzie.Number.call
spróbuje przeanalizować indeks - w naturalny sposób zwróci tę samą wartość.Drugi argument, który przekazałeś
map
funkcji w swoim kodzie, w rzeczywistości nie ma wpływu na wynik. Popraw mnie, jeśli się mylę, proszę.źródło
Number.call
nie jest specjalną funkcją, która analizuje argumenty na liczby. To jest sprawiedliwe=== Function.prototype.call
. Tylko drugi argument, funkcja, która zostanie przekazana jakothis
-value docall
, jest istotne -.map(eval.call, Number)
,.map(String.call, Number)
i.map(Function.prototype.call, Number)
wszystkie są równoważne.Tablica to po prostu obiekt zawierający pole „length” i niektóre metody (np. Push). Zatem arr in
var arr = { length: 5}
jest w zasadzie tym samym, co tablica, w której pola 0..4 mają wartość domyślną, która jest niezdefiniowana (czyliarr[0] === undefined
daje wartość true).Jeśli chodzi o drugą część, map, jak sama nazwa wskazuje, odwzorowuje jedną tablicę na nową. Czyni to przechodząc przez oryginalną tablicę i wywołując funkcję mapowania na każdym elemencie.
Pozostało tylko przekonać cię, że wynikiem funkcji mapowania jest indeks. Sztuczka polega na użyciu metody o nazwie „call” (*), która wywołuje funkcję z małym wyjątkiem, że pierwszy parametr jest ustawiony jako kontekst „ten”, a drugi staje się pierwszym parametrem (i tak dalej). Przypadkowo, gdy wywoływana jest funkcja odwzorowania, drugim parametrem jest indeks.
Wreszcie wywołana metoda to Number "Class", a jak wiemy w JS, "Class" to po prostu funkcja, a ta (Number) oczekuje, że pierwszym parametrem będzie wartość.
(*) znajduje się w prototypie funkcji (a Number to funkcja).
MASHAL
źródło
[undefined, undefined, undefined, …]
anew Array(n)
lub jest ogromna różnica{length: n}
- te ostatnie są nieliczne , czyli nie mają elementów. Jest to bardzo istotne dlamap
i dlategoArray.apply
użyto dziwnego .