Javascript继承与原型链

866 阅读7分钟

各位前端的小伙伴应该都知道,继承与原型链大概是面试中被问过比较多的问题了。它不仅考察面试者对javascript这门语言的理解,更是前端人进阶必须要掌握的知识。今天就让我们来一起学习一下这个js语言的重中之重吧。

说到继承,对于程序员们应该是再熟悉不过了,尤其是开发Java,C++的后端工程师们。因为Java,C++等都是面向对象的语言,继承也是面向对象语言的三大特性(封装,继承,多态)之一。然而js中并没有类的概念,js中的继承是基于原型的(es6推出的class语法也只是原型的语法糖而已)。

继承与原型链

谈到原型,一些刚入行的朋友们可能会大惊失色,这到底是什么东西?其实说白了,原型也只是javascript中的对象而已。而继承,我们可以理解为对象与原型之间的关系(就像面向对象语言中的子类与父类),就是对象与对象之间的关系。那么原型链呢,就是多个对象组成的像链条一样的关系。下面让我们来用一种更通俗的方式来理解。

图中三个矩形代表js中的三个对象,这三个对象组成了像链条一样的关系。其中grandfather是father的原型,可以赋予father一些自己的属性,并且father又是son的原型,赋予son自己的属性,并且son也从grandfather那里继承了属性,此原型链可以一直向下延伸。这样是不是更好理解了呢?

讲完上面的例子让我们回到实际中来,js中的继承关系是如何实现的呢?javascript中的每一个对象都含有一个_proto_私有属性,该属性指向它的构造函数的原型对象。构造函数中同时有一个prototype属性指向它的原型对象。原型对象中又有一个constructor属性指向该构造函数。看起来关系是不是有些乱,让我们通过下面的图片来理解。

从图片中我们可以更清晰的看出原型,构造函数和实例之间的关系。我们可以通过new关键字和构造函数创建出一个实例对象,该构造函数本身会有一个原型对象,通过该构造函数创建出的实例又有一个_proto_属性指向该原型对象。Javascript就是通过这种模型来维持三者之间的关系。

接下来让我们用代码来验证上述关系。

在这里我们首先创建了两个构造函数SupType和SubType,SupType定义了三个属性:type,name和age,SubType定义了两个属性type和name,下面用js的new关键字分别创建两个构造函数的实例对象,然后在浏览器中打印这两个对象。

浏览器打印出了两个对象,分别是Subtype类型和Suptype类型。我们可以看到他们除了构造函数中定义的属性外还有一个_proto_属性,这里_proto_指向的对象便是该实例的原型对象,其中有一个constructor属性指向了此实例构造函数。除此之外还能够发现该原型对象内部仍有一个_proto_属性指向了Object的原型对象。

从上面浏览器打印出来的Object的原型对象可以看出,其内部定义了很多属性和方法,当我们创建一个对象,我们并没有为它赋予这些属性方法,为什么它还能够使用呢?这就要谈到原型链的继承了。

我们在创建Subtype的实例之前将Subtype.prototype指向SupType的实例,然后再最后打印subIns.age,看会出现什么现象。

浏览器打印出来了10,我们并未在subIns中定义属性,却打印出了subIns中定义的age。因为在试图访问一个对象的属性时,找到该属性便返回。如果没有找到就会顺着它的原型链一级一级向上查找,直到一个对象的原型对象为null。根据官方定义,null是没有原型的,作为这个原型链的最终环节。由于我们创建的对象的原型链中存在Object.protptype,这就是我们为什么创建了一个对象就可以使用Object.prototype中定义的属性和方法了。

原生js如何实现继承关系与原型链

创建原型链大概有以下几种方式:

  1. 使用字面量语法;
  2. 使用构造函数;
  3. Object.create();
  4. 使用es6的class关键字

接下来让我们依次来了解一下这些创建原型链的方法:

字面量语法

当我们使用字面量的方式创建对象时,js就已经建立好了继承关系。

在代码中我们创建了三个变量,注释是他们的原型链,数组的原型是Array.prototype,函数的原型是Function.prototype,它们的上一层都是Object.prototype,这意味着数组和函数也可以使用Object.protortpe中定义的属性和方法,可以把它们理解为特殊的对象。

构造函数

使用构造函数时,我们需要使用js中内置的new关键字

Object.create()

Object.create()可创建一个新对象,并且接收一个对象作为参数,该参数对象就是创建对象的原型。

class关键字

es6中引入了class关键字,同时还有extends,super,constructor关键字,使得js从写法上更贴近java等面向对象语言。

entends:实现class与class之间的继承关系;

constructor:构造器,初始化时调用;

super:使得子类可以调用父类的构造函数;

这里创建了一个square对象,Square内部使用super来调用父类的构造器来为创建的实例赋值。

继承创建模式

上一节中讲述了js创建继承的关系的原生方式,现在向大家介绍几种通过自行编码来创建继承的方式。我们在这里先创建两个构造函数:

原型继承

第一节中来分析继承模型的时候使用的例子便是原型继承,将子类的prototype属性指向父类的实例,便创建了一条子类到父类的原型链。

优点:通过原型链,实现了父类属性与方法的复用;

缺点:子类实例没有自己的属性,而是通过继承而来;

复制对象继承

此种继承的方式并非改变子类的原型链,而是将父类的属性复制一份给子类,使子类拥有父类的属性。

这里我们在子类中创建了父类的实例,通过遍历父类实例将父类的属性加到子类上。

优点:子类通过复制父类属性而拥有自己的父类属性;

优点:由于未通过原型链继承,父类方法并未得到复用;

构造继承(调用父类构造函数)

此方法并非改变子类原型链,而是在子类中使用call方法调用父类构造函数将父类属性赋给子类。

优点:子类通过调用父类构造函数拥有自己的属性;

优点:由于未通过原型链继承,父类方法并未得到复用;

原型继承+构造继承

此方法通过在子类构造函数中调用父类构造函数将父类属性赋给子类,同时将子类的prototype属性指向父类实例,这样之类既继承了父类属性,有更改了子类原型链。

优点:继承上述方法的优点,摒弃了缺点,子类拥有自己的属性,父类方法也能得到复用。

缺点:父类构造函数被执行两次,消耗内存。

寄生组合式继承

通过创建一个新的构造函数,将其原型指向父类原型,这样在调用两次父类构造函数是,不会初始化两次实例属性。

以上总结了几种最常见的js继承模式,虽然可能在实际业务中不常使用,但可以帮助我们更好的理解js的继承原理,使我们更好的理解js这门语言。希望本文能为喜欢前端的你带来一些帮助。