夯基础之手撕javascript继承详解

前言

提到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继承内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入了解javascript中的prototype与继承

    通常来说,javascript中的对象就是一个指向prototype的指针和一个自身的属性列表.javascript创建对象时采用了写时复制的理念.只有构造器才具有prototype属性,原型链继承就是创建一个新的指针,指向构造器的prototype属性.prototype属性之所以特别,是因为javascript时读取属性时的遍历机制决定的.本质上它就是一个普通的指针. 构造器包括: 1.Object2.Function3.Array4.Date5.String 下面我们来举一些例子吧 复制代

  • 前端开发必须知道的JS之原型和继承

    一. 原型与构造函数 Js所有的函数都有一个prototype属性,这个属性引用了一个对象,即原型对象,也简称原型.这个函数包括构造函数和普通函数,我们讲的更多是构造函数的原型,但是也不能否定普通函数也有原型.譬如普通函数: 复制代码 代码如下: function F(){ alert(F.prototype instanceof Object) //true; } 构造函数,也即构造对象.首先了解下通过构造函数实例化对象的过程. 复制代码 代码如下: function A(x){ this.x

  • JavaScript是如何实现继承的(六种方式)

    前言:大多OO语言都支持两种继承方式: 接口继承和实现继承 ,而ECMAScript中无法实现接口继承,ECMAScript只支持实现继承,而且其实现继承主要是依靠 原型链 来实现. 1.原型链 基本思想:利用原型让一个引用类型继承另外一个引用类型的属性和方法. 构造函数,原型,实例之间的关系:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针. 原型链实现继承例子: function SuperType() { this.property

  • js的2种继承方式详解

    js中继承可以分为两种:对象冒充和原型链方式 一.对象冒充包括三种:临时属性方式.call()及apply()方式1.临时属性方式 复制代码 代码如下: function Person(name){     this.name = name;     this.say = function(){          alert('My name is '+this.name);     }}function F2E(name,id){     this.temp = Person;     thi

  • JS继承--原型链继承和类式继承

    什么是继承啊?答:别人白给你的过程就叫继承. 为什么要用继承呢?答:捡现成的呗. 好吧,既然大家都想捡现成的,那就要学会怎么继承! 在了解之前,你需要先了解构造函数.对象.原型链等概念...... JS里常用的两种继承方式: 原型链继承(对象间的继承)类式继承(构造函数间的继承) 原型链继承: 复制代码 代码如下: //要继承的对象var parent={name : "baba" say : function(){ alert("I am baba");}} //

  • 不错的一篇关于javascript-prototype继承

    1.最基本的用法 把ClassA的一个实例赋值给ClassB, ClassB就继承了ClassA的所有属性. 代码入下: function ClassA() { this.a='a'; } function ClassB() { this.b='b'; } ClassB.prototype=new ClassA(); var objB=new ClassB(); for(var p in objB)document.write(p+" "); [Ctrl+A 全选 注:如需引入外部Js

  • 实现JavaScript中继承的三种方式

    一.原型链继承 在原型链继承方面,JavaScript与java.c#等语言类似,仅允许单父类继承.prototype继承的基本方式如下: 复制代码 代码如下: function Parent(){} function Child(){} Child.prototype = new Parent(); 通过对象Child的prototype属性指向父对象Parent的实例,使Child对象实例能通过原型链访问到父对象构造所定义的属性.方法等. 构造通过原型链链接了父级对象,是否就意味着完成了对象

  • js中继承的几种用法总结(apply,call,prototype)

    一,js中对象继承 js中有三种继承方式 1.js原型(prototype)实现继承 复制代码 代码如下: <SPAN style="BACKGROUND-COLOR: #ffffff"><SPAN style="FONT-SIZE: 18px"><html>  <body>  <script type="text/javascript">      function Person(na

  • js实现继承的5种方式

    本文实例讲述了js实现继承的5种方式.分享给大家供大家参考,具体如下: 1.继承第一种方式:对象冒充 function Parent(username){ this.username = username; this.hello = function(){ alert(this.username); } } function Child(username,password){ //通过以下3行实现将Parent的属性和方法追加到Child中,从而实现继承 //第一步:this.method是作为

  • Javascript基于对象三大特性(封装性、继承性、多态性)

    Javascript基于对象的三大特征和C++,Java面向对象的三大特征一样,都是封装(encapsulation).继承(inheritance )和多态(polymorphism ).只不过实现的方式不同,其基本概念是差不多的.其实除三大特征之外,还有一个常见的特征叫做抽象(abstract),这也就是我们在一些书上有时候会看到面向对象四大特征的原因了. 一.封装性     封装就是把抽象出来的数据和对数据的操作封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(成员方法),

随机推荐