ES6 中的 Class

ES6 中的 Class

ES6 中的 Class 其实可以看作是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 Class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已,比如下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ES6 写法
class Person {
constructor(name) {
this.name = name
}
say() {
console.log(this.name)
}
}

// ES5 写法
function Person(name) {
this.name = name
}

Person.prototype.say = function() {
console.log(this.name)
}

// 两种写法的结果是一致的
var p = new Person('zhangsan')
p.say() // zhangsan

我们可以看到 ES5 的构造函数 Person,对应 ES6Person 类的 constructor 方法,不过需要注意的是,『Class 的内部所有定义的方法,都是不可枚举的(non-enumerable)』,比如在上面的例子当中

1
2
3
4
5
6
7
// ES6 当中
Object.keys(Person.prototype) // []
Object.getOwnPropertyNames(Person.prototype) // ['constructor', 'sayHello']

// ES5 当中
Object.keys(Person.prototype) // ['say']
Object.getOwnPropertyNames(Person.prototype) // ['constructor', 'sayHello']

静态方法

静态方法一般用来提供一些工具方法,所有在类中定义的方法,都会被实例继承,如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为『静态方法』

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ES6 中
class Person {
static say() {
console.log(`hello`)
}
}

// ES5 中
function Person() { }

Person.say = function () {
console.log(`hello`)
}

// 两者执行结果是一致的
Person.say() // hello
var p = new Person()
p.say() // p.say is not a function

静态属性

Class 内部『只有静态方法,没有静态属性』,静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性,以前,我们添加静态属性只可以这样

1
2
3
4
5
// ES6 中
class Foo { }

Foo.prop = 1
Foo.prop // 1

然而现在有一个提案,对实例属性和静态属性都规定了新的写法,而且 Babel 已经支持,所以现在我们可以写成

1
2
3
4
5
6
// 暂未统一实现
class Person {
static prop = 1
}

Person.prop // 1

对应到 ES5 就是

1
2
3
4
5
function Person() {}

Person.prop = 1

Person.prop // 1

私有方法

ES6『不提供』私有方法,只能通过变通方法模拟实现,一种做法是在命名上加以区别

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget {
// 公有方法
foo(baz) {
this._bar(baz)
}

// 私有方法
_bar(baz) {
return this.snaf = baz
}

// ...
}

另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget {
// foo 是公有方法,内部调用了 bar.call(this, baz)
// 使得 bar 实际上成为了当前模块的私有方法
foo(baz) {
bar.call(this, baz)
}

// ...
}

function bar(baz) {
return this.snaf = baz
}

有一种方法是利用 Symbol 值的唯一性,将私有方法的名字命名为一个 Symbol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const bar = Symbol('bar')
const snaf = Symbol('snaf')

export default class myClass {
// 公有方法
foo(baz) {
this[bar](baz)
}

// 私有方法
[bar](baz) {
return this[snaf] = baz
}

// ...
}

私有属性

与私有方法一样,ES6『不支持』私有属性,但是可以通过 WeakMap 来实现私有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const privateData = new WeakMap()

class Person {
constructor(name, age) {
privateData.set(this, {
name,
age
})
}

getName() {
return privateData.get(this).name
}

getAge() {
return privateData.get(this).age
}
}

const p = new Person('zhangsan', 20)

p.name // undefined
p.getName() // zhangsan

getters & setters

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class People {
constructor(name) {
this.name = name
}

get name() {
return this._name.toUpperCase()
}

set name(name) {
this._name = name
}

sayName() {
console.log(this.name)
}
}

var p = new People('zhangsan')

console.log(p.name) // ZHANGSAN
console.log(p._name) // zhangsan
p.sayName() // ZHANGSAN

因为定义了 name 的读写器,而没有定义 _name 的读写器,所以访问这两个属性的结果是不同的

继承

我们先来看看 ES5 当中比较推荐的继承方法,寄生组合式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Parent(name) {
this.name = name
}

Parent.prototype.sayName = function () {
console.log(this.name)
}

function Child(name, age) {
Parent.call(this, name)
this.age = age
}

Child.prototype = Object.create(Parent.prototype)

var child1 = new Child('zhangsan', '18')
child1.sayName() // zhangsan

这种方式的继承只调用了一次 Parent 构造函数,并且可以避免在 Parent.prototype 上面创建不必要的、多余的属性,与此同时,原型链还能保持不变,所以还能够正常使用 instanceofisPrototypeOf

下面我们再来看看 ES6 当中的继承,Class 通过关键字 extends 来继承一个类,并且可以通过 super 关键字来引用父类,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Parent {
constructor(name) {
this.name = name
}

sayName() {
console.log(this.name)
}
}

class Child extends Parent {
constructor(name, grade) {
super(name)
this.grade = grade
}

sayGrade() {
console.log(this.grade)
}
}

var child1 = new Child('zhangsan', '18')
child1.sayName() // zhangsan

这里有几个需要注意的地方

  • super 关键字表示父类的构造函数,相当于 ES5Parent.call(this)
  • 子类必须显式的在 constructor 方法中调用 super 方法,否则新建实例时会报错,这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工,如果不调用 super 方法,子类就得不到 this 对象
  • 如果子类没有定义 constructor 方法,这个方法会被默认添加,也就是说不管有没有显式定义,任何一个子类都有 constructor 方法
  • 在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错(因为只有 super 方法才能返回父类实例)
  • Object.getPrototypeOf 方法可以用来从子类上获取父类(判断一个类是否继承了另一个类)

子类的 __proto__

ES6 中,父类的静态方法,可以被子类继承,比如下面这个例子

1
2
3
4
5
6
7
8
9
10
class Foo {
static classMethod() {
return 'hello'
}
}

class Bar extends Foo {
}

Bar.classMethod()

这是因为 Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链

  • 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类
  • 子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性

1
2
3
4
5
6
class Parent { }

class Child extends Parent { }

console.log(Child.__proto__ === Parent) // true
console.log(Child.prototype.__proto__ === Parent.prototype) // true

细心观察可以发现,相比寄生组合式继承,ES6Class 多了一个 Object.setPrototypeOf(Child, Parent) 的步骤

继承目标

extends 关键字后面可以跟多种类型的值

1
class B extends A { }

上面代码的 A,只要是一个有 prototype 属性的函数,就能被 B 继承,由于函数都有 prototype 属性(一般情况下,排除一些箭头函数或者 bind(null) 之类的操作),因此 A 可以是任意函数,除了函数之外,A 的值还可以是 null,当 extend null 的时候

1
2
3
4
class A extends null { }

console.log(A.__proto__ === Function.prototype) // true
console.log(A.prototype.__proto__ === undefined) // true

super 关键字

  • 使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错
  • super 作为函数调用时
    • super 作为函数调用时,代表父类的构造函数(子类的构造函数必须执行一次 super 函数)
    • 虽然代表了父类的构造函数,但是 super 内部的 this 指的是子类的实例(相当于 Father.prototype.constructor.call(this)
    • 作为函数时,super() 只能用在『子类』的构造函数之中,用在其他地方就会报错
  • super 作为对象调用时
    • 在普通方法中,指向父类的原型对象,在静态方法中,指向父类(与父类静态方法相呼应)
    • super 指向父类的原型对象时,定义在父类实例上的方法或属性,是无法通过 super 调用的(定义在 prototype 上的则可以取到)
    • 通过 super 调用父类的方法时,super 会绑定子类的 this
    • 通过 super 对某个属性赋值,这时 super 就是 this,赋值的属性会变成子类实例的属性,如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A {
constructor() {
this.x = 1
}
}

class B extends A {
constructor() {
super()
this.x = 2

// 等同于对 this.x 赋值为 3
super.x = 3

// 而当读取 super.x 的时候,读的是 A.prototype.x,所以返回 undefined
console.log(super.x) // undefined
console.log(this.x) // 3
}
}

let b = new B()

总结

  • Class 的内部所有定义的方法,都是不可枚举的
  • 类和模块的内部,默认就是严格模式,所以不需要使用 use strict 指定运行模式
  • 一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加
  • 不存在变量提升(hoist
  • 类的方法内部如果含有 this,它默认指向类的实例
  • class 中的方法有三种类型:构造函数、静态方法、原型方法
  • class 内部只有静态方法,没有静态属性

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×