Использование Object.assign и Object.create для наследования
Обычно я реализую наследование по следующим строкам.
function Animal () { this.x = 0; this.y = 0;}
Animal.prototype.locate = function() {
console.log(this.x, this.y);
return this;
};
Animal.prototype.move = function(x, y) {
this.x = this.x + x;
this.y = this.y + y;
return this;
}
function Duck () {
Animal.call(this);
}
Duck.prototype = new Animal();
Duck.prototype.constructor = Duck;
Duck.prototype.speak = function () {
console.log("quack");
return this;
}
var daffy = new Duck();
daffy.move(6, 7).locate().speak();
Я прочитал этот пост Эрика Эллиотта, и если я правильно понял, я могу вместо этого использовать Object.create
и Object.assign
? Неужели это так просто?
var animal = {
x : 0,
y : 0,
locate : function () {
console.log(this.x, this.y);
return this;
},
move : function (x, y) {
this.x = this.x + x;
this.y = this.y + y;
return this;
}
}
var duck = function () {
return Object.assign(Object.create(animal), {
speak : function () {
console.log("quack");
return this;
}
});
}
var daffy = duck();
daffy.move(6, 7).locate().speak();
Как в стороне, функции конструктора условных обозначений капитализируются, должны ли объектные литералы, которые действуют как конструкторы, также капитализируются?
Я понимаю, что здесь много вопросов о new
и Object.create
, но они, как правило, связаны с Duck.prototype = new Animal();
по сравнению с Duck.prototype = Object.create(Animal.prototype);
Ответы
Ответ 1
Да, это так просто. В вашем примере с Object.create/Object.assign
вы используете фабричную функцию для создания новых экземпляров duck
(подобно тому, как jQuery создает новые экземпляры, если вы выбираете элемент с var body = $('body')
). Преимущество этого стиля кода заключается в том, что он не заставляет вас называть конструктор animal
когда вы хотите создать новый экземпляр duck
(в отличие от классов ES2015).
Различия в инициализации
Может быть, один интересный лакомый кусочек, который работает несколько иначе, чем если бы вы использовали конструктор (или любую другую функцию инициализации):
Когда вы создаете duck
инстас, все свойства animal
находятся в [[Prototype]]
слоте экземпляра duck
.
var daffy = duck();
console.log(daffy); // Object { speak: function() }
Так что у daffy
нет никаких собственных свойств x
и y
. Однако, когда вы вызываете следующее, они будут добавлены:
daffy.move(6, 7);
console.log(daffy); // Object { speak: function(), x: 6, y: 7 }
Зачем? В функциональном теле animal.move
мы имеем следующее утверждение:
this.x = this.x + x;
Поэтому, когда вы называете это с помощью daffy.move
, this
относится к daffy
. Поэтому он попытается присвоить this.x + x
this.x
Так как this.x
еще не определен, [[Prototype]]
цепь daffy
пересечена вниз к animal
, где animal.x
определена.
Таким образом, при первом вызове this.x
в правой части присваивания относится к animal.x
, потому что daffy.x
не определен. Второй раз daffy.move(1,2)
, this.x
с правой стороны будет daffy.x
.
Альтернативный синтаксис
Кроме того, вы также можете использовать Object.setPrototypeOf
вместо Object.create/Object.assign
(стиль OLOO):
var duck = function () {
var duckObject = {
speak : function () {
console.log("quack");
return this;
}
};
return Object.setPrototypeOf(duckObject, animal);
}
Соглашения об именах
Я не знаю каких-либо установленных конвенций. Кайл Симпсон использует заглавные буквы в OLOO, Эрик Эллиот, похоже, использует строчные буквы. Лично я бы придерживался нижнего регистра, потому что объектные литералы, которые выступают в качестве конструкторов, уже являются полностью полноценными объектами (а не только планом, например, классами).
одиночка
Если вам нужен только один экземпляр (например, для одиночного), вы можете просто вызвать его напрямую:
var duck = Object.assign(Object.create(animal), {
speak : function () {
console.log("quack");
return this;
}
});
duck.move(6, 7).locate().speak();
Ответ 2
Я прочитал этот пост Эрика Эллиотта, и если я правильно понял, я могу вместо этого использовать Object.create
и Object.assign
? Неужели это так просто?
Да, create
и assign
гораздо проще, потому что они примитивы, и меньше магии происходит - все, что вы делаете, является явным.
Однако пример mouse
Эрика немного запутан, так как он уходит на один шаг и смешивает наследование мышей от животных с экземплярами мыши.
Скорее давайте попробуем расшифровать ваш пример утенка снова - пусть начнется с этого буквально:
const animal = {
constructor() {
this.x = 0;
this.y = 0;
return this;
},
locate() {
console.log(this.x, this.y);
return this;
},
move(x, y) {
this.x += x;
this.y += y;
return this;
}
};
const duck = Object.assign(Object.create(animal), {
constructor() {
return animal.constructor.call(this);
},
speak() {
console.log("quack");
return this;
}
});
/* alternatively:
const duck = Object.setPrototypeOf({
constructor() {
return super.constructor(); // super doesn't work with 'Object.assign'
},
speak() { … }
}, animal); */
let daffy = Object.create(duck).constructor();
daffy.move(6, 7).locate().speak();
Вы должны видеть, что то, что здесь происходит, действительно ничем не отличается от использования конструкторов (или синтаксиса class
на то пошло), мы просто сохранили наши прототипы непосредственно в переменных, и мы создаем экземпляр с явным вызовом create
и constructor
.
Теперь вы можете понять, что наш duck.constructor
ничего не делает, кроме вызова его супер-метода, поэтому мы можем фактически полностью его опустить и позволить наследованию выполнять свою работу:
const duck = Object.assign(Object.create(animal), {
speak() {
console.log("quack");
return this;
}
});
Другая вещь, которая часто изменяется, - это инициализация свойств экземпляра. На самом деле нет причин для их инициализации, если они им действительно не нужны, достаточно указать некоторые значения по умолчанию для прототипа:
const animal = {
x: 0,
y: 0,
locate() {
console.log(this.x, this.y);
}
};
const duck = … Object.create(animal) …;
let daffy = Object.create(duck); // no constructor call any more!
daffy.x = 5; // instance initialisation by explicit assignment
daffy.locate();
Проблема в том, что он работает только для примитивных значений, и он становится повторяющимся. Вот где производятся заводские функции:
function makeDuck(x, y) {
return Object.assign(Object.create(duck), {x, y});
}
let daffy = makeDuck(5, 0);
Чтобы обеспечить легкое наследование, инициализация часто не выполняется на заводе, а в специальном методе, поэтому его можно также вызвать в экземплярах "подкласса". Вы можете вызвать этот метод init
, или вы можете назвать его constructor
как я сделал выше, это в основном то же самое.
Как в стороне, функции конструктора условных обозначений капитализируются, должны ли объектные литералы, которые действуют как конструкторы, также капитализируются?
Если вы не используете какие-либо конструкторы, вы можете присвоить новое значение именам заглавных переменных, да. Однако это может смутить всех, кто не привык к этому. И, кстати, они не "объектные литералы, которые действуют как конструкторы", а просто прототипы.
Ответ 3
Вместо "наследования" вы должны подумать о том, какой тип "шаблона создания" вы намерены использовать. У них разные цели для реализации.
Верхний пример - прототипный и нижний, функционально разделенный. Проверьте эту ссылку: Шаблоны настройки JS
Кроме того, это не связано с es6.