prototype、__proto__与JavaScript的原型链继承

 2015年03月09日    192     声明


JavaScript没有传统面向对象语言的类继承机制,而是基于原型链继承实现的,其本质是使用函数模拟类的特征。我们可以通过prototype将属性写到原型链上,调用new操作符创建对象(实例化)时,对象实例会把类原型链上的属性关联到自身的__proto__属性上;而子类继承父类时,是将子类的prototype属性指向父类的prototype属性,并在子类prototype属性添加自己的方法和属性实现对父类的扩展。

  1. 类与实例
  2. 类的继承

1. 类与实例

1.1 类定义

在JavaScript中我们会像下面这样模拟一个类:

function Person(name, sex) {
  this.name = name;
  this.sex = sex;
}

Person.prototype.sayName = function() {
  console.log(this.name);
}

该类包含两个属性和一个方法。

1.2 实例化

类定义后,就可以通过new关键创建类实例:

var person = new Person('王二小', '女');
person.__proto__ === Person.prototype;  //	true

JavaScript中实例化不同与传统面向对象语言(如:Java、C++等),其实例化基于对象原型。

__proto__是一个对象内部属性,继承自Object.prototype.__proto__。当类被实例化时,对象的(类实例)的__proto__属性会指向类的原型,即:类的prototype属性。

这就是基于原型的类的实现方式,也就是原型链的实现方式。实例化后当调用对象的属性或方法时,会有如下过程:

  • 查找对象是否有该属性或方法,如果则在则返回或调用
  • 如果不存在,则通过__proto__属性,在原型链上查找有没有属性或方法


2. 类的继承

继承、封装、多态是面向对象语言三大特征,JavaScript基于原型同样可以模拟出这三个特性。单就继承来说又分为单继承和多继承,但JavaScript只能实现单继承。

我可以像下面这样模拟一类继承:

function Person(name, sex) {
  this.name = name;
  this.sex = sex;
}

Person.prototype.sayName = function() {
  console.log(this.name + ',我是一个人');
}

// 等同于Object.create的方法
function object(prop) {
  var F = function () {};
  F.prototype = prop;
  return new F();
}

function Student(name, sex) {
  Person.call(this, name, sex);
}	

Student.prototype = object(Person.prototype)

Student.prototype.constructor = Student;

Student.prototype.work = function() {
  console.log('我的工作是学习');
}

在这个继承过程中,我们做了以下几件事:

  • 在子类的构造函数中调用父类的构造函数
  • 将父类的原型属性prototype复制到子类的原型prototype
  • 将子类的构造器constructor指向子类的构造函数

JavaScript中继承的本质是原型链的复制,创建子类的实例后,其__proto__属性会指定子类的prototype,但它同时是一个父类的实例。

student.__proto__ === Person.prototype;  // false
student.__proto__ === Student.prototype; // true
student instanceof Student; // true
student instanceof Person;  // true

仍然可以通过__proto__.__proto__访问父类中的方法:

student.__proto__.__proto__.sayName(); // undefined,我是一个人

除上述的自定义object方法外,还可以使用Object.create()实现类原型的复制。无论使用哪种方式,这只对面向对象继承的一种模拟,其与传统的类的实例化与继承相比有以下几个特点:

  • 在传统的类中,“类名”同进是一个构造函数,而JavaScript使用函数模拟了构造函数
  • 在传统的类中有可以被继承到子类中的属性和方法,而而JavaScript使用prototype实现了继承
  • 传统的类通过new关键字来实例化一个类,而JavaScript的实例化过程是对prototype属性的复制

注意:在ECMAScript 2015(ES6)中新增了class类定义的方式,但其本质仍然是基于函数的类模拟。