JavaScript高级程序设计-类的基础

290 阅读5分钟

1.类定义

与函数类型相似,定义类也有两种主要方式:类声明和类表达式

// 类声明
class Person {}

// 类表达式
class Animal = class {}

类表达式在它们被求值之前不能引用,声明不可以提升。函数受函数作用域限制,而类受块作用域限制

类的构成

类可以包括构造函数方法、实例方法、获取函数、设置函数和静态类方法。 类名的首字母要大写,以区别于通过它创建的实例。

// 空类定义,有效
class Foo {}

// 有构造函数的类,有效
class Bar {
  constructor() {}
}

// 有获取函数的类, 有效
class Baz {
  get myBaz() {}
}

// 有静态方法的类,有效
class Qux {
  static myQux() {}
}

类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过 name 属性取得类表达式的名称字符串。但不能在类表达式作用域外部访问这个标识符。

2.类构造函数

constructor 关键字用于在类定义块内部创建类的构造函数。方法名 constructor 会告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数。构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数。

1.实例化

使用 new 操作符实例化 Person 的操作等于使用 new 调用其构造函数。唯一可感知的不同之处就是,JavaScript 解释器知道使用 new 和类意味着应该使用 constructor 函数进行时实例化。 使用 new 调用类的构造函数会执行如下操作。

  1. 在内存中创建一个新对象
  2. 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

2.把类当成特殊函数

ESMAScript 中没有正式的类这个类型。从个方面来看,ECMAScript 类就是一种特殊函数。声明一个类之后,通过 typeof 操作符检测类标识符,表明它是一个函数:

class Person {}

console.log(Person) // class Person
console.log(typeof Person) // function

类标识符有 prototype 属性,而这个原型也有一个 constructor 属性指向类本身:

class Person {}

console.log(Person.prototype) // {constructor: f()}
console.log(Person === Person.prototype.constructor) // true

使用 instanceof 操作符检查构造函数原型是否在实例的原型链中

class Person {}

let p = new Person()
console.log(p instanceof Person) // true

由此可知,可以使用 instanceof 操作符检查一个对象与类构造函数,以确定这个对象是不是类的实例 类本身具有与普通构造函数一样的行为。在类的上下文中,类本身在使用 new 调用时就会被当成构造函数。重点在于,类中定义的 constructor 方法不会被当成构造函数,对它使用 instanceof 会返回 false。但是如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么 instanceof 的返回值会反转:

class Person {}

let p1 = new Person()

console.log(p1.constructor === Person) // true
console.log(p1 instanceof Person) // true
console.log(p1 instanceof Person.constructor) // false

let p2 = new Person.constructor()

console.log(p2.constructor === Person) // false
console.log(p2 instanceof Person) // false
console.log(p2 instanceof Person.constructor) // true

3.实例、原型和类成员

类的语法可以非常方便地定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在于类本身的成员

1.实例成员

每次通过 new 调用类标识符时,都会执行类构造函数。在函数内部,可以为新创建的实例添加“自有属性”。 每个实例都对应一个唯一的成员对象,这意味着所有成员都不会在原型上共享:

class Person {
  constructor() {
    this.name = new String('Jack')
    this.sayName = () => console.log(this.name)
    this.nicknames = ['Jack', 'J-Dog']
  }
}

let p1 = new Person(),
  p2 = new Person()

p1.sayName() // Jack
p2.sayName() // Jack

console.log(p1.name === p2.name) // false
console.log(p1.sayName === p2.sayName) // false

2.原型方法与访问器

为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。

class Person {
  construtor() {
    this.locate = () => console.log('instance')
  }
  // 在类块中定义的多有内容都会定义在类的原型上
  locate() {
    console.log('prototype')
  }
}

let p = new Person()
p.locate() // instance
Person.prototype.locate() // prototype

可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据 类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键:

const symbolKey = Symbol('key')

class Person {
  stringKey() {
    console.log('stringKey')
  }
  [symbolKey]() {
    console.log('symbolKey')
  }
  ['computed' + 'Key']() {
    console.log('computedKey')
  }
}

类定义也支持获取和设置访问器。语法行为跟普通对象一样

class Person {
  set name(newName) {
    this.name_ = newName
  }
  get name() {
    return this.name_
  }
}

3.静态类方法

可以在类上定义静态方法。这些方法通常用语执行不特定于实例的操作,也不要求存在类的实例。与原型成员类似,静态成员每个类上只能有一个。 静态类成员在类定义中使用 static 关键字作为前缀。在静态成员中,this 引用类自身。

class Person {
  constructor() {
    // 添加到this的所有内容都会存在于不同的实例上
    this.locate = () => console.log('instance', this)
  }
  // 定义在类的原型对象上
  locate() {
    console.log('prototype', this)
  }
  // 定义在类本身
  static locate() {
    console.log('class', this)
  }
}

let p = new Person()
p.locate() // instance, Person {}
Person.prototype.locate() //prototype, {constructor: ...}
Person.locate() // class class Person {}

静态类方法非常适合作为实例工厂

class Person {
  constructor(age) {
    this.age_ = age
  }
  sayAge() {
    console.log(this.age_)
  }
  static create() {
    return new Person(Math.floor(Math.random() * 100))
  }
}
console.log(Person.create())

4.非函数原型和类成员

类定义不显式支持在原型或类上添加成员数据,在类定义外部可以手动添加

5.迭代去与生成器方法

类定义语法支持在原型和类本身定义生成器方法

class Person {
  // 在原型上定义生成器方法
  *createNameIterator() {
    yield 'Jack'
    yield 'Jake'
    yield 'J-Dog'
  }

  static *createJobIterator() {
    yield 'BUTCHER'
    yield 'ADD'
    yield 'UZI'
  }
}

let jobIter = Person.createJobIterator()
jobIter.next().value // jack
jobIter.next().value // jake
jobIter.next().value // j-dog

let p = new Person()
let nameIter = p.createNameIterator()
nameIter.next().value // BUTCHER
nameIter.next().value // ADD
nameIter.next().value // UZI

因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类实例变成可迭代对象:

class Person {
  constructor() {
    this.names = ['Faker', 'the Shy', 'UZI']
  }
  *[Symbol.interator]() {
    yield* this.names.entries()
  }
}

let p = new Person()
for (let [idx, name] of p) {
  console.log(name)
}

也可以只返回迭代器实例:

class Person {
  constructor() {
    this.names = ['Faker', 'the Shy', 'UZI']
  }
  [Symbol.interator]() {
    return this.names.entries()
  }
}

let p = new Person()
for (let [idx, name] of p) {
  console.log(name)
}