一、简介
JavaScript 语言的传统方法是通过构造函数定义并生成新对象。
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 (类)这个概念作为对象的模板。
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 (类)这个概念作为对象的模板。ES6中的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class 写法只是让对象原型的写法更加清晰,更像面向对象编程的语法而已。
改写上面的 代码
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
toString() {
return '(' + this.x + ', ' + this.y + ')'
}
}
ES6 的类完全可以看作构造函数的另一种写法
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
使用的时候也是直接对类使用 new 命令,跟构造函数的用法完全一致
class Bar {
doStuff() {
console.log('stuff')
}
}
var b = new Bar()
b.doStuff() // "stuff"
构造函数的 prototype 属性在 ES6的 “类” 上继续存在。事实上,类的所有方法都定义在类的 prototypep 属性上。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {}
}
在类的实例上调用方法,其实就是调用原型上的方法
prototype 对象的 constructor 属性直接指向 “类” 本身,这与 ES5 的行为是一致的。
Point.prototype.constructor === Point // true
另外,在 类 的内部定义的所有方法都是不可枚举的(non-enumerable),这一点与ES5的行为不一致
类的属性名可以采用表达式
let methodName = 'getArea'
class Square {
constructor(length) {
// ...
}
[methodName]() {
// ...
}
}
二、严格模式
类和模块的内部默认使用严格模式,所以不需要使用
use strict指定运行模式。只要将 代码写在类或模块之中,那么就只有严格模式可用。
三、constructor 方法
constructor 方法是类的默认方法,通过 new 命令生成对象实例时自动调用该方法。一个类必须有 constructor 方法,如果没有显示定义,一个空的 constructor 方法会被默认添加
class Point {}
// 等同于
class Point {
constructor() {}
}
constructor 方法默认返回实例对象(即 this),不过完全可以指定返回另外一个对象。
class Foo {
constructor() {
return Object.create(null)
}
}
new Foo() instanceof Foo
// false
类必须使用 new 来调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用 new 也可以执行。
四、类的实例对象
生成实例对象的写法与 ES5 完全一样,也是使用 new 命令。
与 ES5 一样,实例的属性除非显示定义在其本身(即 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.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
x 和 y 都是实例对象 point 自身的属性,hasOwnProperty 方法返回 true。而 toString 是原型对象的属性,hasOwnProperty 方法返回 false。这些都与 ES5 的行为保持一致
与 ES5 一样,类的所有实例共享一个原型对象
var p1 = new Point(2, 3)
var p2 = new Point(3, 4)
p1.__proto__ === p2.__proto__ // true
这也意味着,可以通过实例的 __proto__ 属性为 “类” 添加方法
proto 并不是语言本身的特性,而是各大厂商具体实现是添加的私有属性,虽然目前很多浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。
五、Class 表达式
与函数一样,Class 也可以使用表达式的形式定义
const MyClass = Class Me {
getClassName() {
return Me.name
}
}
需要注意的是,这个类的名字是 MyClass 而不是 Me,Me 只在 Class 的内部代码可用,指代当前类
let inst = new MyClass()
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
如果 Class 内部没有用到,那么可以省略 Me,也就是可以写成下面的形式
const MyClass = class { /* .... */ }
采用 Class 表达式,可以写出立即执行的 Class
let person = new class {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}('了凡')
person.sayName() // 了凡
六、不存在变量提升
类不存在变量提升(hoist),这一点与 ES5 完全不同
new Foo() // ReferenceError
class Foo {}
这种规定的原型与下文要提到的继承有关,必须保证之类在父类之后定义。
七、私有方法
私有方法是常见需求,但ES6 不提供,只能通过变通方法来模拟实现。
一种做法是在 命名上加以区别
class Widget {
// 共有方法
foo (baz) {
this._bar(baz)
}
// 私有方法
_bar(baz) {
return this.snaf = baz
}
}
但这种方式只是形式上的,外部仍然可以调用这个方法
另一种方法是索性将私有方法移除模块,因为模块内部的所有方法都是对外可见的
class Widget {
foo(baz) {
baz.call(this, baz)
}
// ...
}
function bar(baz) {
return this.snaf = baz
}
foo 是共有方法,内部调用了 bar.call(this, baz)。这使得bar 实际上成为了当前模块的私有方法
还有一种方法是利用 Symbol 值的唯一性将私有方法的名字命名为一个 Symbol 值。
const bar = Symbol('bar')
const snaf = Symbol('snaf')
class myClass {
// 共有方法
foo(baz) {
this[bar](baz)
}
// 私有方法
[bar](baz) {
return this[snaf] = baz
}
}
bar 和 snaf 都是 Symbol 值,导致第三发无法获取到他们,因此达到了私有方法和私有属性的效果。
八、私有属性
与私有方法一样,ES6 不支持私有属性。目前有一个提案 为 class 加了私有属性,方法是在属性名之前,使用 # 来表示。
class Point {
#x
constructor(x = 0) {
#x = +x // 写成 this.#x 也可
}
get x() { return #x }
ser x(value) { #x = value }
}
#x 就表示私有属性 x,在Point 类之外是读取不到这个属性的。还可以看到,私有属性与实例的属性是可以同名的(比如,#x 与 get x() )
私有属性可以指定初始值在构造函数时进行初始化
class Point {
#x = 0
constructor() {
#x // 0
}
}
之所以引入的一个新的前缀 # 来表示私有属性,而没有采用 private 关键字,是因为 JavaScript是一门动态语言,使用独立的符号似乎是唯一可靠的方法,能够准确地区分一种属性是否为私有属性。
Ruby 语言使用@ 表示私有属性,ES6 没有用这个符号而使用 #,是因为 @ 留给了 Decorator(修饰器)
该提案只规定了私有属性的写法。但是,很自然地,它可以用来编写私有方法。
class Foo {
#a
#b
#sum() { return #a + #b }
printSum() { console.log(#sum()) }
constructor(a, b) { #a = a, #b = b }
}
九、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 方法中 this 默认指向 Logger 类的实例。如果将这个方法提取出来单独使用,this 会指向改方法运行时所在的环境,因为找不到 print 方法而导致报错。
一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到 print 方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this)
}
// ...
}
另一种解决方法是使用 箭头函数
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`)
}
}
// ...
}
还有一种解决方法是使用 proxy,在获取方法的时候自动绑定 this。
function selfish(target) {
const cache = new WeakMap()
const handler = {
get(target, key) {
const value = Reflect.get(target, key)
if (typeof value !== 'function') { // 不是 函数直接返回
return value
}
if (!cache.has(value)) { // 存储在 WeakMap 结构中
cache.set(value, value.bind(target)) // 改变 this 指向
}
return cache.get(value) // 返回
}
}
const proxy = new Proxy(target, handler)
return proxy
}
const logger = selfish(new Logger())
十、name 属性
name 属性总是返回紧跟在 class 关键字后面的类名
class Point {}
Point.name // "Point"
十一、Class 的取值函数(getter)和 存值函数(setter)
与 ES5 一样,在“类” 的内部可以使用 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
存值函数和取值函数是设置在属性的 Descriptor 对象上的
class CustomHTMLElement {
constructor(element) {
this.element = element
}
get html() {
return this.element.innerHTML
}
set html(value) {
this.element.innerHTML = value
}
}
var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, 'html')
'get' in descriptor
// true
'set' in descriptor
// true
存值函数和取值函数是定义在 html 属性的描述对象上面,这与 ES5 完全一致。
十二、Class 的 Generator 方法
如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数
class Foo {
* [Symbol.iterator]() {
// ...
}
}
十三、Class 的静态方法
如果在一个方法前面加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类调用,称为“静态方法”
class Foo {
static classMethod() {
return 'hello'
}
}
Foo.classMethod() // 'hello'
var foo = new Foo()
foo.classMethod()
// TypeError: foo.classMethod is not a function
父类的静态方法可以被之类继承。
class Foo{
static classMethod() {
return 'hello'
}
}
class Bar extends Foo {}
Bar.classMethod() // 'hello'
静态方法也可以从 super 对象上调用
class Foo {
static classMethod() {
return 'hello'
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too'
}
}
Bar.classMethod() // "hello, too"
十四、Class 的静态属性 和 实例属性
静态属性指的是 Class 本身的属性,即
Class.propname,而不是定义在实例对象(this)上的属性
class Foo {}
Foo.prop = 1
Foo.prop // 1
14.1、Class 的实例属性
Class 的实例属性可以用等式写入类的定义之中。
class MyClass {
myProp = 42
constructor() {
console.log(this.myProp) // 42
}
}
myProp 就是 MyClass 的实例属性。在MyClass 的实例上可以读取这个属性
有了新的写法以后,可以不再 constructor 方法里面定义
class ReactCounter extends React.Component {
state = {
count: 0
}
}
这种写法更加清晰
为了获得更强的可读性,对于那些在 constructor 里面已经定义的实例属性,新写法允许直接列出
class ReactCounter extends React.Component {
state
constructor(props) {
super(props)
this.state = {
count: 0
}
}
}
14.2、Class 的静态属性
Class 的静态属性只要在上面的实例属性写法前面加上 static 关键字就可以了
class MyClass {
static myStaticProp = 42
constructor() {
console.log(MyClass.myStaticProp)
}
}
const cl = new MyClass() // 42
cl.myStaticProp // undefined
MyClass.myStaticProp // 42
这个写法大大方便了静态属性的表达。
// 旧写法
class Foo {
// ...
}
Foo.prop = 1
// 新写法
class Foo {
static prop = 1
}
十五、new.target 属性
ES6 为 new 命令引入了
new.target属性,(在构造函数中)返回new 命令所作用的构造函数。如果构造函数不是通过 new 命令调用的,那么 new.target 会返回 undefined,因此这个属性可以用于构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name
} else {
throw new Error('必须使用 new 生成实例')
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) { // 返回当前 Class
this.name = name
} else {
throw new Error('必须使用 new 生成实例')
}
}
var person = new Person('张三') // 正确
var notAPerson = Person.call(person, '张三') // 报错
上面代码确保 构造函数只能通过 new 命令调用,Class 内部调用 new.target,返回当前 class
需要注意的是,之类继承父类是 new.target 会返回之类
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle)
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
}
var obj = new Square(3) // false
利用这个特点,可以写出不能独立使用而必须继承后才能使用的类
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化')
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super()
}
}
var x = new Shape() // 报错
var y = new Rectangle() // 正确
上面代码中,Shape 类不能实例化,只能用于继承。
注意。在函数外部,使用 new.target 会报错。