混子前端所知道关于ES6的Class

2,054 阅读12分钟
ES6提供了更接近传统语言的写法,引入Class(类)这个概念,作为对象的模版。通过 class 关键字,可以定义类。基本上,ES6的 class 可以看作是一个语法糖,它的绝大部分功能,ES5都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
---本文摘选阮一峰《ECMAScript 6 标准入门》

Class基本语法

概述

以下是 JavaScript 语言的传统构造函数方法,定义并生成新对象:


上面的代码用ES6的"类"改写,就是下面这样:


说明:上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而 this 关键字则代表实例对象。也就是说,ES5 的构造函数 F,对应ES6的 F 类的构造方法。


F 类除了构造方法,还定义一个 toString 方法,注意:定义”类“方法的时候,前面不需要加function关键字,直接把函数定义放进去就可以了。另外,方法之间不需要逗号分割,会报错。

ES6 的类,完全可以看作构造函数的另一种写法。


说明:类的数据类型就是函数,类本身就指向构造函数,使用的时候,也是直接对类使用 new 命令,跟构造函数的用法完全一致。


构造函数的prototype属性,在ES6的“类”上面继续存在


说明:类的所有方法都定义在类的prototype上面


另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable),这点与ES5的行为不一致,来看代码:


PS:  Object.keys(obj);    // 返回一个表示给定对象的所有可枚举属性的字符串数组


constructor 方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法也会默认被添加。

constructor方法默认返回实例对象(即this),完全可以指定返回另一个对象


说明:constructor函数返回一个新的对象,结果导致实例对象不是F类的实例

注意:类的构造函数,不使用new是没法调用的,会报错。


类的实例对象

生成类的实例对象的写法,与ES5完全一样使用 new 命令。

与ES5一样,实例的属性除非显式定义在其本身,否则都是定义在原型上,来看代码:


PS:hasOwnProperty 用来判断某个对象是否含有指定的属性, 返回Boolean值

说明: 

            1、x 和 y 都是实例对象 F 的自身属性( 因为定义在this变量上 ),所以 hasOwnProperty返回 true

            2、 toString 是原型对象的属性( 因为定义在F类上 ),所以 hasOwnProperty 返回 false

            3、Fn 和 Fn2 都是 F 的实例,他们的原型都是 F.prototype,所以 __proto__ 属性相等

            4、既然 __proto__ 属性相等,Fn 的原型就是 Fn2 的原型,所以 Fn2 可以调用sayName


不存在变量提升

Class 不存在变量提升( hoist ),因为ES6不会把类的生命提升到代码头部,这种规定的原因 class 继承有关,必须保证子类在父类之后定义


这段代码不会报错,因为 Bar 继承 F 的时候,F 已经定义了。但是如果存在 class 的提升,上面代码就会报错,class 会被提升到块顶部,而 let 命令是不提升的,所以 Bar 继承F的时候,F还没有定义


Class表达式

与函数一样,类也可以使用表达式的形式定义,来看代码


说明:上面代码使用表达式定义了一个类,需要注意的是这个类的名字是Fn 而不是 F,F只在class内部代码可用,指代当前类,如:F.num    // F 只在class内部有定义


但class表达式,可以写出立即执行的class


上面代码,person 是一个立即执行的类的实例。


this的指向

类的方法如果含有this,它默认指向类的实例,但是,使用必须小心,一旦单独使用该方法,很有可能报错,来看一个错误代码:


上面代码,printName 方法中的 this,默认指向 Logger 类的实例,但是,如果将这个方法提取出来单独使用,this 会指向该方法运行时所在的环境,因为找不到 print 方法而导致报错。

解决方法一:在构造方法中绑定 this ,这样就不会找不到 print 方法了:


解决方法二:使用箭头函数:


解决方案三:使用Proxy,获取方法的时候,自动绑定 this



严格模式

类和模块的内部,默认就是严格模式,所以不需要使用

 
use strict 指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。

考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。


name属性

由于本质上,ES6的类只是ES5的构造函数的一层包装,所以函数的许多特性都被 Class 继承,包括 name 属性。


name 属性总是返回紧跟在 class 关键字后面的类名。



Class的继承

基本用法

Class 之间可以通过

 
extends 关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。


说明:上面代码定义了一个

 
ColorPoint 类,该类通过 extends 关键字,继承了 Point 类的所有属性和方法。

super:它在这里表示父类的构造函数,用来新建父类的

 
this 对象,子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错,因为子类没有自己的
 
this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用
 
super 方法,子类就得不到
 
this 对象。


ES5的继承,实质是先创造子类的实例对象 this ,然后再将父类的方法添加到 this 上面( Parent.apply(this),可以翻看之前混子前端介绍JS的六种继承方式 这篇文章 

ES6的继承机制完全不同,实质是先创造父类的实例对象 this (所以必须先调用 super 方法),然后再用子类的构造函数修改 this

如果子类没有定义 constructor 方法,这个方法会被默认添加,来看代码:


说明:不管有没有显式定义,任何一个子类都有 constructor 方法。

另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。


类的prototype属性和__proto__属性

大多数浏览器的ES5实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的prototype 属性。

Class 作为构造函数的语法糖,同时有prototype属性和 __proto__ 属性,因此同时存在两条继承链。

  1. 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
  2. 子类 prototype 属性的 __proto__ 属性,表示方法的继承总是指向父类的 prototype 属性。


说明:子类 B 的 __proto__ 属性指向父类 A,子类 B  prototype 属性的 __proto__ 属性指向父类 A  prototype 属性。

这样的结果是因为,类的继承是按照下面的模式实现的。

PS: MDN 解释 setPrototypeOf 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象 或 null 

《对象的扩展》一章给出过 Object.setPrototypeOf 方法的实现


说明:作为一个对象,子类 B 的原型 ( __proto__属性) 是父类 A;作为一个构造函数,子类 B 的原型 (prototype属性) 是父类的实例。


Extends 的继承目标


extends 关键字后面可以跟随多种类型的值,上面代码A,只要是一个有 prototype 属性的函数,就能被 B 继承。由于函数都有 prototype 属性(除了 Function.prototype 函数),因此 A 可以是任意函数。

下面讨论三种特殊情况:

第一种,子类继承 Object 类:


这种情况下,A 其实就是构造函数 Object 的复制,A 的实例就是 Object 的实例。


第二种,不存在任何继承:


这种情况下,A作为一个普通函数,所以直接继承Funciton.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。


第三种,子类继承 null:


这种情况下,A是一个普通函数,所以直接继承Funciton.prototype。但是,A调用后返回的对象不继承任何方法,所以它的__proto__指向Function.prototype,即实质上执行了下面的代码。


Object.getPrototypeOf()

Object.getPrototypeOf 方法可以用来从子类上获取父类。


可以使用这个方法判断,一个类是否继承了另一个类。


super 关键字

super 关键字,既可以当作函数使用,也可以当作对象使用。


第一种情况,super作为函数调用时,代表父类的构造函数,ES6 要求,子类的构造函数必须执行一次super函数。

注意:super 虽然代表了父类的构造函数,但是返回的是子类的实例,即 super 内部的 this 指向当前子类,因此 super() 在子类相当于:

父类.prototype.constructor.call(this)


说明:new.target 指向当前正在执行的函数,在super()执行时,它指向的是子类 B 的构造函数,而不是父类 A 的构造函数。也就是说,super() 内部的 this 指向的是B。


第二种情况,super 作为对象时,指向父类的原型对象。


说明:子类B当中的super.p(),就是将super当作一个对象使用。这时,super指向A.prototype,所以super.p()就相当于A.prototype.p()。

注意:由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super 调用的。

ES6 规定,通过 super 调用父类的方法时,super 会绑定子类的 this,来看代码:


说明:super.print() 虽然调用的是 A.prototype.print(),但是 A.prototype.print() 会绑定子类 B的 this,就是说实际上执行的是super.print.call(this)


最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用 super 关键字,来看代码:



实例的__proto__属性

子类实例的 __proto__ 属性的 __proto__ 属性,指向父类实例的 __proto__ 属性。也就是子类原型的原型,是父类的原型。


说明:上面代码中,ColorPoint 继承了 Point,导致前者原型的原型是后者的原型。

因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。



Class的取值函数(getter)和存值函数(setter)

与ES5一样,在 Class 内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为,来看代码:


说明:prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。


存值函数和取值函数是设置在属性的 descriptor 对象上的,还是上面的代码补充:



Class的静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。

如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。


说明:Foo 类的 classMethod 方法前有static关键字,表明是一个静态方法,可以直接在 Foo 类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

补充:父类的静态方法,可以被子类继承。



Class的静态属性和实例属性

静态属性指的是 Class 本身的属性,即 Class.propname,而不是定义在实例对象(this)上的属性,来看代码:


说明:上面的写法为 Foo 类定义了一个静态属性 prop,目前只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。


但ES7有一个静态属性的提案,目前Babel转码器支持,这个提案对实例属性和静态属性,都规定了新的写法。

1. 类的实例属性:类的实例属性可以用等式,写入类的定义之中。


2. 类的静态属性:类的静态属性只要在上面的实例属性写法前面,加上 static 关键字即可。



new.target属性

ES6为 new 命令引入了一个new.target属性,(在构造函数中)返回 new 命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的。


注意:子类继承父类时,new.target会返回子类,利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。


说明:上面代码中,Shape 类不能被实例化,只能用于继承。

注意:在函数外部,使用 new.target 会报错。


结尾还是老规矩,欢迎大家点在和纠错,会在第一时间给出详解。

最后祝大家工作日愉快,加班狗继续加班,晚安!