JavaScript创建对象的常用模式
对象
面向对象语言有一个标志,那就是它们都有类的概念,通过类可以创建任意多个具有相同属性和方法的对象。
ECMAScript没有类的概念,它的对象也与基于类的语言中的对象有所不同。ECMAScript把对象定义为:
无序属性的集合,其属性可以包含基本值、对象或函数。
每个对象实例都是基于一个引用类型创建的,这个引用类型可以是ECMAScript原生类型,也可以是开发者定义的类型。
工厂模式
我们可以通过Object构造函数或对象字面量来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
为解决上述问题,可以使用工厂模式创建对象。工厂模式抽象了创建具体对象的过程。
由于ECMAScript没有类,可以定义一种函数,用函数来封装以特定接口创建对象的细节。例如:
function createStudent(name,age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function(){
alert(obj.name);
};
return obj;
}
var Bob = createStudent("Bob", 24);
var Tom = createStudent("Tom", 28);
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。上面代码,我们的本意是创建一个Student类,Bob和Tom是Student类型,但实际上根本不存在Student类型,而Bob和Tom是Object类型。
构造函数模式
构造函数可用来创建特定类型的对象,这意味着可以将构造函数的实例标识为一种特定的类型。
构造函数与与工厂模式的不同之处在于:
- 没有显示地创建对象
- 直接将属性和方法赋给this对象
- 没有return语句
- 按照惯例,构造函数始终都应该以一个大写字母开头
使用new操作符调用构造函数时,会经历以下4个步骤:
- 创建一个新对象
- 经构造函数的作用域赋给新对象(this指向这个新对象)
- 执行构造函数中的代码
- 返回新对象
function Student(name,age) {
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
};
}
var Bob = new Student("Bob", 24);
var Tom = new Student("Tom", 28);
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // false
由于ECMAScript中的函数是对象,因此只要定义一个函数,就会实例化一个对象。这会导致使用构造函数模式创建对象时,构造函数中的每个方法都要在每个实例上重新创建一遍。这样做浪费内存,降低执行效率。
我们可以把方法定义从构造函数内部移到外部,如:
function Student(name,age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var Bob = new Student("Bob", 24);
var Tom = new Student("Tom", 28);
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // true
这样会导致新的问题:sayName本应是Student的私有方法,现在却可以被任意调用,这破坏类的封装特性。
原型模式
当我们创建一个函数时,就会同时创建它的prototype对象,这个prototype对象也会自动获得constructor属性,这个属性指向构造函数对象。
当我们通过构造函数实例化一个对象时,实例对象内部将包含一个指针(内部属性),它指向构造函数的原型对象。这个内部属性称为[[Prototype]]
,在脚本中没有标准的方式访问该内部属性。
原型中方法和属性被其全部实例所共享。
function Student() {
}
Student.prototype.name = "xiaohong";
Student.prototype.age = 24;
Student.prototype.sayName = function() {
alert(this.name);
};
var Bob = new Student();
var Tom = new Student();
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // true
重写原型
前面的代码中每添加一个属性和方法就要敲一遍Student.prototype
,为了减少不必要的输入,也为了从视觉上更好地封装原型的功能,可以用对象字面量重写整个原型。
function Student() {
}
Student.prototype = {
name = "xiaohong",
age = 24,
sayName = function() {
alert(this.name);
}
};
var Bob = new Student();
var Tom = new Student();
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // true
alert(Student.prototype.constructor == Student); // false
alert(Student.prototype.constructor == Object); // true
重写原型后,现有原型的constructor属性不再指向构造函数对象,而是指向对象字面量的构造函数Object。
原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。
但是,如果创建实例后重写原型,实例会由于无法查找到属性或方法报错。这是由于实例对象会通过内部属性[[Prototype]]
连接到原型,在原型中查找属性,而重写原型切断了实例对象与原型对象之间的联系。
缺点
- 省略了为构造函数传递参数的环节,所有实例在默认情况下都将取得相同的属性值
- 原型中的属性是共享的,原型属性数据变化,所有实例对象都会获得这一变化
组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。
这种组合方式的优点是:
- 每个实例都会拥有自己的一份实例属性,同时共享着对方法的引用,最大限度地节省了内存
- 这种组合模式还支持向构造函数传递参数
function Student(name,age) {
this.name = name;
this.age = age;
}
Student.prototype = {
sayName = function() {
alert(this.name);
}
};
var Bob = new Student("Bob",24);
var Tom = new Student("Tom",28);
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // true
动态原型模式
原型模式中构造函数和原型是分离,为了把所有信息都封装在构造函数中,可以使用动态原型模式。
构造函数模式的主要缺点是同个方法要在不同实例对象中重复创建,浪费内存,所以引入了原型模式。动态原型模式通过检查某个应该存在的方法是否有效,如果无效则在构造函数中初始化原型,这样就解决方法对象重复创建的问题,而封装性更好。
function Student(name,age) {
this.name = name;
this.age = age;
// sayName不存在,则初始化原型
if (typeof this.sayName != "function") {
this.sayName = function() {
alert(this.name);
};
}
}
var Bob = new Student("Bob",24);
var Tom = new Student("Tom",28);
alert(Bob instanceof Student); // true
alert(Tom instanceof Student); // true
alert(Bob.sayName == Tom.sayName); // true
寄生构造函数模式
寄生构造函数模式就是用new操作符调用工厂模式。由于重写了返回值,返回对象和构造函数及其原型没有任何联系,无法对象类型识别。
function Student(name,age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function(){
alert(this.name);
};
return obj;
}
var Bob = new Student("Bob", 24);
var Tom = new Student("Tom", 28);
稳妥构造函数模式
稳妥对象指没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中或者防止数据被其他应用程序改动时使用
稳妥构造函数与寄生构造函数类似,但有两点不同:
- 新创建对象的实例方法不引用this
- 不使用new操作符调用构造函数
function Student(name,age) {
var obj = new Object();
obj.sayName = function(){
alert(name);
};
return obj;
}
var Bob = Student("Bob", 24);
Bob.sayName();