使用构造函数可以创建特定类型的对象,类似于Array
、Date
等原生JavaScript
对象。
<script>
// 构造函数模式创建自定义对象
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function() {
document.write(this.name + " is " + this.age + " years old.
");
}
}
var p1 = new Person('Mike', 20);
var p2 = new Person('Alice', 18);
p1.say();
p2.say();
document.write('p1 instanceof Person: ', p1 instanceof Person, '
');
document.write('p2 instanceof Person: ', p2 instanceof Person, '
');
document.write('p1.say == p2.say: ', p1.say == p2.say);
</script>
构造函数虽然好用,但也并非没有缺点,使用构造函数的最大的问题在于每次创建实例的时候都要重新创建一次方法(理论上每次创建对象的时候对象的属性均不同,而对象的方法是相同的),也就是说,构造函数内的方法是与对象绑定的。因此p1.say == p2.say
结果是false
。
创建的每个函数都有prototype
(原型)属性,这个属性会被对象副本所继承,这样创建新对象时不用重复已有的属性、方法,节省了内存空间。使用原型对象的好处就是可以让所有对象实例共享它所包含的属性及方法。
<script>
// 利用原型模式创建自定义对象
function Person() {
Person.prototype.name = 'Mike';
Person.prototype.age = 20;
Person.prototype.say = function() {
document.write(this.name + " is " + this.age + " years old.
");
}
}
var p1 = new Person();
var p2 = new Person();
p1.say();
p2.say();
document.write('p1 instanceof Person: ', p1 instanceof Person, '
');
document.write('p2 instanceof Person: ', p2 instanceof Person, '
');
document.write('p1.say == p2.say: ', p1.say == p2.say);
</script>
可以看到,虽然构造函数内没有声明say
方法,但我们创建的对象p1
还是能调用say
方法,这是因为JavaScript
有一个搜索规则,先搜索实例属性和方法,找到则返回;如果没找到,则再到prototype
中去搜索。因此没有污染全局作用域。
原型模式省略了构造函数传递初始化参数这一环节,结果所有实例在默认情况下都取得了相同的属性值,这样非常不方便,但这还不是原型的最大问题,原型模式的最大问题在于共享的本性所导致的,由于共享,因此一个实例修改了引用,另一个也随之更改了引用。因此通常不单独使用原型,而是结合原型模式与构造函数模式。
<script>
// 利用混合模式创建自定义对象
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
document.write(this.name + " is " + this.age + " years old.
");
}
var p1 = new Person('Mike', 20);
var p2 = new Person('Alice', 18);
p1.say();
p2.say();
document.write('p1 instanceof Person: ', p1 instanceof Person, '
');
document.write('p2 instanceof Person: ', p2 instanceof Person, '
');
document.write('p1.say == p2.say: ', p1.say == p2.say);
</script>
混合模式中构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。每个实例都会有自己的一份实例属性,但同时又共享着方法,最大限度的节省了内存。示例中因为只创建say()
函数的一个实例,所以没有内存浪费。另外这种模式还支持传递初始参数。优点甚多。这种模式在ECMAScript
中是使用最广泛、认同度最高的一种创建自定义对象的方法。所有的非函数属性都由构造函数创建,意味着又可用构造函数的参数赋予属性默认值了。
有面向对象编程经验的开发人员可能会觉得将prototype
的声明放在构造函数外面有点别扭,动态原型模式可以实现方法放到构造函数里去。
动态原型方法的基本想法与混合的构造函数/原型方式相同,即在构造函数内定义非函数属性,而函数属性则利用原型属性定义。唯一的区别是赋予对象方法的位置。
动态原型模式将所有信息封装在了构造函数中,而通过构造函数中初始化原型(仅第一个对象实例化时初始化原型),这个可以通过判断该方法是否有效而选择是否需要初始化原型。即先判断原型中的某个属性或方法是不是已经声明过,如果没有声明,则声明整个原型;否则,什么也不用做。
<script>
// 利用动态原型模式创建自定义对象
function Person(name, age) {
this.name = name;
this.age = age;
if (typeof this.say != "function") {
document.write('executed only once...
');
Person.prototype.say = function() {
document.write(this.name + " is " + this.age + " years old.
");
}
}
}
var p1 = new Person('Mike', 20);
var p2 = new Person('Alice', 18);
p1.say();
p2.say();
document.write('p1 instanceof Person: ', p1 instanceof Person, '
');
document.write('p2 instanceof Person: ', p2 instanceof Person, '
');
document.write('p1.say == p2.say: ', p1.say == p2.say);
</script>
可以看到上例中只输出一次executed only once...
,即当p1
初始化后,p2
就不再需要初始化原型。
如前所述,目前使用最广泛的是混合的构造函数/原型方式。些外,动态原型方法也很流行,在功能上与前者等价,可以采用这两种方式中的任何一种。