JavaScript 的原型与继承
在 JavaScript 中,除了字符串、数字、true、false、null 和 undefined ,其他都是对象。
每个对象都有一个原型指针(隐式原型),指向该对象所继承的原型对象。该对象仅供 js 引擎内部使用,一般我们无法直接使用它,也最好不要使用它。但是在一些浏览器中,可以使用对象实例的 __proto__
属性,可以认为它就是那个原型指针。
每个函数都有一个 prototype(显式原型)属性和一个原型指针(连接到原型对象 Function.prototype)。
函数创建过程
首先写一个函数字面量:
1 | function fn() {} |
函数 fn 除了 name 等属性外,还包含一个 prototype 属性和一个原型指针。其中 prototype 属性是一个对象,包含一个 constructor(指向函数 fn 本身)和一个指向 Object.prototype 的原型指针。函数 fn 的原型指针指向 Function.prototype。
1 | function fn() {} |
用更直观的图来表示就是:
构造函数
按照 ECMA 的定义:
Constructor is a function that creates and initializes the newly created object.
构造函数是一个创建并初始化一个新对象的函数。结论是:任何一个函数都可以是一个构造函数。
原型
函数在创建的时候会自动添加一个 prototype 属性,这个属性就是函数的原型。
1 | function Fn(a, b) { |
上面的代码,反映在图中:
那么原型有什么用呢?在这之前,先了解一下 new 运算符。
1 | function Fn(a, b) { |
通过使用 new 运算符和构造函数调用,能够创建一个新对象,需要注意的是这个对象与函数对象不同,它没有 prototype 属性。那为什么不直接使用对象字面量的方式(var obj = {})创建对象呢?通过上图能够发现,使用对象字面量创建的对象继承自 Object.prototype,而使用 new 和构造函数调用创建的对象继承自 Fn.prototype。
1 | function Fn(a, b) { |
其实这个 new 操作,大体上可以分为三步:第一步是新建一个对象并赋值给 fn,即 var fn = {}
。第二步是改变对象的原型指针,将它指向函数 Fn 的原型,即 fn.[[Prototype]] = Fn.prototype
。第三步是调用函数 Fn,同时把 this 指向对象 fn,对对象进行初始化,即 Fn.apply(fn, arguments)
。
原型链
真正体现原型作用的是原型链。
JavaScript 中所有的对象都有一个 [[Prototype]] 属性,保存着对象所继承的原型,由 JS 编译器在对象创建时自动添加。
对象在查找某个属性时,会首先遍历自身的属性,如果没有则会继续查找 [[Prototype]] 所引用的对象的属性,如果没有则继续查找 [[Prototype]].[[Prototype]] ,以此类推,直到 [[Prototype]]…[[Prototype]] 为 undefined (Object 的 [[Prototype]] 就是 undefined)。
继承
有了原型链,就可以进行继承了。
首先我们写一个函数字面量,则这个函数对象的原型指向 Object.prototype ,如何让这个函数对象的原型指向另一个函数对象的原型呢?方式一就是使用 __proto__
。
1 | function A() {} |
__proto__
不是 ECMA 的标准方法,只在某些浏览器中能够使用,如何使用标准的方法呢?方式二就是使用 new 和构造函数调用。
1 | function A() {} |
这里隐含着将 A.prototype.[[Prototype]] = B.prototype。但是这样做会产生一个问题,就是 A.prototype.constructor 为 undefined。
此时需要再将 A.prototype.constructor 重新赋值回去。
1 | function A() {} |
重写原型
1 | function Person(name) { |
为什么是 undefined 呢?因为 p1 = new Person() 时,执行 Person 函数,并将 this 绑定给 p1,即 Person.apply(p1, arguments)。因为 arguments 中没有值,导致 p1.name 为 undefined,所以就返回 undefined,没有再从原型链中查找。
总结
每个对象都有一个原型指针(__proto__
),指向该对象所继承的原型对象。每个函数都有一个原型指针(__proto__
)和一个 prototype 属性。使用函数和 new 来创建新对象(var fn = new Fn()
),隐含了三步操作。
1 | // step 1 |
参考
《JavaScript 权威指南》