ES6 class
在 class 之前,我们一般使用 "构造函数" 构建对象。现在有了 class,我们可以更好地进行面向对象编程。
这是一个简单的 ES5 构造函数:
function User(name, gender) {
this.name = name
this.gender = gender
}
const tom = new User('Tom', 'male')
Notice
在 JavaScript 中,所谓的构造函数与其他语言如 Java 并无相同之处,仅仅是因为该函数被 new 调用才叫做构造函数。
这是 ES6 class 版本的:
class User {
constructor(name, gender) {
this.name = name
this.gender = gender
}
}
const tom = new User('Tom', 'male')
class 实际上是一种特殊的函数,就像你能够定义的函数表达式和函数声明一样,类语法有两个组成部分:类表达式和类声明。
// 类声明
class User {}
// 类表达式
const User = class {}
// 类也是函数
console.log(typeof User) // function
// 并且类本身就指向构造函数
User === User.prototype.constructor // true
Notice
类和函数有一个重要的不同就是:函数存在声明提升,而类不会,你必须在类声明了之后再实例化。
实例的属性除非显式定义在其本身(即定义在 this 对象上),否则都是定义在原型上(即定义在 class 上):
//定义类
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
toString() {
return '(' + this.x + ', ' + this.y + ')'
}
}
var point = new Point(2, 3)
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
构造函数
constructor 方法用于创建和初始化一个由 class 创建的对象,当通过 new 调用时,会自动调用该方法。
Notice
一个类必须拥有 constructor 方法!如果没有显式定义,则会默认添加一个返回当前实例对象 this 的 constructor。
getter&setter
与 ES5 一样,在 class 的内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
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'
static
静态方法调用直接在类上进行,不能在类的实例上调用。静态方法通常用于创建实用程序函数,静态方法不会被子类继承。
class User {
static staticMethod() {
console.log('This is a static method')
}
}
const Tom = new User()
Tom.staticMethod() // error: Tom.staticMethod is not a function
User.staticMethod() // log: This is a static method
静态方法调用同一个类中的其他静态方法,可使用 this 关键字,但在非静态方法中,不能直接使用 this 关键字来访问静态方法。而是要用类名来调用:
class User {
constructor() {
this.method()
}
method() {
User.staticMethod1()
}
static staticMethod1() {
this.staticMethod2()
}
static staticMethod2() {
console.log('This is static method 2')
}
}
new User() // log: This is static method 2
静态属性(ES7):同 ES6 的静态方法声明方式,可以在 Class 内部声明,声明方式为在属性声明前加上 static 关键字:
//ES6:
class Foo {}
Foo.prop = 1 //静态属性(类的属性)
//ES7:
class Foo {
static prop = 1 //静态属性
}
class MyClass {
static myStaticProp = 42
constructor() {
console.log(MyClass.myProp) // 42
}
}
类的 prototype 属性和 __proto__ 属性
大多数浏览器的 ES5 实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的 prototype 属性。Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链。
子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。
super
super 可以当作函数使用:
class Parent {}
class Child extends Parent {
constructor() {
super()
}
}
ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面,而在 ES6 中,子类想要继承父类,必须调用 super,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。因此,当你在子类构造函数中操作 this 的代码必须写在 super 后面。
执行了 super 就是执行了父类构造函数,但 super 内部的 this 指向的是子类,super 执行后返回的也是子类的实例,因此相当于执行 Parent.prototype.constructor.call(this)
super 也可以调用父类上的静态方法,但只有在子类的静态方法上才可以这么做,非静态方法中还是要通过父类名字调用静态方法:
class Parent {
constructor() {}
method() {
console.log('This is a method from parent')
}
static staticMethod() {
console.log('This is a staticMethod from parent')
}
}
class Child extends Parent {
constructor() {
super()
super.method()
}
static staticChildMethod() {
super.staticMethod()
}
}
new Child() // This is a method from parent
Child.staticChildMethod() // This is a staticMethod from parent
可以在子类的静态方法中调用父类的静态方法,此时 super 指向的是父类本身。在子类普通方法和 constructor 中可以调用父类的普通方法,此时 super 指向的是父类的原型。
通过 super 调用父类方法时,super 会绑定子类的 this。
class Parent {
constructor() {
this.x = 1
}
method() {
console.log(this.x)
}
}
class Child extends Parent {
constructor() {
super()
this.x = 2
super.method()
}
}
new Child() // log: 2
因此,如果使用 super 对属性赋值,那么改变的将是子类的属性而不是父类属性:
class Parent {
constructor() {
this.x = 1
}
}
class Child extends Parent {
constructor() {
super()
this.x = 2
super.x = 3
console.log(this.x) // 3
}
}
new Child()
直接输出 super 是会报错的,因为 JS 引擎不知道 super 是作为函数调用还是作为对象使用。
class Parent {}
class Child extends Parent {
constructor() {
console.log(super) // error
}
}
super 只能在类中使用么?
不是,super 还可以在对象字面量中使用:
const obj1 = {
method1() {
console.log("method 1");
}
}
const obj2 = {
method2() {
super.method1();
}
}
Object.setPrototypeOf(obj2, obj1);
obj2.method2(); // logs "method 1"
公共字段
可以在类中声明公共字段。
class Rectangle {
height = 0 // 初始化的公共字段
width // 未初始化的公共字段
static bgc = '#FFFFFF' // 静态公共字段
constructor(height, width, bgc) {
this.height = height
this.width = width
Rectangle.bgc = bgc
}
}
const rectangle = new Rectangle(12, 21)
console.log(rectangle.height) // log: 12
私有字段
私有字段使用 # 开头,只能在类的内部进行调用,而不能在类之外引用。
class Rectangle {
#height = 0
#width
constructor(height, width) {
this.#height = height
this.#width = width
}
getHeight() {
return this.#height
}
}
const rectangle = new Rectangle(12, 21)
console.log(rectangle.#height) // error: Private field '#height' must be declared in an enclosing class
console.log(rectangle.getHeight()) // log: 12
除了这个新特性之外,还有几种方法可以实现:
class SimCard {
constructor(number, type, pinCode) {
this.number = number
this.type = type
let _pinCode = pinCode
// this property is intended to be a private one
this.getPinCode = () => {
return _pinCode
}
}
}
const card = new SimCard('444-555-666', 'Nano SIM', 1515)
console.log(card._pinCode) // outputs undefined
console.log(card.getPinCode()) // outputs 1515
在 JS 界约定俗成使用 _ 开头作为私有属性,然后配合 getter 实现私有属性机制。
也可以使用 Symbol 定义唯一属性来实现:
const SimCard = (() => {
const _pinCode = Symbol('PinCode')
class SimCard {
constructor(number, type, pinCode) {
this.number = number
this.type = type
this[_pinCode] = pinCode
}
get pinCode() {
return this[_pinCode]
}
}
return SimCard
})()
const card = new SimCard('444-555-666', 'Nano SIM', 1515)
console.log(card._pinCode) // outputs undefined
console.log(card.pinCode) // outputs 1515
不过外部仍然可以使用 Object.getOwnPropertySymbols(SimCard) 来获取使用 Symbol 定义的属性。
new.target
new.target 属性允许你检测函数和或者构造方法是否是通过 new 被调用的,如果是 new 调用的,new.target 就会返回一个指向构造方法或函数的引用。在普通函数调用中,new.target 的值是 undefined。
function User() {
if (new.target === undefined) {
console.log('没有使用new调用')
} else {
console.log(new.target === User) // true
}
}
但在有子类继承父类时,实例化子类时 new.target 会返回子类,因此可以用来写出不能独立使用、必须继承后才能使用的类:
class Parent {
constructor(){
if(new.target === Parent) {
throw new Error('本类不能被实例化!')
}
}
}
class Child extends Parent {
constructor{
super()
}
}
new Parent() // 报错:本类不能被实例化!
new Child() // 正确
内部实现
我们知道 JavaScript 中所谓的 class 不过是一个语法糖而已,与 Java 的 class 完全不同。那么 JavaScript 的 class 是如何实现的呢?
在此之前,如果不明白 new 做了什么,可以看 new 绑定。
我们知道在 ES5 中经过 new 调用的构造函数产生的实例会把 __proto__ 指向构造函数的 prototype,如果是 ES6 的 class 呢?
class User {
constructor(name) {
this.name = name
}
}
const tom = new User('Tom')
console.log(tom.__proto__ === User.prototype) // true
console.log(tom.__proto__.constructor === User) // true
因此,new 调用构造函数和类产生的结果都是相同的。只是一个指向构造函数本身,一个指向类。
我们在 ES5 中可以使用原型链继承属性和方法:
function User(name) {
this.name = name
}
User.prototype.getName = function () {
return this.name
}
const tom = new User('tom')
console.log(tom.getName()) // log: tom
上面的代码在 ES6 class 中可以等同于:
class User {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
const tom = new User('tom')
箭头函数和普通函数
我们知道类中可以定义箭头函数也可以定义普通函数:
class A {
constructor() {}
b() {}
c = () => {}
}
但实际上两者完全不同,我们来看一个例子:
class A {
constructor() {}
b() {}
c = () => {}
}
const d = new A()
const e = new A()
console.log(d.b === e.b) // true
console.log(d.c === e.c) // false
在类中箭头函数的定义实际上会变成:
class A {
constructor() {
this.c = () => {}
}
}
A.prototype.b = function () {}
也就是说,箭头函数和普通属性的行为一样,每创建一个子类都会重新定义,而普通函数则是定义在类的原型上的。
总结
class 的本质还是 ES5 的原型链和构造函数,但是比原来的语法更加方便。class 没有声明提升,也不能覆写,但是函数有声明提升,也可以覆写。