JS从诞生之初本就不是面向对象的语言。

如何在JS中实现继承,总结而言会有四种写法。

构造函数继承

function Animal(name) {
    this.name = name
        
    this.sayName = function() {
        console.log(this.name)
    }
}
    
function Dog(name, hobby) {
    // 遍历
    let ani = new Animal(name)
    for(let p in ani) {
        if (ani.hasOwnProperty(p)) {
            this[p] = ani[p]
        }
    }
        
    this.hobby = hobby
}
    
let dog1 = new Dog('xiaohei', 'bone')
let dog2 = new Dog('fofo', 'bone and fish')
console.log(dog1.sayName()) // xiaohei
console.log(dog2.sayName()) // fofo

通过对象冒充实现继承,实际上是在构造函数中,通过获取父类中的所有属性,并保存到自身对象中,这样则可以调用父类的属性和方法了。这里forin的方式遍历父类属性,因为forin会遍历公开的属性和方法,所以通过hasOwnProperty控制写入当前对象的范围。否则则会将所有属性全部变为私有属性。

这样做有一个缺点就是,无法访问父类中的公开方法和属性(prototype中的方法)

Animal.prototype.sayHobby = function() {
    console.log(this.hobby)
}
dog1.sayHobby() // VM2748:1 Uncaught TypeError: dog1.sayHobby is not a function at <anonymous>:1:6

代码优化

在子类中,既然是需要获取父类的私有属性,则可以使用callapply,当调用父类的方法的时候,改变当前上下文为子类对象,则子类对象就可以获取到了父类的所有私有属性。

function Animal(name) {
    this.name = name
        
    this.sayName = function() {
        console.log(this.name)
    }
}
    
function Dog(name, hobby) {
    // 更改构造函数的上下文
    Animal.call(this, name)
    
    this.hobby = hobby
}
    
let dog1 = new Dog('xiaohei', 'bone')
let dog2 = new Dog('fofo', 'bone and fish')
console.log(dog1.sayName()) // xiaohei
console.log(dog2.sayName()) // fofo

类式继承

function Animal(name) {
    this.name = name || 'animal'
    this.types = ['cat', 'dog']
    
    this.sayTypes = function() {
        console.log(this.types.join('-'))
    }
}
Animal.prototype.sayName = function() {
    console.log(this.name)
}

function Dog(name) {
    this.name = name    
}
Dog.prototype = new Animal('animal')

let dog1 = new Dog('xiaohei')
dog1.sayName() // xiaohei

let dog2 = new Dog('feifei')
dog2.sayName() // feifei

这种继承方式是通过对子类的prototype.__proto__引用父类的prototype,从而可以让子类访问父类中的私有方法和公有方法。详情可以查看关键字new的实现。

类式继承会有两方面的缺点

  1. 引用陷阱-子类对象可以随意修改父类中的方法和变量,并影响其他子类对象

    dog1.types.push('fish')
    console.log(dog1.types) // ["cat", "dog", "fish"]
    console.log(dog2.types) // ["cat", "dog", "fish"]

  2. 无法初始化构造不同的实例属性

这个主要是由于类式继承,是通过Dog.prototype = new Animal('animal')实现的,我们只会调用一次父类的构造函数。所以只能在子类中从写父类的属性,如上的name属性,在子类中需要重写一次。

组合继承

组合继承,即结合以上两种继承方式的优点,抛弃两者的缺点,而实现的一种组合方式

function Animal(name) {
    this.name = name
    this.types = ['dog', 'cat']
}
Animal.prototype.sayName = function() {
    console.log(this.name)
}

function Dog(name, hobby) {
    // 获取私有方法并调用父类的构造函数,并传递构造函数的参数,实现初始化不同的构造函数
    Animal.call(this, name)
    this.hobby = hobby
}
// 子类实例可以访问父类prototype的方法和属性
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
Dog.prototype.sayHobby = function() {
    console.log(this.hobby)
}

// test instance of dog1
let dog1 = new Dog('xiaohei', 'bone')
dog1.sayName() // xiaohei
dog1.sayHobby() // bone
dog1.types.push('ant') // types: ['dog', 'cat', 'ant']

// test instance of dog2
let dog2 = new Dog('feifei', 'fish')
dog2.sayName() // feifei
dog2.sayHobby() // fish
dog2.types // ['dog', 'cat']

组合模式,解决了使用构造函数继承类式继承带来的问题,算是一种比较理想的解决继承方式,但是这里还有一些瑕疵,调用了两次父类(Animal)的构造函数。

所以为了解决这个问题,进行了优化,产生了?这种继承方式

组合寄生式继承

function Animal(name) {
    this.name = name
    this.types = ['dog', 'cat']
}
Animal.prototype.sayName = function() {
    console.log(this.name)
}

function Dog(name, hobby) {
    // 获取私有方法并调用父类的构造函数,并传递构造函数的参数,实现初始化不同的构造函数
    Animal.call(this, name)
    this.hobby = hobby
}

/**注意下面这两行代码**/

Dog.prototype = Object.create(Animal.prototype)
// 由于对Animal.prototype进行了浅拷贝,则改变了Dog中的构造函数,所以需要重新赋值Dog为构造函数
Dog.prototype.constructor = Dog
Dog.prototype.sayHobby = function() {
    console.log(this.hobby)
}

// test instance of dog1
let dog1 = new Dog('xiaohei', 'bone')
dog1.sayName() // xiaohei
dog1.sayHobby() // bone
dog1.types.push('ant') // types: ['dog', 'cat', 'ant']

// test instance of dog2
let dog2 = new Dog('feifei', 'fish')
dog2.sayName() // feifei
dog2.sayHobby() // fish
dog2.types // ['dog', 'cat']
MDN解释:Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。

可以理解为:使用Object.create()进行一次浅拷贝,将父类原型上的方法拷贝后赋给Dog.prototype,这样子类上就能拥有了父类的共有方法,而且少了一次调用父类的构造函数。

重写create方法:

function create(target) {
    function F() {}
    F.prototype = target
    return new F()
}

同时需要注意子类的constructor,由于更改了子类的prototype,所以需要重新设定子类的构造函数。

ES6中使用语法糖extends实现

如果之前有学习过,或者有面向对象语言基础的,这个则很容易理解,使用extens关键字作为继承。

class Animal {
    constructor(name) {
        this.name = name
    }
    
    sayName() {
        console.log(this.name)
    }
}

class Dog extends Animal {
    constructor(name, hobby) {
        super(name)
        this.hobby = hobby
    }
    
    sayHobby() {
        console.log(this.hobby)
    }
}

let dog1 = new Dog('xiaohei', 'bone')
dog1.sayName() // xiaohei
dog1.sayHobby() // bone

let dog2 = new Dog('feifei', 'fish')
dog2.sayName() // feifei
dog2.sayHobby() // fish

总结

综上所述,JS中的继承总共分为构造器继承类式继承组合继承组合寄生继承ES6中extends的继承五种继承方式,其中第四种是第三种的优化实现。

最后,实现new关键字的实现

MDN: new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

语法:new constructor[([arguments])]

function new(constructor, arguments) {
    let o = {}
    if (constructor && typeof constructor === 'function') {
        // 获取构造函数的原形
        o.__proto__ = constructor.prototype
        // 获取构造函数的私有变量和私有方法
        constructor.apply(o, arguments)
        return o
    }
}

mdiep
249 声望6 粉丝

软件开发工程师-往大前端开发方向靠拢