各种实现js继承的方法总结小白帮助_继承入门攻略

一、原型链学过java的同学应该都知道,继承是java的重要特点之一,许多面向对象的语言都支持两种继承方式:接口继承和实现继承,接口继承只继承方法签名,而实现继承则继承实际的方法,在js中,由于函数没有签名,因此支持实现继承,而实现继承主要是依靠原型链来实现的,那么,什么是原型链呢?首先,我们先来回顾一下构造函数,原型和实例之间的关系当我们创建一个构造函数时

各种实现js继承的方法总结小白帮助

一、原型链

各种实现js继承的方法总结小白帮助_继承入门攻略

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

首先,我们先来回顾一下构造函数,原型和实例之间的关系

当我们创建一个构造函数时,构造函数会获得一个prototype属性,该属性是一个指针,指向一个原型对象,原型对象包含一个constructor属性,该属性也是一个指针,指向构造函数,而当我们创建构造函数的实例时,该实例其实会获得一个[[Prototype]]属性,指向原型对象

function SubType() {}

var instance = new SubType();

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

各种实现js继承的方法总结小白帮助_继承入门攻略

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

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

function SuperType() {}

function SubType() {}

SubType.prototype = new SuperType();    

var instance = new SubType();

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

各种实现js继承的方法总结小白帮助_继承入门攻略

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

 

 二、继承

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

1、原型链继承

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

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()方法,在子类型构造函数的内部调用超类型构造函数,从而解决该问题

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实例中的,修改其中一个不会影响另一个

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

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、组合继承

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

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、原型式继承

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

function Object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

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

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

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、寄生式继承

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

function createAnother(original) {
    var clone = Object(original);      //  通过调用函数创建一个新对象
    clone.sayHi = function() {
        console.log("hi");
    }
    return clone;
}

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

var person = {
    name: "Nicholas",
    friends:["Shelby","Court","Van"]
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi();      //  hi

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

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

 

6、寄生组合式继承

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

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属性

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

function inheritPrototype(subType,SuperType) {
    var prototype = Object(SuperType);      //  创建对象
    prototype.constructor = subType;          //  增强对象
    subType.prototype = prototype;            //  指定对象
}

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

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 的通过修改原型链实现继承,要清晰和方便很多

class Point {
}

class ColorPoint extends Point {
}

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

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

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方法之后就是正确的,正确的继承之后,我们就可以创建实例了

let cp = new ColorPoint(25, 8, 'green');

cp instanceof ColorPoint // true
cp instanceof Point // true

对于es6的继承,如果大家想继续了解,可以进行更进一步的学习

来自:https://www.cnblogs.com/Yellow-ice/archive/2019/03/04/10473176.html

海计划公众号
(0)
上一篇 2020/03/31 01:49
下一篇 2020/03/31 01:49

您可能感兴趣的内容

  • C3.js小白指南_基于D3.js开发的JavaScript库

    C3.js指南教程 官方网址:http://c3js.org GitHub:https://github.com/c3js/c3 简介描述:基于D3.js开发的JavaScript…

    2020/03/06
  • 互联网上有多少个网站?入门教程_网站使用帮助

    在短短的十来年中,网站数量已从Internet上的9,200万增加到超过10亿。互联网不再仅仅是寻找信息或休闲娱乐的场所,也不只是一种工作工具,互联网就是金钱,是社会的变革。而且它的增长是残酷的,互联网上的零售额每年增加数十亿美元,再加上通过在线查看产品或服务并说服人们去购物而形成的实体商店的销售额!拥有超过十亿个网页和各种Web应用程序,您的潜在客户将如何

    2020/03/21
  • Vue双向绑定原理-数据劫持和发布订阅指南攻略_原理指南教程

    有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了。本文我们一起通过学习双向绑定原理来分析Vue源码。预计接下来会围绕Vue源码来整理一些文章,统一放在我的git仓库:https://github.com/yzsunlei/javascript-series-code-analyzing。觉得有用记得star收藏。简单应用我们先来看一个简单的应

    2020/03/24
  • Processing.js菜鸟指南_擅长创建2D和3D图象,可视化数据套件,音频,视频等

    Processing.js使用说明 官方网址:http://processingjs.org/ GitHub:https://github.com/processing-js/pr…

    2020/03/06
  • 解决Js中处理null和undefined的麻烦事教程视频_null使用帮助

    许多 JavaScript 开发人员正在为怎么处理可选值头痛。有什么好办法来最大程度地减少由值(可能为 null、undefined或在运行时未初始化)引起的错误?在某些情况下,一些语言具有内置功能。在某些静态类型的语言中,你可以说 null 和 undefined 是非法值,并且让你的编程语言在编译时抛出 TypeError,但是即使在那些语言中,也无法防

    2020/03/23
  • vue 全局前置守卫引起死循环的原因与解决方法基础教程_循环小白攻略

    我们经常会用到全局前置守卫,如判断用户有没有登陆过,如果登陆过就直接跳到目的页面,如果没有登陆过,就跳转到登陆页。先看官网对全局前置守卫的介绍使用 router.beforeEach 注册一个全局前置守卫:const router = new VueRouter({ … })router.beforeEach((to, from, next) => {/

    2020/03/20
  • Js栈的实现与应用基础知识入门_数据小白入门

    在计算机编程中,栈是一种很常见的数据结构,它遵从后进先出(LIFO——Last In First Out)原则,新添加或待删除的元素保存在栈的同一端,称作栈顶,另一端称作栈底。在栈中,新元素总是靠近栈顶,而旧元素总是接近栈底。让我们来看看在JavaScript中如何实现栈这种数据结构。function Stack() { let items = [];

    2020/03/29
  • 雪碧图工具基础知识减少http请求数,会将大量的图片图片合成一张雪碧图(Sprite)来使用

    雪碧图工具基础入门 官方网址:https://wearekiss.com/spritepad 简介描述:减少http请求数,会将大量的图片图片合成一张雪碧图(Sprite)来使用

    2020/03/05
  • 这些自动化运维技巧让网络运维不再背锅菜鸟攻略_运维指南攻略

    引言“网络就像wifi,没有故障的时候,就没有人意识到它的存在”,这句话有无数的翻版,但是对于网络工程师来说,这就是现身说法。由于即便是在上千人的公司,网络工程师的人数也仅仅是个位数,所以他们的工作也鲜为人知 。“网络是不是有问题?”这句话几乎成了所有SRE排错时的口头禅,如果这个时候网络工程师表示沉默,或者无法拿出足够的证据,那背锅几乎是无疑的。如何让网络

    2020/03/24
  • 提高效率的 Linux 命令别名菜鸟教程下载_命令入门教程

    在 Linux 环境下工作的工程师,一定会对那些繁琐的指令和参数命令行印象深刻吧。而且,可怕的不是繁琐,而是需要大量重复输入这些繁琐的命令。在 Linux 下我们有个别名命令 alias ,可以将那些繁琐的命令自定义为我们容易记住的别名,可以大大提高我们的效率。但是,alias 命令只对当前终端有效,当终端关闭之后,我们所设置的别名全部失效。所以如果想让这些

    2020/03/30
  • Spring常犯的十大错误使用帮助_错误小白帮助

    1. 错误一:太过关注底层我们正在解决这个常见错误,是因为 “非我所创” 综合症在软件开发领域很是常见。症状包括经常重写一些常见的代码,很多开发人员都有这种症状。虽然理解特定库的内部结构及其实现,在很大程度上是好的并且很有必要的(也可以是一个很好的学习过程),但作为软件工程师,不断地处理相同的底层实现细节对个人的开发生涯是有害的。像 Spring 这种抽象框

    2020/03/26
  • jquery使用on()方法绑定的事件被执行多次的问题指南攻略_事件小白入门

    jQuery用on()方法绑定了事件之后,在代码执行过程中,可能会遇到事件被多次执行的情况。本来以为是事件冒泡的问题,后来发现是on()方法的特性引起的问题。简单还原一下问题的场景这里简单还原一下问题的场景,使用一个按钮给另一个按钮通过on()方法绑定事件。HTML的部分<input id="bindEventBtn" type="button" value

    2020/03/23
  • Nginx服务器 之反向代理与负载均衡入门基础教程_nginx指南教程

    一、反向代理正向代理:客户端要获取的资源就在服务器上,客户端请求的资源路径就是最终响应资源的服务器路径,这就是正向代理。正向代理的特点:就是我们明确知道要访问哪个网站地址。反向代理: 客户端想获取服务器集群中(服务1,服务2,服务3 他们的资源相同)中的资源,但是客户端无法与该服务器集群建立连接,但我们可以与另一台服务器(代理服务器)建立连接且该服务器能获取

    2020/03/31
  • h5落地页中的设计和优化基础知识_落地页入门教程

    广告投放的目标就是转化用户。投放效果的好坏关键在于目标用户的分析,产品或者服务的包装,导入图(导入标题)和落地页的设计和优化。1、导入图、落地页广告的素材一般包括导入图(导入标题)和落地页。流程是用户看到广告导入图,产生好奇点击广告导入图跳转到的落地页,产生好感采取了行动,那么这就是一个转化了。不同的媒体,由于广告位不同,导入图可以是广告小图、广告组图,或者

    2020/03/30
  • 工作中最常用的Git 命令收藏菜鸟教程_命令使用帮助

    分支操作git branch 创建分支git branch -b 创建并切换到新建的分支上git checkout 切换分支git branch 查看分支列表git branch -v 查看所有分支的最后一次操作git branch -vv 查看当前分支git brabch -b 分支名 origin/分支名 创建远程分支到本地git branch –me

    2020/03/23
  • css如何实现保持div等高宽比?菜鸟知识_宽高小白攻略

    那么css如何实现高度height随宽度width变化保持比例不变呢?即给定可变宽度的元素,它将确保其高度以响应的方式保持成比例(即,其宽度与高度的比率保持恒定)。下面以高宽 2:1 为例,通过2种方式来实现这种效果。方式一:利用定位实现.wrapper{position : relative;background: #ccc;width: 10%;padd

    2020/03/21