浅谈Javascript中的继承

244 阅读6分钟

Javascript 继承

JavaScript是一门面向对象的弱类型语言,没有办法像Java那样继承。但是JavaScript作为动态语言,其灵活性非常的大。同时,我们可以通过一系列其他方式完成继承。下面我们就JS实现继承的方式来分析一下,超详细分析。

一、原型链继承

Javascript原生实现的继承方式就是通过原型链的方式 ,将父类的属性方法传递给子类

通过下面代码,我们来看下原型继承的实现

// 定义父类
function SuperClass () {
    this.name = 'father'
    this.books = ['js', 'html', 'css']
}
// 父类原型上添加方法
SuperClass.prototype.show = function () {
    console.log(this.name)
}
// 定义子类
function SubClass () {}
// 将父类的实例赋值给 子类的 原型对象 (prototype)
SubClass.prototype = new SuperClass()

// 实例化 子类
var sub1 = new SubClass()
var sub2 = new SubClass()

我们先看下最后实例化出来的对象和父类对象之间的关系,我们就能理清楚,到底在实例化对象上继承了哪些属性,并且我们可以使用哪些方法

// 根据原型链的基础知识,我们知道实例化对象的__proto__指向构造函数的prototype
sub1.__proto__ === SubClass.prototype
SubClass.prototype.__proto__ === SuperClass.prototype

sub1.name      // 'father'
sub2.name      // 'father'
sub1.books     // ["js", "html", "css"]
sub2.books     // ["js", "html", "css"]

// 改变 sub1的name
sub1.name = 'chencc'

sub1.name     // 'chencc'
sub2.name     // 'father'

// 向books中在添加一个
sub1.books.push('java')
sub1.books     // ["js", "html", "css", "java"]
sub2.books     // ["js", "html", "css", "java"]

这里我们通过将父类的实例赋值给子类的prototype,然后在实例化子类,实现继承

这里我们需要了解一下,改变了基本类型和引用类型的值,为什么会有不一样

// 这里给实例化的子类中的name重新赋值,实际上是在sub1上添加了一个name属性
// 屏蔽了父类上的name属性
sub1.name = 'chencc'  
// 这里就可以看出来,实际上父类的name属性值是没有变的
sub1.__proto__.name   // 'father'

// 这里给引用类型里面添加一个值
sub1.books.push('java')
// 这里实际上往里面添加一个值,是向父类中的这个books里面添加了一个值
// 通过原型继承方式共享这个引用属性的,一个改变,全部改变
sub1.__proto__.books    // ["js", "html", "css", "java"]

// 我们在执行父类的原型上的方法
// 这里sub1 执行的这个show方法里面的this是sub1的,所以拿到的name是chencc
sub1.show()         // 'chencc'
sub2.show()         // 'father'

总结一下,原型继承确实可以实现,但是对于引用类型,一个实例修改了,共享的都变了。原型继承是将父类的实例赋值到子类的prototype上,所以没办法传参。

二、构造函数继承

构造函数的继承和原型链的继承完全不一样,主要是通过两个方法call或者apply来实现的。其核心就是通过改变作用域的方式来在新的作用域内实例了属性和方法

// 定义父类
function SuperClass () {
    this.name = 'father'
    this.books = ['js', 'html', 'css']
}
// 父类原型上添加方法
SuperClass.prototype.show = function () {
    console.log(this.name)
}
// 定义子类
function SubClass (name) {
    SuperClass.call(this)
    this.name = name || 'son'
}
var sub1 = new SubClass('chencc')
var sub1 = new SubClass()

sub1.name       // 'chencc'
sub2.name       // 'son'
sub1.books      // ["js", "html", "css"]
sub2.books      // ["js", "html", "css"]

sub1.books.push('java')
sub1.books      // ["js", "html", "css", "java"]
sub2.books      // ["js", "html", "css"]

sub1.show()     // undefined
sub2.show()     // undefined

通过call 或者 apply的方式实现的继承,只能继承构造函数内的方法和属性,对于构造函数原型的方法无法继承。但是call和apply继承的构造函数,所有属性都相当于在子类的实例中单独实例了,不会共享。所以一个引用类型books改变,其他的不会影响。同时,我们可以实现传参操作了,不像原型继承时,无法进程传参。

sub1.__proto__ === SubClass.prototype
sub2.__proto__ === SubClass.prototype

实例后的sub1和sub2都只是SubClass的实例,并不是父类的实例,所以无法继承SuperClass的prototype的方法。并且对于SuperClass中的属性相当于复制了一份副本。每次都会复制一个,比较消耗性能。

三、组合继承

组合继承的方式就是将上面的原型继承和构造函数继承同时组合一下,不就可以实现每个子类的实例都有用父类单独的属性副本,同时也可以共享父类原型上的方法。

// 定义父类
function SuperClass () {
    this.name = 'father'
    this.books = ['js', 'html', 'css']
}
// 父类原型上添加方法
SuperClass.prototype.show = function () {
    console.log(this.name)
}
// 子类构造函数
function SubClass (name) {
    SuperClass.call(this)
    this.name = name || 'son'
}

var sub1 = new SubClass('checc')
var sub2 = new SubClass()

sub1.name           // 'chencc'
sub2.name           // 'son'
sub1.books          // ["js", "html", "css"]
sub2.books          // ["js", "html", "css"]

sub1.books.push('java')
sub1.books          // ["js", "html", "css", "java"]
sub2.books          // ["js", "html", "css"]

sub1.show()         // 'chencc'
sub2.show()         // 'son'

这里通过使用两种方式组合继承了父类的构造函数,同时也继承了父类的原型方法,我们可以这样理解一下

sub1.__proto__ === SubClass.prototype
SubClass.prototype.__proto__ === SuperClass.prototype

// 这里SubClass的prototype被赋值了SuperClass的实例
// 所以SubClass是有了SuperClass的原型方法和构造函数内的属性
// 在执行了 call 方法后,通过protoype 继承的构造函数方法被屏蔽了

组合继承的方式,调用了两次父类的构造函数,生成了两份实例。一份在SubClass内,另外一份在SubClass的prototype上,SubClass内的将原型上的屏蔽了。但是造成资源的浪费。

四、寄生组合继承

寄生组合继承的方式,就是将上面的组合继承方式进行了一点优化。不两次调用构造函数,降低性能消耗。

// 定义父类
function SuperClass () {
    this.name = 'father'
    this.books = ['js', 'html', 'css']
}
// 父类原型上添加方法
SuperClass.prototype.show = function () {
    console.log(this.name)
}
// 子类构造函数
function SubClass (name) {
    SuperClass.call(this)
    this.name = name || 'son'
}
// 中间继承方法
function inherit (supe, sub) {
    let temp = function(){}
    temp.prototype = supe.prototype
    sub.prototype = new temp()
}

// 备注 继承的方式也可以使用 Object.create的方式
// SubClass.prototype = Object.create(SuperClass)
// 这个方法用于创建一个新对象。被创建的对象继承另一个对象的原型

inherit(SuperClass, SubClass)

这里为了不让父类执行两次,通过了一个中间继承的方法,定义了一个临时的temp,将父类的原型赋值给这个临时的temp的prototype,因为temp这个临时的构造函数内是没有任何属性的。最后将这个temp的实例赋值给子类的prototype,就实现了只是继承了父类的原型上的属性。对于父类的构造函数里面的其他属性通过call的方式继承了。这样就不会浪费资源,是最好的继承方式。

五、拷贝方式继承

通过遍历循环所有属性方法的方式,实现继承。这种方式可以实现多继承,只需要将所有需要继承的属性都循环赋值到子类中去

var extend = function(target, source) {
    // 遍历属性对象
    for (var property in source) {
        target[property] = source[property]
    }
    return target
}
var SuperClass = {
    name: 'super',
    arr: [1,2,3,4],
    show: function(){
        console.log(this.name)
    }
}
var SubClas = {
    color: 'fff'
}
extend(SubClas, SubClas)

SubClas.arr.push(5)

SubClas.bane        // 'super'
SubClas.arr         // [1,2,3,4,5]
SuperClass.arr      // [1,2,3,4,5]

这里 extend 实现的继承方式,实际上就是一个浅复制的过程。他只能复制基本类型的属性,对于引用类型的属性,实际上是复制了引用类型的地址,所以一处改变,还是都会改变。对于浅拷贝和深拷贝,我们后面会专门分析一次。

对于多继承的实现

Object.prototype.mix = function() {
    var i = 0,
        len = arguments.length,
        arg;
    // 遍历要被继承的对象
    for (; i < len; i++) {
        // 缓存当前对象
        arg = arguments[i]
        for (var property in arg) {
            this[property] = arg[property]
        }
    }
}

var Test1 = {
    name: 'chencc',
    type: 1
}
var Test2 = {
    like: 'javascript',
    type: 2
}
var Test3 = {}
Test3.mix(Test1, Test2)
Test3       // {name: 'chencc', type: 2, like: 'javascript'}