Использование 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.