Dobry przykład dziedziczenia opartego na prototypach JavaScript

89

Programuję w językach OOP od ponad 10 lat, ale teraz uczę się JavaScript i pierwszy raz zetknąłem się z dziedziczeniem opartym na prototypach. Uczę się najszybciej, ucząc się dobrego kodu. Jaki jest dobrze napisany przykład aplikacji (lub biblioteki) JavaScript, która prawidłowo wykorzystuje dziedziczenie prototypowe? Czy możesz opisać (krótko), jak / gdzie używane jest dziedziczenie prototypowe, abym wiedział, od czego zacząć czytać?

Alex Reisner
źródło
1
Czy miałeś okazję sprawdzić tę bibliotekę podstawową? Jest naprawdę ładny i dość mały. Jeśli Ci się spodoba, rozważ zaznaczenie mojej odpowiedzi jako odpowiedzi. TIA, roland.
Roland Bouman,
Myślę, że jestem na tej samej łodzi co ty. Chcę też dowiedzieć się trochę o tym prototypowym języku, nie ograniczając się tylko do frameworków oop lub podobnych, nawet jeśli są świetne i wszystko, czego musimy się nauczyć, prawda? Nie tylko niektóre frameworki robią to dla mnie, nawet jeśli mam zamiar go używać. Ale naucz się tworzyć nowe rzeczy w nowych językach na nowe sposoby, myśl nieszablonowo. Lubię Twój styl. Spróbuję mi pomóc i być może ci. Jak tylko coś znajdę, dam ci znać.
marcelo-ferraz

Odpowiedzi:

48

Douglas Crockford ma fajną stronę poświęconą dziedziczeniu prototypów JavaScript :

Pięć lat temu napisałem Classical Inheritance w JavaScript. Okazało się, że JavaScript jest językiem prototypowym bez klas i ma wystarczającą moc ekspresji, aby symulować klasyczny system. Mój styl programowania ewoluował od tamtego czasu, tak jak powinien każdy dobry programista. Nauczyłem się w pełni przyjmować prototypalizm i wyzwoliłem się z ograniczeń modelu klasycznego.

Prace Dean Edward's Base.js , Mootools's Class lub John Resig's Simple Inheritance to sposoby na klasyczne dziedziczenie w JavaScript.

Gregory Pakosz
źródło
Dlaczego nie po prostu, newObj = Object.create(oldObj);jeśli chcesz, aby był wolny od zajęć? W przeciwnym razie zamień oldObjna obiekt prototypowy funkcji konstruktora, która powinna działać?
Cyker
76

Jak już wspomniano, filmy Douglasa Crockforda dobrze wyjaśniają, dlaczego i jak. Ale żeby umieścić to w kilku wierszach JavaScript:

// Declaring our Animal object
var Animal = function () {

    this.name = 'unknown';

    this.getName = function () {
        return this.name;
    }

    return this;
};

// Declaring our Dog object
var Dog = function () {

    // A private variable here        
    var private = 42;

    // overriding the name
    this.name = "Bello";

    // Implementing ".bark()"
    this.bark = function () {
        return 'MEOW';
    }  

    return this;
};


// Dog extends animal
Dog.prototype = new Animal();

// -- Done declaring --

// Creating an instance of Dog.
var dog = new Dog();

// Proving our case
console.log(
    "Is dog an instance of Dog? ", dog instanceof Dog, "\n",
    "Is dog an instance of Animal? ", dog instanceof Animal, "\n",
    dog.bark() +"\n", // Should be: "MEOW"
    dog.getName() +"\n", // Should be: "Bello"
    dog.private +"\n" // Should be: 'undefined'
);

Problem z tym podejściem polega jednak na tym, że będzie on odtwarzał obiekt za każdym razem, gdy go utworzysz. Innym podejściem jest zadeklarowanie obiektów na stosie prototypów, na przykład:

// Defining test one, prototypal
var testOne = function () {};
testOne.prototype = (function () {
    var me = {}, privateVariable = 42;
    me.someMethod = function () {
        return privateVariable;
    };

    me.publicVariable = "foo bar";
    me.anotherMethod = function () {
        return this.publicVariable;
    };

    return me;

}());


// Defining test two, function
var testTwo = ​function() {
    var me = {}, privateVariable = 42;
    me.someMethod = function () {
        return privateVariable;
    };

    me.publicVariable = "foo bar";
    me.anotherMethod = function () {
        return this.publicVariable;
    };

    return me;
};


// Proving that both techniques are functionally identical
var resultTestOne = new testOne(),
    resultTestTwo = new testTwo();

console.log(
    resultTestOne.someMethod(), // Should print 42
    resultTestOne.publicVariable // Should print "foo bar"
);

console.log(
    resultTestTwo.someMethod(), // Should print 42
    resultTestTwo.publicVariable // Should print "foo bar"
);



// Performance benchmark start
var stop, start, loopCount = 1000000;

// Running testOne
start = (new Date()).getTime(); 
for (var i = loopCount; i>0; i--) {
    new testOne();
}
stop = (new Date()).getTime();

console.log('Test one took: '+ Math.round(((stop/1000) - (start/1000))*1000) +' milliseconds');



// Running testTwo
start = (new Date()).getTime(); 
for (var i = loopCount; i>0; i--) {
    new testTwo();
}
stop = (new Date()).getTime();

console.log('Test two took: '+ Math.round(((stop/1000) - (start/1000))*1000) +' milliseconds');

Jeśli chodzi o introspekcję, jest pewna wada. Jeden test zrzutowy da mniej przydatne informacje. Również własność prywatna „privateVariable” w „testOne” jest współdzielona we wszystkich instancjach, co jest pomocne, o czym wspomina shesek w odpowiedziach.

Dynom
źródło
3
Zauważ, że w testOne privateVariablejest po prostu zmienną w zakresie IIFE i jest ona wspólna dla wszystkich instancji, więc nie powinieneś przechowywać na niej danych specyficznych dla instancji. (w testTwo jest to specyficzne dla instancji, ponieważ każde wywołanie testTwo () tworzy nowy zakres dla instancji)
shesek
Głosowałem za, ponieważ pokazałeś inne podejście i dlaczego go nie użyć, ponieważ robi kopie
Murphy316
Problem z odtwarzaniem obiektu za każdym razem wynika głównie z metod odtwarzanych dla każdego nowego obiektu. Możemy jednak złagodzić ten problem, definiując metodę na Dog.prototype. Więc zamiast używać this.bark = function () {...}, możemy zrobić Dot.prototype.bark = function () {...}poza Dogfunkcją. (Zobacz więcej szczegółów w tej odpowiedzi )
C Huang,
26
function Shape(x, y) {
    this.x = x;
    this.y = y;
}

// 1. Explicitly call base (Shape) constructor from subclass (Circle) constructor passing this as the explicit receiver
function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r = r;
}

// 2. Use Object.create to construct the subclass prototype object to avoid calling the base constructor
Circle.prototype = Object.create(Shape.prototype);
Vlad Bezden
źródło
3
Być może dodanie tego linku do odpowiedzi może uzupełnić obraz jeszcze bardziej: developer.mozilla.org/en/docs/Web/JavaScript/Reference/ ...
Dynom
14

Zajrzałbym do YUI i do Basebiblioteki Deana Edwarda : http://dean.edwards.name/weblog/2006/03/base/

W przypadku YUI możesz rzucić okiem na moduł lang , zwł. YAHOO.lang.extend metodą. Następnie możesz przejrzeć źródła niektórych widżetów lub narzędzi i zobaczyć, jak używają tej metody.

Roland Bouman
źródło
YUI 2 zostało wycofane w 2011 r., Więc link do langjest częściowo uszkodzony. Czy ktoś chce to naprawić dla YUI 3?
potwierdzenie
lang w yui 3 nie wydaje się mieć metody rozszerzania. ale ponieważ odpowiedzią jest użycie implementacji jako przykładu, wersja nie ma znaczenia.
eMBee
5

Oto najbardziej przejrzysty przykład, jaki znalazłem, z książki Mixu o Node ( http://book.mixu.net/node/ch6.html ):

Przedkładam kompozycję nad dziedziczenie:

Kompozycja - funkcjonalność obiektu składa się z agregacji różnych klas, zawierających instancje innych obiektów. Dziedziczenie - Funkcjonalność obiektu składa się z jego własnej funkcjonalności oraz funkcjonalności z jego klas nadrzędnych. Jeśli musisz mieć dziedziczenie, użyj zwykłego starego JS

Jeśli musisz zaimplementować dziedziczenie, przynajmniej unikaj używania jeszcze innej niestandardowej implementacji / funkcji magicznej. Oto, jak możesz zaimplementować rozsądną kopię dziedziczenia w czystym ES3 (o ile przestrzegasz zasady nigdy nie definiowania właściwości prototypów):

function Animal(name) {
  this.name = name;
};
Animal.prototype.move = function(meters) {
  console.log(this.name+" moved "+meters+"m.");
};

function Snake() {
  Animal.apply(this, Array.prototype.slice.call(arguments));
};
Snake.prototype = new Animal();
Snake.prototype.move = function() {
  console.log("Slithering...");
  Animal.prototype.move.call(this, 5);
};

var sam = new Snake("Sammy the Python");
sam.move();

To nie to samo, co klasyczne dziedziczenie - ale jest to standardowy, zrozumiały Javascript i ma funkcjonalność, której ludzie najczęściej szukają: konstruktory z możliwością łączenia w łańcuch i możliwość wywoływania metod z nadklasy.

supershnee
źródło
4

ES6 classiextends

ES6 classi extendssą po prostu cukrem składniowym dla wcześniej możliwej manipulacji łańcuchem prototypów, a więc prawdopodobnie najbardziej kanoniczną konfiguracją.

Najpierw dowiedz się więcej o łańcuchu prototypów i .wyszukiwaniu właściwości pod adresem : https://stackoverflow.com/a/23877420/895245

Teraz zdekonstruujmy, co się dzieje:

class C {
    constructor(i) {
        this.i = i
    }
    inc() {
        return this.i + 1
    }
}

class D extends C {
    constructor(i) {
        super(i)
    }
    inc2() {
        return this.i + 2
    }
}
// Inheritance syntax works as expected.
(new C(1)).inc() === 2
(new D(1)).inc() === 2
(new D(1)).inc2() === 3
// "Classes" are just function objects.
C.constructor === Function
C.__proto__ === Function.prototype
D.constructor === Function
// D is a function "indirectly" through the chain.
D.__proto__ === C
D.__proto__.__proto__ === Function.prototype
// "extends" sets up the prototype chain so that base class
// lookups will work as expected
var d = new D(1)
d.__proto__ === D.prototype
D.prototype.__proto__ === C.prototype
// This is what `d.inc` actually does.
d.__proto__.__proto__.inc === C.prototype.inc
// Class variables
// No ES6 syntax sugar apparently:
// /programming/22528967/es6-class-variable-alternatives
C.c = 1
C.c === 1
// Because `D.__proto__ === C`.
D.c === 1
// Nothing makes this work.
d.c === undefined

Uproszczony diagram bez wszystkich predefiniowanych obiektów:

      __proto__
(C)<---------------(D)         (d)
| |                |           |
| |                |           |
| |prototype       |prototype  |__proto__
| |                |           |
| |                |           |
| |                | +---------+
| |                | |
| |                | |
| |                v v
|__proto__        (D.prototype)
| |                |
| |                |
| |                |__proto__
| |                |
| |                |
| | +--------------+
| | |
| | |
| v v
| (C.prototype)--->(inc)
|
v
Function.prototype
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
źródło
1

Najlepsze przykłady, jakie widziałem, znajdują się w JavaScript Douglasa Crockforda : The Good Parts . Zdecydowanie warto kupić, aby uzyskać wyważony pogląd na język.

Douglas Crockford jest odpowiedzialny za format JSON i pracuje w Yahoo jako guru JavaScript.

Chris S.
źródło
7
odpowiedzialny? to brzmi prawie jak „winny” :)
Roland Bouman
@Roland Myślę, że JSON jest całkiem niezłym formatem do przechowywania danych. Jednak na pewno tego nie wymyślił, format był dostępny dla ustawień konfiguracyjnych na Steamie w 2002 roku
Chris S
Chris S, ja też tak myślę - Coraz częściej żałuję, że wszyscy nie pominęliśmy XML jako formatu wymiany i od razu przenieśliśmy się na JSON.
Roland Bouman
3
Niewiele do wymyślenia: JSON jest podzbiorem własnej składni obiektowej JavaScript, która jest w tym języku od około 1997 roku.
Tim Down
@Time dobra uwaga - nie zdawałem sobie sprawy, że istnieje od początku
Chris S
0

Istnieje fragment kodu JavaScript Dziedziczenie oparte na prototypach z implementacjami specyficznymi dla wersji ECMAScript. Automatycznie wybierze, która z implementacji ES6, ES5 i ES3 zostanie użyta, zgodnie z bieżącym czasem wykonywania.

fuweichin
źródło
0

Dodanie przykładu dziedziczenia opartego na prototypie w Javascript.

// Animal Class
function Animal (name, energy) {
  this.name = name;
  this.energy = energy;
}

Animal.prototype.eat = function (amount) {
  console.log(this.name, "eating. Energy level: ", this.energy);
  this.energy += amount;
  console.log(this.name, "completed eating. Energy level: ", this.energy);
}

Animal.prototype.sleep = function (length) {
  console.log(this.name, "sleeping. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "completed sleeping. Energy level: ", this.energy);
}

Animal.prototype.play = function (length) {
  console.log(this.name, " playing. Energy level: ", this.energy);
  this.energy -= length;
  console.log(this.name, "completed playing. Energy level: ", this.energy);
}

// Dog Class
function Dog (name, energy, breed) {
  Animal.call(this, name, energy);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function () {
  console.log(this.name, "barking. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "done barking. Energy level: ", this.energy);
}

Dog.prototype.showBreed = function () {
  console.log(this.name,"'s breed is ", this.breed);
}

// Cat Class
function Cat (name, energy, male) {
  Animal.call(this, name, energy);
  this.male = male;
}

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.meow = function () {
  console.log(this.name, "meowing. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "done meowing. Energy level: ", this.energy);
}

Cat.prototype.showGender = function () {
  if (this.male) {
    console.log(this.name, "is male.");
  } else {
    console.log(this.name, "is female.");
  }
}

// Instances
const charlie = new Dog("Charlie", 10, "Labrador");
charlie.bark();
charlie.showBreed();

const penny = new Cat("Penny", 8, false);
penny.meow();
penny.showGender();

ES6 wykorzystuje znacznie łatwiejszą implementację dziedziczenia z użyciem konstruktora i super słów kluczowych.


źródło