虽然这里的许多人说创建对象没有最好的方法,但截至 2019 年,为什么在 JavaScript 中有这么多创建对象的方法是有道理的,这与 JavaScript 的进步有关可追溯到 1997 年的 EcmaScript 版本的不同迭代。
在 ECMAScript 5 之前,只有两种创建对象的方法:构造函数或文字符号(new Object() 的更好替代方法)。使用构造函数表示法,您可以创建一个可以实例化为多个实例的对象(使用 new 关键字),而文字表示法提供单个对象,例如单例。
// 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”关键字调用。按照惯例,函数标识符是大写的,尽管它不是必需的。在构造函数内部,我们引用 'this' 关键字来向构造函数隐式创建的对象添加属性。构造函数隐式地将具有填充属性的新对象返回给调用函数,除非您显式使用 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 标准的演变相吻合。