JS面向对象--继承

230 阅读9分钟

比伯新专辑真好听 ---题外话

继承

继承 是面向对象软件技术当中的一个概念。这种技术能够精简代码,提高代码的复用性。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

es6之前并没有class,直到es6才提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板,在es6之前也不能使用extends去实现继承

call( ) apply( ) bind( )

call( )、 apply( )、 bind( ) 这三个函数可以改变this指向,这三个函数可以用来实现继承

call()、 apply()函数都可以直接执行前面的函数,并且改变this指向,并且第一个参数都是放入目标this指向, 不同的是call()在第一个参数后,接参数列表并用逗号隔开, apply()则是在第一个参数后接一个包含多个参数的数组。

bind()和其他两个函数不太一样,他并不会直接执行前面的函数,并且他只会返回一个新的改变了this的函数,bind()传入的除第一个参数外其余参数将作为新函数的参数,供调用时使用

let foo = function (a, b) {
    console.log(this)
    console.log(`a:${a}, b:${b}` )  //es6模板字符串
}

var obj = {
    name: 'IRVING'
}

foo.call()                        // this指向了window,并且输出a:undefined, b:undefined(没传入参数)
foo.call(obj, 'kyrie', 'Irving')  // this指向了obj对象,并且输出a:kyrie, b:Irving
foo.apply()                      // this指向了window,并且输出a:undefined, b:undefined
foo.apply(obj, 'kyrie')          //报错, 即使除去第一个参数外只想传一个参数也必须用数组去传参
foo.apply(obj, ['kyrie', 'Irving'])  // this指向了obj对象,并且输出a:kyrie, b:Irving
foo.bind(obj, 'kyrie', 'Irving' )()  // this指向了obj对象,并且输出a:kyrie, b:Irving

ES5实现继承的方法

原型链继承

原型链继承就是将父类的实例直接赋值给子类的原型对象prototype,使得子类可以通过原型链的访问机制,去访问到父类的属性和方法,实现继承

function Dad(car, house, money) {
    this.car = car
    this.house = house
    this.money = money  
}

Dad.prototype.skill = function() {
    console.log("basketball")
}

function Son(car, house, money) {
    this.age = 21
}

Son.prototype = new Dad(2, 1, "1million");
Son.prototype.constructor = Son;       //原型对象被覆盖,constructor属性消失,应对它重新赋值
let kyrie = new Son(2, 1, "1million")

kyrie.skill()             //打印出basketball
console.log(kyrie.money)  //1million
console.log(kyrie.age)    //21

缺点:

  • 因为我们将整个父类实例赋值给了子类原型,如果父类的属性是引用类型的话,对其修改会出现混乱(原因详见下文tips)
  • 不能在创建子类实例时对父类构造函数进行传参,不灵活

tips: 复杂数据类型(引用类型,也就是除基本类型) function array object 它们在内存中存储的是值的一个地址(或者说引用),而不是这个值的本身,在赋值时传递的是这个地址,简单数据类型则是直接存储值,赋值时直接传递值。因此如果我们在原型继承时,如果直接将父类原型赋值给子类原型来实现继承的话,子类对去修改属性和方法时,父类的相关属性和方法也会被修改,父类和子类的原型对象prototype此时存储的时同一个地址

借用构造函数继承

我们可以通过以上三个改变this指向的方法实现继承,使用这三个方法,执行父类函数,并改变父类的this指向,从而在子类中可以拿到父类的属性或方法

function Dad(car, house, money) {
    this.car = car
    this.house = house
    this.money = money
}

function Son(car, house, money) {
    Dad.call(this, car, house, money)      
    // 也可以用apply()  Dad.apply(this, [car, house, money]) 
    // 也可以用bind()   Dad.bind(this, car, house, money)() 
}

let kyrie = new Son(2, 1, "1million") 
console.log(kyrie.money)  // 1million
console.log(kyrie.car)    // 2

缺点:

  • 这个方法并不能继承父类原型中的方法和属性,所有的属性和方法都需要在父类构造函数中定义。每生成一个实例就会新生成一份独立的资源,每一个方法都是独立的,并没有办法复用,有时候也会造成内存浪费

组合继承

组合继承就是将原型继承和构造函数继承结合在一起,原型继承来继承父类的原型方法和属性,构造函数继承来继承父类构造函数里的属性和方法

function Dad(car, house, money) {
    this.car = car
    this.house = house
    this.money = money
}

Dad.prototype.run = function () {
    console.log(this.money)
}

function Son(car, house, money, age) {
    this.age = age 
    Dad.call(this, car, house, money)         // 调用父类构造函数继承属性和方法
    // 也可以用apply()  Dad.apply(this, [car, house, money]) 
    // 也可以用bind()   Dad.bind(this, car, house, money)() 
}

Son.prototype = new Dad()                 //原型对象的继承
Son.prototype.constructor = Son

let kyrie = new Son(2, 1, "1million") 
console.log(kyrie.money)  // 1million  说明可以继承到父类构造函数的属性
kyrie.run()               // 1million  说明可以继承到父类原型的方法

解决了两种方法的一些问题:可以在创建子类实例时对父类构造函数进行传参,并且可以继承到父类原型里的属性和方法,可以实现函数复用,节约内存,同时每个子类又可以有自己的属性和方法

组合继承缺点是 在这个方法中会调用两次父类构造函数,一次是在创建子类原型的时候,另一次是在子类构造函数内部,第二次调用时,生成的实例方法和属性方法会覆盖原型上相同的属性和方法,(在调用属性或方法时优先调用实例上的)原型上不必要的属性和方法依然存在(如car, money, house)

寄生组合式继承

寄生组合式继承依旧是通过构造函数来继承属性,但是不用再为了改变子类原型对象(也就是继承方法)而调用父类构造函数;我们将父类原型对象的一个副本赋值给子类的原型对象实现继承原型的属性和方法。 这个方法是es6之前业内常用的
优点:只调用一次父类构造函数,不会在子类原型上产生不必要多余的属性。

function Dad(car, house, money) {
    this.car = car
    this.house = house
    this.money = money
}

Dad.prototype.run = function () {
    console.log(this.money)
}

function Son(car, house, money, age) {
    this.age = age 
    Dad.call(this, car, house, money)         // 调用父类构造函数继承属性和方法
}

var Fn = function () {}
Fn.prototype = Dad.prototype
var DadPrototype = new Fn()
DadPrototype.constructor = Son
Son.prototype = DadPrototype

let kyrie = new Son(2, 1, "1million") 
console.log(kyrie.money)  // 1million
kyrie.run()               // 1million

也可以直接改写为

function Dad(car, house, money) {
    this.car = car
    this.house = house
    this.money = money
}

Dad.prototype.run = function () {
    console.log(this.money)
}

function Son(car, house, money, age) {
    this.age = age 
    Dad.call(this, car, house, money)         // 调用父类构造函数继承属性和方法
}

Son.prototype =  Object.create(Dad.prototype)  //Object.create()方法创建一个新对象,第一个参数放入对象来提供新创建的对象的__proto__。
Son.prototype.constructor = Son

let kyrie = new Son(2, 1, "1million") 
console.log(kyrie.money)  // 1million
kyrie.run()               // 1million

ES6继承

ES5中通过构造函数模拟类,ES6开始有了class 类

class类基本写法

class里的constructor就是构造函数(构造器),将类的属性放在constructor里面,类的方法写在constructor外面,会自动将方法挂在原型上面,实例化和es5一样用new 运算符来完成

class Person{
    // age = 21   挂载属性可以这样写和在constructor中写this.xxx = xxx 效果是一样的
    constructor(name) {     //constructor如果没有传参,可以不写constructor,属性都按照上一行的写法去写
        this.name = name
        this.age = 21
    }
    hobby() {            //实例化后方法会在实例的__proto__中
        console.log("hobby")
    }
}

let kyrie = new Person("kyrie") //参数传入到constructor

console.log(kyrie.name) // kyrie

kyrie.hobby()  // hobby  

class静态成员

静态成员是属于类的,实例对象不能访问到,不能调用。
es5中直接这样写: 构造函数.属性名 如: Person.num

es6:

class Person{
    static skill = "拉杆" //也可以是方法,静态成员包括静态属性和静态方法
    name = 'irving'
    hobby() {
        console.log("hobby")
    }
}
//静态属性不需要实例化后再调用,可以直接调用
console.log(Person.skill)

class私有属性

es2020 出现了私有属性,只能在类内部访问调用,实例对象访问不到

class Person{
    #skill = "拉杆"
    constructor(name) {
        this.name = name
        this.age = 21
    }
    hobby() {
        console.log("hobby")
    }
}

let kyrie = new Person("kyrie")
console.log(kyrie.#skill)      // 报错 Uncaught SyntaxError: Private field '#skill' must be declared in an enclosing class

如果想在外部调用到私有属性可以通过类的共有方法调取

class Person{
    #skill = "拉杆"
    constructor(name) {
        this.name = name
        this.age = 21
    }
    hobby() {
        console.log("hobby")
    }
    getSkil() {
        return this.#skill
    }
}

let kyrie = new Person("kyrie")
console.log(kyrie.getSkil())      // 拉杆

class继承

在ES6里,用关键字extends进行继承,实例属性方法和原型方法以及静态成员都可以被继承,私有属性子类不能继承

class Dad{
    static a = "静态属性"
    constructor(name) {
        this.name = name
        this.age = 21
    }
    hobby() {
        console.log("hobby")
    }
}

class Son extends Dad {
    constructor(name) {
        super(name);
         // 挂载属性时只能写在super() 后面否则会报错
        this.height = "185" 
    }
}

console.log(Son.a)         // "静态属性"

let kyrie = new Son("kyrie")

console.log(kyrie.name)       // kyrie

如果父类子类都有同一个属性或者方法,在实例调用时会采取就近原则,取子类的属性或方法

super

子类在constructor中必须调用 super 方法,否则报错

super作为函数使用

super有两种用法,一种是作为函数使用(如上面代码),相当于调取父类的constructor,super内部的this指向的是子类实例

class Dad{
    constructor(name) {
        this.name = name
        this.hobby()         // Dad构造函数被调用时触发hobby方法,父类子类都有hobby方法,但是输出不一样,以此判断this指向
    }
    hobby() {
        console.log("后仰")
    }
}

class Son extends Dad {
    constructor(name) {
        super(name);            //相当于Dad.prototype.constructor.call(this)
        this.height = "185" 
    }
    hobby() {
        console.log("变相")
    }
}


let kyrie = new Son("kyrie")   // 实例化时调用子类构造函数,然后调用super()触发父类构造函数,super内部this指向子类实例因此输出 "变相"
super作为对象使用

super作为对象时,如果在普通方法中,指向父类的原型对象;在静态方法中,则指向父类。

super在普通方法中:

class Dad{
    constructor(name) {
        this.name = name
    }
    hobby() {
        console.log("后仰")
    }
}

class Son extends Dad {
    constructor(name) {
        super(name);
        super.hobby()               // 输出:后仰  super指向父类原型
    }
    hobby() {
        console.log("变相")
    }
    getName() {
        super.hobby()               // 输出:后仰  super指向父类原型
            return super.name
    }
}

let kyrie = new Son("kyrie")
console.log(kyrie.getName())       // 输出:undefined   super中的this指向父类原型 父类的属性或方法定义在构造函数(实例)上访问不到

super在静态方法中:

class Dad{
    static  skill() {
        console.log('静态skill');
    }
    constructor(name) {
        this.name = name
        this.age = 21
    }
    skill() {
        console.log('原型skill');
    }
}

class Son extends Dad {
    static  skill() {
        super.skill()
    }
    constructor(name) {
        super(name);
    }
    hobby() {
        super.skill()
    }

}

let kyrie = new Son("kyrie")
console.log(Dad.prototype)
Son.skill()       // 输出静态skill

kyrie.hobby()     // 输出原型skill

使用super的时候,必须指定是作为函数、还是作为对象使用,否则会报错

简单总结下,在ES5 我们用构造函数模拟类,用上文提到的几种方法进行继承,ES6中出现了class这样的语法糖,继承也变得非常简单,也不需要考虑ES5里的很多会出现的问题。

fc59-kmvwsvy8826467.jpg

这是我的学习笔记,可能有一些不足,如有错误,欢迎指正,一起学习进步。