Nie można rozwiązać tajemnicy funkcji w JavaScript

16

Staram się zrozumieć za kulisami JavaScript i trochę utknąłem w zrozumieniu tworzenia wbudowanych obiektów, zwłaszcza Obiektów i Funkcji oraz relacji między nimi.

Kiedy przeczytałem, że wszystkie wbudowane obiekty, takie jak Array, String itp., Są rozszerzeniem (dziedziczonym) z Object, założyłem, że Object jest pierwszym wbudowanym obiektem, który zostaje utworzony, a reszta obiektów dziedziczy po nim. Ale nie ma sensu, gdy się dowiesz, że Obiekty mogą być tworzone tylko przez funkcje, ale wtedy funkcje są również niczym innym jak obiektami Funkcji. Zaczęło to brzmieć jak dylemat kur i kurczaków.

Inną niezwykle mylącą rzeczą jest to, że jeśli console.log(Function.prototype)drukuje funkcję, ale podczas drukowania console.log(Object.prototype)drukuje obiekt. Dlaczego Function.prototypefunkcja, skoro miała być obiektem?

Ponadto, zgodnie z dokumentacją Mozilli, każdy javascript functionjest rozszerzeniem Functionobiektu, ale kiedy console.log(Function.prototype.constructor)znów jest funkcją. Teraz jak możesz użyć czegoś, aby stworzyć to samemu (Mind = blown).

Ostatnią rzeczą Function.prototypejest funkcja, ale mogę uzyskać dostęp do constructorfunkcji za pomocą Function.prototype.constructorczy to znaczy, że Function.prototypefunkcja zwraca prototypeobiekt

Umair Abid
źródło
ponieważ funkcja jest przedmiotem, oznacza to, że Function.prototypemoże być funkcją i mieć wewnętrzne pola. Więc nie, nie wykonujesz funkcji prototypowej podczas przeglądania jej struktury. Na koniec pamiętaj, że istnieje silnik interpretujący Javascript, więc obiekt i funkcja są prawdopodobnie tworzone w silniku, a nie z Javascript i specjalnych odnośników, takich jak Function.prototypei Object.prototypemogą być po prostu interpretowane w specjalny sposób przez silnik.
Walfrat,
1
Jeśli nie chcesz wdrożyć kompilatora JavaScript zgodnego ze standardami, naprawdę nie musisz się tym martwić. Jeśli chcesz zrobić coś pożytecznego, zejdziesz z kursu.
Jared Smith
5
Do waszej informacji używa się w języku angielskim wyrażenia „dylemat kury i kurczaka” to „problem z kurczakiem i jajkiem”, a mianowicie „co było pierwsze, kurczak czy jajko?”. (Oczywiście, że odpowiedź brzmi: jajko. Zwierzęta jajowate istniały przez miliony lat przed kurczętami.)
Eric Lippert

Odpowiedzi:

32

Staram się zrozumieć za kulisami JavaScript i trochę utknąłem w zrozumieniu tworzenia wbudowanych obiektów, zwłaszcza Obiektów i Funkcji oraz relacji między nimi.

Jest to skomplikowane, łatwo jest go źle zrozumieć, a wiele początkujących książek o JavaScript robi to źle, więc nie ufaj wszystkim, co czytasz.

Byłem jednym z wdrażających silnik JS Microsoftu w latach 90. i w komitecie normalizacyjnym i popełniłem wiele błędów, łącząc tę ​​odpowiedź. (Chociaż odkąd nie pracowałem nad tym od ponad 15 lat, być może wybaczę mi.) To trudne zadanie. Ale kiedy zrozumiesz dziedziczenie prototypów, wszystko będzie miało sens.

Kiedy przeczytałem, że wszystkie wbudowane obiekty, takie jak Array, String itp., Są rozszerzeniem (dziedziczonym) z Object, założyłem, że Object jest pierwszym wbudowanym obiektem, który zostaje utworzony, a reszta obiektów dziedziczy po nim.

Zacznij od wyrzucenia wszystkiego, co wiesz o dziedziczeniu klasowym. JS używa dziedziczenia opartego na prototypach.

Następnie upewnij się, że masz bardzo jasną definicję tego, co oznacza „dziedziczenie”. Ludzie przyzwyczajeni do języków OO, takich jak C #, Java lub C ++, uważają, że dziedziczenie oznacza podtyp, ale dziedziczenie nie oznacza podtypu. Dziedziczenie oznacza, że ​​członkowie jednej rzeczy są również członkami innej rzeczy . Nie musi to koniecznie oznaczać, że między tymi rzeczami istnieje poddziałanie! Tak wiele nieporozumień w teorii typów wynika z faktu, że ludzie nie zdają sobie sprawy z różnicy.

Ale nie ma sensu, gdy się dowiesz, że Obiekty mogą być tworzone tylko przez funkcje, ale wtedy funkcje są również niczym innym jak obiektami Funkcji.

To jest po prostu nieprawda. Niektóre obiekty nie są tworzone przez wywołanie new Fjakiejś funkcji F. Niektóre obiekty są tworzone przez środowisko wykonawcze JS zupełnie z niczego. Są jajka, których nie złożył żaden kurczak . Zostały one utworzone przez środowisko uruchomieniowe podczas jego uruchamiania.

Powiedzmy, jakie są reguły i może to pomoże.

  • Każda instancja obiektu ma obiekt prototypowy.
  • W niektórych przypadkach może to być prototyp null.
  • Jeśli uzyskujesz dostęp do elementu w instancji obiektu, a obiekt nie ma tego elementu, wówczas obiekt odkłada się na swój prototyp lub zatrzymuje się, jeśli prototyp jest zerowy.
  • Element prototypeobiektu zazwyczaj nie jest prototypem obiektu.
  • Przeciwnie, prototypeczłonek obiektu funkcji F jest obiekt, który będzie prototyp obiektu utworzonego przez new F().
  • W niektórych implementacjach instancje uzyskują __proto__członka, który naprawdę daje swój prototyp. (To jest już przestarzałe. Nie polegaj na tym.)
  • Obiekty funkcyjne otrzymują zupełnie nowy domyślny obiekt, do prototypektórego są przypisywane podczas ich tworzenia.
  • Prototyp obiektu funkcyjnego jest oczywiście Function.prototype.

Podsumujmy.

  • Prototypem ObjectjestFunction.prototype
  • Object.prototype jest obiektem prototypowym obiektu.
  • Prototypem Object.prototypejestnull
  • Prototypem Functionjest Function.prototype- to jedna z rzadkich sytuacji, w których Function.prototypetak naprawdę jest prototypem Function!
  • Function.prototype jest obiektem prototypowym funkcji.
  • Prototypem Function.prototypejestObject.prototype

Załóżmy, że tworzymy funkcję Foo.

  • Prototypem Foojest Function.prototype.
  • Foo.prototype jest prototypowym obiektem Foo.
  • Prototypem Foo.prototypejest Object.prototype.

Załóżmy, że mówimy new Foo()

  • Prototyp nowego obiektu to Foo.prototype

Upewnij się, że to ma sens. Narysujmy to. Owale są instancjami obiektowymi. Krawędzie __proto__oznaczają albo „prototyp”, albo prototypeprototypewłaściwość”.

wprowadź opis zdjęcia tutaj

Środowisko wykonawcze musi tylko utworzyć wszystkie te obiekty i odpowiednio przypisać ich różne właściwości. Jestem pewien, że widzisz, jak by to było zrobione.

Teraz spójrzmy na przykład, który sprawdza twoją wiedzę.

function Car(){ }
var honda = new Car();
print(honda instanceof Car);
print(honda.constructor == Car);

Co to drukuje?

Co to instanceofznaczy? honda instanceof Caroznacza „jest Car.prototyperówny dowolnemu obiektowi w hondałańcuchu prototypów?”

Tak to jest. hondaPrototyp jest Car.prototype, więc skończone. To drukuje się prawda.

A co z drugim?

honda.constructornie istnieje, więc konsultujemy prototyp Car.prototype. Kiedy Car.prototypeobiekt został utworzony, automatycznie otrzymał właściwość constructorrówną Car, więc to prawda.

A co z tym?

var Animal = new Object();
function Reptile(){ }
Reptile.prototype = Animal;
var lizard = new Reptile();
print(lizard instanceof Reptile);
print(lizard.constructor == Reptile);

Co drukuje ten program?

Znów lizard instanceof Reptileoznacza „jest Reptile.prototyperówny dowolnemu obiektowi w lizardłańcuchu prototypów?”

Tak to jest. lizardPrototyp jest Reptile.prototype, więc skończone. To drukuje się prawda.

A teraz co

print(lizard.constructor == Reptile);

Możesz pomyśleć, że to również drukuje się prawdą, ponieważ lizardzostał skonstruowany z, new Reptileale byłbyś w błędzie. Rozumuj to.

  • Czy lizardma constructornieruchomość? Nie. Dlatego patrzymy na prototyp.
  • Prototypem lizardjest Reptile.prototype, który jest Animal.
  • Czy Animalma constructornieruchomość? Nie. Więc patrzymy na jego prototyp.
  • Prototyp Animaljest Object.prototypei Object.prototype.constructorjest tworzony przez środowisko wykonawcze i równy Object.
  • Więc to drukuje fałsz.

Reptile.prototype.constructor = Reptile;W pewnym momencie powinniśmy byli to powiedzieć , ale nie pamiętaliśmy!

Upewnij się, że wszystko ma dla ciebie sens. Narysuj kilka pól i strzałek, jeśli nadal są mylące.

Inną niezwykle mylącą rzeczą jest to, że jeśli console.log(Function.prototype)drukuje funkcję, ale podczas drukowania console.log(Object.prototype)drukuje obiekt. Dlaczego Function.prototypefunkcja, skoro miała być obiektem?

Prototyp funkcji jest zdefiniowany jako funkcja, która po wywołaniu zwraca undefined. Wiemy już, że Function.prototypeto Functionprototyp, co dziwne. Dlatego Function.prototype()jest to legalne, a kiedy to zrobisz, undefinedwrócisz. To jest funkcja.

ObjectPrototyp nie posiada tę właściwość; to nie jest na żądanie. To tylko przedmiot.

kiedy console.log(Function.prototype.constructor)znowu jest to funkcja.

Function.prototype.constructorjest po prostu Function, oczywiście. I Functionjest funkcją.

Teraz jak możesz użyć czegoś, aby stworzyć to samemu (Mind = blown).

Nadmiernie o tym myślisz . Wszystko, co jest wymagane, to aby środowisko uruchomieniowe tworzyło kilka obiektów podczas uruchamiania. Obiekty to tylko tabele wyszukiwania, które łączą ciągi z obiektami. Gdy środowisko wykonawcze uruchamia się, wszystko to musi zrobić, to utworzyć kilka obiektów tuzin puste, a następnie rozpocząć przypisywanie prototype, __proto__, constructor, i tak dalej właściwości każdego obiektu, dopóki oni wykres że trzeba zrobić.

Pomocne będzie, jeśli weźmiesz ten schemat, który ci dałem powyżej, i dodasz constructordo niego krawędzie. Szybko zobaczysz, że jest to bardzo prosty wykres obiektowy i że środowisko wykonawcze nie będzie miało problemu z jego utworzeniem.

Dobrym ćwiczeniem byłoby zrobienie tego samemu. Tutaj zacznę. Użyjemy my__proto__w znaczeniu „obiekt prototypowy” i myprototype„właściwości prototypowej”.

var myobjectprototype = new Object();
var myfunctionprototype = new Object();
myfunctionprototype.my__proto__ = myobjectprototype;
var myobject = new Object();
myobject.myprototype = myobjectprototype;

I tak dalej. Czy możesz wypełnić resztę programu, aby zbudować zestaw obiektów o tej samej topologii, co „prawdziwe” wbudowane obiekty JavaScript? Jeśli to zrobisz, przekonasz się, że jest to niezwykle łatwe.

Obiekty w JavaScript to po prostu tabele odnośników, które łączą ciągi z innymi obiektami . Otóż ​​to! Tu nie ma magii. Wiązujesz się w węzły, ponieważ wyobrażasz sobie ograniczenia, które tak naprawdę nie istnieją, tak że każdy obiekt musiał zostać stworzony przez konstruktora.

Funkcje to tylko obiekty, które mają dodatkową możliwość: być wywoływanym. Więc przejrzyj swój mały program symulacyjny i dodaj .mycallablewłaściwość do każdego obiektu, który wskazuje, czy można go wywołać, czy nie. To takie proste.

Eric Lippert
źródło
9
Wreszcie, krótkie, zwięzłe, łatwe do zrozumienia wyjaśnienie JavaScript! Doskonały! Jak ktokolwiek z nas może być zdezorientowany? :) Chociaż z całą powagą, ostatni kawałek o obiektach będących tabelami odnośników jest naprawdę kluczem. Istnieje metoda na szaleństwo --- ale to wciąż szaleństwo ...
Greg Burghardt
4
@GregBurghardt: Zgadzam się, że na początku wygląda na skomplikowany, ale złożoność jest konsekwencją prostych zasad. Każdy obiekt ma __proto__. __proto__Prototypu obiektu jest zerowy. __proto__O new X()to X.prototype. Wszystkie obiekty funkcji mają prototyp funkcji, z __proto__wyjątkiem samego prototypu funkcji. Objecta Functionprototyp funkcji są funkcjami. Wszystkie te reguły są proste i określają topologię wykresu obiektów początkowych.
Eric Lippert,
6

Masz już wiele doskonałych odpowiedzi, ale chcę tylko krótko i jasno odpowiedzieć na twoją odpowiedź na temat tego, jak to wszystko działa, a ta odpowiedź to:

MAGIA!!!

Naprawdę, to wszystko.

Ludzie, którzy wdrażają silniki wykonanie ECMAScript muszą wdrożyć zasady ECMAScript, ale nie przestrzegania przez nich terminie ich realizacji.

Specyfikacja ECMAScript mówi, że A dziedziczy po B, ale B jest instancją A? Nie ma problemu! Najpierw utwórz A z prototypowym wskaźnikiem NULL, utwórz B jako instancję A, a następnie napraw prototypowy wskaźnik A, aby następnie wskazywał na B. Bułka z masłem.

Mówisz, ale czekaj, nie ma sposobu, aby zmienić wskaźnik prototypu w ECMAScript! Ale o to chodzi: ten kod nie działa w silniku ECMAScript, ten kod to silnik ECMAScript. To ma mieć dostęp do wewnętrznych obiektów, które kod ECMAScript działa na silniku nie ma. W skrócie: może robić, co chce.

Nawiasem mówiąc, jeśli naprawdę chcesz, musisz to zrobić tylko raz: później możesz na przykład zrzucić pamięć wewnętrzną i załadować ten zrzut przy każdym uruchomieniu silnika ECMAScript.

Zauważ, że wszystko to nadal obowiązuje, nawet jeśli sam silnik ECMAScript został napisany w ECMAScript (tak jak na przykład w przypadku Narcyza Mozilli). Nawet wtedy kod ECMAScript, który implementuje silnik, nadal ma pełny dostęp do silnika, który implementuje , chociaż oczywiście nie ma dostępu do silnika, na którym działa .

Jörg W Mittag
źródło
3

Z specyfikacji ECMA 1

ECMAScript nie zawiera odpowiednich klas, takich jak klasy C ++, Smalltalk lub Java, ale obsługuje konstruktory, które tworzą obiekty, wykonując kod, który przydziela pamięć dla obiektów i inicjuje całość lub część z nich, przypisując wartości początkowe do ich właściwości. Wszystkie funkcje, w tym konstruktory, są obiektami, ale nie wszystkie obiekty są konstruktorami.

Nie rozumiem, jak to może być bardziej jasne !!! </sarcasm>

Dalej widzimy:

Prototyp Prototyp to obiekt służący do implementacji dziedziczenia struktury, stanu i zachowania w ECMAScript. Kiedy konstruktor tworzy obiekt, obiekt ten domyślnie odwołuje się do powiązanego z nim prototypu konstruktora w celu rozstrzygania odwołań do właściwości. Powiązany prototyp konstruktora może być przywoływany przez wyrażenie programu konstruktor.prototyp, a właściwości dodane do prototypu obiektu są współdzielone, poprzez dziedziczenie, przez wszystkie obiekty współdzielące prototyp.

Widzimy więc, że prototyp jest obiektem, ale niekoniecznie obiektem funkcji.

Mamy też ten interesujący titbit

http://www.ecma-international.org/ecma-262/8.0/index.html#sec-object-objects

Konstruktor obiektu to wewnętrzny obiekt% Object% i początkowa wartość właściwości Object obiektu globalnego.

i

Konstruktor funkcji to wewnętrzny obiekt% funkcji% i początkowa wartość właściwości funkcji obiektu globalnego.

Ewan
źródło
Teraz to robi. ECMA6 umożliwia tworzenie klas i tworzenie z nich obiektów.
ncmathsadist
2
@ncmathsadist Klasy ES6 to tylko cukier składniowy, semantyka jest taka sama.
Hamza Fatmi
1
W sarcasmprzeciwnym razie ten pseudonim jest naprawdę nieprzejrzysty dla początkującego.
Robert Harvey
prawda, źle dodam później, trzeba trochę kopać
Ewan
1
erm? aby zaznaczyć, że nie jest to jasne z dokumentu
Ewan
1

Następujące typy obejmują każdą wartość w JavaScript:

  • boolean
  • number
  • undefined(która obejmuje pojedynczą wartość undefined)
  • string
  • symbol (abstrakcyjne unikalne „rzeczy”, które są porównywane przez odniesienie)
  • object

Każdy obiekt (tj. Wszystko) w JavaScript ma prototyp, który jest rodzajem obiektu.

Prototyp zawiera funkcje, które są również rodzajem obiektu 1 .

Obiekty mają również konstruktor, który jest funkcją, a zatem rodzajem obiektu.

zagnieżdżone

Wszystko jest rekurencyjne, ale implementacja może to zrobić automatycznie, ponieważ w przeciwieństwie do kodu JavaScript, może tworzyć obiekty bez konieczności wywoływania funkcji JavaScript (ponieważ obiekty są tylko pamięcią kontrolowaną przez implementację).

Większość systemów obiektowych w wielu językach o typie dynamicznym jest okrągłych 2 w ten sposób. Na przykład w Pythonie klasy są obiektami, a klasa klas jest type, dlatego typeteż jest instancją samego siebie.

Najlepiej jest po prostu korzystać z narzędzi udostępnianych przez język i nie zastanawiać się nad tym, jak się tam dostali.

1 Funkcje są raczej szczególne, ponieważ można je wywoływać i są to jedyne wartości, które mogą zawierać nieprzezroczyste dane (ich ciało i być może zamknięcie).

2 To właściwie bardziej torturowana, rozgałęziona wstążka wygięta do tyłu nad sobą, ale „okrągły” jest wystarczająco blisko.

Challenger5
źródło