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?
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?
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!
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ła key => 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 nowych key=>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? Masz undefined
.
Teraz możemy trochę unieść głowy i zobaczyć, dlaczego funkcje takie jak arr.map
nie obejmują tych właściwości. Gdyby arr[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 definiujemy len
elementów w tablicy, nie tworzymy key => value
mapowań, po prostu zmieniamy length
właściwość.
Skoro mamy to już na uboczu, spójrzmy na drugą magiczną rzecz:
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ąc arguments
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 celu apply
, ale na pewno istnieje w arguments
zmiennej. Z tego samego powodu działa ostatni przykład: klucze nie istnieją w przekazywanym obiekcie, ale istnieją w arguments
.
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ęść:
- Niech len będzie wynikiem wywołania metody wewnętrznej [[Get]] argArray z argumentem „length”.
Co w zasadzie oznacza: argArray.length
. Następnie specyfikacja wykonuje prostą for
pętlę na length
elementach, tworząc a list
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 z length
właściwością. Teraz możemy zobaczyć, dlaczego wartości są niezdefiniowane, ale klucze nie są włączone arguments
: Tworzymy key=>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);
Array
obsługuje wiele argumentówWięc! Widzieliśmy, co się dzieje, gdy przekazujesz length
argument do Array
, 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.
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.
Number
traktuje dane wejścioweWykonanie Number(something)
( sekcja 15.7.1 ) konwertuje something
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.
Function.prototype.call
call
jest apply
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 wiele thisArg
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}])
.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ślnie window
. 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ę.
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]
ahaExclamationMark.apply(null, Array(2)); //2, true
. Dlaczego wraca 2
i true
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 pierwszych apply
przykładach. Pierwsza console.log
pokazuje, że rzeczywiście otrzymaliśmy dwa argumenty (dwa elementy tablicy), a druga console.log
pokazuje, że tablica ma key=>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 tylko Window
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 sama this
jak w związku według 15.3.4.3.2.a.new Array
nazywane jest przekazaniem obiektu z length
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 plony undefined
konstruktor tablica jest wywoływana z pięciu argumentów, których wartość jest undefined
(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})
i Array(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 przypadku Number.call
) na każdym elemencie tablicy i używa określonej this
wartości (w tym przypadku ustawiając this
wartość na `Number).Number.call
) jest indeks, a pierwszym jest ta wartość.Number
jest wywoływana z this
as undefined
(wartością tablicy) i indeksem jako parametrem. Jest to więc w zasadzie to samo, co mapowanie każdego undefined
z nich na indeks tablicy (ponieważ wywołanie Number
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.
Array.apply(null,[2])
jest podobne do Array(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óre Array
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 przypadku foo
, byłoby this
.
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 funkcja baz
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ę.
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 jako this
-value do call
, 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 (czyli arr[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
[undefined, undefined, undefined, …]
a new Array(n)
lub jest ogromna różnica {length: n}
- te ostatnie są nieliczne , czyli nie mają elementów. Jest to bardzo istotne dla map
i dlatego Array.apply
użyto dziwnego .
Array.apply(null, Array(30)).map(Number.call, Number)
jest łatwiejsze do odczytania, ponieważ unika udawania, że zwykły obiekt jest tablicą.