夯基础之手撕javascript继承详解

 更新时间:2020年11月10日 09:38  点击:2318

前言

提到JS继承,你首先想到的什么? 面试 继承方式 优缺点...,js继承作为曾经的苦主,我看了忘,忘了看,看了又忘,OMG,都9012年了面试官还不放过我。

ok,开开玩笑,接下来言归正传,来聊聊js继承这个经典的话题。

JS的“类”

javascript不像java,php等传统的OOP语言,js本身并没有类这个概念,那么它是怎么实现类的模拟呢?

  1. 构造函数方式
  2. 原型方式
  3. 混合方式

构造函数方式

Function Foo (name) {
 this.name = name
 this.like = function () {
 console.log(`like${this.name}`)
 }
}
let foo = new Foo('bibidong')

像这样就是通过构造函数的方式来定义类,其实和普通函数一样,但为了和常规函数有个区分,一般把函数名首字母大写。

缺点:无法共享类的方法。

原型方式

function Foo (name) {}
Foo.prototype.color = 'red'
Foo.prototype.queue = [1,2,3]
let foo1 = new Foo()
let foo2 = new Foo()

foo1.queue.push(4)
console.log(foo1) // [1, 2, 3, 4]
console.log(foo2) // [1, 2, 3, 4]

我们通过原型方式直接把属性和方法定义在了构造函数的原型对象上,实例可以共享这些属性和方法,解决了构造函数方式定义类的缺点。

缺点:可以看到我们改变了foo1的数据,结果foo2的queue属性也变了,这便是原型方式最大的问题,引用类型的属性会被其它实例修改。除此之外,这种方式下也无法传参。

混合方式

function Foo (name) { // 属性定义在构造函数里面
 this.name = name
 this.color = 'red'
 this.queue = [1,2,3]
}
Foo.prototype.like = function () { // 方法定义在原型上
 console.log(`like${this.name}`)
}
let foo1 = new Foo()
let foo2 = new Foo()

所谓混合模式,便是把上面两种方式混合起来,我们在构造函数里面定义属性,在原型对象上定义要共享的方法,既能传参,也避免了原型模式的问题。

小结一下:js类的能力是模拟出来的,可以通过构造函数方式,原型方式来定义,混合模式则聚合了前两者的优点。除此,还有Object.create(), es6的class,都可以来创建对象,定义类。

常见的继承方式

一、原型链继承

基于原型链查找的特点,我们将父类的实例作为子类的原型,这种继承方式便是原型链继承。

function Parent () {
 this.color = 'red'
 this.queue = [1,2,3]
}
Parent.prototype.like = function () {
 console.log('')
}

function Child () { }
Child.prototype = new Parent() // constructor指针变了 指向了Parent
Child.prototype.constructor = Child // 手动修复

let child = new Child()

Child.prototype相当于是父类Parent的实例,父类Parent的实例属性被挂到了子类的原型对象上面,拿color属性举个例子,相当于就是这样

Child.prototype.color = 'red'

这样父类的实例属性都被共享了,我们打印一下child,可以看到child没有自己的实例属性,它访问的是它的原型对象。

我们创建两个实例child1,child2

let child1 = new Child()
let child2 = new Child()
child1.color = 'bulr'
console.log(child1)
console.log(child2)

我们修改了child1的color属性,child2没有受到影响,并非是其它实例拥有独立的color属性,而是因为这个color属性直接添加到了child1上面,它原型上的color并没有动,所以其它实例不会受到影响从打印结果也可以清楚看到这一点。那如果我们修改的属性是个引用类型呢?

child1.queue = [1,2,3,'我被修改了'] // 重新赋值
child1.like = function () {console.log('like方法被我修改了')}
console.log(child1)
console.log(child2)

我们重写了引用类型的queue属性和like方法,其实和修改color属性是完全一样的,它们都直接添加到了child1的实例属性上。从打印结果能看到这两个属性已经添加到了child1上了,而child2并不会受到影响,再来看下面这个。

child1.queue.push('add push') // 这次没有重新赋值
console.log(child1)
console.log(child2)

如果进行了重新赋值,会添加到到实例属性上,和原型上到同名属性便无关了,所以并不会影响到原型。这次我们采用push方法,没有开辟新空间,修改的就是原型。child2的queue属性变化了,子类Child原型上的queue属性被实例修改,这样肯定就影响到了所有实例。

缺点

  • 子类的实例会共享父类构造函数引用类型的属性
  • 创建子类实例的时候无法传参

二、构造函数式继承

相当于拷贝父类的实例属性给子类,增强了子类构造函数的能力

function Parent (name) {
 this.name = name
 this.queue = [1,2,3]
}
Parent.prototype.like = function () {
 console.log(`like${this.name}`)
}

function Child (name) {
 Parent.call(this, name) // 核心代码
}

let child = new Child(1)

我们打印了一下child,可以看到子类拥有父类的实例属性和方法,但是child的__proto__上面没有父类的原型对象。解决了原型链的两个问题(子类实例的各个属性相互独立、还能传参)

缺点

  • 子类无法继承父类原型上面的方法和属性。
  • 在构造函数中定义的方法,每次创建实例都会再创建一遍。

三、组合继承

人如其名,组合组合,一定把什么东西组合起来。没错,组合继承便是把上面两种继承方式进行组合。

function Parent (name) {
 this.name = name
 this.queue = [1,2,3]
}
Parent.prototype.like = function () {
 console.log(`like${this.name}`)
}

function Child (name) {
 Parent.call(this, name)
}

Child.prototype = new Parent()
Child.prototype.constructor = Child  // 修复constructor指针
let child = new Child('')

接下来我们做点什么,看它组合后能不能把原型链继承和构造函数继承的优点发扬光大

let child1 = new Child('bibidong')
let child2 = new Child('huliena')
child1.queue.push('add push')
console.log(child1)
console.log(child2)

我们更新了child1的引用属性,发现child2实例没受到影响,原型上的like方法也在,不错,组合继承确实将二者的优点发扬光大了,解决了二者的缺点。组合模式下,通常在构造函数上定义实例属性,在原型对象上定义要共享的方法,通过原型链继承方法让子类继承父类构造函数原型上的方法,通过构造函数继承方法子类得以继承构造函数的实例属性,是一种功能上较完美的继承方式。

缺点:父类构造函数被调用了两次,第一次调用后,子类的原型上拥有了父类的实例属性,第二次call调用复制了一份父类的实例属性作为子类Child的实例属性,那么子类原型上的同名属性就被覆盖了。虽然被覆盖了功能上没什么大问题,但这份多余的同名属性一直存在子类原型上,如果我们删除实例上的这个属性,实际上还能访问到,此时获取到的是它原型上的属性。

Child.prototype = new Parent() // 第一次构建原型链
Parent.call(this, name) // 第二次new操作符内部通过call也执行了一次父类构造函数

四、原型式继承

将一个对象作为基础,经过处理得到一个新对象,这个新对象会将原来那个对象作为原型,这种继承方式便是原型式继承,一句话总结就是将传入的对象作为要创建的新对象的原型。

先写下这个有处理能力的函数

function prodObject (obj) {
 function F (){
  
 }
 F.prototype = obj
 return new F() // 返回一个实例对象
}

这也是Object.create()的实现原理,所以用Object.create直接替换prodObject函数是ok的

这也是Object.create()的实现原理,所以用Object.create直接替换prodObject函数是ok的

let base = {
 color: 'red',
 queue: [1, 2, 3]
}
let child1 = prodObject(base)
let child2 = prodObject(base)
console.log(child1)
console.log(child2)

原型式继承基于prototype,和原型链继承类似,这种继承方式下实例没有自己的属性值,访问到也是原型上的属性。

缺点:同原型链继承

五、寄生式继承

原型式继承的升级,寄生继承封装了一个函数,在内部增强了原型式继承产生的对象。

function greaterObject (obj) {
 let clone = prodObject(obj)
 clone.queue = [1, 2, 3]
 clone.like = function () {}
 return clone
}
let parent = {
 name: 'bibidong',
 color: ['red', 'bule', 'black']
}
let child = greaterObject(parent)

打印了一下child,它的缺点也很明显了,寄生式继承增强了对象,却也无法避免原型链继承的问题。

缺点

  • 拥有原型链继承的缺点
  • 除此,内部的函数无法复用

六、寄生组合式继承

大招来了,寄生组合闪亮登场!

上面说到,组合继承的问题在于会调用二次父类,造成子类原型上产生多余的同名属性。Child.prototype = new Parent(),那这行代码该怎么改造呢?

我们的目的是要让父类的实例属性不出现在子类原型上,如果让Child.prototype = Parent.prototype,这样不就能保证子类只挂载父类原型上的方法,实例属性不就没了吗,代码如下,看起来好像是简直不要太妙啊。

function Parent (name) {
 this.name = name
 this.queue = [1,2,3]
}
Parent.prototype.like = function () {
 console.log(`like${this.name}`)
}

function Child (name) {
 Parent.call(this, name)
}

Child.prototype = Parent.prototype // 只改写了这一行
Child.prototype.constructor = Child
let child = new Child('')

回过神突然发现改写的那一行如果Child.prototype改变了,那岂不是直接影响到了父类,举个栗子

Child.prototype.addByChild = function () {}
Parent.prototype.hasOwnProperty('addByChild') // true

addByChild方法也被加到了父类的原型上,所以这种方法不够优雅。同样还是那一行,直接访问到Parent.prototype存在问题,那我们可以产生一个以Parent.prototype作为原型的新对象,这不就是上面原型式继承的处理函数prodObject吗

Child.prototype = Object.create(Parent.prototype) // 改为这样

这样就解决了所有问题,我们怕改写Child.prototype影响父类,通过Object.create返回的实例对象,我们将Child.prototype间接指向Parent.prototype,当再增加addByChild方法时,属性就和父类没关系了。

寄生组合式继承也被认为是最完美的继承方式,最推荐使用。

总结

js的继承方式主要就这六种,es6的继承是个语法糖,本质也是基于寄生组合。这六种继承方式,其中原型链继承和构造函数继承最为基础和经典,组合继承聚合了它们二者的能力,但在某些情况下会造成错误。原型式继承和原型链相似,寄生式继承是在原型式继承基础上变化而来,它增强了原型式继承的能力。最后的寄生组合继承解决了组合继承的问题,是一种最为理想的继承方式。

到此这篇关于夯基础之手撕javascript继承的文章就介绍到这了,更多相关手撕js继承内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!

[!--infotagslink--]

相关文章

  • JavaScript判断浏览器及其版本信息

    本篇文章主要分享了通过window.navigator来判断浏览器及其版本信息的实例代码。具有一定的参考价值,下面跟着小编一起来看下吧...2017-01-23
  • js实现浏览器打印功能的示例代码

    这篇文章主要介绍了js如何实现浏览器打印功能,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下...2020-07-15
  • 利用JS实现点击按钮后图片自动切换的简单方法

    下面小编就为大家带来一篇利用JS实现点击按钮后图片自动切换的简单方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2016-10-25
  • 详解前端安全之JavaScript防http劫持与XSS

    作为前端,一直以来都知道HTTP劫持与XSS跨站脚本、CSRF跨站请求伪造。防御这些劫持最好的方法是从后端入手,前端能做的太少。而且由于源码的暴露,攻击者很容易绕过防御手段。但这不代表我们去了解这块的相关知识是没意义的,本文的许多方法,用在其他方面也是大有作用。...2021-05-24
  • Nest.js参数校验和自定义返回数据格式详解

    这篇文章主要给大家介绍了关于Nest.js参数校验和自定义返回数据格式的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-28
  • JavaScript仿支付宝密码输入框

    那么今天我就用JavaScript代码来实现这个效果吧,那么首先介绍一下整个的思路,首先我们先将确定输入密码的位数,我的需求是5位,那么就用一个div标签包住5个input标签...2016-01-02
  • js实现上传图片及时预览

    这篇文章主要为大家详细介绍了js实现上传图片及时预览的相关资料,具有一定的参考价值,感兴趣的朋友可以参考一下...2016-05-09
  • js+css实现回到顶部按钮(back to top)

    这篇文章主要为大家详细介绍了js+css实现回到顶部按钮back to top回到顶部按钮,感兴趣的小伙伴们可以参考一下...2016-03-03
  • 一个关于JS正则匹配的踩坑记录

    这篇文章主要给大家介绍了一个关于JS正则匹配的踩坑记录,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-04-13
  • 如何使用JavaScript实现无缝滚动自动播放轮播图效果

    这篇文章主要介绍了如何使用JavaScript实现“无缝滚动 自动播放”轮播图效果,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-08-20
  • js实现调用网络摄像头及常见错误处理

    这篇文章主要介绍了js实现调用网络摄像头及常见错误处理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-07
  • JS实现随机生成验证码

    这篇文章主要为大家详细介绍了JS实现随机生成验证码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-06
  • js实现列表按字母排序

    这篇文章主要为大家详细介绍了js实现列表按字母排序,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-08-11
  • js组件SlotMachine实现图片切换效果制作抽奖系统

    这篇文章主要介绍了js组件SlotMachine实现图片切换效果制作抽奖系统的相关资料,需要的朋友可以参考下...2016-04-19
  • 基于JavaScript实现文字超出部分隐藏

    这篇文章主要介绍了基于JavaScript实现文字超出部分隐藏 的相关资料,需要的朋友可以参考下...2016-03-01
  • js屏蔽F12审查元素,禁止修改页面代码等实现代码

    有时候我们需要屏蔽客户端的F12,以防菜鸟也可以随意修改我们的代码,也处于源码的保护等操作,这里就为大家分享一下常见的代码...2020-10-03
  • JS创建Tag标签的方法详解

    这篇文章主要介绍了JS创建Tag标签的方法,结合具体实例形式分析了javascript动态操作页面HTML元素实现tag标签功能的步骤与相关操作技巧,需要的朋友可以参考下...2017-06-15
  • vue.js 表格分页ajax 异步加载数据

    Vue.js通过简洁的API提供高效的数据绑定和灵活的组件系统.这篇文章主要介绍了vue.js 表格分页ajax 异步加载数据的相关资料,需要的朋友可以参考下...2016-10-20
  • 基于JavaScript实现表单密码的隐藏和显示出来

    为了网站的安全性,很多朋友都把密码设的比较复杂,但是如何密码不能明显示,不知道输的是对是错,为了安全起见可以把密码显示的,那么基于js代码如何实现的呢?下面通过本文给大家介绍JavaScript实现表单密码的隐藏和显示,需要的朋友参考下...2016-03-03
  • JS实现响应鼠标点击动画渐变弹出层效果代码

    这篇文章主要介绍了JS实现响应鼠标点击动画渐变弹出层效果代码,具有非常自然流畅的动画过度效果,涉及JavaScript针对鼠标事件的响应及页面元素样式的动态操作相关技巧,需要的朋友可以参考下...2016-03-28