类的语法结构(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
- 继承父类的普通函数
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() // 唱歌
- 继承父类的构造函数
- 切记:调用父类的构造函数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指向问题
- 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
- 方法里的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)
- 构造函数中的静态成员:在构造函数本身上添加的成员,只能由构造函数本身访问
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
- 构造函数中的实例成员:在构造函数内部创建的对象成员,只能由实例化的对象来访问
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) // 访问不到
- 原型对象prototype
- 对象原型__proto__
- 原型链
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的构造函数