前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >JS 继承的多种方法

JS 继承的多种方法

作者头像
grain先森
发布于 2019-03-28 08:57:33
发布于 2019-03-28 08:57:33
2.9K10
代码可运行
举报
文章被收录于专栏:grain先森grain先森
运行总次数:0
代码可运行

一、原型链

学过java的同学应该都知道,继承是java的重要特点之一,许多面向对象的语言都支持两种继承方式:接口继承和实现继承,接口继承只继承方法签名,而实现继承则继承实际的方法,在js中,由于函数没有签名,因此支持实现继承,而实现继承主要是依靠原型链来实现的,那么,什么是原型链呢?

首先,我们先来回顾一下构造函数,原型和实例之间的关系。当我们创建一个构造函数时,构造函数会获得一个prototype属性,该属性是一个指针,指向一个原型对象,原型对象包含一个constructor属性,该属性也是一个指针,指向构造函数,而当我们创建构造函数的实例时,该实例其实会获得一个[[Prototype]]属性,指向原型对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function SubType() {}
var instance = new SubType();

比如上面的代码,其中,SubType是构造函数,SubType.prototype是原型对象,instance是实例,这三者的关系可以用下面的图表示。

三者的关系

而这个时候呢,如果我们让原型对象等于另一个构造函数的实例,此时的原型对象就会获得一个[[Prototype]]属性,该属性会指向另一个原型对象,如果另一个原型对象又是另一个构造函数的实例,这个原型对象又会获得一个[[Prototype]]属性,该属性又会指向另一个原型对象,如此层层递进,就构成了实例与原型的链条,这就是原型链。

我们再看下上面的例子,如果这个时候,我们让SubType.prototype是另一个构造函数的实例,此时会怎么样呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function SuperType() {}
function SubType() {}
SubType.prototype = new SuperType();
var instance = new SubType();

上面的代码中,我们先是让SubType继承了SuperType,接着创建出SubType的实例instance,因此,instance可以访问SubType和SuperType原型上的属性和方法,也就是实现了继承,继承关系我们可以用下面的图说明。

继承关系

最后,要提醒大家的是,所有引用类型默认都继承了Object,这个继承也是通过原型链实现的,因此,其实原型链的顶层就是Object的原型对象啦。

二、继承

上面我们弄清了原型链,接下来主要就介绍一些经常会用到的继承方法,具体要用哪一种,还是需要依情况而定的。

1. 原型链继承

最常见的继承方法就是使用原型链实现继承啦,也就是我们上面所介绍的,接下来,还是看一个实际的例子。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() {
  return this.property;
}
function SubType() {
  ths.subproperty = true;
}
SubType.prototype = new SuperType();// 实现继承
SubType.prototype.getSubValue = function() {
  return this.subprototype;
}
var instance = new SubType();
console.log(instance.getSuperValue());// true

上面的例子中,我们没有使用SubType默认提供的原型,而是给它换了一个新原型,这个新原型就是SuperType的实例,因此,新原型具有作为SuperType实例所拥有的全部实现和方法,并且指向SuperType的原型,因此,instance实例具有subproperty属性,SubType.prototype具有property属性,值为true,并且拥有getSubValue方法,而SuperType拥有getSuperValue方法。

当调用instance的getSuperValue()方法时,因此在instance实例上找不到该方法,就会顺着原型链先找到SubType.prototype,还是找不到该方法,继续顺着原型链找到SuperType.prototype,终于找到getSuperValue,就调用了该函数,而该函数返回property,该值的查找也是同样的道理,会在SubType.prototype中找到该属性,值为true,所以显示true。

存在的问题:通过原型链实现继承时,原型实际上会变成另一个类型实例,而原先的实例属性也会变成原型属性,如果该属性为引用类型时,所有的实例都会共享该属性,一个实例修改了该属性,其它实例也会发生变化,同时,在创建子类型时,我们也不能向超类型的构造函数中传递参数。

2. 借用构造函数

为了解决原型中包含引用类型值所带来的问题,开发人员开始使用借用构造函数的技术实现继承,该方法主要是通过apply()和call()方法,在子类型构造函数的内部调用超类型构造函数,从而解决该问题。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function SuperType() {
  this.colors = ["red","blue","green"]
}
function SubType() {
  SuperType.call(this);// 实现继承
}
var instance1 = new SubType();
var instance2  = new SubType();
instance2.colors.push("black");
console.log(instance1.colors);// red,blue,green
console.log(instance2.colors);// red,blue,green,black

在上面的例子中,如果我们使用原型链继承,那么instance1和instance2将会共享colors属性,因为colors属性存在于SubType.prototype中,而上面我们使用了借用构造函数继承,通过使用call()方法,我们实际上是在新创建的SubType实例的环境下调用了SuperType的构造函数,因此,colors属性是分别存在instance1和instance2实例中的,修改其中一个不会影响另一个。

使用这个方法,我们还可以在子类型构造函数中向超类型构造函数传递参数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function SuperType(name) {
  this.name = name;
}
function SubType() {
  SuperType.call(this,"Nicholas");
  this.age = 29;
}
var instance = new SubType();
console.log(instance.name);// Nicholas
console.log(instance.age);// 29

优点:解决了原型链继承中引用类型的共享问题,同时可以在子类型构造函数中向超类型构造函数传递参数。 缺点:定义方法时,将会在每个实例上都会重新定义,不能实现函数的复用。

3. 组合继承

组合继承主要是将原型链和借用构造函数的技术组合到一块,从而发货两者之长的一种继承模式,主要是使用原型链实现对原型属性和方法的基础,通过借用构造函数实现对实例属性的基础,这样,可以通过在原型上定义方法实现函数的复用,又能够保证每个实例都有自己的属性。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function SuperType(name) {
  this.name = name;
  this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
}
function SubType(name,age) {
  SuperType.call(this,name);
  this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
}

var instance1 = new SubType("Nicholas", 29);
var instance2 =new SubType("Greg", 27);
instance1.colors.push("black");
console.log(instance1.colors); // red,blue,green,black
console.log(instance2.colors); // red,blue,green
instance1.sayName(); // Nicholas
instance2.sayName(); // 29
instance1.sayAge(); // Greg
instance2.sayAge(); // 27 

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,现在已经成为js中最常用的继承方法。

缺点:无论什么情况下,都会调用两次超类型构造函数,一次是在创建子类型的时候,另一次是在子类型构造函数内部,子类型最终会包含超类型对象的全部实例属性,但是需要在调用子类型构造函数时重写这些属性。

4. 原型式继承

原型式继承主要的借助原型可以基于已有的对象创建新的对象,基本思想就是创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function Object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

从上面的例子我们可以看出,如果我们想创建一个对象,让它继承另一个对象的话,就可以将要被继承的对象当做o传递到Object函数里面去,Object函数里面返回的将会是一个新的实例,并且这个实例继承了o对象。

其实,如果我们要使用原型式继承的话,可以直接通过Object.create()方法来实现,这个方法接收两个参数,第一个参数是用作新对象原型的对象,第二个参数是一个为新对象定义额外属性的对象,一般来说,第二个参数可以省略。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var person = {
  name: "Nicholas",
  friends: ["Shelby","Court","Van"]
}
var anotherPerson = Object.create(person, {
  name: {
    value: "Greg"
  }
});
console.log(anotherPerson.name); // Greg

上面的例子中,我们让anotherPerson继承了person,其中,friends作为引用类型,将会被所有继承该对象的对象所共享,而通过传入第二个参数,我们可以定义额外的属性,修改person中的原有信息。

缺点:原型式继承中包含引用类型的属性始终都会共享相应的值。

5. 寄生式继承

寄生式继承其实和我们前面说的创建对象方法中的寄生构造函数和工程模式很像,创建一个仅用于封装继承过程的函数,该函数在内部以某种方法来增强对象,最后再返回该对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function createAnother(original) {
  var clone = Object(original);      
  // 通过调用函数创建一个新对象
  clone.sayHi = function() {
    console.log("hi");
  }
  return clone;
}

我们其实可以把寄生式继承看做是传进去一个对象,然后对该对象进行一定的加工,也就是增加一些方法来增强该对象,然后再返回一个包含新方法的对象的一个过程。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var person = {
  name: "Nicholas",
  friends:["Shelby","Court","Van"]
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi

从上面的代码中我们可以看出,原来person是没有包含任何方法的,而通过将person传进去createAnother方法中进行加工,返回的新对象就包含了一个新的方法。

缺点:不能实现函数的复用。

6. 寄生组合式继承

组合继承是js中最经常用到的一种继承方法,而我们前面也已经说了组合继承的缺点,组合继承需要调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部,子类型最终会包含超类型对象的全部实例属性,但是我们不得不在调用子类型构造函数时重写这些属性。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function SuperType(name) {
  this.name = name;
  this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
}
function SubType(name,age) {
  SuperType.call(this,name); // 第二次调用超类型构造函数
  this.age = age;
}
SubType.prototype = new SuperType(); // 第一次调用超类型构造函数
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
}

上面的代码中有两次调用了超类型构造函数,那两次调用会带来什么结果呢?结果就是在SubType.prototype和SubType的实例上都会创建name和colors属性,最后SubType的实例上的name和colors属性会屏蔽掉SubType.prototype上的name和colors属性。

寄生组合式继承就是可以解决上面这个问题,寄生组合式继承主要通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,其实就是不必为了指定子类型的原型而调用超类型的构造函数,只需要超类型原型的一个副本就可以了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function inheritPrototype(subType,SuperType) {
  var prototype = Object(SuperType); // 创建对象
  prototype.constructor = subType; // 增强对象
  subType.prototype = prototype; // 指定对象
}

在上面的例子中,第一步创建了超类型原型的一个副本,第二步为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性,最后一步将副本也就是新对象赋值给子类型的原型,因此,我们可以用这个函数去替换前面说到为子类型原型赋值的语句。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function SuperType(name) {
  this.name = name;
  this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
}
function SubType(name,age) {
  SuperType.call(this,name);
  this.age = age;
}
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function() {
  console.log(this.age);
}

寄生组合式继承只调用了一次SuperType构造函数,避免了在SubType.prototype上面创建的不必要的,多余的属性,现在也是很多人使用这种方法实现继承啦。

7. es6中的继承

我们在前面创建对象中也提到了es6中可以使用Class来创建对象,而同样的道理,在es6中,也新增加了extends实现Class的继承,Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Point {}
class ColorPoint extends Point {}

上面这个例子中可以实现ColorPoint类继承Point类,这种简洁的语法确实比我们上面介绍的那些方法要简洁的好多呀。

但是呢,使用extends实现继承的时候,还是有几点需要注意的问题,子类在继承父类的时候,子类必须在constructor方法中调用super方法,否则新建实例时会报错,这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法,如果不调用super方法,子类就得不到this对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Point {  
  constructor(x,  y) {    
    this.x = x;    
    this.y = y;  
  }
}
class ColorPoint extends Point {  
  constructor(x,  y,  color) {    
    this.color = color; // ReferenceError    
    super(x, y);    
    this.color = color; // 正确  
  }
}

上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的,正确的继承之后,我们就可以创建实例了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint; // true
cp instanceof Point; // true
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.03.21 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
1 条评论
热度
最新
文章写的很好,就是寄生组合继承这里inheritPrototype(SubType,SuperType);,父类应该传的是父类原型SuperType.prototype吧,今天报错半天,总算找到原因了
文章写的很好,就是寄生组合继承这里inheritPrototype(SubType,SuperType);,父类应该传的是父类原型SuperType.prototype吧,今天报错半天,总算找到原因了
回复回复点赞举报
推荐阅读
编辑精选文章
换一批
JavaScript之对象继承
该方法创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
laixiangran
2018/07/25
4430
JavaScript实现继承
本文不准备深入细节,主要是对《JavaScript高级程序设计中》介绍的JS如何实现继承做一个总结,毕竟好记性不如烂笔头。文末会附带一张神图,搞清楚这张图,原型链也就没有什么问题了。
leocoder
2018/10/31
6740
JavaScript实现继承的6种方式
  许多面向对象语言都支持两种继承的方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。在 JavaScript 中由于函数没有签名也就无法实现接口继承,而只支持实现继承,而且实现继承主要通过原型链来实现的。
Leophen
2019/08/24
8330
JS进阶:继承
我们会发现 obj已经有几个属性(方法)了。那么问题来了:valueOf/toString/constructor 是怎么来?我们并没有给 obj.valueOf 赋值。
4O4
2022/04/25
4.4K0
JS进阶:继承
一文完全吃透 JavaScript 继承(面试必备良药)
继承给我们提供了一种优雅可复用的编码方式,继承也是面试中经常被问到的问题,本文全面总结了JavaScript 中的继承类型、各个继承类型的优缺点及使用场景等,一文吃透 JavaScript 继承,收藏起来吧~
winty
2020/02/24
4540
一文完全吃透 JavaScript 继承(面试必备良药)
JS继承有哪些,你能否手写其中一两种呢?
本篇是 JS系列中第 3 篇,文章主讲 JS 继承,包括原型链继承、构造函数继承、组合继承、寄生组合继承、原型式继承、 ES6 继承,以及 多继承与 new 。
helloworld1024
2022/10/17
4140
JavaScript 面试要点: 继承
ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。对属性和方法的搜索会一直持续到原型链的末端。
Cellinlab
2023/05/17
1900
JavaScript 面试要点: 继承
来自原形与原型链的拷问
在JS中,我们经常会遇到原型。字面上的意思会让我们认为,是某个对象的原型,可用来继承。但是其实这样的理解是片面的,下面通过本文来了解「原型与原型链」的细节,再顺便谈谈继承的几种方式。
JowayYoung
2020/04/01
4830
来自原形与原型链的拷问
JS入门难点解析12-继承的实现方式与优缺点
(注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)
love丁酥酥
2018/08/27
1.3K0
JS入门难点解析12-继承的实现方式与优缺点
JavaScript 高级程序设计(第 4 版)- 对象、类与面向对象编程
通过字面量来重写原型时,Person.prototype被设置为等于一个通过对象字面量创建的新对象,此时Person.prototype的constructor属性就不指向Person了
Cellinlab
2023/05/17
6520
JavaScript 高级程序设计(第 4 版)- 对象、类与面向对象编程
JavaScript(七)
这一篇,我们说说 ES 中的面向对象。 ECMAScript 中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。 ECMA-262 把对象定义为: “无序属性的集合,其属性可以包含基本值、对象或者函数。” 对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。我们可以把 ECMAScript 的对象想象成散列表: 一组键值对,其中值可以是数据或函数。
1ess
2021/10/29
3070
“工厂、构造、原型” 设计模式与 JS 继承
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情
掘金安东尼
2022/09/19
1K0
JavaScript寄生式组合继承
组合继承也被称为伪经典继承,它综合了我们昨天说的原型链和盗用构造函数,将俩者的有点结合在了一起。它的基本思想是使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性,这样的好处就是可以把方法定义在原型上复用,每个实例又有自己的属性。
大熊G
2022/11/14
3020
前端-一篇文章理解 JS 继承
说实在话,以前我只需要知道“寄生组合继承”是最好的,有个祖传代码模版用就行。最近因为一些事情,几个星期以来一直心心念念想整理出来。本文以《JavaScript高级程序设计》上的内容为骨架,补充了ES6 Class的相关内容,从我认为更容易理解的角度将继承这件事叙述出来,希望大家能有所收获。
grain先森
2019/03/28
8370
前端-一篇文章理解 JS 继承
深入理解javascript对象
对象被定义为一组属性的无序集合,对象就是一组没有特定顺序的值。对象的每个value值都由一个key来标识,一个key映射一个value值。
javascript艺术
2022/11/22
3890
javaScript 的面向对象程序
数据属性(数据属性包含一个数据值的位置,这个位置可以读取和写入值,数据属性有4描述)
用户1197315
2019/12/30
1.1K0
重读《JavaScript高级程序设计》
ECMAScript 函数不能像传统意义上那样实现重载。而在其他语言(如Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数类型和数量)不同即可[p66]。ECMAScript的类型是松散形的,没有签名,所以是没有重载的。
Jimmy_is_jimmy
2019/07/31
1.1K0
重学JavaScript之面向对象的程序设计(继承)
SuperType 和SubType。每个类型分别有一个属性和一个方法。它们的主要区别是:
执行上下文
2022/07/26
3600
ES5、ES6 如何实现继承
完整高频题库仓库地址:https://github.com/hzfe/awesome-interview
HZFEStudio
2021/09/25
6790
【JS精粹】原型链继承和构造函数继承的 “毛病”
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情
掘金安东尼
2022/09/19
1.3K0
相关推荐
JavaScript之对象继承
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验