乐趣区

JS 继承的实现

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

代码优化
在子类中,既然是需要获取父类的私有属性,则可以使用 call 和 apply,当调用父类的方法的时候,改变当前上下文为子类对象,则子类对象就可以获取到了父类的所有私有属性。
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 的实现。
类式继承会有两方面的缺点

引用陷阱 - 子类对象可以随意修改父类中的方法和变量,并影响其他子类对象 dog1.types.push(‘fish’)console.log(dog1.types) // [“cat”, “dog”, “fish”]console.log(dog2.types) // [“cat”, “dog”, “fish”]

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

这个主要是由于类式继承,是通过 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
}
}

退出移动版