【JavaScript】一文学会JavaScript继承

1. 原型链继承

原理:让子类的原型等于父类的实例

function Animal() {
    this.name = 'dog'
}

Animal.prototype.getName = function() {
    console.log(this.name)
}

function Dog() {
    
}

Dog.prototype = new Animal()

const dog = new Dog()

console.log(dog.getName()) // 'dog'

优点:
继承了父类的所有属性和方法
缺点:

  1. 在创建子类实例时,不能向父类构造函数传参,继承单一
  2. 创建的所有实例共享父类的属性,原型上的属性是共享的,一个实例修改了原型引用类型的属性,另一个实例原型的引用类型属性也会被修改,如下
function Animal() {
    this.name = ['dog']
}

function Dog() {
    
}

Dog.prototype = new Animal()

const dog1 = new Dog()
dog1.name.push('rabbit')
console.log(dog1.name) // ['dog', 'rabbit']

const dog2 = new Dog()
console.log(dog2.name) // ['dog', 'rabbit']

2. 借用构造函数(经典继承)

原理:在子类的构造函数中,通过 apply()call() 的形式,调用父类构造函数。

function Animal() {
    this.name = ['dog']
}

function Dog() {
    Animal.call(this)
}

const dog1 = new Dog()
dog1.name.push('rabbit')
console.log(dog1.name) // ['dog', 'rabbit']

const dog2 = new Dog()
console.log(dog2.name) // ['dog']

优点(解决了原型链继承产生的问题):

  1. 避免了引用类型的属性被实例共享
  2. 子类可以向父类构造函数传值

缺点:

  1. 每次创建实例都会创建一遍父类构造函数中的方法,当方法过多的时候,内存也相应会增大
  2. 只能继承父类构造函数的属性,没有继承父类原型的属性,例如以下代码
...
Animal.prototype.age = '111'
...
dog1.age // undefined
...
3. 组合继承

上面介绍的原型链继承和经典继承都有各自的缺点。
所以,我们考虑能否将这二者结合到一起,即在继承过程中,既可以避免引用类型属性共享,又能够复用一些属性和方法。
双剑合璧、天下无敌。

function Animal(name) {
    this.name = name
}

Animal.prototype.getName = function() {
    console.log(this.name)
}

function Dog(name) {
    Animal.call(this, name)
}

Dog.prototype = new Animal()
Dog.prototype.constructor = Dog

const dog = new Dog('dog')
dog.getName() // dog

优点:

  1. 可以继承父类原型上的属性,可传参、可复用
  2. 实例的继承的属性是私有的

缺点:
调用了两次父类构造函数

4. 原型式继承

原理:Object.create 的模拟实现,将传入的对象作为创建的对象的原型。

function Animal(Obj) {
    function F() {}
    F.prototype = Obj
    return new F()
}

const animal = {
    name: 'dog',
    fere: ['rabbit']
}

const dog1 = Animal(animal)
const dog2 = Animal(animal)

dog1.name = 'dog1'
console.log(dog2.name) // 'dog' 

dog1.fere.push('cat')
console.log(dog2.fere) // ['dog', 'cat']

dog1.name值改变了,为什么dog2.name值未变,因为dog1不是改变的原型上面的name,而是给dog1加了name属性并赋值

dog1.name = 'dog1'
dog1.name// dog1
Object.getPrototypeOf(dog1).name // dog

缺点:
和原型链继承一样,引用类型的属性值都被共享

5. 寄生式继承

原理:原型式继承外面套一层壳子,在函数内部增强对象,最后返回对象

function Animal(Obj) {
    function F() {}
    F.prototype = Obj
    return new F()
}

function create(obj) {
    let Obj = Animal(obj)
    Obj.getName = function() {
        console.log(this.name)
    }
    return Obj
}

const animal = {
    name: 'dog'
}

const dog1 = create(animal)
dog1.getName() //dog

缺点:
和经典继承一样,每次创建实例都会创建一遍父类构造函数中的方法

6. 寄生组合式继承

原理:寄生和组合结合

function Animal(name) {
    this.name = name
}

Animal.prototype.getName = function() {
    console.log(this.name)
}

function Dog(name) {
    Animal.call(this, name)
}

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

function inheritPrototype(Child, Parent) {
    let prototype = object(Parent.prototype);   // 创建对象
    prototype.constructor = Child;              // 增强对象
    Child.prototype = prototype;                // 赋值对象
}

inheritPrototype(Dog, Animal)

优点:
修复了组合继承中调用两次构造函数的问题

7. class继承

class是ES6的新的语法糖,虽然是新语法,但背后的逻辑依旧是使用的原型链。

1. 基础继承

使用extends关键字,就可以继承任何拥有[[Construct]]和原型的对象。

// 继承类
class Animal {}

class Dog extends Animal {}

const dog = new Dog()
console.log(dog instanceof Dog) // true
console.log(dog instanceof Animal) // true

// 继承构造函数
function Animal1 {}

class Dog1 extends Animal1 {}

const dog1 = new Dog1()
console.log(dog1 instanceof Dog1) // true
console.log(dog1 instanceof Animal1) // true
2. super

派生类的方法可以使用super关键字引用他们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。

在类构造函数中使用super可以调用父类构造函数。

class Animal {
    constructor() {
        this.name = 'dog'
    }
}

class Dog extends Animal {
    constructor() {
        console.log(this); // Uncaught ReferenceError
        super(); // 相当于super.constructor()
        
        console.log(this instanceof Animal);  // true
        console.log(this);                     // Dog { name: 'dog' }
    }
}

new Dog()

注意:不能在调用super之前引用this,否则会抛出错误ReferenceError

在静态方法中可以通过super调用继承的类上定义的静态方法

class Animal {
    static getName() {
        console.log('dog')
    }
}

class Dog extends Animal {
    static getName() {
        super.getName()
    }
}

Dog.getName() // 'dog'

注意

  • super只能在派生类和静态方法中使用
class Animal {
    constructor() {
        super() // Uncaught SyntaxError: 'super' keyword unexpected here
    }
}
  • super关键字不能单独引用,要么用它调用构造函数,要么用它引用静态方法
class Animal {}

class Dog extends Animal {
    constructor() {
        console.log(super) // Uncaught SyntaxError: 'super' keyword unexpected here
    }
}

  • 调用super()会调用父类构造函数,并将返回的实例赋值给this
class Animal {}

class Dog extends Animal {
    constructor() {
        super()
        
        console.log(this instanceof Animal) // true
    }
}

new Dog()
  • 需要给父类构造函数传参,需要手动传入
class Animal {
    constructor(name) {
        this.name = name
    }
}

class Dog extends Animal {
    constructor(name) {
        super(name)
    }
}
new Dog('dog') // Dog {name: 'dog'}
  • 如果在派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返回一个对象。
class Animal {}

class Dog extends Animal {
    constructor() {
        super()
    }
}
class Cat extends Animal {
    constructor() {
        return {}
    }
}
class Rabbit extends Animal {
    constructor() {
    }
}
new Dog() // Dog {}
new Cat() // {}
new Rabbit() // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
3. 抽象基类与new.target

有时我们需要一个可供其他类继承,但自身不会被实例化的类。new.target保存通过new关键字调用的类或函数。通过在实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化:

class Animal {
    constructor() {
        if(new.target === Animal) {
            throw new Error('Animal cannot be directly instantiated');
        }
    }
}

class Dog extends Animal {}

new Dog()    // class Dog {}
new Animal() // class Animal {}
// Error: Animal cannot be directly instantiated

另外,还可以通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法

class Animal {
    constructor() {
        if(new.target === Animal) {
            throw new Error('Animal cannot be directly instantiated');
        }
        if(!this.getName) {
            throw new Error('Inheriting class must define getName');
        }
    }
}

class Dog extends Animal {
    getName() {}
}
class Cat extends Animal {}

new Dog()  
new Cat() // Error: Inheriting class must define getName
4. 内置类型继承
  • 扩展内置类型
class reverseString extends String {
    // 字符串翻转
    reverse() {
        return this.split('').reverse().join('')
    }
}
let a = new reverseString('12345')
console.log(a instanceof String)    // true
console.log(a instanceof reverseString)  // true
console.log(a);  // 12345
console.log(a.reverse())  // 54321

你可能感兴趣的:(javascript,javascript)