尽管这里很多人说没有最好的对象创建方法,但是有一个合理的理由说明为什么到2019年为止,有这么多方法可以用JavaScript创建对象,这与JavaScript在不同迭代中的进展有关的EcmaScript版本可追溯到1997年。
在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标准的发展相吻合。