理解 JavaScript 继承与原型链

Mar 20, 2018Tagged in JavaScript

相信很多小伙伴在刚开始接触 JavaScript 的继承机制时都会感到一头雾水,特别是对于有面向对象编程语言(如Java,Python等)经验的同学。因为 JavaScript 本身不提供 class 实现,而是使用一种称作『原型链』的方法来模拟了类的继承。

要想理解原型链,首先需要知道的一点就是 JavaScript 是基于对象的。每一个对象都有一个私有属性[[prototype]],它指向它的原型对象(prototype)。该原型对象又有一个自己的 prototype,层层向上直到一个对象的原型为null。这就是原型链,原型链的作用就是把这些对象联系起来。

使用构造函数创建对象

构造函数本质上就是函数声明,与普通函数不同的是,我们人为地使用首字母大写来将其与普通函数区分开。因为 JavaScript 中,函数本质上也是对象,所以构造函数具有 prototype 属性。我们可以利用这一点模拟出类的继承。

prototype

为了更好的理解,我们来举个栗子:

//定义一个 Cat 构造函数
function Cat(name) {
this.name = name;
}
//将 Cat 原型指向一个对象
//该对象定义了一个 meow 方法
Cat.prototype = {
meow: function(){
console.log("meow meow meow~~")
}
}
//通过 new 关键字创建两个 Cat 实例
let kitty = new Cat("Kitty");
let tom = new Cat("Tom");
kitty.name //=>"Kitty"
tom.name //=>"Tom"
kitty.meow() //=>"meow meow meow~~"
tom.meow() //=>"meow meow meow~~"

当我们调用实例对象的属性和方法时,JavaScript 会先尝试在当前对象自有的属性和方法中查找,如果没有,则会去该对象的原型,即 prototype 指向的对象中查找,并以此类推。如果直到原型链的终端 null 都没有,则返回 undefined。因此当调用 meow 方法时,因为构造函数中没有定义此方法,所以 JavaScript 会去对象的原型中查找 meow 方法。这样就实现了在各个实例间共享方法。

注意:当我们调用继承的属性时,this 始终指向当前继承的对象,而不是继承的函数所在的原型对象。因此当我们调用 name 属性时,会返回对应的属性值。

__proto__

每一个 JavaScript 对象(null除外)都会有一个 __proto__ 属性。如果说 prototype 属性表达了构造函数和实例原型之间的关系,那么 __proto__ 表明了实例与实例原型之间的关系。

kitty.__proto__ === Cat.prototype //=>true
//ES5中定义的另一个获取对象原型的方法
Object.getPrototypeOf(kitty) === Cat.prototype //=>true

constructor

对象还有另外一个属性 constructor 。与 prototype 相反,constructor 是原型的属性,指向该原型的构造函数。

Cat.prototype.constructor === Cat //=>true

详细关系图如下,摘自Github: mqyqingfeng/Blog

new 的时候发生了什么?

根据 MDN 上的解释:当代码 new Foo(…) 执行时,会发生以下事情:

  1. 一个继承自 Foo.prototype 的新对象被创建。
  2. 使用指定的参数调用构造函数 Foo ,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
  3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)

举例:

function Car(name) {
this.name = name
}
Car.prototype.move = function(){
console.log(this.name)
}
let car = new Car("Porsche")
// 等同于
let car = (function(func) {
let ret = {};
if (func.prototype !== null) {
ret.__proto__ = func.prototype
}
let ret1 = func.apply(ret, [].slice.call(arguments, 1));
if (typeof ret1 === "object" || typeof ret1 === "function" && ret1 !== null) {
return ret1;
}
return ret;
})(Car, "Porsche")

使用Object.create()方法

ES5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数:

var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)

使用 class 关键字

ES6 中引入了一套新的关键字用来实现 class,但它仍然是基于原型的。详见类 - MDN

参考资料:

  1. 继承与原型链 - MDN
  2. Object.create() - MDN
  3. Javascript继承机制的设计思想 - 阮一峰的网络日志
  4. mqyqingfeng/Blog - Github
  5. 理解JavaScript的原型链和继承 - blog.oyanglul.us