Class 的基本语法

一、简介

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 会报错。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容