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

严格模式
类和模块的内部,默认就是严格模式,所以不需要使用
考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。
name属性
由于本质上,ES6的类只是ES5的构造函数的一层包装,所以函数的许多特性都被 Class 继承,包括 name 属性。

name 属性总是返回紧跟在 class 关键字后面的类名。
Class的继承
基本用法
Class 之间可以通过

说明:上面代码定义了一个
super:它在这里表示父类的构造函数,用来新建父类的
ES5的继承,实质是先创造子类的实例对象 this ,然后再将父类的方法添加到 this 上面( Parent.apply(this),可以翻看之前混子前端介绍JS的六种继承方式 这篇文章 )
ES6的继承机制完全不同,实质是先创造父类的实例对象 this (所以必须先调用 super 方法),然后再用子类的构造函数修改 this。
如果子类没有定义 constructor 方法,这个方法会被默认添加,来看代码:

说明:不管有没有显式定义,任何一个子类都有 constructor 方法。
另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。
类的prototype属性和__proto__属性
大多数浏览器的ES5实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的prototype 属性。
Class 作为构造函数的语法糖,同时有prototype属性和 __proto__ 属性,因此同时存在两条继承链。
- 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
- 子类 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 会报错。
结尾还是老规矩,欢迎大家点在和纠错,会在第一时间给出详解。
最后祝大家工作日愉快,加班狗继续加班,晚安!