一、class的概念和基本语法
class是ES6新出的一个语法糖,用来替代ES5的构造函数,它的绝大多数功能,使用ES5的语法都能做到。
ES5:
function Animal(name) {
this.name = name
}
Animal.play = function () {
console.log('paly')
}
Animal.prototype.eat = function () {
console.log(this.name + 'eat')
}
ES6:
class Animal {
constructor(name) {
this.name = name
}
eat() {
console.log(this.name + 'eat')
}
static play() {
console.log('play')
}
}
Animal类用Object.prototype.toString.call(Animal)打印结果为[object Function],说明class的本质还是函数,内部实现还是使用的ES5的构造函数。
二、类的实例化
1、类在调用时必须要结合new
ES5的函数在非严格模式下,调用时可以不加new,它就是一个普通函数;在严格模式下必须结合new调用。ES6的class默认就是开启了严格模式
var cat = new Animal('喵喵')
cat.eat()
Animal.play()
2、类中的所有方法都是定义在类的prototype上
class Animal {
eat() {
console.log('eat')
}
}
const animal = new Animal()
console.log(animal)
但是要注意,只有手动地往prototype上添加的属性/方法才是可枚举的。对于这一点,ES5都是手动添加到prototype上的,所以都可以枚举,ES6若是直接写在类中的方法,是不可枚举的
class Animal {
eat() {
console.log('eat')
}
}
Animal.prototype.sleep = function () {
console.log('sleep')
}
console.log(Object.keys(Animal.prototype)) // ['sleep']
3、和ES5一样,实例的属性直接定义在this身上
ES5定义实例的属性:
function Animal(name, age) {
this.name = name
this.age = age
}
var animal = new Animal('喵喵', 2)
console.log(animal)
ES6定义实例的属性:
class Animal {
constructor(name, age) {
this.name = name
this.age = age
}
}
const animal = new Animal('喵喵', 2)
console.log(animal)
三、constructor方法
1、constructor是class中默认的方法,如果没写,会自动加上:
class Animal {
// constructor() {} // 这一句不写会自动加上
eat() {
console.log('eat')
}
}
console.log(Object.getOwnPropertyNames(Animal.prototype)) // ['constructor', 'eat']
constructor中默认会return this,如果手动改变返回值,当return简单数据类型时会被忽略,当return复杂数据类型时会修改原来的return this。这一点和ES5构造函数的返回值是一致的
四、this的指向
class Animal {
getName(name = '旺财') {
this.print(name)
}
print(text) {
console.log(text)
}
}
const animal = new Animal()
animal.getName()
const { getName } = animal
getName() // Uncaught TypeError: Cannot read properties of undefined
类的方法中的this指向类的实例,但是将该方法提取出来,再进行调用时,此处getName中的this指向undefined
解决办法一:在构造函数中绑定getName的this,这样就不会找不到了
constructor() {
this.getName = this.getName.bind(this)
}
解决办法二:使用箭头函数
constructor() {
this.getName = (name = 'xx') => {
this.print(name)
}
}
相当于直接在类中写:
getName = (name = 'xx') => {
this.print(name)
}
解决办法三:使用Proxy,获取方法的时候,自动绑定this
function selfish(target) {
const cache = new WeakMap()
const handler = {
get(target, key) {
const value = Reflect.get(target, key)
if (typeof value !== 'function') {
return value
}
if (!cache.has(value)) {
cache.set(value, value.bind(target))
}
return cache.get(value)
}
}
const proxy = new Proxy(target, handler)
return proxy
}
// 实例化
const animal = selfish(new Animal())
五、class的继承
ES5的继承:(个人比较推崇圣杯模式继承,而不是通过Animal.apply(this, [...args]))
function Animal(name) {
this.name = name
}
Animal.prototype.eat = function () {
console.log(this.name + 'eat')
}
function Cat(name, age) {
Animal.call(this, name)
this.age = age
}
inherit(Cat, Animal)
var cat = new Cat('喵喵', 2)
console.log(cat)
cat.eat()
// 圣杯模式继承
function inherit(Target, Origin) {
function Buffer() {}
Buffer.prototype = Origin.prototype
Target.prototype = new Buffer()
Target.prototype.constructor = Target
Target.prototype.super_class = Origin
}
ES6:
在class中,通过extends关键字来实现继承,这比ES5修改原型链继承要清晰和方便许多。
class Animal {
constructor(name) {
this.name = name
}
eat() {
console.log(this.name + 'eat')
}
}
class Cat extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}
const cat = new Cat('喵喵', 2)
console.log(cat)
cat.eat()
在Cat类中,constructor函数中必须要调用super,super表示父类的构造函数,用来创建父类的this对象。
子类必须要在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,如果不调用super,子类就得不到this对象。
这和ES5的机制完全不同,ES6中子类中必须要先创建父类的this对象(调用super),然后在子类的构造函数中才能修改this。
子类中如果不写constructor,默认会加上constructor函数并调用super,就像这样:
constructor(...args) {
super(...args)
}
六、类的prototype和__proto__
在ES5中,实例的__proto__等于其构造函数的prototype。class作为构造函数的语法糖,同时具有prototype和__proto__,因此存在两条继承链
- 子类的__proto__指向父类
- 子类的prototype的__proto__,指向父类的prototype
class Animal {}
class Cat extends Animal {}
console.log(Cat.__proto__ === Animal) // true
console.log(Cat.prototype.__proto__ === Animal.prototype) // true
实际上,extends等同于:
Cat.__proto__ = Animal
Cat.prototype.__proto__ = Animal.prototype
七、super
super可以当做函数使用,也可以当做对象使用。
第一种情况,super当做函数使用,代表父类的构造函数,ES6要求,子类的构造函数必须要执行一次super。当子类中不写构造函数时,系统会默认加上。
class Animal {}
class Cat extends Animal {
constructor() {
super() // super()代表父类Animal的构造函数。作为函数时,super()只能用在constructor中
}
}
super中的this:
class Animal {
constructor() {
console.log(new.target.name) // Cat
}
}
class Cat extends Animal {}
new Cat()
第二种情况,当super当做对象使用时,指向父类的prototype:
class Animal {
getFood() {
return '鱼'
}
}
class Cat extends Animal {
eat() {
console.log(super.getFood()) // 相当于Animal.prototype.getFood()
}
}
const cat = new Cat()
cat.eat()
ES6规定,通过super调用父类的方法时,super会绑定子类的this。
class A {
constructor() {
this.x = 1
}
print() {
console.log(this.x)
}
}
class B extends A {
constructor() {
super()
this.x = 2
}
m() {
super.print()
}
}
let b = new B()
b.m() // 2
super.print()实际上是super.print.call(this)
由于super绑定了子类的this,通过super对属性赋值实际上就是通过this对属性赋值:
class A {
constructor() {
this.x = 1
}
}
class B extends A {
constructor() {
super()
this.x = 2
super.x = 3
console.log(super.x) // undefined
console.log(this.x) // 3
}
}
let b = new B()
super.x = 3实际上是this.x = 3,而super.x实际上是A.prototype.x
另外,在使用super时,必须明确super是作为函数还是对象使用,函数就要加括号,对象就要跟着属性或方法,直接打印super会报错
八、class的静态方法
在方法名前加上static关键字,表示这个方法是类的静态方法,只能由类来调用
class Animal {
static sleep() {
console.log('sleep')
}
}
Animal.sleep()
静态方法可以被子类继承:
class Animal {
static sleep() {
console.log('sleep')
}
}
class Cat extends Animal {}
Cat.sleep()
静态方法也可以被super直接调用:
class Cat extends Animal {
static catSleep() {
console.log(super.sleep())
}
}
Cat.catSleep()
九、class的静态属性和实例属性
ES6明确规定,在类中只有静态方法,没有静态属性,所以ES7之前只能通过这种方式定义静态属性:
class Animal {}
Animal.height = 20
在ES7的提案中,weight = 5是在constructor中this.weight = 5的简写
class Animal {
weight = 5 // 实例属性
static age = 2 // 静态属性
}
十、new.target
new.target是ES6对new操作符的一个补充属性,必须在函数内使用,如果这个函数是被new作用的,那么new.target返回这个函数。如果这个函数不是通过new调用的,那么new.target返回undefined
function Animal() {
console.log(new.target)
}
Animal() // undefined
new Animal() // f Animal(){}
利用new.target的这个特点,可以让构造函数必须使用new来调用:
function Animal(name) {
if (new.target === undefined) throw new Error('必须要使用new生成实例') // 只需加上这句判断
this.name = name
}
new了谁,new.target指向谁:
class Animal {
constructor() {
console.log(new.target)
}
}
class Cat extends Animal {}
new Animal() // Animal类
new Cat() // Cat类
利用这个特点,可以设置一个类不能独立使用,必须继承后才能使用:
class Animal {
constructor() {
if (new.target === Animal) throw new Error('Animal不可以被实例化')
}
}
class Cat extends Animal {}
// new Animal() // 报错
new Cat() // Cat类
十一、get和set
class Animal {
name = 'xx'
get name() {
return this.name
}
set name(val) {
this.name = val
}
}
const animal = new Animal()
console.log(animal.name) // xx
animal.name = 'yy'
console.log(animal.name) // yy