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

JS 继承的多种方法

作者头像
grain先森
发布于 2019-03-28 08:57:33
发布于 2019-03-28 08:57:33
2.9K00
代码可运行
举报
文章被收录于专栏: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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
c语言指针的基本使用
指针(pointer)是C语言中一个重点和难点,以下是对其基本使用的一些总结,适合入门的同学。除了是对自己的学习的总结之外,也希望能对大家有所帮助。
梦飞
2022/06/23
9300
【C语言】指针
本文目录 直接引用 一、什么是指针? 二、指针的定义 三、指针的初始化 四、指针运算符 五、指针的用途举例 六、关于指针的疑问 指针是C语言中非常重要的数据类型,如果你说C语言中除了指针,其他你都学得很好,那你干脆说没学过C语言。究竟什么是指针呢?我们先来看一个概念。 回到顶部 直接引用 1. 回想一下,之前我们是如何更改某个变量的值? 我们之前是通过变量名来直接引用变量,然后进行赋值: char a; a = 10; 2. 看上去是很简单,其实程序内部是怎么操作的呢? 其实,程序对变量的读写操作,实际上是
用户1941540
2018/05/11
3.4K0
【超详细】*和&在C/C++中的常见用法(附示例讲解)
可用作代码块的注释说明。与//不同的,//用于一行代码的注释说明,类似于python中的#,而/* code block */用于一个代码块的注释说明,类似于python中的``` code block ```。
自学气象人
2023/01/13
4.7K0
C语言中的指针详解
计算机系统的内存拥有大量的存储单元,每个存储单元的大小为1字节,为了便于管理,必须为每个存储单元编号,该编号就是存储单元的“地址”,每个存储单元拥有一个唯一的地址。
越陌度阡
2021/11/09
3.2K0
C++基础入门_C语言入门基础
​ Visual Studio是我们用来编写C++程序的主要工具,我们先将它打开
全栈程序员站长
2022/09/30
5.7K0
C++基础入门_C语言入门基础
C语言中函数参数传递的三种方式
(1)传值,就是把你的变量的值传递给函数的形式参数,实际就是用变量的值来新生成一个形式参数,因而在函数里对形参的改变不会影响到函数外的变量的值。 (2)传址,就是传变量的地址赋给函数里形式参数的指针,使指针指向真实的变量的地址,因为对指针所指地址的内容的改变能反映到函数外,也就是能改变函数外的变量的值。 (3)传引用,实际是通过指针来实现的,能达到使用的效果如传址,可是使用方式如传值。 说几点建议:如果传值的话,会生成新的对象,花费时间和空间,而在退出函数的时候,又会销毁该对象,花费时间和空间。 因而如果int,char等固有类型,而是你自己定义的类或结构等,都建议传指针或引用,因为他们不会创建新的对象。
全栈程序员站长
2022/07/02
4.6K0
C语言中函数参数传递的三种方式
C++ 引用与引用作为函数的参数
对一个数据建立一个“引用”,他的作用是为一个变量起一个别名。这是C++对C语言的一个重要补充。
chaibubble
2022/05/07
2.3K0
C++ 引用与引用作为函数的参数
C++随记(七)--引用变量
TeeyoHuang
2017/12/28
1.1K0
C++基础语法
包含了一个iostream的文件头。头文件作为一种包含功能函数、数据接口声明的载体文件,通常编译器通过头文件找到对应的函数库,把引用的函数实际内容导出来。
全栈程序员站长
2022/07/13
9580
还记得指针与引用吗?说下呗!
在C++中,我们常常使用到指针和引用,但对于它们的区别,很多C++的老手也容易混淆。
灿视学长
2021/05/28
5390
C和指针小结(C/C++程序设计)
C和指针 相关基础知识:内存的分配(谭浩强版) 1、整型变量的地址与浮点型/字符型变量的地址区别?(整型变量/浮点型变量的区别是什么) 2、int *p,指向整型数据的指针变量。 3、通过指针变量访问整型变量。 4、*p :指针变量p指向的存储单元(变量) 5、p = &a——>> *p = *&a 6、用指针作函数参数 7、调用函数中,由于虚实结合是采用单向的“值传递”方式,只能从实参向形参传数据,形参值的改变无法回传给实参。 8、引用一个数组元素可以用(1)下标法(2)指针法(占内存小,运行速度快) 9
互联网金融打杂
2018/04/03
6290
C和指针小结(C/C++程序设计)
C语言——指针(2)
前面我们已经了解了指针的基本概念以及简单的使用,那么什么问题一定要使用指针解决呢?
用户11352420
2024/11/07
1210
C语言——指针(2)
C++引用高级使用!
(4)引用声明完毕后,相当于目标变量有两个名称即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。
用户6280468
2022/03/21
5600
C++中指针与引用详解
在计算机存储数据时必须要知道三个基本要素:信息存储在何处?存储的值为多少?存储的值是什么类型?因此指针是表示信息在内存中存储地址的一类特殊变量,指针和其所指向的变量就像是一个硬币的两面。指针一直都是学习C语言的难点,在C++中又多了一个引用的概念。初学时很容易把这两个概念弄混,下面就来通过一些例子来说明二者之间的差别。
呆呆
2021/07/05
7470
【C++ 语言】引用 ( 引用简介 | 指针常量 | 常量指针 | 常引用 | 引用参数 | 引用 指针 对比 )
C++ 对 C 扩充 : 引用 ( Reference ) 概念 , 是 C++ 在 C 的基础上进行的扩充 , 在 C 语言中是没有引用的 ;
韩曙亮
2023/03/27
1.3K0
C++中引用详解
引用简介   引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。   引用的声明方法:类型标识符 &引用名=目标变量名;   【例1】:int a; int &ra=a; //定义引用ra,它是变量a的引用,即别名   说明:   (1)&在此不是求地址运算,而是起标识作用。   (2)类型标识符是指目标变量的类型。 (3)声明引用时,必须同时对其进行初始化。   (4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其
Linux云计算网络
2018/01/10
1.3K0
介绍C语言指针
引用传递是C++才有的特性,C语言只支持值传递。所以C语言只能通过传指针来达到在函数内修改函数外变量的功能。也就是swap(int &a,int &b)在C语言中是错的,swap(int *a,int *b)是对的。
_春华秋实
2018/08/17
2.3K0
介绍C语言指针
【C语言】深入解开指针(二)
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?
学习起来吧
2024/02/29
1430
【C语言】深入解开指针(二)
我的C++奇迹之旅:值和引用的本质效率与性能比较
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
学习起来吧
2024/04/09
2390
我的C++奇迹之旅:值和引用的本质效率与性能比较
C++引用分析实例与案例刨析及使用场景分析详解
可以修饰实参。本质:接收(int *const a ,int * const b) 传入(&a,&b),编译器自动把识别引用所以使用引用时只传入(a,b)即可。
CtrlX
2022/08/10
2940
C++引用分析实例与案例刨析及使用场景分析详解
相关推荐
c语言指针的基本使用
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验