前文

总所周知,继承是所有OO语言中都拥有的一个共性。在JavaScript中,它的继承机制与其他OO语言有着很大的不同,尽管ES6为我们提供了像面向对象继承一样的语法糖,但是其底层依然是构造函数,所以理解继承的底层原理非常重要,所以今天让我们来探讨一下JavaScript中的继承机制。

原型与原型链

要理解继承,必须理解JavaScript中的原型与原型链,我在之前的上一篇文章对原型进行了深入的探讨,有兴趣的小伙伴可以看看~

《理解原型与原型链》

继承

在JavaScript中,有六种主要常见的继承方式,下面我会对每一种继承方式进行分析并总结它们的优缺点

1.原型链继承

原型链继承的概念

在JavaScript中,实现继承主要是依靠原型链来实现的。其基本思想是是利用原型让一个引用类型继承另一个引用类型的属性和方法。

让我们简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象prototype,原型对象都包含一个指向构造函数的指针constructor,而实例都包含一个指向原型对象的内部指针__proto__

假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?让我们来看下面这段代码。

function Father() {
this.name = 'zhang';
} Father.prototype.sayName = function() {
console.log(this.name);
} function Son() {
this.age = 18;
} // 继承了Father
Son.prototype = new Father();
Son.prototype.sayAge = function() {
console.log(this.age);
} const xiaoming = new Son();
console.log(xiaoming.sayName()) // 'zhang'

以上代码,Son继承了Father,而继承是通过创建Father的实例,并将Son.prototype指向new出来的Father实例。实现的本质是重写了原型对象,待之是一个新类型的实例,也就是说,原来存在于Father构造函数中的所有属性和方法,现在也存在于Son.prototype中。

通过上图可知,我们没有使用Son默认提供的原型,而是给它换了一个新原型,这个原型就是Father的实例,其内部还有一个指针,指向Father的原型。由于Son的原型被重写了,所以xiaoming这个实例的constructor属性现在指向的是Father。一句话总结就是Son继承了Father,而Father继承Object,当调用xiaoming.toString()方法时,实际上是调用Object.prototype中的toString方法。

注意:给子类原型添加方法的代码一定要放到替换原型的语句之后

还有一点需要提醒各位小伙伴们,在使用原型链继承时,千万不能使用对象字面量创建原型方法,因为这样做会重写原型链,来看下面这段代码。

function Father() {
this.name = 'zhang';
} Father.prototype.sayName = function() {
console.log(this.name);
} function Son() {
this.age = 18;
} // 继承了Father
Son.prototype = new Father();
Son.prototype = {
sayAge: function() {
console.log(this.age)
}
} const xiaoming = new Son();
console.log(xiaoming.sayName()) // '报错'

使用对象字面量创建原型方法,会切断FatherSon之间的继承关系哦~

原型链继承的优点

子类型的实例对象拥有超类型的全部属性和方法。

原型链继承的缺点

我在上面的那篇文章提到过,包含引用类型值的原型属性会被所有实例共享。在通过原型实现继承时,原型实际上会变成另一个类型的实例原先的实例属性也就顺理成章地变成了现在的原型属性了。

function Father() {
this.cars = ['奔驰', '宝马', '兰博基尼'];
} Father.prototype.sayName = function() {
console.log(this.name);
} function Son() {
this.age = 18;
} // 继承了Father
Son.prototype = new Father(); const xiaoming = new Son();
xiaoming.cars.push('五菱宏光');
console.log(xiaoming.cars); //'奔驰, 宝马, 兰博基尼, 五菱宏光' const xiaohong = new Son();
console.log(xiaohong.cars); //'奔驰, 宝马, 兰博基尼, 五菱宏光'

可以从上述代码中发现,当Father中的属性是引用类型的时候,当然Father的每个实例都会有各自的数组cars属性。当Son继承Father之后,Son.prototype就变成了Father的一个实例,结果就是xiaomingxiaohong两个实例对象共享一个cars属性,这是在继承中我们不希望出现的。

第二个问题是创建Son的实例时,不能向Father的构造函数中传递参数,也就是说,没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

接下来我要将的第二种继承方式是构造函数继承,它可以解决包含引用类型值所带来的问题。

2.构造函数继承

构造函数继承的概念

实现构造函数继承的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。

让我们来看下面这段代码:

function Father() {
this.cars = ['奔驰', '宝马', '兰博基尼'];
} function Son() {
// 继承Father
Father.call(this);
} const xiaoming = new Son();
xiaoming.cars.push('五菱宏光');
console.log(xiaoming.cars); //'奔驰, 宝马, 兰博基尼, 五菱宏光' const xiaohong = new Son();
console.log(xiaohong.cars); //'奔驰, 宝马, 兰博基尼'

通过使用call()方法(或apply()方法),在创建xiaoming实例的同时,调用了Father构造函数,这样一来,就会在Son的实例对象上执行Father构造函数所定义的所有对象初始化代码,因此xiaomingxiaohong就具有属于自己的cars属性了。

构造函数继承还有一个优点是可以给超类型构造函数传参,让我们来看下面这段代码。

function Father(name) {
this.name = name;
} function Son(name, age) {
Father(this, name);
this.age = age;
} const xiaoming = new Son('小明', 19);
console.log(xiaoming.name); //'小明'
console.log(xiaoming.age); //19

我们创建了xiaoming实例并传递两个参数nameagename参数通过调用Father构造函数传递参数给了Father构造函数中的name,因此xiaoming实例拥有nameage两个实例属性。

构造函数继承的优点

可以在子类型构造函数中向超类型构造函数传参;子类型构造函数创建的对象都拥有各自的属性和方法(引用类型)

构造函数继承的缺点

很明显,方法都在构造函数中定义的话,函数复用就无从谈起了,因此构造函数继承很少单独使用。接下来介绍的这种继承方式,通过原型链构造函数结合实现的继承,叫做组合继承

3.组合继承

组合继承的概念

组合继承的基本思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

使用组合继承的优点是即通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性,来看下面这段代码。

function Father(name) {
this.name = name;
this.cars = ['奔驰', '宝马', '兰博基尼'];
} Father.prototype.sayName = function() {
console.log(this.name);
} function Son(name, age) {
// 继承属性
Father.call(this, name); //第二次调用Father()
this.age = age;
} // 继承方法
Son.prototype = new Father(); //第一次调用Father()
Son.prototype.constructor = Son;
Son.prototype.sayAge = function() {
console.log(this.age);
} const xiaoming = new Son('xiaoming', 18);
xiaoming.cars.push('五菱宏光');
console.log(xiaoming.cars); //'奔驰, 宝马, 兰博基尼, 五菱宏光'
xiaoming.sayName(); //'xiaoming'
xiaoming.sayAge(); //18 const xiaohong = new Son('xiaohong', 20);
console.log(xiaohong.cars); //'奔驰, 宝马, 兰博基尼'
xiaohong.sayName(); //'xiaohong'
xiaohong.sayAge(); //20 console.log(xiaoming instanceof Son) //true
console.log(xiaoming instanceof Father) //true
console.log(xiaoming instanceof Object) //true

组合继承的优点

组合继承避免了原型链继承构造函数继承的缺陷,融合它们的优点,成为JavaScript中最常用的继承模式。

组合继承的缺点

组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数。一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

4.原型式继承

原型式继承的概念

原型式继承的就是借助原型可以基于已有的对象创建新对象

我们来看下面这段代码。

function object(o) {
function F() {}
F.prototype = o;
return new F();
} const person = {
name: 'zhangsan',
cars: ['奔驰', '宝马', '兰博基尼']
} const anotherPerson = object(person);
anotherPerson.name = 'lisi';
anotherPerson.cars.push('五菱宏光');
console.log(anotherPerson.name); //'lisi'
console.log(anotherPerson.cars); //'奔驰, 宝马, 兰博基尼, 五菱宏光' const yetAnotherPerson = object(person);
yetAnotherPerson.name = 'wangwu';
console.log(yetAnotherPerson.name); //'wangwu'
console.log(yetAnotherPerson.cars); //'奔驰, 宝马, 兰博基尼, 五菱宏光'

object()实际上是对对象的一次浅复制,实现原型式继承的前提是要求你必须有一个对象可以作为另一个对象的基础。

ES5新增了Object.create()方法,这个方法规范化了原型式继承。这个方法我在这里不多介绍,感兴趣的小伙伴可以参考MDN的说明文档Object.create()

原型式继承优点

如果只想让一个对象与另外一个对象保持类似的情况下,原型式继承可以完全胜任。

原型式继承缺点

原型式继承的缺点相信各位小伙伴们已经看出来了,包含引用类型值的属性始终都会共享相应的值,就像使用原型链继承一样。

5.寄生式继承

寄生式继承的概念

寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。

废话不多说,让我们来看下面这段代码。

function createAnother(original) {
const clone = Object.create(original);
clone.sayHi = function() {
console.log('hi');
}
return clone;
} const person = {
name: 'zhangsan',
cars: ['奔驰', '宝马', '兰博基尼']
} const anotherPerson = createAnother(person);
anotherPerson.sayHi(); //'hi' const yetAnotherPerson = createAnother(person);
yetAnotherPerson.sayHi(); //'hi'
console.log(anotherPerson.sayHi == yetAnotherPerson.sayHi) //false

这个例子中,封装了一个createAnother的函数,这个函数接收一个参数,也就是将要作为新对象的基础对象,我们可以看到,anotherPersonyetAnotherPerson两个对象拥有各自的sayHi方法。

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

寄生式继承优点

继承的对象都拥有各自的属性和方法(引用类型)。

寄生式继承缺点

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数继承模式类似。

6.寄生组合式继承

寄生组合继承的概念

所谓寄生组合式继承,就是通过构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需的无非就是超类型原型的一个副本而已。

本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。让我们来看下面这段代码。

function inheritPrototype(Son, Father) {
const prototype = Object.create(Father.prototype);
prototype.constructor = Son;
Son.prototype = prototype;
} function Father(name) {
this.name = name;
this.cars = ['奔驰', '宝马', '兰博基尼'];
} Father.prototype.sayName = function() {
console.log(this.name);
} function Son(name, age) {
Father.call(this, name); //调用Father
this.age = age;
} inheritPrototype(Son, Father); Son.prototype.sayAge = function() {
console.log(this.age);
}

这个例子的高效率体现在它只调用了一次Father构造函数,并且因此避免在Son.prototype上面创建不必要、多余的属性。

寄生组合式继承优点

寄生组合式继承只调用了一次超类型构造函数,是被开发人员普遍认为是引用类型最理想的继承范式。

寄生组合式继承无缺点

总结

前端的学习之路还有很长很长,这篇文章只不过是冰山一角,希望前端cc写的这篇文章能给小伙伴们带来新的知识拓展,愿前端cc与各位前端小伙伴们在前端生涯中一起共同成长,冲鸭!

带你理解【JavaScript】中的继承机制的更多相关文章

  1. JavaScript大杂烩4 - 理解JavaScript对象的继承机制

    JavaScript是单根的完全面向对象的语言 JavaScript是单根的面向对象语言,它只有单一的根Object,所有的其他对象都是直接或者间接的从Object对象继承.而在JavaScript的 ...

  2. 深入理解JavaScript中的继承

    1前言 继承是JavaScript中的重要概念,可以说要学好JavaScript,必须搞清楚JavaScript中的继承.我最开始是通过看视频听培训班的老师讲解的JavaScript中的继承,当时看的 ...

  3. 深入理解JavaScript中的继承:原型链篇

    一.何为原型链 原型是一个对象,当我调用一个对象的方法时,如果该方法没有在对象里面,就会从对象的原型去寻找.JavaScript就是通过层层的原型,形成原型链. 二.谁拥有原型 任何对象都可以有原型, ...

  4. 理解JavaScript中的原型继承(2)

    两年前在我学习JavaScript的时候我就写过两篇关于原型继承的博客: 理解JavaScript中原型继承 JavaScript中的原型继承 这两篇博客讲的都是原型的使用,其中一篇还有我学习时的错误 ...

  5. javascript 之 prototype继承机制

    理解Javascript语言的继承机制 javascript没有"子类"和"父类"的概念,也没有"类"(class)和"实例&qu ...

  6. Javascript prototype 及 继承机制的设计思想

    我一直很难理解Javascript语言的继承机制. 它没有"子类"和"父类"的概念,也没有"类"(class)和"实例" ...

  7. JavaScript中的继承(原型链)

    一.原型链 ECMAScript中将原型链作为实现继承的主要方法,基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法. 实例1: function SupType() { this.pro ...

  8. 理解 JavaScript 中的 this

    前言 理解this是我们要深入理解 JavaScript 中必不可少的一个步骤,同时只有理解了 this,你才能更加清晰地写出与自己预期一致的 JavaScript 代码. 本文是这系列的第三篇,往期 ...

  9. 深入理解JavaScript中创建对象模式的演变(原型)

    深入理解JavaScript中创建对象模式的演变(原型) 创建对象的模式多种多样,但是各种模式又有怎样的利弊呢?有没有一种最为完美的模式呢?下面我将就以下几个方面来分析创建对象的几种模式: Objec ...

随机推荐

  1. I. 蚂蚁上树

    蚂蚁上树(Sauteed Vermicelli with minced Pork),又名肉末粉条,是四川省及重庆市的特色传统名菜之一.因肉末贴在粉丝上,形似蚂蚁爬在树枝上而得名.这道菜具体的历史,已不 ...

  2. Jmeter接口测试、性能测试详细介绍

    下面主要就是讲一下Jmeter工具的用法,用法非常简单,比起loadrunner不知道简单多少,并且开源免费~~ 1.接口简介 接口定义 接口: 就是数据交互的入口和出口,是一套标准规范. 接口(硬件 ...

  3. Java集合案例(产生不重复随机数)

    获取10个1-20之间的随机数,要求不能重复 用数组实现,但是数组的长度是固定的,长度不好确定.所以我们使用集合实现. 分析:A:创建产生随机数的对象B:创建一个存储随机数的集合C:定义一个统计变量. ...

  4. Ubuntu下的eclipse配置MapReduce

    下载配置文件: 链接:https://pan.baidu.com/s/13vatPHpDP5HaW0mKuHydUA提取码:pjxi 1)启动hadoop cd /usr/local/hadoop . ...

  5. 是时候学习python了

    “ 学习Pyhton,如何学以致用 -- 知识往问题靠,问题往知识靠” 01 为什么学Python 一直有听说Python神奇,总是想学,虽然不知道为啥.奈何每天写bug,修bug忙得不亦乐乎,总是不 ...

  6. python学习14集合

    '''''''''集合:set1.定义:是一个无序的不重复元素序列.2.表示:大括号 { } 或者 set() 函数创建集合,注意:创建一个空集合必须用 set() 而不是 { },因为 { } 是用 ...

  7. (第一篇)linux简介与发展历史以及软件的安装

    1.Linux操作系统基本结构介绍: 操作系统: 英文名称Operating System,简称OS,是计算机系统中必不可少的基础系统软件,它是应用程序运行以及用户操作必备的基础环境支撑,是计算机系统 ...

  8. Hyperledger Fabric基础知识

    文章目录 什么是Hyperledger Fabric? Hyperledger架构是怎么工作的? Hyperledger交易如何执行 总结 Hyperledger Fabric基础知识 本文我们会介绍 ...

  9. 关于LinearLayout设置权重后width或height不设置0dp的影响说明

    摘要 平时没那么注意LinearLayout布局时权重的问题,设置了权重属性后,通常建议将width或height的属性值设置为0dp,有时候设置权重后,还是习惯将width或height的属性设置为 ...

  10. inotifywait实现文件监控

    应用场景文件监控可以配合rsync实现文件自动同步,例如监听某个目录,当文件变化时,使用rsync命令将变化的文件同步.(可用于代码自动发布) 安装noitify下载地址:http://github. ...