原型与原型链
javascript 创建对象
类与构造函数是大多数编程语言所拥有的,而借鉴了 C 与 JAVA 的 javascript 也是有类和构造函数的,不过 javascript 的实现不太一样。
// before ES6
// 构造函数模式
function Person(name){
this.name = name;
this.sayName = function() {
console.log(this.name);
}
}
var p = new Person();
console.log(p instanceof Person); // true
上面的例子中,Person
就是构造函数,p
就是它的实例,但是它对共享性不太好,所以有了原型模式。
// 原型模式
function Person(){}
Person.prototype.name = 'kobe';
Person.prototype.sayName = function() {
console.log(this.name);
}
var p1 = new Person();
var p2 = new Person();
p1.sayName(); // kobe
p2.sayName(); // kobe
原型模式创建的实例都可以共享Person.prototype
的属性和方法,而prototype
是 javascript 实现类的一个关键所在。
prototype
在 javascript 中,每个函数都有一个prototype
属性,这个属性指向函数的原型对象,上面的例子,可以用下图解释,图中用Person.prototype
表示 Person 的原型对象。
constructor
Person 是构造函数,有prototype
指针指向原型对象,而Person.prototype
也有一个constructor
属性指向构造函数。
简单来说,就是 javascript 会在函数创建的时候为函数添加一个prototype
属性,这个属性是指向函数的原型对象的指针,而原型对象也会获得一个constructor
属性,这个属性是一个指向函数本身的指针,即指向构造函数。
function Person(){}
Person.prototype.name = 'kobe';
Person.prototype.sayName = function() {
console.log(this.name);
}
Person.prototype.constructor == Person; // true
__proto__
函数有prototype
,那对象呢?其实对象内部也有一个[[prototype]]
指针,这个指针指向创建实例对象的构造函数的原型对象。
在 ES5 之前的语言标准里,是不允许访问这个属性的,但浏览器都实现了一个__proto__
对[[prototype]]
进行访问操作,所以 ES5 增加了一个Object.getPrototypeOf()
方法来访问[[prototype]]
。
Object.getPrototypeOf(object)
方法返回指定对象的原型(内部[[Prototype]]
属性的值)。在 ES5 中,如果参数不是一个对象类型,将抛出一个 TypeError 异常。在 ES6 中,参数会被强制转换为一个 Object。
建议在代码中使用Object.getPrototypeOf()
来访问内部[[Prototype]]
属性,不建议使用非标准的__proto__
,为了方便区别,文中使用__proto__
来表示[[prototype]]
。
回到上面的例子,当调用 Person 来创建实例的时候,该实例会获得一个__proto__
指针指向构造函数的原型对象。
正是因为创建的 p1、p2 实例都拥有__proto__
指向构造函数(Person)的原型对象(Person.prototype),它们才能共享Person.prototype
的属性和方法。
function Person(){}
Person.prototype.name = 'kobe';
Person.prototype.sayName = function() {
console.log(this.name);
}
let p1 = new Person();
let p2 = new Person();
Object.getPrototypeOf(p1) == Object.getPrototypeOf(p2); // true
简单来说,就是在 javascript 中,每一个对象(除null)在创建的时候都会将其与另一个对象进行关联,而关联的这个对象就是所谓的原型。
使用isPrototypeOf()
方法就能确定这种关联:
Person.prototype.isPrototypeOf(p1); // true
Person.prototype.isPrototypeOf(p2); // true
屏蔽性
还是这个例子:
function Person(){}
Person.prototype.name = 'kobe';
Person.prototype.sayName = function() {
console.log(this.name);
}
let p1 = new Person();
// 访问 p1 的 name
p1.name; // kobe
当我们通过实例 p1 访问name
属性的时候,javascript 引擎会先在实例 p1 中查找name
,当发现 p1 没有这个属性,就会通过__proto__
向它指向的原型对象进行查找,找到就返回这个属性的值,也就是说访问name
执行了两次查找。
如果 p1 自己添加了name
属性,就会在第一次查找时返回值:
// 在 p1 中添加 name
p1.name = 'jordan';
// 只会执行一次查询
p1.name; // jordan
// 原型中的值并未改变
Object.getPrototypeOf(p1).name; // kobe
也就是说,在实例上添加name
属性后,该属性就会屏蔽原型中的那个属性,但并不会改变原型中的该属性的值。
原型链
既然在 javascript 中,所有对象(除null)都有__proto__
,而实例 p1 指向Person.prototype
,那Person.prototype
对象的__proto__
又指向哪里呢?
Object.getPrototypeOf(Person.prototype) == Object.prototype; // true
答案是Object.prototype
,因为原型对象是通过构造函数 Object 来创建的,所以它指向Object.prototype
。
那 Object.prototype 的__proto__
又指向哪里呢?它指向这条链的终点:null,代表无。
Object.getPrototypeOf(Object.prototype); // null
回到上面所说的查找name
属性,如果在第二次都没有查找到name
属性,就会沿着这条链继续向上查找,直到终点都没有找到,就会返回 undefined。
function Person(){}
let p1 = new Person();
p1.name; // undefined
上图中虚线部分由原型相互关联组成的链状结构就是所谓的原型链。
备注
不得不说,《javascript高级程序设计》这本书把原型讲得很透彻,今天再看一遍依然感觉通透。