-
Object构造函数
创建自定义对象的最简单方式就是创建一个Object
的实例,然后再为它添加属性和方法。
var person = new Object();
person.name = "Ciel";
person.age = 28;
person.job = "Web Developer";
person.sayName = function() {
return this.name;
};
-
对象字面量
var person = {
name: "Ciel",
age: 28,
job: "Web Developer",
sayName: function() {
return this.name;
}
};
-
工厂模式
虽然Object
构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量重复代码。为了解决这个问题,人们开始使用工厂模式的一种变体。
function createPerson(name, age, job) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayName = function() {
return this.name;
};
return obj;
}
var person1 = createPerson("Ciel", 28, "Web Developer");
var person2 = createPerson("Frank", 28, "Android Developer");
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
-
构造函数模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
return this.name;
};
}
var person1 = new Person("Ciel", 28, "Web Developer");
var person2 = new Person("Frank", 28, "Android Developer");
构造函数模式与工厂模式的不同之处在于:
1.没有显示的创建对象;
2.直接将属性和方法赋给了this对象;
3.没有return语句。
此外还应该注意到函数名Person
使用的是大写字母P。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其它OO语言,主要是为了区别于ECMAScript中的其它函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。
在前面的例子中,person1
和person2
分别保存着Person
的不同实例。这两个对象都有一个constructor
(构造函数)属性,该属性指向Person
。
console.log(person1.constructor === Person); //true
console.log(person2.constructor === Person); //true
对象的constructor
属性最初是用来标识对象类型的。但是提到检测对象类型,还是instanceof
操作符要更可靠一些。我们在这个例子中创建的所有对象既是Object
的实例,同时也是Person
的实例,这一点可以通过instanceof
操作符得到验证。
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person); //true
创建自定义的构造函数意味着可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。在这个例子中person1
和person2
之所以同时是Object
的实例,是因为所有对象均继承自Object
。
-
原型模式
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
function Person() {
}
Person.prototype.name = "Ciel";
Person.prototype.age = 28;
Person.prototype.job = "Web Developer";
Person.prototype.sayName = function() {
return this.name;
};
var person1 = new Person();
person1.sayName(); //"Ciel"
var person2 = new Person();
person2.sayName(); //"Ciel"
//更简单的原型语法
function Person() {
}
Person.prototype = {
constructor: Person, //确保通过该属性访问到适当的值
name: "ciel",
age: 28,
job: "Web Developer",
sayName: function() {
return this.name;
}
};
原型模式不是没有缺点,首先它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来不便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟(如前面例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。
function Person() {
}
Person.prototype = {
constructor: Person,
name: "ciel",
age: 28,
job: "Web Developer",
friends: ["Frank", "Rose"],
sayName: function() {
return this.name;
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Summer");
console.log(person1.friends); //["Frank", "Rose", "Summer"]
console.log(person2.friends); //["Frank", "Rose", "Summer"]
console.log(person1.friends === person2.friends); //true
修改person1.friends
引用的数组,person2.friends
也会跟着改变,这是由于 friends 数组存在于 Person.prototype
中。如果我们的初衷就是让所有实例共享一个数组,那结果确实没什么问题。可是,实例一般都是要有属于自己的全部属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
-
组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数,可谓是集两种模式之长。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Frank", "Rose"];
}
Person.prototype = {
constructor: Person,
sayName: function() {
return this.name;
}
}
var person1 = new Person("Ciel", 28, "Web Developer");
var person2 = new Person("Frank", 28, "Android Developer");
person1.friends.push("Summer");
console.log(person1.friends); //["Frank", "Rose", "Summer"]
console.log(person2.friends); //["Frank", "Rose"]
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true
这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
-
动态原型模式
动态原型模式把所有的信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法使用有效,来决定是否需要初始化原型。
function Person(name, age, job) {
//属性
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Frank", "Rose"];
//方法
if(typeof this.sayName != "function") {
Person.prototype.sayName = function() {
return this.name;
};
}
}
var person1 = new Person("Ciel", 28, "Web Developer");
person1.sayName();
使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的联系。
-
寄生构造函数模式
通常,在前面几种模式都不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。
function Person(name, age, job) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayName = function() {
return this.name;
};
return obj;
}
var person1 = new Person("Ciel", 28, "Web Developer");
person1.sayName(); //"Ciel"
这种函数模式可以在特殊情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改 Array
构造函数,因此可以使用这个模式。
function SpecialArray() {
//创建数组
var arr = new Array();
//添加值
arr.push.apply(arr, arguments);
//添加方法
arr.toPipedString = function() {
return this.join("|");
}
//返回数组
return arr;
}
var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString());
关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数模式或者构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。因此,不能依赖
instanceof
操作符来确定对象类型。由于存在上述问题,建议在可以使用其他模式的情况下,不要使用这种模式。
-
稳妥构造函数模式
function Person(name, age, job) {
//创建要返回的对象
var obj = new Object();
//可以在这里定义私有变量和函数
//添加方法
obj.sayName = function() {
return name;
};
//返回对象
return obj;
}
var person1 = new Person("Ciel", 28, "Web Developer");
person1.sayName(); //"Ciel"
注意,在以这种模式创建的对象中,除了调用sayName()
方法外,没有其他办法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境——例如,Dsafe 和 Caja 提供的环境中使用。
与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此
instanceof
操作符对这种对象也没有意义。
参考书籍:《JavaScript高级程序设计(第3版)》。
网友评论