JS中继承的实现

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

如何在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
复制代码

代码优化this

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

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的实现。prototype

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

  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
    }
}复制代码
相关文章
相关标签/搜索