React理解ES6 Class

一、前言

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

二、ES6之前的仿类

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

functionPerson() {

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 instanceofObject) //true

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

三、基本的类声明

classPersonClass{

// 等价于上面的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 instanceofObject) //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')

classPerson{

constructor(name) {

this.name = name

}

}

  1. 类声明中的所有代码会自动运行并锁定在严格模式下。
  2. 类的所有方法都是不可枚举的,这是对于函数类型的显著变化,函数类型必须用Object.defineProperty()才能将方法改变为不可枚举。

// 类的所有方法都是不可枚举的

classPerson{

constructor(name) {

this.name = name

}

getName() {

console.log('getName')

}

}

for (prop innew Person()) {

// name

console.log(prop)

}

functionAnimal(name) {

this.name = name

}

Animal.prototype = {

getName() {

console.log('getName')

}

}

for (prop innew Animal()) {

// name

// getName

console.log(prop)

}

  1. 类的所有方法内部都没有[[Construct]],因此使用new来调用它们会抛出错误。
  2. 调用类构造器时不使用new,会抛出错误。
  3. 在类的方法内部重写类名,会抛出错误。
  4. 只有在类的内部,类名才被视为是使用const声明的,外部的Foo就像是用let声明的,但不能在类的方法内部这么做。

classFoo{

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 instanceofObject) // true

console.log(typeof PersonClass) // "function"

console.log(typeof PersonClass.prototype.sayName) // "function"

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

4.1、具名表达式

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

let PersonClass = classPersonClass2{

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 (typeofnew.target === 'undefined') {

thrownewError('Constructor must be called with new.')

}

this.name = name

}

Object.defineProperty(PersonClass2.prototype, 'sayName', {

value: function () {

if (typeofnew.target === 'undefined') {

thrownewError('Method cannot be called with new.')

}

console.log(this.name)

},

enumerable: false,

writable: true,

configurable: true

})

return PersonClass2

}())

  1. 对于类声明来说,用let定义的外部绑定与用const定义的内部绑定有着相同的名称。如下:

classHome{

}

console.log(typeof Home) // "function"

  1. 对于类表达式可在内部使用const来定义它的不同名称,于是此处的PersonClass2就只能在类的内部使用。

let PersonClass = classPersonClass2{

constructor(name) {

this.name = name

}

sayName() {

console.log(this.name)

}

}

console.log(typeof PersonClass) // "function"

console.log(typeof PersonClass2) // "undefined"

五、一等公民的类

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

5.1、类作为参数传入函数

functioncreateObject(classDef) {

returnnew classDef()

}

const obj = createObject(class{

sayHi() {

console.log('Hi!')

}

})

obj.sayHi() // "Hi!"

5.2、类作为匿名表达式

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

let person = newclass{

constructor(name) {

this.name = name

}

sayName() {

console.log(this.name)

}

}('zhangsan')

person.sayName() // "zhangsan"

六、类的访问器属性

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

classcustomHTMLElement{

constructor(element) {

this.element = element

}

get html() {

returnthis.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') {

thrownewError("Constructor must be called with new.");

}

this.element = element

}

Object.defineProperty(CustomHTMLElement.prototype, 'html', {

enumerable: false,

configurable: true,

get() {

returnthis.element.innerHTML

},

set(value) {

this.element.innerHTML = value

}

})

return CustomHTMLElement

}())

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

七、类的可计算成员名

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

let methodName = 'sayName'

classPerson{

constructor(name) {

this.name = name

}

[methodName]() {

console.log(this.name)

}

}

const me = new Person('zhangsan')

me.sayName() // "zhangsan"

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

let propertyName = 'html'

classCustomHTMLElement{

constructor(element) {

this.element = element

}

get [propertyName]() {

returnthis.element.innerHTML

}

set [propertyName](value) {

this.element.innerHTML = value

}

}

八、生成器方法

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

classMyClass{

*createIterator() {

yield1

yield2

yield3

}

}

const instance = new MyClass()

const iterator = instance.createIterator()

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

classCollection{

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及更早版本中的模式如下:

functionPerson(name) {

this.name = name

}

// 静态方法

Person.create = function (name) {

returnnew Person(name)

}

// 实例方法

Person.prototype.sayName = function () {

console.log(this.name)

}

const person = Person.create('zhangsan')

person.sayName() // "zhangsan"

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

classPerson{

constructor(name) {

this.name = name

}

sayName() {

console.log(this.name)

}

static create(name) {

returnnew Person(name)

}

}

const person = Person.create('zhangsan')

person.sayName() // "zhangsan"

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

十、类继承

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

functionFather(house, knowledge) {

this.house = house

this.knowledge = knowledge

}

Father.prototype.getKnowledge = function () {

returnthis.knowledge

}

functionSon(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代码:

classFather{

constructor(house, knowledge) {

this.house = house

this.knowledge = knowledge

}

getKnowledge() {

returnthis.knowledge

}

}

classSonextendsFather{

// 与 Father.call(this, house, knowledge)

constructor(house, knowledge) {

super(house, knowledge)

}

}

console.log(new Son('别墅', '写代码'))

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

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

classsonextendsFather{

// 没有构造器

}

// 等价于:

classsonextendsFather{

constructor(...args) {

super(...args)

}

}

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

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

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

十一、屏蔽类方法

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

classSonextendsFather{

constructor(house, knowledge) {

super(house, knowledge)

}

// 重写并屏蔽 Father.prototype.getKnowledge()

getKnowledge() {

return'儿子自己的知识!'

}

}

console.log(new Son('别墅', '写代码'))

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

classFather{

constructor(house, knowledge) {

this.house = house

this.knowledge = knowledge

}

getKnowledge() {

returnthis.knowledge

}

}

classSonextendsFather{

constructor(house, knowledge) {

super(house, knowledge)

}

// 重写、屏蔽并调用了 Father.prototype.getArea()

getKnowledge() {

returnsuper.getKnowledge()

}

}

console.log(new Son('别墅', '写代码'))

十二、继承静态成员

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

classFather{

constructor(house, knowledge) {

this.house = house

this.knowledge = knowledge

}

static create(house, knowledge) {

returnnew Father(house, knowledge)

}

}

classSonextendsFather{

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。如下面代码:

functionFather(house, knowledge) {

this.house = house

this.knowledge = knowledge

}

Father.prototype.getKnowledge = function () {

returnthis.knowledge

}

classSonextendsFather{

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后面能接受任意类型的表达式,如动态地决定所要继承的类:

functionFather(house, knowledge) {

this.house = house

this.knowledge = knowledge

}

Father.prototype.getKnowledge = function () {

returnthis.knowledge

}

functiongetBase() {

return Father

}

classSonextendsgetBase() {

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 排版

以上是 React理解ES6 Class 的全部内容, 来源链接: utcz.com/a/22550.html

回到顶部