面试之手撕 JS 继承

Souleigh ✨ 等级 609 1 1

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

面试之手撕 JS 继承

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

面试之手撕 JS 继承

我们创建两个实例child1,child2

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

面试之手撕 JS 继承

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

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

面试之手撕 JS 继承

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

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

面试之手撕 JS 继承

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

面试之手撕 JS 继承

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

面试之手撕 JS 继承

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

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

面试之手撕 JS 继承

原型式继承基于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) 

面试之手撕 JS 继承

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

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

六、寄生组合式继承

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

面试之手撕 JS 继承

上面说到,组合继承的问题在于会调用二次父类,造成子类原型上产生多余的同名属性。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的继承是个语法糖,本质也是基于寄生组合。这六种继承方式,其中原型链继承和构造函数继承最为基础和经典,组合继承聚合了它们二者的能力,但在某些情况下会造成错误。原型式继承和原型链相似,寄生式继承是在原型式继承基础上变化而来,它增强了原型式继承的能力。最后的寄生组合继承解决了组合继承的问题,是一种最为理想的继承方式。


收藏
评论区

相关推荐

项目实战之---AES 加密
ajax/index.js import axiosApi from '../js/fetch'; import { baseUrl, headerParams } from '../js/baseUrl'; // import引用AES源码js import CryptoJS from 'cryptojs/cryptojs'; console.lo
web性能优化的15条实用技巧
javascript在浏览器中运行的性能,可以认为是开发者所面临的最严重的可用性问题。这个问题因为javascript的阻塞性而变得复杂,事实上,多数浏览器使用单一进程来处理用户界面和js脚本执行,所以同一时刻只能做一件事。js执行过程耗时越久,浏览器等待响应的时间越长。 加载和执行 1.提高加载性能 1.IE8,FF,3.5,Safari 4和
每天学点 JS 编码规范(2):Objects
(给前端大全加星标,提升前端技能) 英文:AirBnB,翻译:Kakaka Hou 为什么要在前端大全推送这个系列? 前段时间有读者通过我的个人微信,提议能不能推送一些编程规范的文章。我们以前关注过 Airbnb 在 GitHub 上开源的 JS 编程规范(现在有10万 star,可见受欢迎程度),但因为篇幅太长,一直没有推送过。现在化整为零,设立这
面试之手撕 JS 继承
提到JS继承,你首先想到的什么? 面试 继承方式 优缺点...,js继承作为曾经的苦主,我看了忘,忘了看,看了又忘,OMG,都0212年了面试官还不放过我。 ok
js 惰性载入函数( 能力检测 )
今天在做项目时,需要对地址栏参数做处理,于是便作了如下处理 javascript getUrlParam() { let params {}; const url window.location.href.replace(/\//g,'').replace(/\?/g,'&'); url.replace(/?&(^&)
几个常用js库,别再重复造轮子了
年底了,总结下今年用到的一些有意思的《js轮子》(只是大概列出些比较有意思的库,每个标题都是超链接,可点击自行查阅) 希望能对您有用! 如有意思的 轮子 可以在评论列出一起讨论下 color(https://links.jianshu.com/go?tohttps%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fco
JS 手撕-经典面试题
引言首先出这篇文章,一方面是为了记录巩固我所学的知识,明白面试的高频考点。不鼓励大家背题的,初衷是希望总结的一些面试题能帮助你查漏补缺,温故知新。这些题并不是全部,如果你还想看得更多,可以访问,目前已经有552道大厂真题了,涵盖各类前端的真题,欢迎加入我们一起来讨论~函数 call 语法:fn.call(obj,...args) 功
前端面试题自检 JS CSS 部分
JS类型 JavaScript的简单数据类型Number , String , Boolean , Undefined , Null , Symbol typeof 操作符的返回值 number string boolean undefined object function
「组件」返回顶部按钮
样式如图1:在components文件夹下新建BackTop.vue js<template <div class"backTopBtn" <a href"javascript:;" <div vif"btnFlag" class"btn" @click"backTop"TOP</div
js去除字符串
js去除字符串js<DOCTYPE html<html<head <title</title</head<body</body<script type"text/javascript" function delHtmlTag(str){   return str.replace(/<^/g,""); } var s
js删除表格中的某一行
点击表格中的内容,删除某一行正文js代码如下 function removeTd(obj) { obj.parentNode.parentNode.remove();}
只听说过CSS in JS,怎么还有JS in CSS?
CSS in JS是一种解决css问题想法的集合,而不是一个指定的库。从CSS in JS的字面意思可以看出,它是将css样式写在JavaScript文件中,而不需要独立出.css、.less之类的文件。将css放在js中使我们更方便的使用js的变量、模块化、treeshaking。还解决了css中的一些问题,譬如:更方便解决基于状态的样式,更容易追溯依赖关
React - Fiber原理
浏览器渲染 屏幕刷新率(FPS) 浏览器的正常绘制频率是60次/秒,小于这个值时,用户会感觉到卡顿 绘制一次的称为一帧,平均每帧16.6ms 帧 每个帧的开头包括样式计算、布局和绘制 js的执行是单线程,js引擎和页面渲染引擎都占用主线程,GUI渲染和Javascript执行两者是互斥的 如果某个js任务执行时间过长,浏览器会推迟渲染,每
从 生成器 到 promise+async
本文主要讲解js中关于生成器的相关概念和作用,以及到后面结合 promise 实现 es7中的 async 原理,你将学习到js中异步流程控制相关知识 1、认识生成器思考如下代码:javascript let x 1 function foo() x++ bar() console.log(x) // 3 function bar(
javascript实践教程-02-javascript入门
本节目标1. 掌握如何编写javascript代码。2. 掌握javascript的3个弹框。3. 掌握javascript的注释。4. 掌握浏览器的调试工具控制台。 内容摘要本篇介绍了如何在网页上编写js代码,如何引入外部js代码文件,js的3个弹框、注释语法,还有浏览器调试工具的控制台使用。阅读时间1520分钟。 script标签如果我们需要在网页中编写