Хоча багато людей тут говорять, що немає найкращого способу створення об’єктів, існує обґрунтування того, чому існує так багато способів створення об’єктів у JavaScript на 2019 рік, і це пов'язано з прогресом JavaScript за різними ітераціями випусків EcmaScript з 1997 року.
До ECMAScript 5 існували лише два способи створення об'єктів: конструкторська функція або буквальне позначення (краща альтернатива новому об’єкту ()). За допомогою позначення функції конструктора ви створюєте об'єкт, який можна інстанціювати в декілька екземплярів (за допомогою нового ключового слова), тоді як буквальне позначення доставляє один об'єкт, як синглтон.
// constructor function
function Person() {};
// literal notation
var Person = {};
Незалежно від методу, який ви використовуєте, об’єкти JavaScript є просто властивостями пар ключових значень:
// Method 1: dot notation
obj.firstName = 'Bob';
// Method 2: bracket notation. With bracket notation, you can use invalid characters for a javascript identifier.
obj['lastName'] = 'Smith';
// Method 3: Object.defineProperty
Object.defineProperty(obj, 'firstName', {
value: 'Bob',
writable: true,
configurable: true,
enumerable: false
})
// Method 4: Object.defineProperties
Object.defineProperties(obj, {
firstName: {
value: 'Bob',
writable: true
},
lastName: {
value: 'Smith',
writable: false
}
});
У ранніх версіях JavaScript єдиним реальним способом імітувати спадкування на основі класу було використання конструкторських функцій. функція конструктора - це спеціальна функція, яка викликається ключовим словом "new". За умовою, ідентифікатор функції пишеться з великої літери, але він не потрібен. Всередині конструктора ми посилаємось на ключове слово "це", щоб додати властивості до об'єкта, який функція конструктора неявно створює. Функція конструктора неявно повертає новий об'єкт із заповненими властивостями назад в функцію виклику неявно, якщо ви явно не використовуєте ключове слово return та повертаєте щось інше.
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.sayName = function(){
return "My name is " + this.firstName + " " + this.lastName;
}
}
var bob = new Person("Bob", "Smith");
bob instanceOf Person // true
Виникає проблема з методом sayName. Зазвичай в об'єктно-орієнтованих мовах програмування на основі класів ви використовуєте класи як фабрики для створення об'єктів. Кожен об'єкт матиме власні змінні екземпляра, але він матиме вказівник на методи, визначені у кресленні класу. На жаль, при використанні функції конструктора JavaScript щоразу, коли він викликається, він визначатиме нове властивість sayName для новоствореного об'єкта. Таким чином, кожен об’єкт матиме своє унікальне властивість sayName. Це витратить більше ресурсів пам'яті.
Крім збільшення ресурсів пам'яті, визначення методів усередині функції конструктора виключає можливість успадкування. Знову ж таки, метод буде визначений як властивість для новоствореного об'єкта і ніякого іншого об'єкта, тому успадкування не може працювати як. Отже, JavaScript забезпечує ланцюжок прототипу як форму спадкування, перетворюючи JavaScript на прототипну мову.
Якщо у вас є батько і батько ділиться багатьма властивостями дитини, то дитина повинна успадкувати ці властивості. До ES5 це було виконано наступним чином:
function Parent(eyeColor, hairColor) {
this.eyeColor = eyeColor;
this.hairColor = hairColor;
}
Parent.prototype.getEyeColor = function() {
console.log('has ' + this.eyeColor);
}
Parent.prototype.getHairColor = function() {
console.log('has ' + this.hairColor);
}
function Child(firstName, lastName) {
Parent.call(this, arguments[2], arguments[3]);
this.firstName = firstName;
this.lastName = lastName;
}
Child.prototype = Parent.prototype;
var child = new Child('Bob', 'Smith', 'blue', 'blonde');
child.getEyeColor(); // has blue eyes
child.getHairColor(); // has blonde hair
Те, як ми використали прототип ланцюга вище, має вигадку. Оскільки прототип є живою ланкою, змінюючи властивість одного об'єкта в ланцюзі прототипу, ви також будете змінювати те саме властивість іншого об'єкта. Очевидно, що зміна успадкованого методу дитини не повинна змінювати метод батьків. Object.create вирішив цю проблему, використовуючи polyfill. Таким чином, за допомогою Object.create ви можете безпечно змінювати властивість дитини в ланцюзі прототипу, не впливаючи на те саме властивість батьків у ланцюзі прототипу.
ECMAScript 5 представив Object.create для вирішення вищезгаданої помилки у функції конструктора для створення об'єкта. Метод Object.create () ТВОРЧАЄ новий об'єкт, використовуючи існуючий об'єкт як прототип новоствореного об'єкта. Оскільки новий об’єкт створений, у вас більше не виникає питання, коли зміна дочірнього властивості в ланцюзі прототипу змінює посилання батьків на це властивість у ланцюжку.
var bobSmith = {
firstName: "Bob",
lastName: "Smith",
sayName: function(){
return "My name is " + this.firstName + " " + this.lastName;
}
}
var janeSmith = Object.create(bobSmith, {
firstName : { value: "Jane" }
})
console.log(bobSmith.sayName()); // My name is Bob Smith
console.log(janeSmith.sayName()); // My name is Jane Smith
janeSmith.__proto__ == bobSmith; // true
janeSmith instanceof bobSmith; // Uncaught TypeError: Right-hand side of 'instanceof' is not callable. Error occurs because bobSmith is not a constructor function.
До ES6 тут використовувався загальний шаблон творчості для використання конструкторів функцій та Object.create:
const View = function(element){
this.element = element;
}
View.prototype = {
getElement: function(){
this.element
}
}
const SubView = function(element){
View.call(this, element);
}
SubView.prototype = Object.create(View.prototype);
Зараз Object.create у поєднанні з конструкторськими функціями широко використовуються для створення об’єктів та успадкування в JavaScript. Однак ES6 запровадив концепцію класів, які в першу чергу є синтаксичним цукром над існуючим прототипом наслідування JavaScript. Синтаксис класу не вводить нову об'єктно-орієнтовану модель успадкування в JavaScript. Таким чином, JavaScript залишається мовою прототипу.
Класи ES6 значно спадкують. Нам більше не доведеться копіювати прототипні функції батьківського класу вручну та скидати конструктор дочірнього класу.
// create parent class
class Person {
constructor (name) {
this.name = name;
}
}
// create child class and extend our parent class
class Boy extends Person {
constructor (name, color) {
// invoke our parent constructor function passing in any required parameters
super(name);
this.favoriteColor = color;
}
}
const boy = new Boy('bob', 'blue')
boy.favoriteColor; // blue
Загалом ці 5 різних стратегій створення об’єктів у JavaScript співпадали з еволюцією стандарту EcmaScript.