原型对象
概念
我们每创建一个函数,都有一个*prototype(原型)*属性,这个属性是一个指针,指向一个对象,这个对象包含特定类型所有实例共享的属性和方法。
理解原型对象
无论什么时候,只要创建一个新函数,就会为这个函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
function Girl(){}
Girl.prototype.enName = 'Gakki';
Girl.prototype.cnName = '加基';
Girl.prototype.birthday = '1997/06/21';
Girl.prototype.job = 'actress';
Girl.prototype.sayHello = function(){
alert(`你好,我是${this.cnName}`);
};
var gakki = new Girl();
gakki.sayHello(); //你好,我是Gakki
拿上边这个例子来说,创建函数Girl后Girl自动获得prototype属性,指prototype属性作为一个指针向一个原型对象,这个对象会自动获得一个constructor(构造函数)属性,这个Girl.prototype.constructor指向Girl,即Girl.prototype.constructor = Girl,而通过这个构造函数,我们可以继续为原型对象添加其他属性和方法。创建构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,都是从Object继承而来。
当调用构造函数创建一个实例之后,该实例将包含一个指针,指向构造函数的原型对象。这个指针叫[[prototype]],FF、Safari、Chrome中每个对象都支持一个__proto__属性。
下图展示了Girl构造函数、Girl的原型实行以及Girl实例之间的关系,Girl.prototype指向了原型对象,而Girl.prototype.constructor又指回了Girl,原型对象除了包含constructor属性外,还包含了后来添加的其他属性,Girl的实例gakki包含一个内部属性,指向Gilr.prototype,即实例与构造函数没有直接的关系。
所以就有了
Girl.prototype.constructor === Person
gakki.constructor === Girl
虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Girl.prototype),那么这个方法就返回true,如下所示:
console.log(Girl.prototype.isPrototypeOf(gakki)); // true
ES5新增一个方法,Object.getPrototypeOf(),这个方法会返回[[Prototype]],如下:
console.log(Object.getPrototypeOf(gakki) === Girl.prototype); // true
console.log(Object.getPrototypeOf(gakki).cnName) //加基
我们可以通过实例来访问存在原型中的值,但是不能通过实例重写原型中的值。如果我们在实例中添加一个属性,该属性与实例原型中的属性名相同,那么该属性会屏蔽原型中的那个属性,如下:
function Girl(){}
Girl.prototype.enName = 'Gakki';
Girl.prototype.cnName = '加基';
Girl.prototype.birthday = '1997/06/21';
Girl.prototype.job = 'actress';
Girl.prototype.sayHello = function(){
alert(`你好,我是${this.cnName}`);
};
var gakki = new Girl();
var tom = new Girl();
tom.cnName = '汤姆';
console.log(gakki.cnName); //加基
console.log(tom.cnName); //汤姆
那么,在实际的生产环境中,我们需要知道一个属性是存在于实例中,还是原型中。hasOwnProperty()方法可以帮我们检测一个属性存在于实例还是原型中,当给定的属性存在于对象实例中时,返回true,如下:
console.log(gakki.hasOwnProperty('cnName')); //false
console.log(tom.hasOwnProperty('cnName')); //true
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,如下所示。
delete tom.cnName;
console.log(tom.cnName); //汤姆
和hasOwnProperty()类似功能的,in操作符只要通过对象能访问到属性就返回true,无论属性存在于实例还是原型。
//接着上边的代码
tom.cnName = '汤姆';
console.log('cnName' in gakki); //true
console.log('cnName' in tom); //true
在有些情况下,我们需要取得对象上所有可枚举的实例属性,Object.keys()方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。如下:
//接上边
console.log(Object.keys(Girl.prototype)); //["enName", "cnName", "birthday", "job", "sayHello"]
console.log(Object.keys(gakki)); // []
console.log(Object.keys(tom)); //["cnName"]
如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。如下:
// 接上边
// 注意结果中包含了不可枚举的constructor 属性
console.log(Object.getOwnPropertyNames(Girl.prototype)); //["constructor", "enName", "cnName", "birthday", "job", "sayHello"]
console.log(Object.getOwnPropertyNames(gakki)); //[]
console.log(Object.getOwnPropertyNames(tom)); //["cnName"]
更简单的原型语法
前面例子中每添加一个属性和方法就要敲一遍Girl.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下所示:
function Girl(){}
Girl.prototype = {
enName : 'Gakki',
cnName : '加基',
birthday : '1997/06/21',
job : 'actress',
sayHello : function(){
alert(`你好,我是${this.cnName}`);
}
};
是不是简便了很多,但是这样写有一个问题,看下边的代码:
console.log(Girl.prototype.constructor == Girl); // false
console.log(Girl.prototype.constructor == Object) // true
我们会发现,constructor属性不再指向Girl了,为什么会产生这种现象呢?我们前边说过,每创建一个函数,就会同时创建它的prototype对象,这个对象会自动获得constructor属性,我们在这里的写法,本质上完全重写了原型对象,因此constructor属性就变成了新对象的constructor属性(指向Object构造函数)。
很多情况下,constructor的值对我们有用,那么我们可以对其进行显示设置,如下:
function Girl(){}
Girl.prototype = {
constructor : Girl,
enName : 'Gakki',
cnName : '加基',
birthday : '1997/06/21',
job : 'actress',
sayHello : function(){
alert(`你好,我是${this.cnName}`);
}
};
console.log(Girl.prototype.constructor == Girl); // true
原型的动态性
原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。例子:
//接上边
var Erika = new Girl();
Girl.prototype.sayHi = function(){
alert('Hi,我是杰瑞!');
};
Erika.sayHi(); //你好,我是杰瑞!
我们先创建了一个Girl实例,然后我们给Girl.prototype添加一个sayHi()方法,即使girl实例是在添加新方法前创建的,它仍然可以访问这个新方法,为什么会这样呢?其实实例和原型之间的连接是一个指针,而非副本,当我们调用Erika.sayHi()时,先会在实例中搜索sayHi属性,没有找到的情况下,会继续搜索原型。
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。先看个演示:
function Girl(){}
var satomi = new Girl();
Girl.prototype = {
constructor : Girl,
enName : 'Jerry',
cnName : '杰瑞',
birthday : '1997/06/21',
job : 'actress',
sayHello : function(){
alert(`你好,我是${this.cnName}`);
}
};
satomi.sayHello(); //Uncaught TypeError: satomi.sayHello is not a function
它竟然报错了,为什么会这样呢?调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初型之间的联系。实例中的指针仅指向原型,而不指向构造函数。
在上边的例子中,我们先创建了一个Girl实例,然后又重写其原型对象,在调用satomi.sayHello()时发生错误,因为satomi指向的原型中不包含以改名字命名的属性,具体的可以看下边的图:
原生对象原型
所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。在Array.prototype中可以找到sort()方法,而在String.prototype中可以找到substring()方法。通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。如下:
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true
尽管可以这样做,但我们不推荐在产品化的程序中修改原生对象的原型。如果因某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突。而且,这样做也可能会意外地重写原生方法。