类、构造函数以及js的继承方法

类的语法结构(ES6中新增)

  • 类的语法结构和构造函数非常相似
    • 构造函数的语法结构
    function Person(name, age) {
        this.name = name
        this.age = age
        this.say = function () {
            console.log('hello')
        }
    }
    
    const p = new Person('top',28)
    console.log(p) // Person {name: 'tom', age: 28}
    p.say() // hello
    
    • 类的语法结构
    class Person {
        constructor(name, age) {
            this.name = name
            this.age = age
        }
        // 注意:写成类的形式以后,say()就可以不用this.say = function(){}的形式了,当然写成this.say的形式也可以
        say() {
            console.log('hello')
        }
    }
    const p = new Person('tom', 28)
    console.log(p) // Person {name: 'tom', age: 28}
    p.say() // hello
    
    • 都是需要new,只不过class多了一个constructor构造函数函数

类的继承 extends

  1. 继承父类的普通函数
class Father {
    say() {
        console.log('hello')
    }
}
class Son extends Father {
    // 这里面可以不写say方法,完全继承父类的,调用的时候向上查找找到父类的
    // say() {
    //     super.say()
    // }
}
const s = new Son()
s.say() // hello
  • 继承父类的普通函数的同时也可以拥有自己的普通函数(方法)
class Father {
    say() {
        console.log('hello')
    }
}
class Son extends Father {
    // 子类自己的方法
    sing() {
        console.log('唱歌')
    }
}
const s = new Son()
s.say() // hello
s.sing() // 唱歌
  1. 继承父类的构造函数
  • 切记:调用父类的构造函数constructor里面的属性必须用super关键字
class Father {
    constructor(x, y) {
        this.x = x
        this.y = y
    }
    sum() {
        console.log(this.x + this.y);
    }
}
class Son extends Father {
    constructor(x, y) {
        super(x, y) // 必须要加
    }
}
const s = new Son(1, 2)
console.log(s) // Son {x: 1, y: 2}
s.sum() // 3  这里就是继承父类的普通函数

类里面的this指向问题

  1. constructor里面的this指向实例对象
class Father {
    constructor() {
        console.log(this)
    }
}
const f = new Father()
// 输出 Father {}  因为是Father进行实例化,所以constructor里面的this就指向Father的实例对象f
class Father {
    constructor() {
        console.log(this)
    }
}
class Son extends Father {

}
let s = new Son()
// 输出 Son {} 因为是Son进行实例化,所以constructor里面的this就指向Son的实例对象s
  1. 方法里的this指向这个方法的调用者
  • Father的实例对象f调用时:

    class Father {
        say() {
            console.log(this) 
        }
    }
    const f = new Father()
    class Son extends Father {
    
    }
    let s = new Son()
    f.say()
    // 输出 Father {}  说明this确实指向的是Father的实例对象f
    
  • Son的实例对象s调用时

    class Father {
        say() {
            console.log(this);
        }
    }
    const f = new Father()
    class Son extends Father {
    
    }
    const s = new Son()
    s.say()
    // 输出 Son {}  说明this确实指向的是Son的实例对象s
    
  • 综合上面两点做个题:

class Father {
    constructor(name, age) {
        this.name = name
        this.age = age
        console.log('1', this)
    }
    say() {
        console.log('2', this)
    }
}
let f = new Father('ls', 30)
class Son extends Father {
    constructor(name, age) {
        super(name, age)
    }
}
let s = new Son('zs', 8)
s.say()
f.say()

// 1 Father {name: 'ls', age: 30}
// 1 Son {name: 'zs', age: 8}
// 2 Son {name: 'zs', age: 8}
// 2 Father {name: 'ls', age: 30}
  • 总结:谁进行实例化,constructor里面的this就指向实例化的对象,谁调用类里面的函数,函数里面的this就指向哪一个实例化对象

构造函数和原型链(ES5)

  1. 构造函数中的静态成员:在构造函数本身上添加的成员,只能由构造函数本身访问
function Person(name, age) {
    this.name = name
    this.age = age
}
Person.sex = 'male'
let p = new Person('zs', 30)
console.log(p.sex)  // undefined
console.log(Person.sex)  // male
  1. 构造函数中的实例成员:在构造函数内部创建的对象成员,只能由实例化的对象来访问
function Person(name, age) {
    this.name = name
    this.age = age
}
let p = new Person('zs', 30)
console.log(p.name)  // zs
console.log(Person.name)  // 访问不到
  1. 原型对象prototype
  2. 对象原型__proto__
  3. 原型链

1、利用构造函数继承(.call)

优点:
1、可以继承父级的本身上的属性和方法,并且可以给父级传值;也可以有自己的属性和方法。

function Father(name, age) {
    this.name = name
    this.age = age
    this.arr = [1, 2, 3]
    this.go = function () {
        console.log('go')
    }
}
function Son(name, age, hobby) {
    Father.call(this, name, age, hobby)
    this.hobby = hobby
}
let s = new Son('zs', 40, 'eat')
console.log(s.name, s.age, s.arr, s.hobby)
s.go()

// 输出:
// zs 40 [1, 2, 3] eat
// go

2、在子级上更改继承过来的引用属性不会影响到父级的原本属性,多个子例也相互独立

function Father() {
    this.arr = [1, 2, 3]
}
function Son() {
    Father.call(this)
}
let s = new Son()
let s1 = new Son()
let f = new Father()
s.arr.push(4)
s1.arr.push(5)

console.log(s.arr)
console.log(s1.arr)
console.log(f.arr)

// 输出:
// [1, 2, 3, 4] 
// [1, 2, 3, 5]
// [1, 2, 3]

缺点:
1、由于是通过构造函数生成的对象,所以继承父级的方法每次都是全新的,即不能复用父级方法(引用属性)

function Father() {
    this.go = function () {
        console.log('go')
    }
}
function Son() {
    Father.call(this)
}
let s = new Son()
let s1 = new Son()
console.log(s.go == s1.go)

// 输出:
// false

2、继承不了父级原型对象上的方法(call方法其实就是在构造函数的原型上挂载一个方法)

function Father(name) {
    this.name = name
}
Father.prototype.go = function () {
    console.log('go')
}

function Son(name) {
    Father.call(this, name)
}
let s = new Son('zs')
console.log(s.go)
Father.prototype.go()

// 输出:
// undefined
// go

2、利用原型链继承(将父级的实例对象作为子级的原型对象)

优点:
1、可以继承父级原型上的方法,且可复用

function Father() {

}
Father.prototype.go = function () {
    console.log('go')
}

function Son() {

}

Son.prototype = new Father()
// 要切记修正constructor指向,因为s的构造函数终究是要指向Son的
Son.prototype.constructor = Son
let s = new Son()
let s1 = new Son()
s.go()
console.log(s.go === s1.go)
// 因为s和s1都指向同一个原型对象,也就是此时的Father构造函数的实例对象

// 输出:
// go
// true

2、子级可以在自己的原型上挂载方法,且是独有的,父级获取不到

function Father() {

}
Father.prototype.go = function () {
    console.log('go')
}

function Son() {

}

Son.prototype = new Father()
Son.prototype.constructor = Son

// 这里要注意,先把实例挂载到Son的原型上,然后再再原型上挂载方法
Son.prototype.eat = function () {
    console.log('eat')
}

let s = new Son()
console.log(Son.prototype)
console.log(Father.prototype)

// 输出:
// Father {constructor: ƒ, eat: ƒ}
// {go: ƒ, constructor: ƒ}

// 可以看到Son的原型上可以访问到eat和Father上的go
// 但是Father的原型上只能访问到go

缺点:
1、子级无法传递参数给父级
2、子级修改了继承过来的父级属性中的引用属性后,所有用过这个引用属性的子级都会跟着改变

function Father() {
    this.arr = [1, 2, 3, 4, 5]
}

function Son() {

}

Son.prototype = new Father()
// 要切记修正constructor指向,因为s的构造函数终究是要指向Son的
Son.prototype.constructor = Son
let s = new Son()
let s1 = new Son()

s.arr.push(6)
console.log(s.arr)
console.log(s1.arr)

// 输出:
// [1, 2, 3, 4, 5, 6]
// [1, 2, 3, 4, 5, 6]
 
// 因为这两个实例都是由Son创造出来的,所以这两个实例共用一个原型对象,一个发生改变了,全都发生改变

3、利用组合继承(构造函数 + 原型链)

核心:利用构造函数传递父级本身的属性,并且也可以传递参数;利用与原型链传递共用的方法,让所有子级复用。
优点:
1、保留了构造函数的优点:创建子类实例,可以向父类构造函数传参
2、保留了原型链的优点:父类的方法定义在父类的原型对象上,可以实现方法复用
3、不共享父类的引用属性,比如arr属性

缺点:调用了两次父级的构造函数,第一次是call,将父级属性和方法拷贝一份,作为子级的实例属性,第二次是new Father,把创造出来的实例作为子级的原型对象,但是Son.prototype上父级属性和方法会被第一次拷贝过来的实例属性屏蔽,所以多余

function Father(name) {
    this.name = name
    this.arr = [1, 2, 3]
}
Father.prototype.go = function () {
    console.log('go')
}

function Son(name) {
    Father.call(this, name)
}

Son.prototype = new Father()
Son.prototype.constructor = Son
let s = new Son('zs')
let s1 = new Son()
// 说明可以向父级传递参数
console.log(s.name) // zs
s.arr.push(66)     
// 说明父级本身上的引用类型不是复用的,相互独立
console.log(s.arr)  // [1, 2, 3, 66]
console.log(s1.arr)  // [1, 2, 3]
// 说明父级原型上的方法都可以继承到
s.go() // go
s1.go() // go
// 说明方法可以复用,一变全变
console.log(s.go === s1.go) // 
// 可以看出不管是Son还是Father的构造函数指向都是正确的
console.log(Son.prototype.constructor) // Son 的构造函数
console.log(Father.prototype.constructor) // Father的构造函数

4、组合继承优化1

将父级的原型对象直接赋给子级的原型对象,也就是说二者共用一个原型对象

优点:
1、保留了构造函数的优点:创建子类实例,可以向父类构造函数传参
2、保留了原型链的优点:父类的方法定义在父类的原型对象上,可以实现方法复用
3、不共享父类的引用属性,比如arr属性
4、只调用一次父级构造函数

缺点:
子级和父级的构造函数指向同一个原型对象,当修改子级构造函数的指向之后,父级构造函数的指向也会随之变化。

function Father(name) {
    this.name = name
    this.arr = [1, 2, 3]
}
Father.prototype.go = function () {
    console.log('go')
}

function Son(name) {
    Father.call(this, name)
}

Son.prototype = Father.prototype
Son.prototype.constructor = Son
let s = new Son('zs')
let s1 = new Son()
// 说明可以向父级传递参数
console.log(s.name) // zs
s.arr.push(66)     
// 说明父级本身上的引用类型不是复用的,相互独立
console.log(s.arr)  // [1, 2, 3, 66]
console.log(s1.arr)  // [1, 2, 3]
// 说明父级原型上的方法都可以继承到
s.go() // go
s1.go() // go
// 说明方法可以复用,一变全变
console.log(s.go === s1.go) // 
// 可以看出Father的构造函数指向是错误的,因为上面修改了指向为Son
console.log(Son.prototype.constructor) // Son 的构造函数
console.log(Father.prototype.constructor) // Son的构造函数

5、组合继承优化2(寄生组合继承)

将父级构造函数的原型对象复制一份赋到子级的原型对象上,而父级原本的原型对象不受干扰

优点:
1、保留了构造函数的优点:创建子类实例,可以向父类构造函数传参
2、保留了原型链的优点:父类的方法定义在父类的原型对象上,可以实现方法复用
3、不共享父类的引用属性,比如arr属性
4、只调用一次父级构造函数
5、构造函数的指向不发生错误

function Father(name) {
    this.name = name
    this.arr = [1, 2, 3]
}
Father.prototype.go = function () {
    console.log('go')
}

function Son(name) {
    Father.call(this, name)
}

Son.prototype = Father.prototype
Son.prototype.constructor = Son
let s = new Son('zs')
let s1 = new Son()
// 说明可以向父级传递参数
console.log(s.name) // zs
s.arr.push(66)     
// 说明父级本身上的引用类型不是复用的,相互独立
console.log(s.arr)  // [1, 2, 3, 66]
console.log(s1.arr)  // [1, 2, 3]
// 说明父级原型上的方法都可以继承到
s.go() // go
s1.go() // go
// 说明方法可以复用,一变全变
console.log(s.go === s1.go) // 
// 可以看出不管是Son还是Father的构造函数指向都是正确的
console.log(Son.prototype.constructor) // Son 的构造函数
console.log(Father.prototype.constructor) // Father的构造函数
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值