JavaScript中的class

这篇文章讲一下ES6新增的特性:class上一篇文章已经详细的讲了JavaScript中的原型机制,实现继承的两种主要方式,有了这些基础,这篇文章不用啰嗦很长了。我们先通过调试一个例子,看class背后是如何运作的,然后用构造函数模拟实现一下class。最后再盘点一下class与构造函数的区别,避免日后在开发中踩坑。

class背后的机制和语法

先准备个🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class User {
#username = ''
gender = ''

constructor(username, gender) {
this.#username = username
this.gender = gender
}

sayHi() {
console.log('Hi ' + this.#username)
}

get username() {
console.log('super get username called')
return this.#username
}

set username(val) {
console.log('super set username called')
this.#username = val
}

static isFemale(gender) {
return gender === 'female'
}
}

class VIPUser extends User {

constructor(username, gender) {
super(username, gender)
}

sayHi() {
console.log(`Hi ${this.username}, ${VIPUser.greetings()}`)
}

static greetings() {
return 'nice to meet you!'
}
}

const user = new VIPUser('Nick', 'male', 10)
user.sayHi()

在chrome中打断点调试,展开User查看:

嗯,可以看到,class和构造函数非常像了。红色箭头表示了User本身的原型链如下,User也是继承于Function。所以class声明的其实也是一个函数,是类构造函数。

1
User ------> Function -------> Object

class中是支持声明类的静态属性和方法的。本例中isFemale这个方法,我在声明它时在前面加上了static关键字,于是它成了User自身的一个方法,因此需要用类名去访问:User.isFemale()

再看上图中绿色框中的内容,定义在class中的方法会放在prototype中,比如sayHi(),这样这些方法就成了类的实例对象的原型方法。

class中支持getter/setter语法,使得能正常用点语法访问和设置属性,但当属性被访问和赋值时可以拦截到。本例中的username使用了getter/setter语法,这导致在User.prototype自动生成了一个username属性,User的实例对象就可以访问该属性了。

User.prototype.username是由于get username()生成的,我们声明一个getter,就会在prototype中自动生成一个对应的属性。

class中支持使用extends关键字继承其他类,使用super调用父类中的属性和方法VIPUser继承自User,先展开VIPUser.prototype看一下:

可以看到,VIPUser.prototype继承了User.prototype,这样VIPUser的实例对象即可访问VIPUser中的方法也可以访问User中的方法了。VIPUser重写了sayHi()方法,所以在VIPUser.prototype中出现了该方法。

再展开VIPUser.__proto__看一下,VIPUser本身的原型链是怎样的:

VIPUser也同时继承了User,因此User的静态方法也一起被VIPUser继承,VIPUser.greetings()可以正常访问。

1
VIPUser -------> User ---------> Function ----------> Object

最后看一下user的展开的内容:

user的原型链如下:

1
user ------> VIPUser.prototype -------> User.prototype --------> Object

UserVIPUser中声明的变量#usernamelevel,最终成了实例对象user自身的属性。

class中支持声明私有属性,私有属性以#开头。尝试调用user.#username会报错。

上面的分析已经基本列出了要如何使用class,下面再将class语法总结一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass extends OtherClass {  // MyClass继承自OtherClass 
publicInstanceProp = value // 声明实例对象的公有属性

#privateInstanceProp = value // 声明实例对象的私有属性,该属性只能用于当前类的内部

static staticProp = value // 声明类的静态属性,只能通过MyClass.staticProp访问

constructor(...) { // 构造器,一个类只能有一个构造器,允许没有构造器
super(...) // 继承了其他类,并且有构造器时,必须用super调用父类的构造器
// ...
}

method(...) {} // 声明实例对象的原型方法

static method(...) {} // 声明类的静态方法,只能通过MyClass.method()调用。

get method() {} // getter方法
set method(val) {} // setter方法

[Symbol.iterator]() {} // 迭代器方法,比如用for ... of 遍历实例对象时输出什么
}

模拟实现class声明的类

接下来用构造函数模拟一下上面User类和VIPUser类的实现,以便加深对class理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const User = (function() {
let _username = ''

function User(username) {
_username = username
}

User.prototype.sayHi = function() {
console.log('Hi ' + _username)
}

Object.defineProperty(User.prototype, 'username', {
get: function() { return _username },
set: function(val) { _username = val }
})

User.greetings = function() {
return 'nice to meet you!'
}

return User
})()

const VIPUser = (function() {
function VIPUser(username, level) {
User.call(this, username)
this.level = level
}

Object.setPrototypeOf(VIPUser, User)
Object.setPrototypeOf(VIPUser.prototype, User.prototype)

VIPUser.prototype.sayHi = function() {
console.log(`Hi ${this.username}, ${VIPUser.greetings()}`)
}
return VIPUser
})()

const user = new VIPUser('Nick', 10)
user.sayHi()

可以再打开Chrome打断点调试看看,跟上面class声明的UserVIPUser的原型结构一样。

class和构造函数的区别

尽管class和构造函数很像,但还是有一些区别:

  1. class声明的类,必须使用new运算符调用,否则会抛错误。

  2. class中定义的方法都不可枚举,类的prototype中的所有方法,enumerable标记的都为false。上面模拟User中的sayHi方法,更准确地应该像下面这样创建,VIPUser中的sayHi方法也一样:

1
2
3
4
5
6
Object.defineProperty(User.prototype, 'sayHi', {
value: function() {
console.log('Hi ' + _username)
},
enumerable: false
})

这样在用for...in遍历对象属性时,拿到的就都是对象本身的属性了,不需要hasOwnProperty去判断。对普通函数构造出来的对象,for...in会一起输出原型属性和对象本身的属性。

  1. class声明的类,跟letconst一样,不能提升。重复定义会报错,未声明先使用会报错。即使定义在全局作用域中,也不会挂在全局对象上,所以上面我们看到,UserVIPUser并没有在window上。

  2. class中的方法默认使用严格模式。

其他

  1. 也可以用表达式定义一个类:
1
2
3
4
5
const Test = class {
constructor(...) {
// ...
}
}
  1. extends允许后接任何表达式:
1
2
3
4
5
6
7
8
9
function f(phrase) {
return class {
sayHi() { console.log(phrase) }
}
}

class User extends f('Hello') {}

new User().sayHi()

函数f可以根据条件决定返回什么类,这样User就不是继承于一个固定的类了。

参考资料

【1】 ES6语法中的class、extends与super的原理

本文作者:意林
本文链接:http://shinancao.cn/2019/09/12/JS-Class/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!