夯基础之手撕javascript继承详解


Posted in Javascript onNovember 09, 2020

前言

提到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没有自己的实例属性,它访问的是它的原型对象。

夯基础之手撕javascript继承详解

我们创建两个实例child1,child2

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

夯基础之手撕javascript继承详解

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

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

夯基础之手撕javascript继承详解

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

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

夯基础之手撕javascript继承详解

如果进行了重新赋值,会添加到到实例属性上,和原型上到同名属性便无关了,所以并不会影响到原型。这次我们采用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)

夯基础之手撕javascript继承详解

我们打印了一下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)

夯基础之手撕javascript继承详解

我们更新了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)

夯基础之手撕javascript继承详解

原型式继承基于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继承内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JavaScript实际应用:innerHTMl和确认提示的使用
Jun 22 Javascript
JS下拉缓冲菜单示例代码
Aug 30 Javascript
js操作输入框提示信息且响应鼠标事件
Mar 25 Javascript
jquery重复提交请求的原因浅析
May 23 Javascript
jquery实现类似EasyUI的页面布局可改变左右的宽度
Sep 12 Javascript
window.location.hash知识汇总
Nov 09 Javascript
浅谈JavaScript中的this指针和引用知识
Aug 05 Javascript
angularjs点击图片放大实现上传图片预览
Feb 24 Javascript
webpack-dev-server远程访问配置方法
Feb 22 Javascript
JavaScript设计模式之建造者模式实例教程
Jul 02 Javascript
Node.js console控制台简单用法分析
Jan 04 Javascript
vue 集成 vis-network 实现网络拓扑图的方法
Aug 07 Javascript
vue实现动态表格提交参数动态生成控件的操作
Nov 09 #Javascript
es5 类与es6中class的区别小结
Nov 09 #Javascript
vue实现标签云效果的示例
Nov 09 #Javascript
写一个Vue loading 插件
Nov 09 #Javascript
解决Vue大括号字符换行踩的坑
Nov 09 #Javascript
vue data有值,但是页面{{}} 取不到值的解决
Nov 09 #Javascript
vue 防止页面加载时看到花括号的解决操作
Nov 09 #Javascript
You might like
我用php+mysql写的留言本
2006/10/09 PHP
PHP实现提取一个图像文件并在浏览器上显示的代码
2012/10/06 PHP
解析WordPress中的post_class与get_post_class函数
2016/01/04 PHP
Laravel修改验证提示信息为中文的示例
2019/10/23 PHP
Javascript 键盘keyCode键码值表
2009/12/24 Javascript
Javascript类定义语法,私有成员、受保护成员、静态成员等介绍
2011/12/08 Javascript
js修改地址栏URL参数解决url参数问题
2012/12/15 Javascript
getComputedStyle与currentStyle获取样式(style/class)
2013/03/19 Javascript
JS 对输入框进行限制(常用的都有)
2013/07/30 Javascript
百度移动版的url编码解码示例
2014/04/29 Javascript
NodeJS学习笔记之(Url,QueryString,Path)模块
2015/01/13 NodeJs
js中substr,substring,indexOf,lastIndexOf,split,replace的用法详解
2015/11/09 Javascript
jquery实现简单的全选和反选功能
2016/01/02 Javascript
基于Bootstrap实现tab标签切换效果
2020/04/15 Javascript
Javascript中的数组常用方法解析
2016/06/17 Javascript
使用AngularJS 跨站请求如何解决jsonp请求问题
2017/01/16 Javascript
JavaScript限制在客户区可见范围的拖拽(解决scrollLeft和scrollTop的问题)(2)
2017/05/17 Javascript
利用webstrom调试Vue.js单页面程序的方法教程
2017/06/06 Javascript
利用JS hash制作单页Web应用的方法详解
2017/10/10 Javascript
JS实现带导航城市列表以及输入搜索功能
2018/01/04 Javascript
vue-cli webpack2项目打包优化分享
2018/02/07 Javascript
基于Vuex无法观察到值变化的解决方法
2018/03/01 Javascript
用vue2.0实现点击选中active其他选项互斥的效果
2018/04/12 Javascript
如何为vuex实现带参数的 getter和state.commit
2019/01/04 Javascript
python使用urllib2提交http post请求的方法
2015/05/26 Python
python生成二维码的实例详解
2017/10/29 Python
python实现嵌套列表平铺的两种方法
2018/11/08 Python
OpenCV 表盘指针自动读数的示例代码
2020/04/10 Python
CSS3实现大小不一的粒子旋转加载动画
2016/04/21 HTML / CSS
英国计算机产品零售商:Novatech(定制个人电脑、笔记本电脑、工作站和服务器)
2018/01/28 全球购物
本科生详细的自我评价
2013/09/19 职场文书
保护环境倡议书
2014/04/14 职场文书
教师廉洁自律承诺书
2014/05/26 职场文书
小时代观后感
2015/06/10 职场文书
爱的教育读书笔记
2015/06/26 职场文书
vue的项目如何打包上线
2022/04/13 Vue.js