JS中的原型可以类比于Java中的父类。
在Java中实现继承有接口继承与实现继承两种方式,接口继承只继承方法签名,而实现继承则继承实际的方法。
由于JS中的函数没有签名,在ECMAScript
无法实现接口继承,ECMAScript
只支持实现继承,而其实现继承的主要依靠原型与原型链来实现的。
原型模式
我们创建的每个函数都有一个prototype(原型)
属性,这个属性是一个指针,指向一个对象(原型对象),而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如下面例子所示:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
复制代码
在上面代码中,我们将sayName
方法直接添加到Person
的prototype
属性中,构造函数为空,此时通过构造函数生成的person1
与person2
两个实例对象,均可共享Person.prototype
的属性与方法。
理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造)
属性,这个属性是一个指向prototype
属性所在函数的指针。拿上面的代码示例来说,Person.prototype.constructor
指向Person
,通过Person.prototype
可继续为原型对象添加方法与属性。
创建了自定义的构造函数后,其原型对象默认只会取得constructor
属性,至于其他方法,都是从Object
继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性[[Prototype]]
),指向构造函数的原型对象。虽然在脚本中没有标准方式访问[[Prototype]]
,但每个对象上支持一个属性__proto__
,可通过该属性访问原型对象。
需要注意的是,
__proto__
这个连接存在于实例对象与原型对象之间,而不是存在于实例于构造函数之间。
以前面使用Person
构造函数创建实例对象的代码为例,下图展示了Person
与Person1
、Person2
及原型对象之间的关系。
原型关系判断
对于判断对象之间是否存在原型关系,有以下三种方式实现。
__proto__
alert(person1.__proto__ == Person.prototype) //true
alert(person2.__proto__ == Person.prototype) //true
复制代码
由于
person1.__proto__
与person2.__proto__
都指向原型对象,而Person.prototype
也指向原型对象,所以返回值都为true
isPrototypeOf()
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
复制代码
A.isPrototypeOf(B)
,判断A
是否是B
的原型对象。对于person1.__proto__.isPrototypeOf(person1)
的返回值,也是为true,因为person1.__proto__
等于Person.prototype
。
getPrototypeOf()
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
复制代码
Object.getPrototypeOf(person1)
返回person1
的原型对象。
属性操作
属性读取
每当代码读取某个对象的属性时,都会执行一次搜索,目标是给定名字的属性。搜索首先从实例对象本身开始,如果对象本身存在该属性,则直接返回该属性的值,如果没有找到,则继续搜索该对象的原型对象,若有就返回属性值,如果都没找到,则会返回undefined
前面提到过,原型对象最初只包含
constructor
属性,而该属性也是共享的,因此可以通过实例对象访问
alert(person2.constructor) //function Person(){}
复制代码
调用person2.constructor
时返回function Person(){}
,证明了原型对象的constructor
属性确实指向构造函数。
属性修改
虽然可以通过实例访问保存在原型中的值,但却不能通过实例对象重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性会屏蔽原型中的那个属性,如下所示。
person1.name = "Greg";
alert(person1.name); //"Greg" ——来自实例
alert(person2.name); //"Nicholas" ——来自原型对象
复制代码
当为对象添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性,虽然原型对象中的同名属性依旧存在;想要取消屏蔽,可以使用delete
操作符完全删除实例对象中的属性,然后才能访问原型中的属性,如下所示。
person1.name = "Greg";
alert(person1.name); //"Greg" ——来自实例
alert(person2.name); //"Nicholas" ——来自原型对象
delete person1.name;
alert(person1.name); //"Nicholas" ——来自原型对象
复制代码
判断属性是否存在hasOwnProperty
此外,使用hasOwnProperty()
方法可以检测一个属性是存在于实例中(该方法也是从Object
中继承而来),只有在给定属性存在于实例对象中时,才会返回true
。
alert(person1.hasOwnProperty("name")); //false
person1.name = "Greg";
alert(person1.name); //"Greg"——来自实例
alert(person1.hasOwnProperty("name")); //true
alert(person2.name); //"Nicholas"——来自原型
alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); //"Nicholas"——来自原型
alert(person1.hasOwnProperty("name")); //false
复制代码
判断属性是否存在in
操作符
此外,使用in
操作符可以检测一个属性是存在于实例或其原型对象中,存在返回true
。
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
复制代码
对于属性name
,它存在于person1
的原型对象中,但不存在于person1
实例中,所以调用hasOwnProperty
方法返回false
,调用in
操作符返回true
.
组合使用
hasOwnProperty
与in
操作符可正确判断属性是存在于实例中还是原型对象中。
原型对象的问题
- 省略了为构造函数传递初始化参数这一环节,导致所有实例默认情况下都将取得相同的属性值。
- 原型中的属性被很多实例共享,这对于函数来说非常合适,对于那些包含基本值得属性来说也可以,但对于引用类型来说,问题就比较突出了。
function Person(){
}
Person.prototype = {
constructor: Person,
friends : ["Shelby", "Court"],
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true
复制代码
person1
对属性friends
的任何操作,都会立马反应到person2
上。