W czerwcu 2013 I odpowiedział na pytanie o benefits of prototypal inheritance over classical. Od tego czasu spędziłem dużo czasu zastanawiając się nad dziedziczeniem, zarówno prototypowym, jak i klasycznym, i napisałem obszernie o tym, jak prototype-classisomorphism.
Tak, podstawowym zastosowaniem prototypowego dziedziczenia jest symulacja zajęć. Można go jednak używać znacznie częściej niż po prostu symulować klasy. Na przykład łańcuchy prototypów są bardzo podobne do łańcuchów zasięgu.
Prototype-Scope Izomorfizm także
Prototypy i zasięgi w JavaScripcie mają ze sobą wiele wspólnego. W JavaScript są trzy popularne typy łańcuchów:
Łańcuchy prototypów.
var foo = {};
var bar = Object.create(foo);
var baz = Object.create(bar);
// chain: baz -> bar -> foo -> Object.prototype -> null
Łańcuchy zasięgu.
function foo() {
function bar() {
function baz() {
// chain: baz -> bar -> foo -> global
}
}
}
Łańcuchy metod.
var chain = {
foo: function() {
return this;
},
bar: function() {
return this;
},
baz: function() {
return this;
}
};
chain.foo().bar().baz();
z trzech, łańcuchy prototypowe i łańcuchy zakres są najbardziej zbliżone. W rzeczywistości można dołączyć łańcuch prototypów do łańcucha zasięgu za pomocą instrukcji notoriouswith
.
function foo() {
var bar = {};
var baz = Object.create(bar);
with (baz) {
// chain: baz -> bar -> Object.prototype -> foo -> global
}
}
Więc jaki jest pożytek z izomorfizmu w zakresie prototypu? Jednym z bezpośrednich zastosowań jest modelowanie łańcuchów zasięgu za pomocą łańcuchów prototypów. Dokładnie to zrobiłem dla mojego własnego języka programowania Bianca, który zaimplementowałem w JavaScript.
raz pierwszy zdefiniował globalny zakres Bianca, zapełnianie go z wieloma przydatnymi funkcjami matematycznymi w pliku o wdzięcznej nazwie global.js następująco:
var global = module.exports = Object.create(null);
global.abs = new Native(Math.abs);
global.acos = new Native(Math.acos);
global.asin = new Native(Math.asin);
global.atan = new Native(Math.atan);
global.ceil = new Native(Math.ceil);
global.cos = new Native(Math.cos);
global.exp = new Native(Math.exp);
global.floor = new Native(Math.floor);
global.log = new Native(Math.log);
global.max = new Native(Math.max);
global.min = new Native(Math.min);
global.pow = new Native(Math.pow);
global.round = new Native(Math.round);
global.sin = new Native(Math.sin);
global.sqrt = new Native(Math.sqrt);
global.tan = new Native(Math.tan);
global.max.rest = { type: "number" };
global.min.rest = { type: "number" };
global.sizeof = {
result: { type: "number" },
type: "function",
funct: sizeof,
params: [{
type: "array",
dimensions: []
}]
};
function Native(funct) {
this.funct = funct;
this.type = "function";
var length = funct.length;
var params = this.params = [];
this.result = { type: "number" };
while (length--) params.push({ type: "number" });
}
function sizeof(array) {
return array.length;
}
pamiętać, że stworzył globalny zakres korzystania Object.create(null)
. Zrobiłem to, ponieważ zasięg globalny nie ma żadnego zakresu nadrzędnego.
Następnie dla każdego programu stworzyłem osobny zakres programu, który zawiera definicje najwyższego poziomu programu. Kod jest przechowywany w pliku o nazwie analyzer.js, który jest zbyt duży, aby zmieścić się w jednej odpowiedzi.Oto pierwsze trzy wiersze pliku:
var parse = require("./ast");
var global = require("./global");
var program = Object.create(global);
Jak widać, zakres globalny jest rodzajem zakresu programu. W związku z tym program
dziedziczy po global
, dzięki czemu wyszukiwanie zmiennej zakresu jest tak proste, jak wyszukiwanie właściwości obiektu. To sprawia, że środowisko wykonawcze języka jest znacznie prostsze.
Zakres programu zawiera definicje najwyższego poziomu programu. Na przykład, rozważmy następujący program mnożenia macierzy, które są zapisane w pliku matrix.bianca:
col(a[3][3], b[3][3], i, j)
if (j >= 3) a
a[i][j] += b[i][j]
col(a, b, i, j + 1)
row(a[3][3], b[3][3], i)
if (i >= 3) a
a = col(a, b, i, 0)
row(a, b, i + 1)
add(a[3][3], b[3][3])
row(a, b, 0)
Definicje najwyższego poziomu col
, row
i add
. Każda z tych funkcji ma swój własny zakres funkcji, który dziedziczy po zasięgu programu. Kod dla które można znaleźć na line 67 of analyzer.js:
scope = Object.create(program);
Na przykład, zakres funkcji add
ma definicje macierzy a
i b
.
W związku z tym prototypy obok klas są również przydatne do modelowania zakresów funkcji.
Prototypy dla typów danych modelowanie algebraicznych
Klasy nie są jedynym dostępnym rodzajem abstrakcji. W językach programowania funkcjonalnego dane są modelowane przy użyciu algebraic data types.
Najlepszym przykładem algebraicznych typu danych jest to, że z listy:
data List a = Nil | Cons a (List a)
Ta definicja danych oznacza po prostu, że wykaz a może być albo pustą listę (tj Nil
) albo wartości wpisz “ a ” wstawiony do listy a (tzn. Cons a (List a)
). Na przykład, następujące są wszystkie listy:
Nil :: List a
Cons 1 Nil :: List Number
Cons 1 (Cons 2 Nil) :: List Number
Cons 1 (Cons 2 (Cons 3 Nil)) :: List Number
Typem zmiennej a
w definicji danych umożliwia parametric polymorphism (tj pozwala listę do przechowywania wszelkiego rodzaju wartości). Na przykład Nil
może być wyspecjalizowany do listy liczb lub listy booleans, ponieważ ma ona typ List a
, gdzie a
może być dowolna.
To pozwala nam tworzyć funkcje parametryczne jak length
:
length :: List a -> Number
length Nil = 0
length (Cons _ l) = 1 + length l
Funkcja length
mogłyby zostać użyte do znalezienia długości dowolnej listy, niezależnie od rodzaju wartości w niej zawartych, ponieważ funkcja length
prostu nie robi dbaj o wartości na liście.
Oprócz polimorfizmu parametrycznego większość funkcjonalnych języków programowania ma również pewną postać ad-hoc polymorphism. W przypadku polimorfizmu ad-hoc wybrana jest jedna konkretna implementacja funkcji w zależności od rodzaju zmiennej polimorficznej.
Na przykład operator +
w kodzie JavaScript jest używany zarówno do dodawania, jak i łączenia ciągów w zależności od typu argumentu.Jest to forma polimorfizmu ad-hoc.
Podobnie w funkcjonalnych językach programowania funkcja map
jest zwykle przeciążana. Na przykład możesz mieć inną implementację map
dla list, inną implementację dla zestawów itp. Klasy typów są jednym ze sposobów implementacji polimorfizmu ad-hoc. Na przykład, klasa Functor
typ zapewnia map
funkcję:
class Functor f where
map :: (a -> b) -> f a -> f b
Następnie tworzymy konkretne przypadki Functor
dla różnych typów danych:
instance Functor List where
map :: (a -> b) -> List a -> List b
map _ Nil = Nil
map f (Cons a l) = Cons (f a) (map f l)
Prototypy w JavaScript pozwalają modelować zarówno algebraiczne typy danych i polimorfizm ad-hoc. Na przykład, powyższy kod może być tłumaczone jeden do jednego do JavaScript w następujący sposób:
var list = Cons(1, Cons(2, Cons(3, Nil)));
alert("length: " + length(list));
function square(n) {
return n * n;
}
var result = list.map(square);
alert(JSON.stringify(result, null, 4));
<script>
// data List a = Nil | Cons a (List a)
function List(constructor) {
Object.defineProperty(this, "constructor", {
value: constructor || this
});
}
var Nil = new List;
function Cons(head, tail) {
var cons = new List(Cons);
cons.head = head;
cons.tail = tail;
return cons;
}
// parametric polymorphism
function length(a) {
switch (a.constructor) {
case Nil: return 0;
case Cons: return 1 + length(a.tail);
}
}
// ad-hoc polymorphism
List.prototype.map = function (f) {
switch (this.constructor) {
case Nil: return Nil;
case Cons: return Cons(f(this.head), this.tail.map(f));
}
};
</script>
Chociaż klasy mogą być wykorzystane do modelowania polimorfizm ad-hoc, jak również wszystkie przeciążone funkcje trzeba zdefiniować w jednym miejscu. Dzięki prototypom możesz je zdefiniować w dowolnym miejscu.
Wniosek
Jak widać, prototypy są bardzo uniwersalne. Tak, są używane głównie do modelowania klas. Można je jednak wykorzystać do wielu innych rzeczy.
Niektóre z innych rzeczy, które prototypy mogą być wykorzystywane do:
Tworzenie persistent data structures z podziału strukturalnego.
Podstawową ideą podziału strukturalnego jest to, że zamiast modyfikowania obiektu, należy utworzyć nowy obiekt, który dziedziczy z oryginalnego obiektu i dokonywać żadnych modyfikacji ty chcieć. Dziedzictwo prototypowe jest w tym doskonałe.
Jak wspominali inni, prototypy są dynamiczne. W związku z tym można z mocą wsteczną dodawać nowe prototypowe metody i będą one automatycznie dostępne we wszystkich instancjach prototypu.
Mam nadzieję, że to pomoże.
powiązane, jeśli nie jest duplikatem: [Czy jakaś biblioteka JavaScript używa dynamicznych aspektów prototypowego systemu?] (Http://stackoverflow.com/q/10609822/1048572) – Bergi
Zgaduję, że znacznie łatwiej jest dynamicznie dodawać metody i korzystając z prototypów, możesz bardzo łatwo rozszerzyć swoją "klasę". – theonlygusti
Warto nadmienić, że klasyczny system klasy może również trywialnie naśladować dziedziczenie prototypowe. –