Хороший пример наследования на основе прототипов в JavaScript

89

Я программировал на языках ООП более 10 лет, но сейчас изучаю JavaScript, и впервые столкнулся с наследованием на основе прототипов. Я стараюсь учиться быстрее всего, изучая хороший код. Каков хорошо написанный пример приложения (или библиотеки) JavaScript, которое правильно использует прототипное наследование? И не могли бы вы описать (вкратце), как / где используется прототипное наследование, чтобы я знал, с чего начать?

Алекс Рейснер
источник
1
У вас была возможность проверить эту базовую библиотеку? Он действительно красивый и довольно маленький. Если вам это нравится, отметьте мой ответ как ответ. TIA, Роланд.
Роланд Боуман
Думаю, я в той же лодке, что и ты. Я также хочу немного узнать об этом прототипном языке, не ограничиваясь только OOP-фреймворками или подобными, даже если они отличные и все такое, нам нужно изучить, верно? Для меня это делает не какой-то фреймворк, даже если я собираюсь его использовать. Но научитесь создавать новые вещи на новых языках новыми способами, мыслите нестандартно. Мне нравится твой стиль. Я постараюсь помочь мне и, возможно, помочь вам. Как только найду что-нибудь, дам тебе знать.
marcelo-ferraz

Ответы:

48

У Дугласа Крокфорда есть хорошая страница о наследовании прототипов JavaScript :

Пять лет назад я написал « Классическое наследование в JavaScript». Он показал, что JavaScript является бесклассовым языком-прототипом и обладает достаточной выразительной способностью для моделирования классической системы. С тех пор мой стиль программирования изменился, как и положено любому хорошему программисту. Я научился полностью принимать прототипализм и освободился от рамок классической модели.

Работы Дина Эдварда Base.js , Класс Mootools или Простое наследование Джона Ресига - это способы реализовать классическое наследование в JavaScript.

Грегори Пакош
источник
Почему не просто, newObj = Object.create(oldObj);если вы хотите, чтобы это было бесплатно? В противном случае заменить oldObjна объект прототипа функции конструктора должна работать?
Cyker
76

Как уже упоминалось, фильмы Дугласа Крокфорда дают хорошее объяснение того, почему, и как. Но чтобы выразить это в нескольких строках 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'
);

Однако проблема с этим подходом заключается в том, что он будет воссоздавать объект каждый раз, когда вы его создаете. Другой подход - объявить ваши объекты в стеке прототипов, например:

// 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');

Когда дело доходит до самоанализа, есть небольшая обратная сторона. Сброс testOne приведет к получению менее полезной информации. Кроме того, частное свойство privateVariable в testOne используется во всех экземплярах, как это было полезно упомянуто в ответах shesek.

Dynom
источник
3
Обратите внимание, что в testOne privateVariable- это просто переменная в области действия IIFE , которая используется во всех экземплярах, поэтому вам не следует хранить в ней данные, относящиеся к конкретному экземпляру. (на testTwo это зависит от экземпляра, так как каждый вызов testTwo () создает новую область для каждого экземпляра)
shesek
Я поддержал, потому что вы показали другой подход, и почему бы не использовать его, потому что он делает копии
Murphy316
Проблема воссоздания объекта каждый раз в основном связана с тем, что методы воссоздаются для каждого нового объекта. Однако мы можем смягчить проблему, определив метод на Dog.prototype. Поэтому вместо использования this.bark = function () {...}мы можем действовать Dot.prototype.bark = function () {...}вне Dogфункции. (Подробнее см. В этом ответе )
Ч Хуанг
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);
Влад Безден
источник
3
Возможно, добавление этой ссылки к вашему ответу может еще больше дополнить картину: developer.mozilla.org/en/docs/Web/JavaScript/Reference/…
Dynom
14

Я бы взглянул на YUI и Baseбиблиотеку Дина Эдварда : http://dean.edwards.name/weblog/2006/03/base/

Для YUI вы можете быстро взглянуть на модуль lang , особенно. YAHOO.lang.extend метод. А затем вы можете просмотреть источник некоторых виджетов или утилит и посмотреть, как они используют этот метод.

Роланд Боуман
источник
YUI 2 устарел с 2011 года, поэтому ссылка на langнего частично не работает. Кто-нибудь хочет исправить это для YUI 3?
ack
lang в yui 3, похоже, не имеет метода расширения. но поскольку в ответе предполагается использовать реализацию в качестве примера, версия не имеет значения.
eMBee 08
5

Это самый яркий пример, который я нашел, из книги Mixu Node ( http://book.mixu.net/node/ch6.html ):

Я предпочитаю композицию наследованию:

Композиция - Функциональность объекта состоит из совокупности различных классов, содержащих экземпляры других объектов. Наследование. Функциональность объекта складывается из его собственных функций и функций его родительских классов. Если у вас должно быть наследование, используйте простой старый JS

Если вы должны реализовать наследование, по крайней мере, избегайте использования еще одной нестандартной реализации / магической функции. Вот как вы можете реализовать разумное факсимиле наследования в чистом ES3 (при условии, что вы следуете правилу никогда не определять свойства в прототипах):

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();

Это не то же самое, что классическое наследование, но это стандартный, понятный Javascript и имеет функции, которые в основном ищут люди: цепные конструкторы и возможность вызывать методы суперкласса.

сверхнизкое
источник
4

ES6 classиextends

ES6 classи extendsявляются всего лишь синтаксическим сахаром для ранее возможных манипуляций с цепочкой прототипов и, возможно, наиболее канонической установкой.

Сначала узнайте больше о цепочке прототипов и .поиске свойств по адресу: https://stackoverflow.com/a/23877420/895245

Теперь давайте разберемся, что происходит:

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

Упрощенная диаграмма без всех предопределенных объектов:

      __proto__
(C)<---------------(D)         (d)
| |                |           |
| |                |           |
| |prototype       |prototype  |__proto__
| |                |           |
| |                |           |
| |                | +---------+
| |                | |
| |                | |
| |                v v
|__proto__        (D.prototype)
| |                |
| |                |
| |                |__proto__
| |                |
| |                |
| | +--------------+
| | |
| | |
| v v
| (C.prototype)--->(inc)
|
v
Function.prototype
Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
1

Лучшие примеры, которые я видел, находятся в книге Дугласа Крокфорда « JavaScript: хорошие части». . Это определенно стоит покупать, чтобы помочь вам получить сбалансированное представление о языке.

Дуглас Крокфорд отвечает за формат JSON и работает в Yahoo как гуру JavaScript.

Крис С
источник
7
ответственный? это звучит почти как «виновный» :)
Роланд Боуман
@Roland Я думаю, что JSON - довольно хороший, не подробный формат для хранения данных. Однако он определенно не изобретал его, формат настроек конфигурации в Steam существовал еще в 2002 году
Крис С.
Крис С., Я тоже так думаю. Мне все чаще и чаще хотелось бы, чтобы мы все пропустили XML как формат обмена и сразу перешли на JSON.
Роланд Боуман
3
Нечего изобретать: JSON - это подмножество синтаксиса объектного литерала собственного JavaScript, который используется в языке примерно с 1997 года.
Тим Даун,
@Time хороший момент - я не понимал, что он там был с самого начала
Chris S
0

Добавление примера наследования на основе прототипа в 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 использует гораздо более простую реализацию наследования с использованием конструктора и ключевых слов super.


источник