Преимущества использования `Object.create` для наследования
Я пытался обвести вокруг нового метода Object.create
, который был введен в ECMAScript 5.
Обычно, когда я хочу использовать наследование, я делаю что-то вроде этого:
var Animal = function(name) { this.name = name; }
Animal.prototype.print = function() { console.log(this.name); }
var Dog = function()
{
return Animal.call(this, 'Dog');
}
Dog.prototype = new Animal();
Dog.prototype.bark = function() { console.log('bark'); }
Я просто назначил вновь созданный объект Animal прототипу Dog, и все работает как шарм:
var dog1 = new Dog();
dog1.print(); // prints 'Dog'
dog1.bark(); // prints 'bark'
dog1.name; //prints 'Dog'
но люди (без объяснения) говорят, что Dog.prototype = new Animal();
не способ наследования, и что я должен использовать подход Object.create:
Dog.prototype = Object.create(Animal.prototype);
который также работает.
В чем преимущество использования Object.create
или я что-то пропустил?
ОБНОВЛЕНИЕ: Некоторые говорят, что Dog.prototype = Animal.prototype;
также может работать. Итак, теперь я полностью смущен
Ответы
Ответ 1
Далее я предполагаю, что вас интересует только то, почему Object.create
является предпочтительным для настройки наследования.
Чтобы понять преимущества, давайте сначала разъясним, что из класса "класс" сделано в JavaScript. У вас есть две части:
-
Функция конструктор. Эта функция содержит всю логику для создания экземпляра "класса", то есть конкретного кода.
-
Объект прототип. Это объект, наследуемый экземпляром. Он содержит все методы (и другие свойства), которые должны быть разделены между всеми экземплярами.
Наследование устанавливает отношение is-a, например a Dog
является Animal
. Как это выражается в терминах функции-конструктора и объекта-прототипа?
Очевидно, что собака должна иметь те же методы, что и животное, то есть прототип объекта Dog
должен каким-то образом включать методы из объекта прототипа Animal
. Существует несколько способов сделать это. Вы часто увидите это:
Dog.prototype = new Animal();
Это работает, потому что экземпляр Animal
наследуется от объекта-прототипа Animal
. Но также подразумевает, что каждая собака наследуется от одного конкретного экземпляра Animal
. Кажется, это немного странно. Не следует ли использовать конкретный код экземпляра только в функции конструктора? Внезапно специфический код экземпляра и методы прототипа кажутся смешанными.
В настоящий момент мы действительно не хотим запускать код экземпляра Animal
, нам нужны только все методы из объекта прототипа Animal
. Это то, что Object.create
позволяет нам делать:
Dog.prototype = Object.create(Animal.prototype);
Здесь мы не создаем новый экземпляр Animal
, мы получаем только прототипы. Конкретный код выполняется точно там, где он должен быть, внутри конструктора:
function Dog() {
Animal.call(this, 'Dog');
}
Самое большое преимущество заключается в том, что Object.create
всегда будет работать. Использование new Animal()
работает только в том случае, если конструктор не ожидает никаких аргументов. Представьте, что конструктор выглядел так:
function Animal(name) {
this.name = name.toLowerCase();
}
Вам всегда нужно передать строку в Animal
, иначе вы получите сообщение об ошибке. Что вы пройдете, когда вы сделаете Dog.prototype = new Animal(??);
? На самом деле не важно, какую строку вы проходите, пока что-то пропускаете, что, надеюсь, покажет вам, что это плохой дизайн.
Некоторые говорят, что Dog.prototype = Animal.prototype;
также может работать. Так что теперь я совершенно смущен
Все, что "добавляет" свойства от Animal.prototype
до Dog.prototype
, будет "работать". Но решения имеют разное качество. В этом случае у вас возникнет проблема, что любой метод, который вы добавляете в Dog.prototype
, также будет добавлен в Animal.prototype
.
Пример:
Dog.prototype.bark = function() {
alert('bark');
};
Так как Dog.prototype === Animal.prototype
, все экземпляры Animal
теперь имеют метод bark
, что, конечно же, не то, что вы хотите.
Object.create
(и даже new Animal
) добавляют один уровень косвенности к наследованию, создавая новый объект, который наследует от Animal.prototype
, и что новый объект становится Dog.prototype
.
Наследование в ES6
ES6 вводит новый синтаксис для создания конструкторских функций и прототипов, которые выглядят следующим образом:
class Dog extends Animal {
bark() {
alert('bark');
}
}
Это более удобно, чем описано выше, но, как оказалось, extends
также использует внутренний эквивалент Object.create
для настройки наследования. См. Шаги 2 и 3 в проекте ES6.
Это означает, что использование Object.create(SuperClass.prototype)
является "более правильным" подходом в ES5.
Ответ 2
Во-первых, запуск конструктора Animal
может иметь нежелательные побочные эффекты. Рассмотрим это:
var Animal = function(name) {
this.name = name;
Animal.instances.push(this);
};
Animal.instances = [];
Эта версия будет отслеживать все созданные экземпляры. Вы не хотите, чтобы ваш Dog.prototype
был записан там.
Во-вторых, Dog.prototype = Animal.prototype
- плохая идея, так как это означает, что bark
станет методом Animal
.
Ответ 3
Я немного проиллюстрирую разницу:
Вот что в основном происходит, когда вы пишете new Animal()
:
//creating a new object
var res = {};
//setting the internal [[prototype]] property to the prototype of Animal
if (typeof Animal.prototype === "object" && Animal.prototype !== null) {
res.__proto__ = Animal.prototype;
}
//calling Animal with the new created object as this
var ret = Animal.apply(res, arguments);
//returning the result of the Animal call if it is an object
if (typeof ret === "object" && ret !== null) {
return ret;
}
//otherise return the new created object
return res;
И вот что в основном происходит с Object.create
:
//creating a new object
var res = {};
//setting the internal [[prototype]] property to the prototype of Animal
if (typeof Animal.prototype !== "object") {
throw "....";
}
res.__proto__ = Animal.prototype;
//return the new created object
return res;
Таким образом, он делает то же самое, но не вызывает функцию Animal
, а также всегда возвращает новый созданный объект.
В вашем случае вы получаете два разных объекта. С помощью первого метода вы получите:
Dog.prototype = {
name: undefined,
__proto__: Animal.prototype
};
и со вторым способом вы получите:
Dog.prototype = {
__proto__: Animal.prototype
};
Вам действительно не нужно иметь свойство name
в вашем прототипе, потому что вы уже назначили его экземпляру Dog
с Animal.call(this, 'Dog');
.
Ваша основная цель - предоставить экземпляру Dog
доступ ко всем свойствам прототипа Animal
, который достигается обоими методами. Первый метод, однако, делает некоторые дополнительные вещи, которые на самом деле не нужны в вашем случае или могут даже вызывать нежелательные результаты, о которых упоминал Pumbaa80.
Ответ 4
Пусть понимает только код:
A.prototype = B.prototype;
function B() {console.log("I am B");this.b1= 30;}
B.prototype.b2 = 40;
function A() {console.log("I am A");this.a1= 10;}
A.prototype.a2 = 20;
A.prototype = B.prototype;
A.prototype.constructor = A;
var a = new A;
var b = new B;
console.log(a);//A {a1: 10, b2: 40}
console.log(b);//B {b1: 30, b2: 40}
console.log(A.prototype.constructor);//A
console.log(B.prototype.constructor);//A
console.log(A.prototype);//A {b2: 40}
console.log(B.prototype);//A {b2: 40}
console.log(a.constructor === A); //true
console.log(b.constructor === A); //true
console.log(a.a2);//undefined
![enter image description here]()
A.prototype = Object.create(B.prototype);
function B() {console.log("I am B");this.b1= 30;}
B.prototype.b2 = 40;
function A() {console.log("I am A");this.a1= 10;}
A.prototype.a2 = 20;
A.prototype = Object.create(B.prototype);
A.prototype.constructor = A;
var a = new A;
var b = new B;
console.log(a);//A {a1: 10, constructor: function, b2: 40}
console.log(b);//B {b1: 30, b2: 40}
console.log(A.prototype.constructor);//A
console.log(B.prototype.constructor);//B
console.log(A.prototype);//A {constructor: function, b2: 40}
console.log(B.prototype);//B {b2: 40}
console.log(a.constructor === A); //true
console.log(b.constructor === B); //true
console.log(a.a2);//undefined
![enter image description here]()