Chociaż wiele osób twierdzi, że nie ma najlepszego sposobu tworzenia obiektów, istnieje uzasadnienie, dlaczego istnieje tak wiele sposobów tworzenia obiektów w JavaScript od 2019 r., A ma to związek z postępem JavaScript w różnych iteracjach wydań EcmaScript z 1997 roku.
Przed ECMAScript 5 istniały tylko dwa sposoby tworzenia obiektów: funkcja konstruktora lub notacja dosłowna (lepsza alternatywa dla new Object ()). Za pomocą notacji funkcji konstruktora tworzysz obiekt, którego instancję można utworzyć w wielu instancjach (za pomocą słowa kluczowego new), podczas gdy notacja literału dostarcza pojedynczy obiekt, taki jak singleton.
// constructor function
function Person() {};
// literal notation
var Person = {};
Niezależnie od zastosowanej metody obiekty JavaScript są po prostu właściwościami par klucz-wartość:
// 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
}
});
We wczesnych wersjach JavaScript jedynym prawdziwym sposobem naśladowania dziedziczenia opartego na klasach było użycie funkcji konstruktora. funkcja konstruktora jest specjalną funkcją wywoływaną za pomocą słowa kluczowego „new”. Zgodnie z konwencją identyfikator funkcji jest pisany wielkimi literami, ale nie jest to wymagane. Wewnątrz konstruktora odwołujemy się do słowa kluczowego „this”, aby dodać właściwości do obiektu, który funkcja konstruktora niejawnie tworzy. Funkcja konstruktora niejawnie zwraca nowy obiekt z wypełnionymi właściwościami z powrotem do funkcji wywołującej niejawnie, chyba że jawnie użyjesz słowa kluczowego return i zwrócisz coś innego.
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
Wystąpił problem z metodą sayName. Zazwyczaj w językach programowania opartych na klasach zorientowanych obiektowo używa się klas jako fabryk do tworzenia obiektów. Każdy obiekt będzie miał własne zmienne instancji, ale będzie miał wskaźnik do metod zdefiniowanych w schemacie klasy. Niestety, podczas korzystania z funkcji konstruktora JavaScript, za każdym razem, gdy jest ona wywoływana, definiuje nową właściwość sayName w nowo utworzonym obiekcie. Zatem każdy obiekt będzie miał swoją własną unikalną właściwość sayName. Spowoduje to zużycie większej ilości zasobów pamięci.
Oprócz zwiększonych zasobów pamięci, zdefiniowanie metod wewnątrz funkcji konstruktora eliminuje możliwość dziedziczenia. Ponownie, metoda zostanie zdefiniowana jako właściwość nowo utworzonego obiektu i żadnego innego obiektu, więc dziedziczenie nie może działać tak, jak. W związku z tym JavaScript zapewnia łańcuch prototypów jako formę dziedziczenia, czyniąc JavaScript językiem prototypowym.
Jeśli masz rodzica, a rodzic ma wiele wspólnych cech dziecka, to dziecko powinno odziedziczyć te właściwości. Przed ES5 było to realizowane w następujący sposób:
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
Sposób, w jaki wykorzystaliśmy powyższy łańcuch prototypów, jest dziwny. Ponieważ prototyp jest aktywnym łączem, zmieniając właściwość jednego obiektu w łańcuchu prototypów, zmieniasz również tę samą właściwość innego obiektu. Oczywiście zmiana odziedziczonej metody dziecka nie powinna zmienić metody rodzica. Object.create rozwiązało ten problem przy użyciu polyfill. Tak więc za pomocą Object.create można bezpiecznie modyfikować właściwość dziecka w łańcuchu prototypów bez wpływu na tę samą właściwość rodzica w łańcuchu prototypów.
ECMAScript 5 wprowadził Object.create, aby rozwiązać wyżej wymieniony błąd w funkcji konstruktora do tworzenia obiektów. Metoda Object.create () TWORZY nowy obiekt, używając istniejącego obiektu jako prototypu nowo utworzonego obiektu. Ponieważ tworzony jest nowy obiekt, nie występuje już problem polegający na tym, że modyfikacja właściwości potomnej w łańcuchu prototypów zmodyfikuje odniesienie rodzica do tej właściwości w łańcuchu.
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.
Przed ES6 istniał wspólny wzorzec tworzenia wykorzystujący konstruktory funkcji i 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);
Obecnie Object.create w połączeniu z funkcjami konstruktora jest szeroko stosowany do tworzenia i dziedziczenia obiektów w JavaScript. Jednak ES6 wprowadził koncepcję klas, które są przede wszystkim cukrem syntaktycznym w stosunku do istniejącego dziedziczenia opartego na prototypach JavaScript. Składnia klas nie wprowadza do JavaScript nowego modelu dziedziczenia zorientowanego obiektowo. Zatem JavaScript pozostaje językiem prototypowym.
Klasy ES6 znacznie ułatwiają dziedziczenie. Nie musimy już ręcznie kopiować funkcji prototypowych klasy nadrzędnej i resetować konstruktora klasy potomnej.
// 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
Podsumowując, te 5 różnych strategii tworzenia obiektów w JavaScript zbiegło się z ewolucją standardu EcmaScript.