JavaScript设计模式2-多种继承方式的实现及原理

37次阅读

共计 5107 个字符,预计需要花费 13 分钟才能阅读完成。

原文出自于本人个人博客网站:https://www.dzyong.com(欢迎访问)

本文链接地址: https://www.dzyong.com/#/View…(转载请标注来源)

什么是继承

继承是面向对象软件技术当中的一个概念。如果一个类别 A“继承自”另一个类别 B,就把这个 A 称为“B 的子类别”,而把 B 称为“A 的父类别”也可以称“B 是 A 的超类”。继承可以使得子类别具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类别追加新的属性和方法也是常见的做法。
一般静态的面向对象编程语言,继承属于静态的,意即在子类别的行为在编译期就已经决定,无法在执行期扩充

了解了什么是继承后,接下来看一下在 JavaScript 实现继承的 6 种方式

子类的原型对象——类式继承

let superClass = function(){
        this.superVal = true
        this.books = ['a', 'b', 'c']
    }
    superClass.prototype.getSuperVal = function(){return this.superVal}
    let sup = new superClass()
    // 声明子类 1
    let subClass = function(){this.subVal = false}
    subClass.prototype = new superClass()
    subClass.prototype.getSuperVal = function(){this.subVal}
  }

类式继承就是声明两个类,只不过是把第一个类的实例赋值给第二个类的原型。

类的原型对象的作用就是为类的原型添加共有的方法,但类不能直接访问这些属性和方法,必须通过原型 prototype 来访问。而我们实例化一个父类的时候,新创建的对象复制了父类的构造函数与方法并将原型_proto_指向了父类的原型对象,这样就拥有了父类原型对象上的属性和方法,并且这个新创建的对象可直接访问到父类原型对象上的属性与方法。

let sub = new subClass1()
console.log(sub.superVal)   //true
console.log(sub.getSuperVal)   //false

我们可以使通过 instanceof 来检测某个对象是否是某个类的实例,instanceof 是通过判断对象的 prototype 链来确定关系的,而不关心对象与类的自身结构。

console.log(sub instanceof superClass)    //true
console.log(sub instanceof subClass)    //true
console.log(subClass instanceof superClass)    //false

一定要注意:instanceof 是判断前面的对象是否是后面类(对象)的实例,并不表示两者的继承关系。

console.log(subClass.prototype instanceof superClass)    //true

Object 是所有对象的祖先

console.log(sub instanceof Object)    //true

但是类式继承有两个缺点:(1)由于子类是通过其原型 prototype 对父类的实例化,继承了父类。所以说父类中的共有属性要是引用类型,就会在子类被所有实例共用,因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其他子类。

let sub1 = new subClass()
let sub2 = new subClass()
console.log(sub1.books)   //['a', 'b', 'c']
sub2.books.push('d')
console.log(sub1.books)   ////['a', 'b', 'c', 'd']

sub2 的修改影响到了 sub1 的 book 属性。(2)由于子类实现继承是靠 prototype 对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而是实例化父类的时候也无法对父类构造函数内的属性进行初始化。

创建即继承——构造函数继承

let superClass = function(id){this.books = ['JavaScript', 'html', 'css']
    this.id = id
}
superClass.prototype.showBooks = function(){console.log(this.books)
}
let subClass = function(id){superClass.call(this, id)
}
let instance1 = new subClass(10)
let instance2 = new subClass(11)
instance1.books.push('设计模式')
console.log(instance1.books)  //["JavaScript", "html", "css", "设计模式"]
console.log(instance2.books)  //["JavaScript", "html", "css"]

call 这个方法可以更改函数的作用环境,对 superClass 调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给 this 绑定属性的,因此子类自然就继承了父类的共有属性。

这种类型的继承没有涉及原型 prototype,所以父类的原型方法自然不会被子类继承,而如果想被子类继承就必须要放在构造函数中,这样创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用原则。为了综合这两种模式的有点,后来有了组合式继承。

将优点为我所用——组合继承
组合继承顾名思义就是将上面讲的两种继承方式组合起来用,综合各自的优点。

let supClass = function(name){
    this.name = name
    this.books = ['JavaScript', 'html', 'css']
}
supClass.prototype.showBooks = function(){console.log(this.books)
}
let subClass = function(name, time){
    this.time = time
    supClass.call(this, name)
}
subClass.prototype = new supClass()
let instance1 = new subClass('dzy', 2018) 
let instance2 = new subClass('hxy', 2019) 
console.log(instance1.name, instance1.time)   //dzy 2018
console.log(instance2.name, instance2.time)   //hxy 2019
instance1.books.push('设计模式')
instance1.showBooks()   //["JavaScript", "html", "css", "设计模式"]
instance2.showBooks()   //["JavaScript", "html", "css"]

在子类构造函数中执行父类构造函数,在子类原型上实例化父类就是组合模式,这样就融合了类式继承和构造函数继承的优点。

洁净的继承者——原型式继承

借助原型 prototype 可以根据已有的对象创建一个新的对象,同时不必创建新的自定义对象类型。
                                                          —— 道格拉斯·克罗克福德《JavaScript 中原型式继承》

let inheritObject = function(o){
    // 声明一个过渡函数对象
    function F(){}
    // 过渡对象的原型继承父对象
    F.prototype = o
    // 返回过渡对象的一个实例,该实例的原型继承了父对象
    return new F()}

它是对类式继承的一个封装,其中的过渡对象就相当于类式继承中的子类,只不过在原型式中作为一个过渡对象出现,目的是为了创建要返回的新的实例化对象。

如虎添翼——寄生式继承

let book = {
    name: 'js book',
    alikeBook: ['JavaScript', 'html', 'css']
}
let createBook = function(obj){
    // 通过原型继承方式创建新对象
    var o = new inheritObject(obj)
    // 拓展新对象
    o.getName = function(){console.log(name)
    }
    return o
}

寄生式继承就是对原型继承的第二次封装,并在第二次封装过程中对继承的对象进行了拓展,这样新创建的对象不仅仅有父类中的属性和方法而且还添加新增属性和方法。

终极继承者——寄生组合式继承

在上面讲到过类式继承同构造函数继承组合使用,但是有一个问题,就是子类不是父类的实例,所以才有了寄生组合式继承。

寄生是寄生式继承,依托于原型继承,原型继承与类式继承相像。

let inheritProject = function(subClass, superClass){
    // 复制一份父类的原型副本保存到变量中
    var p = inheritProject(superClass.prototype)
    // 修正因为重写子类原型导致子类的 constructor 属性被次改
    p.constructor = subClass
    // 设置子类原型
    subClass.prototype = p
}

组合式继承中,通过构造函数继承的属性和方法是没有问题的,所以这里我们主要探究通过寄生式继承重新继承父类的原型。我们需要继承的仅仅是父类的原型,不再需要调用父类的构造函数,换句话说,在构造函数继承中我们已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们通过原型继承便可得到,但是这么直接赋值给子类会有问题的,因为对父类原型对象复制得到的复制对象 p 中的 constructor 指向的不是 subClass 子类对象,因此在寄生式继承中要对复制对象 p 做次增强,修复其 constructor 属性指向不正确的问题,最后将得到的复制对象 p 赋值给子类的原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。

// 定义父类
let supClass = function(name){
    this.name = name
    this.colors = ['red', 'green', 'blue']
}
// 定义父类原型方法
supClass.prototype.getName = function(){console.log(this.name)
}
// 定义子类
let subClass = function(name, time){
    // 构造函数式继承
    supClass.call(this, name)
    // 子类新增属性
    this.time = time
}
// 寄生式继承父类原型
inheritProject(subClass, superClass)
// 子类新增原型方法
subClass.prototype.getTime = function(){console.log(this.time) 
}
// 创建两个测试方法
let instance1 = new subClass('js book', 2014)
let instance2 = new subClass('css book', 2013)
instance1.colors.push('black')
console.log(instance1.colors)    //['red', 'green', 'blue', 'black']
console.log(instance2.colors)    //['red', 'green', 'blue']
instance2.getName()   //css book
instance2.getTime()   //2013

这种继承方式如下图所示:

其中最大的改变就是对子类原型的处理,被赋予父类原型的一个引用,这是一个对象,这里要注意一点,就是子类再想添加原型方法必须通过 prototype 对象,否则知己赋予对象就会覆盖掉从父类原型继承的对象了。

本内容来源总结于《JavaScript 设计模式》一书

原文出自于本人个人博客网站:https://www.dzyong.com(欢迎访问)

转载请注明来源: 邓占勇的个人博客 –《JavaScript 设计模式(2)—— 多种继承方式的实现及原理》

本文链接地址: https://www.dzyong.com/#/View…

正文完
 0