/ ECMAScript6

ECMAScript6 之 Class 语法

标签: ECMAScript 6 javaScript

简介

ES6class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

简单的对比

// ES5
function Point(x, y) {
    this.x = x
    this.y = y
}

Point.prototype.toString = function () {
    return '(' + this.x + ', ' + this.y + ')'
}

var p = new Point(1, 2)

// ES6
class Point {
    constructor(x, y) {
        this.x = x
        this.y = y
    }
    
    toString() {
        return '(' + this.x + ', ' + this.y + ')'
    }
}

let p = new Point(1, 2)

上述的代码分别定义了在 ES5ES6 中类声明的方式,可以看出有几点区别

  • ES5function 声明类,ES6 使用 class 声明
  • ES5 中函数中的内容移到了 ES6/constructor
  • ES5 中在 prototype 上配置原型链上的方法,在 ES6 中直接定义

由上可以看出,ES6 中的 class 一定程度上可以认为是 ES5 类写法的语法糖。

我们思考下 ES5 中类是如何实例化的

  1. 直接调用类的函数,然后把 this 对象给返回出来
  2. prototype 上的内容定义在实例的原型链上

首先我们来看第一点,在 ES5 的类编写过程中,会有这么一句

Point.prototype.constructor === Point // true

这句说明了一个类 prototype 上的 constructor 属性是自己。
那么现在在来看实例化过程中第一句,是否是可以认为调用的是类下 prototype 上的 constructor 方法。

那么现在一个类的实例化过程其实和类本身就没多大关系了,仅仅和 prototype 有关。

接着在来看 ES6 中的构造方式,我们定义了一个名字和一系列的方法,这其实就是在定义一个类的 prototype

但需要注意的一点:

  • ES5 中在 prototype 上定义的方法是可枚举的,ES6 中定义的所有方法都是不可枚举的。

类的属性名,可以用 [] 表示

let methodName = 'getArea';

class Square {
    constructor(length) {
        // ...
    }
    
    [methodName]() {
        // ...
    }
}

constructor 方法

constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。

class Point {
}

// 等同于
class Point {
    constructor() {}
}

constructor 方法默认返回实例对象( this ),当然也可以指定对象返回,但是这就会照成实例对象并不是类的实例。

class Foo {
    constructor() {
        return Object.create(null)
    }
}

new Foo() instanceof Foo
// false

Class 表达式

Class 表达式的效果与函数一致

const MyClass = class Me {
    getClassName() {
        return Me.name
    }
}

let inst = new MyClass()
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined

如果一个类被定义成表达式的形式,那么跟在 class 关键字后的类名在类外部不可见,仅仅只能在类内部使用,如果内部不使用的话,可以去掉。

采用 Class 表达式,可以写出立即执行的 Class

let person = new class {
    constructor(name) {
        this.name = name
    }
    
    sayName() {
        console.log(this.name)
    }
}('张三')

person.sayName() // "张三"

但是感觉这很没必要,这还不如直接写一个对象。

不存在变量提升

类在 ES6 中的定义方式与 let 变量一致,不存在变量提升。

new Foo() // ReferenceError
class Foo {}

this 指向

ES6 实例对象下的方法表现与 ES5 一致,在方法中使用 this 对象需要小心,this 代指拥有这个方法的对象。

class Logger {
    printName(name = 'there') {
        this.print(`Hello ${name}`)
    }
    
    print(text) {
        console.log(text)
    }
}

const logger = new Logger()
const { printName } = logger
printName()
// TypeError: Cannot read property 'print' of undefined

上述写法会报错,原因在于声明的 printName 变量的拥有者是 window 。这与函数下的 this 对象的绑定一致,并不存在说 ES6 下有特殊情况,解决方式和函数确定 this 指向方式一致。

  1. 使用 bind
  2. 使用箭头函数

不过使用这两种方法,都需要在 this 对象上绑定同名的原型链上的方法

class Logger {
    constructor() {
        this.printName = this.printName.bind(this)
    }
    
    // ...
}

class Logger {
    constructor() {
        this.printName = (name = 'there') => {
            this.print(`Hello ${name}`)
        }
    }
    
    // ...
}

Class get/set

ES5 一样,在“类”的内部可以使用 getset 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class MyClass {
    constructor() {
        // ...
    }
    get prop() {
        return 'getter'
    }
    set prop(value) {
        console.log('setter: ' + value)
    }
}

let inst = new MyClass()

inst.prop = 123
// setter: 123

inst.prop
// 'getter'

这其实也是在语法糖内的简单实现,在 ES5prototype 是使用 defineProperty 的效果一致。

Class 的静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
    static classMethod() {
        return 'hello'
    }
}

Foo.classMethod() // 'hello'

var foo = new Foo()
foo.classMethod()
// TypeError: foo.classMethod is not a function

当然如果在静态方法包含 this 关键字,这个 this 指的是类,而不是实例,这其实也是遵循函数执行时的 this 指向问题,总是执行调用这个函数的对象,而类仅仅是一个特殊的对象。

父类的静态方法,可以被子类继承。

class Foo {
    static classMethod() {
        return 'hello'
    }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'

但是目前 ES6 规定,类下仅仅只能有静态方法,不能有静态属性。

注: 上面的例子大多数来自《ECMAScript 6入门》这本书。