在 ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。
基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,全新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
上面示例,可以使用 ES6 的 class 改写为:
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
toString() {
return '(' + this.x + ', ' + this.y + ')'
}
}
上面的示例定义了一个“类”,可以看到里面有一个 constructor() 方法,这就是构造方法,而 this 关键字则代表实例对象。这种全新的 Class 写法,本质上与开头的 ES5 的构造函数 Point 是一致的。
Point 类除了构造方法,还定义了一个 toString() 方法。注意,定义了 toString() 方法的时候,前面不需要加上 function 这个关键字,直接把函数定义放进去就可以了。另外,方法与方法之间不需要逗号 , 分隔,加了会报错。
ES6 的类,完全可以看作构造函数的另一种写法。
class Point {
// ...
}
console.log(typeof Point) // "function"
Point.prototype.constructor === Point // true
// 上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
使用的时候,也是直接对类使用 new 关键字,跟构造函数的用法完全一致。还有,当实例化不指定参数列表时,new Point() 等同于 new Point。
与 ES5 有一点区别的是,类不能直接当作函数一样调用,即 Point() 是会抛出错误的:TypeError: Class constructor Point cannot be invoked without 'new'。而 ES5 中,若构造函数不使用 new 关键字进行实例化,而是直接当作函数调用是没问题的。
class Bar {
doStuff() {
console.log('stuff')
}
}
const b = new Bar()
b.doStuff() // "stuff"
构造函数的 prototype 属性,在 ES6 的“类”上依然存在。事实上,类的所有方法都定义在类的 prototype 属性上面。我们在控制台打印下 point 实例对象:
class Point {
constructor() {}
toString() {}
toValue() {}
}
const point = new Point()
class Foo {
static bar() {
ths.baz() // this 指向 Foo 本身
}
static baz() {
console.log('baz')
}
// 这是没问题的,允许静态方法与非静态方法重名
// static qux() {
// // ...
// }
// 该方法只会在实例化时才会挂载到实例对象上
// 而 Foo 类本身是不含此方法的
// 因此,静态方法与非静态方法是可以重名的。
qux() {
console.log('qux')
}
}
Foo.bar() // "baz"
Foo.qux() // TypeError: Foo.qux is not a function
父类的静态方法,可以被子类继承。
class Foo {
static classMethod() {
console.log('The static method of the parent class.')
}
}
class Bar extends Foo {}
// 可以在子类中调用父类的 classMethod 静态方法
Bar.classMethod() // "The static method of the parent class."
若子类也定义了 classMethod 静态方法,可以通过 super 对象调用父类的 classMethod 静态方法。
class Foo {
static classMethod() {
console.log('The static method of the parent class.')
}
}
class Bar extends Foo {
static classMethod() {
super.classMethod() // 调用父类静态方法
console.log('Static method of subclass.')
}
}
Bar.classMethod()
// "The static method of the parent class."
// "Static method of subclass."
九、静态属性
静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象上的属性。
目前,根据 ECMAScript 规定,Class 内部只有静态方法,没有静态属性。静态属性只能通过在 Class 外部定义。
class Foo {}
Foo.prop = 1 // 静态属性 prop
const foo = new Foo()
console.log(foo.prop) // undefined
console.log(Foo.prop) // 1
class Foo {
// 静态属性
static prop = 'private property'
// 静态私有属性
static #prop = 'static private property'
// 静态方法
static bar() {
console.log(Foo.prop)
console.log(Foo.#prop)
}
// 静态私有方法
static #bar() {
console.log(Foo.prop)
console.log(Foo.#prop)
}
}
// 正常访问
Foo.prop // "private property"
Foo.bar() // "private property"、"static private property"
// 以下报错
Foo.#prop // Private field '#prop' must be declared in an enclosing class
Foo.#bar()
上面示例中,#prop 是静态私有属性,#bar 是静态私有方法,在 Class 外部是不能访问的,只能在内部使用。
还有,静态的私有属性或方法,都是可以被子类继承的。
class Bar extends Foo {}
Bar.prop // Correct
Bar.bar() // Correct
十一、new.target 属性
new 运算符是从构造函数生成实例对象的关键字。在构造函数是通过 new 或 Reflect.constructor() 调用的,那么 new.target 指向被被调用的构造函数,否则返回 undefined。
因此,可以利用它来确保构造函数只能通过 new 关键字来调用。例如:
function Point(x, y) {
// 也可以这样判断:`new.target === Point`
if (new.target !== undefined) {
this.x = x
this.y = y
} else {
throw new TypeError('Point() must be called with new.')
}
}
const p1 = new Point(1, 2) // 正确使用方式
const p2 = Point(3, 4) // TypeError: Point() must be called with new.
而在类的构造方法中,new.target 指向“直接”被 new 执行的构造函数。那么,当子类继承父类时,在父类的构造方法中 new.target 指向子类。
class Point {
constructor(x, y) {
this.x = x
this.y = y
// 若子类继承父类时,new.target 指向子类。
console.log(new.target === Point)
// if (new.target === Point) {
// throw new TypeError('The Point class cannot be instantiated.')
// }
}
}
class P extends Point {
constructor(x, y, z) {
super(x, y)
this.z = z
}
}
const point = new Point(1, 2) // 会打印 true
const p = new P(1, 2, 3) // 会打印 false
利用此特性,可以写出不可独立使用,必须继承后才会使用的父类。如注释部分。
若在函数外部使用 new.target 会抛出错误:
new.target // SyntaxError: new.target expression is not allowed here
来持续学习吧!
此前写了两篇关于 JavaScript 原型以及继承的文章(源自 ULIVZ)。
然后,今天仔细看下 ES6 中的 Class 语法。
一、简介
1. 类的由来
在 JavaScript 中,生成实例对象的传统方法是通过构造函数。
上面这种写法,跟传统的面向对象语言(比如 C++、Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
在 ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过
class
关键字,可以定义类。基本上,ES6 的
class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,全新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面示例,可以使用 ES6 的
class
改写为:上面的示例定义了一个“类”,可以看到里面有一个
constructor()
方法,这就是构造方法,而this
关键字则代表实例对象。这种全新的 Class 写法,本质上与开头的 ES5 的构造函数Point
是一致的。Point
类除了构造方法,还定义了一个toString()
方法。注意,定义了toString()
方法的时候,前面不需要加上function
这个关键字,直接把函数定义放进去就可以了。另外,方法与方法之间不需要逗号,
分隔,加了会报错。ES6 的类,完全可以看作构造函数的另一种写法。
使用的时候,也是直接对类使用
new
关键字,跟构造函数的用法完全一致。还有,当实例化不指定参数列表时,new Point()
等同于new Point
。构造函数的
prototype
属性,在 ES6 的“类”上依然存在。事实上,类的所有方法都定义在类的prototype
属性上面。我们在控制台打印下point
实例对象:上面的示例中,
constructor()
、toString()
、toValue()
这三个方法,其实都是定义在Point.prototype
上面。上面的示例中,
point
是Point
类的实例,它的constructor()
方法就是Point
类原型的constructor()
方法。小结:
二、constructor
constructor()
方法是类的默认方法,通过new
关键字实例化对象是,内部会自动调用该方法。一个类必须有constructor()
方法。当你定义一个类时,若无显式定义,会自动添加一个空的默认constructor()
方法(由 JS 引擎自动添加),即:constructor()
方法默认返回实例对象(即this
),亦可返回任意一个对象(引用类型的值)。上面示例中,
constructor()
返回了一个全新对象,导致了point
对象并不是Point
的实例对象。三、类的实例
上面提到,Class 不能当做函数直接调用,否则会抛出语法错误的。正确地,应使用
new
关键字进行实例化。在 Class 中,如何定义属性和方法?那它们是挂载到实例对象,还是类的原型上?
下面我们来看看吧:
我们来打印一下
point
实例对象,一目了然:与 ES5 一样,类的所有实例共享一个原型对象。
因此,不建议在实例中,利用
__proto__
去改写原型,它会改变类的定义,进而影响到该类的所有实例。请注意,以下这种写法及其结果。
四、setter、getter
在 JavaScript 中,我们可以借助
setter
和getter
语法,以安全的方式来访问对象的属性。使用getter
可以访问属性值,而setter
可以修改属性值。上面示例中,
prop
属性有对应的存值函数和取值函数,因此存取行为都被自定义了。还有getter
、setter
方法是设置在属性的 Descripter 对象上的。五、属性表达式
类的属性名,可以采用表达式,即计算属性名。
六、类的表达方式
类可以这样定义:
以上三种类的表达方式,可以对应上:函数声明、匿名函数表达式、(具名)函数表达式,这点是相同的。
还有,利用“类表达式”的形式,可以写出立即执行的 Class,这点与函数表达式是相同的。
以上三种方式都可以定义一个类,但需要注意的是:
七、注意点
严格模式 在类和模块的内部,默认就是严格模式,无需通过
use strict
来指定,也仅有严格模式可用。提升问题 刚才提到使用
class
关键字声明的类,不存在“提升” (Hoisting)问题。这种规定的原因与类的继承有关,必须保证子类在父类之后定义。name 属性 本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被
class
继承,包括name
属性。它总是返回class
关键字后面的类名,若是匿名类表达式声明,则返回变量名。Generator 方法 如果在某个方法之前加上星号(
*
),则表示该方法是一个 Generator 函数。以下示例中,
Foo
类的Symbol.iterator
方法就是一个 Generator 函数。Symbol.iterator
方法返回一个Foo
类的默认遍历器,for...of
循环会自动调用这个遍历器。const foo = new Foo('Hello', 'World') for (let x of foo) { console.log(x) } // "Hello" // "World"
在上述示例中,
getX
方法的this
默认指向Point
实例对象。但是,如果将这个方法提取出来单独使用,this
会指向该方法运行时所在的环境(由于class
内部是严格模式,所以this
实际指向的是undefined
),导致找不到getX
方法而报错。解决方法如下:
八、静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上
static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这个就被称为“静态方法”。上述示例中,
Foo
类的classMethod
方法前有static
关键字,表示该方法是一个静态方法,可以直接在Foo
类上调用,而不是在Foo
类的实例对象上调用。若通过实例对象调用静态方法,会抛出错误,因为实例对象上并没有classMethod
方法。注意,静态方法内
this
指向类本身,而非实例对象。父类的静态方法,可以被子类继承。
若子类也定义了
classMethod
静态方法,可以通过super
对象调用父类的classMethod
静态方法。九、静态属性
静态属性指的是 Class 本身的属性,即
Class.propName
,而不是定义在实例对象上的属性。目前,根据 ECMAScript 规定,Class 内部只有静态方法,没有静态属性。静态属性只能通过在 Class 外部定义。
现在有一个提案提供了类的静态属性,写法是在属性签名加上
static
关键字。通过以上方式来定义静态属性,显然要比老式写法更好地组织代码,其语义更好。而老式写法往往很容易让人忽略这个静态属性。
十、私有方法和私有属性
在目前,在 Class 内部定义的属性和方法,在类的外部都是可以访问到的。
而私有方法和私有属性的目的在于,它们只允许在 Class 内部访问,而外部是不能访问的。但由于目前 ECMAScript 标准并未提供,只能通过变通的方式模拟实现。
通过命名加以区别
但显然这仍然可在
Foo
实例对象中访问到instance._baz()
。以上示例,间接使得
baz
成了类的“私有方法”,它对类的实例是不可见的。Symbol
的唯一性,将私有方法的名称命名为Symbol
值。以上示例中,
_bar
是Symbol
值,一般在封装类时不让其在获取到,以达到私有方法和私有属性的效果。但是仍然可通过Reflect.ownKeys()
依然可以获取到。私有属性的提案
目前,有一个提案为 Class 添加私有属性。在属性名之前,使用
#
表示。在上述示例中,
#prop
、#bar
就是私有属性和私有属性,且#
是属性名的一部分,使用时也必须带有#
,因此#prop
和prop
是两个不同的属性。另外,私有属性也可以设置
setter
和getter
方法。还有,私用属性和私有方法,前面也可以加上
static
关键字,使其成为静态的私有属性或方法。上面示例中,
#prop
是静态私有属性,#bar
是静态私有方法,在 Class 外部是不能访问的,只能在内部使用。还有,静态的私有属性或方法,都是可以被子类继承的。
十一、new.target 属性
new
运算符是从构造函数生成实例对象的关键字。在构造函数是通过new
或Reflect.constructor()
调用的,那么new.target
指向被被调用的构造函数,否则返回undefined
。因此,可以利用它来确保构造函数只能通过
new
关键字来调用。例如:而在类的构造方法中,
new.target
指向“直接”被new
执行的构造函数。那么,当子类继承父类时,在父类的构造方法中 new.target 指向子类。利用此特性,可以写出不可独立使用,必须继承后才会使用的父类。如注释部分。
若在函数外部使用
new.target
会抛出错误:未完,下一篇接着介绍 Class 继承...
十二、参考