H5W3
当前位置:H5W3 > 其他技术问题 > 正文

React理解ES6 Class

一、前言

最近准备记录自己从0到1学习React的过程的一些知识点和心得总结。第一章就是理解ES6Class。如有错误,请及时指出。

二、ES6之前的仿类

JSES5及更早版本中都不存在类。与类最接近的是:创建一个构造函数,然后将方法添加到该构造器的原型上。如下面代码:

function Person() {
  this.name = name
}
Person.prototype.sayName = function () {
  console.log(this.name)
}
let person = new Person('张三')
person.sayName()    //'张三'

console.log(person instanceof Person)   //true
console.log(person instanceof Object)   //true
 

上面这种基本模式在许多对类进行模拟的JS库中都存在,而这也是ES6类要解决的问题。ES6的类起初是作为ES5传统继承模型的语法糖,但添加了许多特性来减少错误。我们要总是使用class,避免直接操作prototype。因为class语法更简洁也更易读。

三、基本的类声明

class PersonClass {
  // 等价于上面的Person构造器
  constructor(name) {
    this.name = name
  }
  // 等价于上面的Person.prototype.sayName
  sayName() {
    console.log(this.name)
  }
}
let person = new PersonClass('zhangsan')
person.sayName()    //"zhangsan"
console.log(person instanceof PersonClass)            //true
console.log(person instanceof Object)                 //true
console.log(typeof PersonClass)                       //"function"
console.log(typeof PersonClass.prototype.sayName)     //"function"
 

使用class的时候需要注意的几点:

  1. 类声明以class关键字开始,其后是类的名称。
  2. 方法之间不需要使用逗号。
  3. 在其中使用特殊的constructor方法名称直接定义一个构造器。
  4. 自有属性(Own properties)属性出现在实例上而不是原型上,只能在类的构造器或方法内部进行创建。上面name就是一个自有属性。
  5. 上面的sayName()方法最终也成为PersonClass.prototype上的一个方法。

由于类的方法使用了简写语法,就不再需要使用function关键字。PersonClass声明实际上创建了一个拥有constructor方法和其他行为的函数,这也是typeof PersonClass会得到"function"结果的原因。

3.1、 类声明和函数声明的区别

尽管类与自定义函数类型之间有相似性,但还是有一些重要的区别:

  1. 类声明不会被提升,这与函数定义不同。类声明的行为与let相似,因此在程序执行到声明处之前,类都会位于暂时性死区内。
// 类声明不会被提升:Uncaught ReferenceError: Cannot access 'Person' before initialization
const person = new Person('zhangsan')
class Person {
  constructor(name) {
    this.name = name
  }
}
 
  1. 类声明中的所有代码会自动运行并锁定在严格模式下。
  2. 类的所有方法都是不可枚举的,这是对于函数类型的显著变化,函数类型必须用Object.defineProperty()才能将方法改变为不可枚举。
// 类的所有方法都是不可枚举的
class Person {
  constructor(name) {
    this.name = name
  }
  getName() {
    console.log('getName')
  }
}
for (prop in new Person()) {
  // name
  console.log(prop)
}
function Animal(name) {
  this.name = name
}
Animal.prototype = {
  getName() {
    console.log('getName')
  }
}
for (prop in new Animal()) {
  // name
  // getName
  console.log(prop)
}
 
  1. 类的所有方法内部都没有[[Construct]],因此使用new来调用它们会抛出错误。
  2. 调用类构造器时不使用new,会抛出错误。
  3. 在类的方法内部重写类名,会抛出错误。
  4. 只有在类的内部,类名才被视为是使用const声明的,外部的Foo就像是用let声明的,但不能在类的方法内部这么做。
class Foo {
  constructor() {
    //执行时抛出错误
    Foo = 'bar'
  }
}

//在类声明之后没问题
Foo = 'baz'
 

四、类表达式

类与函数相似的点在于都有两种形式:声明与表达式。类和函数类似,也有不需要标识符的表达式形 式。类表达式可以用于变量声明,也可以作为参数传递给函数。如下面代码:

let PersonClass = class {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }
}
let person = new PersonClass('zhangsan')
person.sayName()  // "zhangsan"
console.log(person instanceof PersonClass)        // true
console.log(person instanceof Object)             // true
console.log(typeof PersonClass)                   // "function"
console.log(typeof PersonClass.prototype.sayName) // "function"
 

使用类声明还是类表达式,主要是代码风格问题。相对于函数声明与函数表达式之间的区别,类声明与类表达式都不会被提升。

4.1、具名表达式

类表达式可以为类表达式命名。如果需要在class关键字后添加标识符,如下面代码:

let PersonClass = class PersonClass2 {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }
}
console.log(typeof PersonClass)   // "function"
console.log(typeof PersonClass2)  // "undefined"
 

上面例子中的类表达式被命名为PersonClass2PersonClass2标识符只在类定义内部存在,只能用在类方法内部(如sayName()内)。在类的外部,typeof PersonClass2的结果为"undefined",是因为外部不存在PersonClass2绑定。要理解为什么,请查 看未使用类语法的等价声明,如下面代码:

// 等价于 PersonClass 具名的类表达式
let PersonClass = (function () {
  'use script'
  const PersonClass2 = function (name) {
    if (typeof new.target === 'undefined') {
      throw new Error('Constructor must be called with new.')
    }
    this.name = name
  }

  Object.defineProperty(PersonClass2.prototype, 'sayName', {
    value: function () {
      if (typeof new.target === 'undefined') {
        throw new Error('Method cannot be called with new.')
      }
      console.log(this.name)
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
  return PersonClass2
}())
 
  1. 对于类声明来说,用let定义的外部绑定与用const定义的内部绑定有着相同的名称。如下:
class Home {

}
console.log(typeof Home) // "function"
 
  1. 对于类表达式可在内部使用const来定义它的不同名称,于是此处的PersonClass2就只能在类的内部使用。
let PersonClass = class PersonClass2 {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }
}
console.log(typeof PersonClass)   // "function"
console.log(typeof PersonClass2)  // "undefined"
 

五、一等公民的类

一等公民意味着着它能作为参数传给函数、能作为函数返回值、能用来给变量赋值。ES6类同样成为一等公民。这就使得类可以被多种方式所使用。

5.1、类作为参数传入函数

function createObject(classDef) {
  return new classDef()
}
const obj = createObject(class {
  sayHi() {
    console.log('Hi!')
  }
})
obj.sayHi() // "Hi!"
 

5.2、类作为匿名表达式

可以使用匿名类表达式,立即调用类构造器,用于创建单例(Singleton)。

let person = new class {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }
}('zhangsan')

person.sayName()  // "zhangsan"
 

六、类的访问器属性

自有属性需要在类构造器中创建,还可以在原型上定义访问器属性。创建一个getter,要使用get关键字,并要与后方标识符之间留出空格。创建setter只是要改用set关键字就可以了。

class customHTMLElement {
  constructor(element) {
    this.element = element
  }
  get html() {
    return this.element.innerHTML
  }

  set html(value) {
    this.element.innerHTML = value
  }
}
// 取得指定对象上一个自有属性(非继承属性)的描述符
const descriptor = Object.getOwnPropertyDescriptor(customHTMLElement.prototype, 'html')
console.log("get" in descriptor)    // true
console.log("set" in descriptor)    // true
console.log(descriptor.enumerable)  // false
 

上面代码中customHTMLElement类用于包装一个已存在的DOM元素。它的属性html拥有gettersetter,委托了元素自身的innerHTML方法。访问器属性被创建在CustomHTMLElement.prototype上,并且像其他类属性那样被创建为不可枚举属性。非类的等价表示如下代码:

let CustomHTMLElement = (function () {
  const CustomHTMLElement = function (element) {
    if (new.target === 'undefined') {
      throw new Error("Constructor must be called with new.");
    }
    this.element = element
  }
  Object.defineProperty(CustomHTMLElement.prototype, 'html', {
    enumerable: false,
    configurable: true,
    get() {
      return this.element.innerHTML
    },
    set(value) {
      this.element.innerHTML = value
    }
  })
  return CustomHTMLElement
}())
 

上面这个例子说明了使用类语法能够少写大量的代码。

七、类的可计算成员名

类方法与类访问器属性也都能使用可计算的名称。如下面代码:

let methodName = 'sayName'
class Person {
  constructor(name) {
    this.name = name
  }
  [methodName]() {
    console.log(this.name)
  }
}
const me = new Person('zhangsan')
me.sayName() // "zhangsan"
 

访问器属性能以相同方式使用可计算的名称。如下面代码:

let propertyName = 'html'
class CustomHTMLElement {
  constructor(element) {
    this.element = element
  }
  get [propertyName]() {
    return this.element.innerHTML
  }
  set [propertyName](value) {
    this.element.innerHTML = value
  }
}
 

八、生成器方法

类允许将任何方法变为一个生成器。如下面代码:

class MyClass {
  *createIterator() {
    yield 1
    yield 2
    yield 3
  }
}
const instance = new MyClass()
const iterator = instance.createIterator()
 

上面代码创建了一个拥有createIterator()生成器的MyClass类。该方法返回了一个迭代器对象。也可以定义类的默认迭代器,如下面代码:

class Collection {
  constructor() {
    this.items = []
  }
  *[Symbol.iterator]() {
    yield* this.items.values()
  }
}
var collection = new Collection()
collection.items.push(1)
collection.items.push(2)
collection.items.push(3)
for (let item of collection) {
  // 1
  // 2
  // 3
  console.log(item)
}
 

九、静态成员

直接在构造器上添加额外方法来模拟静态成员,ES5及更早版本中的模式如下:

function Person(name) {
  this.name = name
}
// 静态方法
Person.create = function (name) {
  return new Person(name)
}
// 实例方法 
Person.prototype.sayName = function () {
  console.log(this.name)
}
const person = Person.create('zhangsan')
person.sayName()   // "zhangsan"
 

ES6的类简化了静态成员的创建,只要在方法与访问器属性的名称前添加正式的static标注。下面有个与上个例子等价的类:

class Person {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }

  static create(name) {
    return new Person(name)
  }
}

const person = Person.create('zhangsan')
person.sayName()   // "zhangsan"
 

能在类中的任何方法与访问器属性上使用static关键字。但是不能将它用于constructor方法的定义。静态成员不能用实例来访问,始终需要直接用类自身来访问。

十、类继承

ES6之前,实现自定义类型的继承很麻烦。例如下面的代码:

function Father(house, knowledge) {
  this.house = house
  this.knowledge = knowledge
}
Father.prototype.getKnowledge = function () {
  return this.knowledge
}

function Son(house, knowledge) {
  Father.call(this, house, knowledge)
}
// 指定原型对象为Father
Son.prototype = Object.create(Father.prototype, {
  constructor:
  {
    value: Son,
    enumerable: true,
    writable: true,
    configurable: true
  }
})
console.log(new Son('别墅', '写代码'))
 

Son继承了FatherSon必须使用Father.prototype所创建的一个新对象来重写Son.prototype ,并且还要调用Father.call()方法。上面的图片可以看到继承关系。

类让继承工作变得更轻易,使用熟悉的extends关键字来指定当前类所需要继承的函数。生成的类的原型会被自动调整,而你还能调用super()方法来访问基类的构造器。此处是与上个例子等价的`ES6v代码:

 class Father {
  constructor(house, knowledge) {
    this.house = house
    this.knowledge = knowledge
  }

  getKnowledge() {
    return this.knowledge
  }
}

class Son extends Father {
  // 与 Father.call(this, house, knowledge)
  constructor(house, knowledge) {
    super(house, knowledge)
  }
}
console.log(new Son('别墅', '写代码'))
 

Son使用了extends关键字继承了FatherSon构造器使用了super()配合指定参数调用了Father的构造器。

继承了其他类的类被称为派生类(其实个人认为也可以成为子类)。如果派生类指定了构造器,就需要 使用super(),否则会造成错误。若选择不使用构造器,super()方法会被自动调用,并会使用创建新实例时提供的所有参数。例如下面两个类是完全相同的:

class son extends Father {
  // 没有构造器
}
// 等价于:
class son extends Father {
  constructor(...args) {
    super(...args)
  }
}
 

第二个类展示了与所有子类默认构造器等价的写法,所有的参数都按顺序传递给了基类的构造器。这种做法并不完全准确,最好手动定义构造器。

使用 super() 时需牢记以下几点:

  1. 你只能在派生类中使用super()。若尝试在非派生的类(使用extends关键字的类)或函数中使用它,就会抛出错误。
  2. 在构造器中,你必须在访问this之前调用super()。由于super()负责初始化 this,因此试图先访问this就会造成错误。
  3. 若在类的构造器中不调用super(),唯一避免出错的办法是在构造器中返回一个对象。

十一、屏蔽类方法

派生类中的方法总是会屏蔽基类的同名方法。例如将getKnowledge()方法添加到Son类,以便重定义它的功能:

 class Son extends Father {
  constructor(house, knowledge) {
    super(house, knowledge)
  }
  // 重写并屏蔽 Father.prototype.getKnowledge()
  getKnowledge() {
    return '儿子自己的知识!'
  }
}
console.log(new Son('别墅', '写代码'))
 

由于getKnowledge()已经被定义为Son的一部分,Father.prototype.getKnowledge()方法就不能在Son的任何实例上被调用。但是可以通过使用super.getKnowledge()方法来调用父类中的该方法,如下面代码:

 class Father {
  constructor(house, knowledge) {
    this.house = house
    this.knowledge = knowledge
  }

  getKnowledge() {
    return this.knowledge
  }
}
class Son extends Father {
  constructor(house, knowledge) {
    super(house, knowledge)
  }
  // 重写、屏蔽并调用了 Father.prototype.getArea()
  getKnowledge() {
    return super.getKnowledge()
  }
}
console.log(new Son('别墅', '写代码'))
 

十二、继承静态成员

如果父类包含静态成员,那么这些静态成员在派生类中也是可用的。如下面代码:

class Father {
  constructor(house, knowledge) {
    this.house = house
    this.knowledge = knowledge
  }

  static create(house, knowledge) {
    return new Father(house, knowledge)
  }
}
class Son extends Father {
  constructor(house, knowledge) {
    super(house, knowledge)
  }
}
const father = Son.create('别墅', '写代码')
console.log(father instanceof Father)  // true
console.log(father instanceof Son)     // false
 

在此代码中,一个新的静态方法create()被添加到Father类中。通过继承,该方法会以Son.create()的形式存在,并且其行为方式与Father.create()一样。

十三、从表达式中派生类

在ES6中派生类的最强大能力或许就是能够从表达式中派生类。只要一个表达式能够返回一个具有[[Construct]]属性以及原型的函数,就可以对其使用extends。如下面代码:

function Father(house, knowledge) {
  this.house = house
  this.knowledge = knowledge
}
Father.prototype.getKnowledge = function () {
  return this.knowledge
}

class Son extends Father {
  constructor(house, knowledge) {
    super(house, knowledge)
  }
}
const son = new Son('别墅', '写代码')
console.log(son.getKnowledge());    // 写代码
console.log(son instanceof Father)  // true
 

Father被定义为ES5风格的构造器,而Son则是一个类。由于Father具有[[Construct]]以及原型,Son类就能直接继承它。

extends后面能接受任意类型的表达式,如动态地决定所要继承的类:

function Father(house, knowledge) {
  this.house = house
  this.knowledge = knowledge
}
Father.prototype.getKnowledge = function () {
  return this.knowledge
}
function getBase() {
  return Father
}
class Son extends getBase() {
  constructor(house, knowledge) {
    super(house, knowledge)
  }
}
const son = new Son('别墅', '写代码')
console.log(son instanceof Father)  // true
 

参考链接

https://book.douban.com/subject/27072230/

https://wizardforcel.gitbooks.io/exploring-es6/md/3/3.2.html

https://es6.ruanyifeng.com/#docs/class-extends

https://segmentfault.com/a/1190000015424508

https://www.stefanjudis.com/today-i-learned/not-every-javascript-function-is-constructable/

本文使用 mdnice 排版

本文地址:H5W3 » React理解ES6 Class

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址