关于原型继承

190 阅读5分钟

在基于类的语言中,对象是类的实例,并且类可以从另一个类继承。Javascript是一门基于对象的语言,这意味着对象直接从其他对象继承。

Javascript的原型存在诸多矛盾。它的某些复杂语法看起来像那些基于类的语言,这些语法问题掩盖了它的原型机制。它不直接让对象从其他对象继承,反而插入一个多余的间接层:通过构造函数产生对象。

——《Javascript语言精粹》

本文基本是用来解释这段话的,如果你看到这里就已经十分清楚,那大概就不需要往下看了。

如果你和曾经的我一样,迷失在原型、原型链、构造函数、constructor、prototype、__proto__、[[prototype]]这些名词中,希望本文能为你指引一二。

原型的定义

在MDN中,对继承与原型链的解释是这样的

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object )都有一个私有属性(称之为[[prototype]])指向它的原型对象(prototype)。该原型对象也有一个自己的原型对象 ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject的原型。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__

几乎所有 JavaScript 中的对象都是位于原型链顶端的Object的实例。

这条原型链构成了继承的基础 —— 对象可以通过原型链去访问原型对象的属性。

原型继承

其实js中原型继承的概念十分简单。

先构建一个有用的对象,然后以这个对象作为原型创建一个新的对象,新对象就继承了原有对象的属性。用Object.create()来构造的原型链就十分简单干净。

const animal = { a: 1 };
const dog = Object.create(animal);
console.log(dog, dog.a);

// {} 1
  • dog打印出来是一个空对象,但访问dog下面的a属性却能拿到值,展开对象之后会发现一个__proto__属性指向animal。dog.a取值的时候,在dog的作用域中找不到a,然后会向上追溯原型链,追溯到animal有这个属性。

  • 继续展开原型链,会发现这样一个结构 b => a => Object.prototype => null

  • Object 是一个构造方法,ƒ Object() { [native code] } ,Object.prototype 才是原型对象。

  • JavaScript中的所有对象都来自Object;所有对象从Object.prototype继承方法和属性。 上面的animal = { a: 1 }中,用对象字面量的方式创建了一个对象实例,它的原型就是Object.prototype。

  • 根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

  • [[prototype]]属性是在对象创建时默认初始化的,回想几种对象的创建方式

    1. 对象字面量,生成了一个Object的实例,所以原型就是Object.prototype。
    2. Object.create(obj),原型是obj。如果想要一个没有原型的对象,Object.create(null)就好啦
    3. 构造函数,也就是new function生成一个对象,原型指向构造函数的prototype属性指向的那个对象(这个下面会展开)
    4. 类生成,new class生成一个对象,这个本质上也是构造函数。

伪类继承(其实还是原型继承)

我们常见的继承方式是构造函数和class关键字,它们实际上都是基于原型继承。class其实只是语法糖,js这种弱类型语言中并没有类这种结构。

构造函数定义

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符来作用这个函数时,它就可以被称为构造方法(构造函数)。

理解原型对象与构造函数的关系

只要创建一个新函数,就会根据一组特定的规则为这个函数创建一个prototyoe属性,这个属性指向函数的原型对象。默认情况下,这个原型对象会constructor属性,这个属性是一个指回函数的指针。

在new一个构造函数生成一个实例时,实例的原型对象[[prototype]]会被初始化为构造函数的prototype属性指向的对象

function Graph() {};
console.log(Graph, Graph.prototype, Graph.prototype.constructor)
// ƒ
// {constructor: ƒ}
// ƒ Graph () {}

明白了这一点,就很好解释构造函数是怎样实现原型继承的了

function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v){
    this.vertices.push(v);
  }
};

var g = new Graph();
// g是生成的对象,他的自身属性有'vertices''edges'.
// 在g被实例化时,g.[[Prototype]]指向了Graph.prototype.

上面说过,构造函数的prototype初始的时候指向一个带有constructor属性的对象,当对一个实例取constructor属性时,其实是取到了实例的原型对象上的constructor属性。就是这里将构造函数的prototype指向了一个新对象,就会产生一个不太算缺点的缺点——丢失了constructor属性。

解决方法也很简单, 加回去就好了

function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype = {
	constructor: Graph.prototype.constructor,
	addVertex: function(v){
	    this.vertices.push(v);
	  }
	}
;
var g = new Graph();
console.log(g.constructor);
// Graph

构造函数实现继承的过程

// 构造函数Animal有一个默认的属性Animal.prototype,这个属性指向一个初始的原型对象,对象是Object的实例,对象的原型是Object.prototype.
function Animal() {}

// 构造函数Dog有一个默认的属性Dog.prototype,这个属性指向一个初始的原型对象,对象是Object的实例,对象的原型是Object.prototype.
function Dog() {}

// 构造函数Animal生成对象,对象原型为Animal.prototype
// 再将这个对象赋给Dog.prototype,作为Dog的原型
Dog.prototype = {
	constructor: Dog.prototype.constructor,
	...new Animal()
}

// 构造函数Dog将原型Dog.prototype赋给foo
const foo = new Dog();

如果只看原型链,是这样的

将构造函数考虑进来,就是这样

参考资料

  1. github.com/mqyqingfeng…
  2. developer.mozilla.org/zh-CN/docs/… 继承与原型链
  3. 《javascript高级程序设计》
  4. 《javascript语言精粹》