ES6学习笔记(二)

ES6 学习笔记(二)

Class 的语法

基本语法

ES6 中的 class 类的定义可以看作是一个语法糖,其绝大部分功能 ES5 都可以做到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ES5 类的定义
function Point(x ,y) {
this.x = x;
this.y = y;
}
Point.prototype.add = function () {
return '(' + this.x + ', ' + this.y + ')';
}

// ES6 类的定义
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

add() {
return `(${this.x}, ${this.y})`;
}
}
  1. 从中可以看到,class 里面有一个 constructor 方法,即构造方法,ES5 中的构造函数就对应着这个构造方法。
  2. 其中的关键字 this 和 ES5 中的作用一样都是代表实例对象。
  3. 类的方法之间不需要用逗号分隔。调用的时候也和 ES5 中构造函数的使用方式一致,使用 new 命令。
  4. ES6 类和模块的内部,默认就是 严格模式,所以必须使用 new 调用,否则会报错。
  5. ES6 类的所有方法都定义在类的 prototype 属性上面,且都是 不可枚举的
  6. 类的属性名,可以使用表达式。

constructor

constructor 方式是类的默认方法,如果没有显式定义,则一个空的 constructor 方法会被默认添加。其默认返回实例对象 - this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {

}
// 等同于
class Point _{
constructor() {

}
}
let point = new Point();
// 在类的实例上调用方法,其实就是调用原型上的方法
point.constructor === Point.prototype.constructor // true
// 原型对象 prototype 上的 constructor 属性也是指向类的本身
Point.prototype.constructor === Point // true

类的实例对象

与 ES5 中一样,实例的属性除非显式定义在其本身(this 对象上),否则都是定义在原型(class)上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
add() {
return `(${this.x}, ${this.y})`;
}
}
var point = new Point(2, 3);
point.add() // (2, 3)
point.hasOwnProperty('x'); // true
point.hasOwnProperty('y'); // true
point.hasOwnProperty('add'); // false
Object.getPrototypeOf('point').hasOwnProperty('add'); // true

var point1 = new Point(1, 4);
point1.__proto__ === point2.__proto__;

Class 表达式与变量提升

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 下面这个类的名字是 MyClass 而不是 Me,Me只能在 Class 的内部代码可用,指代当前类,也可省略
// name 属性则总是返回紧跟在 class 关键字后面的类名
const MyClass = class Me {
getClassName() {
return Me.name
}
};

let my = new MyClass();
my.getClassName(); // Me
myClass.name // Me

// 下面这个例子采用 Class 表达式的方式,写出立即执行的 Class
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('Lily');
person.sayName(); // 'Lily'

// 类 Class 不存在变量提升,和 ES5 完全不同
new Foo();
class Foo {} // ReferenceError: Foo is not defined

this 的指向

类的方法内部如果含有 this,则默认指向类的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 可以在类中绑定 this,避免运行环境改变时发生错误
// 在构造方法中绑定
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
}

// 使用箭头函数
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
};
}
}

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

存值函数和取值函数是设置在属性的 Descriptor 对象上的,与 ES5 完全一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
constructor() {

}
get prop() {
return 'getter';
}
set prop(value) {
console.log(`setter: ${value}`);
}
}
let my = new MyClass();
my.prop = 233; // setter: 233
my.prop; // 'getter'
var descriptor = Object.getOwnPropertyDescriptor(MyClass.prototype, 'prop');
'set' in descriptor; // true
'get' in descriptor; // true

Class 的静态方法和静态属性

类相当于实例的原型,所有在类中定义的方法都会被实例继承。如果在一个方法前加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用。这就是所谓的 静态方法,但是父类的静态方法会被子类继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Foo {
static bar () {
this.baz(); // 这里的 this 指向 Foo,而不是 Foo 生成的实例
}
static baz () {
console.log('static baz');
}
baz () {
console.log('baz');
}
}
var foo = new Foo();
foo.bar(); // TypeError: foo.bar is not a function
Foo.bar(); // 'static baz'
Foo.baz(); // 'static baz'
foo.baz(); // 'baz'
// 静态方法可以被子类继承
class Bar extends Foo { }
var bar = new Bar();
bar.bar(); // TypeError: foo.bar is not a function
Bar.bar(); // 'static baz'
Bar.baz(); // 'static baz'
bar.baz(); // 'baz'

静态属性 指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象 this 上的属性

1
2
3
4
5
class Foo {

}
Foo.prop = 1;
Foo.prop // 1

new.target 属性

new 是从构造函数生成实例对象的命令,而 new.target 属性一般用在构造函数中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令调用的,new.target 会返回 undefined。所以该属性可以用来确定构造函数是怎么调用的。另外,new.target 在函数外部使用会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
var obj = new Rectangle(3, 4); // true
// 子类继承父类的时候,new.target 会返回子类
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
var obj = new Square(3); // false 此时的 new.target 为子类 Square

Class 的继承

基本概念

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象 this(所以务必先调用 super 方法),然后再用子类的构造函数修改 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
}
let cp = new ColorPoint(3, 4, 'red');
cp instanceof ColorPoint; // true
cp instanceof Point; // true
// Object.getPrototypeOf 方法可以用来从子类上获取父类
Object.getPrototypeOf(ColorPoint) === Point // true

// 如果子类没有定义 constructor 方法,这个方法会被默认添加,即
class ColorPoint extends Point {
// ...
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}

// 父类的静态方法也会被继承
class A {
static hello() {
console.log('Hello world');
}
}
class B extends A {

}
B.hello(); // hello world

super 关键字

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

第一种情况,super 作为函数调用时,代表父类的构造函数。(子类的构造函数必须执行一次 super 函数)。且只能用在子类的构造函数之中,用在其他地方就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
// super 内部的 this 指向的是 B,所以此时的 super() 在这里相当于 A.prototype.constructor.call(this)
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A(); // A
new B(); // B

第二种情况,super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中指向父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
new B() // 2

// 普通方法中 super 指向的是父类的原型对象,所以定义在父类实例上的方法或是属性是无法通过 this 调用的
class A {
constructor() {
this.p = 2;
}
}
A.prototype.x = 3;
class B extends A {
constructor() {
super();
console.log(super.x); // 3
}
get m() {
return super.p;
}
}
new B() // 3
var b = new B();
b.m // undefined

// 由于 this 指向子类实例,所以如果通过 super 对某个属性赋值,这时 super 就是 this,赋值的属性会变成子类实例的属性
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x);
console.log(this.x);
}
}
new B(); // undefined 3, 读取的时候 super.x 相当于 A.prototype.x 所以返回 undefined

// super 作为对象,用在静态方法中指向父类,而不是父类的原型对象
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2

类的 prototype 属性和 proto 属性

Class 同时有 prototype 属性和 proto 属性,因此同时存在两条继承链,子类的 __proto__ 属性,表示构造函数的继承,总是指向父类;子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。

1
2
3
4
5
6
7
8
9
10
11
12
// 因为类的继承是按照下面的模式实现的
class A {

}
class B {

}
Object.setPrototypeOf(B.prototype, A.prototype);
Object.setPrototypeOf(B, A);
const a = new A();
const b = new B();
b.__proto__.__proto__ === a.__proto__ // true

extends 的继承目标

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 子类继承 Object 类,此时 A 的实例就是 Object 的实例
class A extends Object {
// ...
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true

// 不存在任何继承,此时 A 就是一个普通函数,所以直接继承 Function.prototype,但 A 调用后返回一个空对象,所以 A.prototype.__proto__ 指向构造函数(Object)的 prototype 属性
class A {
// ...
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

// 子类继承 null
class A extends null {
// ...
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true

原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。原生构造函数大致有下面几类,Boolean()Number()StringArray()Date()Function()RegExp()Error() 以及 Object

1
2
3
4
5
6
7
8
9
class MyArray extends Array {
constructor() {
super();
}
}
var arr = new MyArray();
arr.push(1); // [1]
arr.push(2); // [1, 2]
arr.length; // 2

Module 的语法

在 ES6 之前,社区定制了一些模块加载的方案,最主要的有 CommonJS 和 AMD 两种,前者用于服务器,后者用于浏览器。ES6 在语言标准层面上实现了模块功能,完全可以取代 CommonJS 和 AMD 规范成为浏览器和服务器通用的模块解决方案。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。而 CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

ES6 的模块自动采用严格模式,不应该在顶层代码中使用 this,顶层的 this 指向 undefined

模块的功能主要由两个命令构成:exportimport

export 命令

export 命令用于规定模块的对外接口,一个模块就是一个独立的文件,该文件内部的所有变量,外部都无法获取。必须使用 export 关键字输出该变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// profile.js
// 通过 export 命令输出变量
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

// 通过 export 命令输出函数
export function multiply(x, y) {
return x * y;
}

// 使用 as 关键字,可以重命名对外接口
function v1() {}
function v2() {}
export {
v1 as stream1,
v2 as stream2
}

// export 语句输出的接口,与其对应的值是动态绑定关系,和 CommonJS 规范不用,CommonJS 输出的是值得缓存,不存在动态更新。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 1000); // 1s 后输出的变量变为 baz

import 命令

import 命令规定用于输入其他模块提供的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// main.js
// 下面的 import 命令,用于加载 profile.js 文件,并从中输出变量,大括号里面的变量名,必须与被导入模块的对外接口的名称相同
import {firstName, lastName, year} from './profile.js'

// 如果想为输入的变量重新取一个名字,import 命令要使用 as 关键字,将输入的变量重新命名
import {lastName as surename} from './profile.js'

// 凡是输入的变量,都当作完全只读,轻易不要改变它的属性
import {a} from './xxx.js'
a. = {}; // Syntax Error: 'a' is read=only;
a.foo = 'Hello'; // 合法,但是不建议

// import 指令具有提升效果,会提升到整个模块的头部
foo();
import {foo} from 'my_module';

// 由于 import 是静态执行,所以不能使用表达式和变量这些只有在运行时才能得到结果的语法结构
import {'f' + 'oo'} from 'my_module';

// 如果多次重复执行同一句 import 语句,只会执行一次,而不会多次执行
import 'loadash';
import 'loadash';

// 通过 Babel 转码,CommonJS 模块的 require 命令和 ES6 模块的 import 命令,可以写在同一个模块中,但 import 是在静态解析阶段执行的,所以它是一个模块中最早执行的。
require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React' // 不会得到预期的结果

模块的整体加载

模块的整体加载所在的那个对象。应该是静态分析的,所以不允许运行时改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
// main.js
import * as circle from './circle.js'
console.log(circle.area(4));
console.log(circle.circumference.(8));
// 下面操作是不予许的
circle.foo = 'hello';
circle.area = function () {};

export default 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// module.js
function add(x, y) {
return x * y;
}
export default add; // 等同于:export {add as defaul}

// main.js
import foo from ./'module.js'; // 等同于:import {default as foo} from './module.js'

// 因为 export default 命令其实只是输出一个叫做 default 的变量(将后面的值赋给变量 default),所以后面不能跟变量声明语句
export default var a = 1; // error
export default 23; // OK
export 11; // error

// export default 也可以用来输出类
// MyClass.js
export default class {// ...}
// main.js
import MyClass from './MyClass.js';
let o = new MyClass();

export 与 import 的复合写法

如果在一个模块中,先输入后输出同一个模块,import 语句可以与 export 语句写在一起。

1
2
3
4
5
6
7
8
9
10
11
// 写成一行后,foo 和 bar 实际上并没有导入当前模块,相当于对外转发的作用,当前模块不能直接使用 foo 和 bar
export {foo, bar} from 'my_module';
// 等同于
import {foo, bar} from 'my_module';
export {foo, bar};

// 具名接口改为默认接口的写法
export {es6 as default} from 'my_module';
// 等同于
import {es6} from 'my_module';
export default es6;

跨模块常量

1
2
3
4
5
6
7
8
// const 声明的常量只在当前代码块有效,可以采用下面的方法,设置跨模块常量
// constants.js
export const A = 1;
export const B = 2;
export const C = 3;
// main.js
import * as constants from './constants.js'
console.log(constants.A); // 1

Module 的加载实现

加载规则

浏览器加载 ES6 模块,使用 <script> 标签,同时还要加入 type="module" 属性。且是异步加载,等到浏览器页面渲染完,再执行模块脚本。不会阻塞浏览器。

在浏览器模块顶层使用 this 关键字会返回 undefined,而不是 window

1
2
3
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

ES6 模块 与 CommonJS 模块的差异

CommonJS 模块输出的是一个值得拷贝,ES6 模块输出的是值的引用
CommonJS 模块加载的是一个对象(module.exports属性),只有在运行时才会生成。ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// CommonJS 模块实例
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib.js');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
// lib.js 模块加载后,它的内部变化就影响不到 mod.counter 了,因为 mod.counter 是一个原始类型的之,会被缓存。只有写成一个函数才能得到内部变动后的值
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter;
},
incCounter: incCounter,
};
// main.js($ node main.js)
var mod = require('./lib.js');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 4
// ES6 模块,在 JS 引擎对脚本静态分析的时候,遇到 import 就会生成一个只读引用,等到脚本真正执行时,才会根据这个引用到被加载的模块中去取值。ES6 模块是动态引用,并且不会缓存值
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import {counter, incCounter} from './lib.js';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
// 另外,export 通过接口,输出的是同一个值,不同的脚本加载这个接口,得到的都是同样的实例

内部变量

由于要保证同一个 ES6 模块不用修改,就可以用在浏览器环境和服务器环境。Node 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量

- this
- arguments
- require
- module
- exports
- __filename
- __dirname

如果一定要使用这些变量,可以写一个 CommonJS 模块输出这些变量,然后用 ES6 模块加载这个 CommonJS 模块,但是这样该 ES6 模块又不能用于浏览器环境了,所以不推荐这样做。

ES6 模块加载 CommonJS 模块

CommonJS 模块的输出都是定义在 module.exports 属性上面的,而 Node 的 import 命令加载 CommonJS 的模块时,Node 会自动将 module.exports 属性,当作模块的默认输出,即等同于 export default xxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 一个用 ES6 模块加载 CommonJS 模块的实例,有三种写法可以拿到 module.exports
// a.js
module.exports = {
foo: 'hello',
bar: 'world'
}
// 相当于 ES6 模块中的
exports default {
foo: 'hello',
bar: 'world'
}
// main.js
// 方法1
import baz from './a.js'; // baz = { foo: 'hello', bar: 'world' };
// 方法2
import {default as baz} from './a.js'; // baz = { foo: 'hello', bar: 'world' };
// 方法3
import * as baz from './a.js'
// baz = {
// get default() { return module.exports; }
// get foo() { return this.default.foo }.bind(baz),
// get bar() { return this.default.bar }.bind(baz)
// }

// CommonJS 模块输出缓存机制,在ES6 加载方式下依然有效
// foo.js
module.exports = 123;
setTimeout(() => module.exports = null, 500);
// 对于加载 foo.js 的脚本,module.exports 的值一直会是 123,而不会变成 null

// 下面的写法会报错,因为 fs 是 CommonJS 的格式,只能在运行时才能确定的接口,而 import 命令要求编译时就要确定这个接口
import {readfile} from 'fs'; // error
// 可以改为整体输入
import express from 'express' // ok
const app = express();

CommonJS 模块的循环加载

CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候就会全部执行,一旦出现某个模块被“循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。另外 CommonJS 输出的是被输出值的拷贝,不是引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ajs
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 中,a.done = %j,b.done = %j', a.done, b.done);
// $ node main.js
// 在 b.js 中,a.done = false
// b.js 执行完毕
// 在 a.js 中,b.done = true
// a.js 执行完毕
// 在 main.js 中,a.done = true,b.done = true

上述代码的执行过程:执行 main.js 的第一行,加载并执行 a.jsa.js 先输出一个 done 的变量,然后加载 b.js。(此时代码 a.js 会暂停,等待 b.js 执行完毕后再往下执行。)执行 b.js 时,首先也输出一个 done 的变量,然后执行到第二行去加载 a.js(此时便发生了“循环加载”)获取相应的 exports 的属性的值,但 a.js 没有执行完,只能取回已经执行部分的值,即 exports.done = false。后面 b.js 接着往下执行,执行完毕后将执行权交还给 a.jsa.js 接着往下执行知道执行完毕。后面在执行 main.js 第二行时,不会再执行 b.js 了,而是输出缓存的 b.js 的执行结果(即 b.js 的第四行)并返回。main.js 执行第三行,完毕。

ES6 模块的循环加载

ES6 处理“循环加载”与 CommonJS 有着本质的不同,ES6 模块是动态引用,加载的变量不会被缓存,而是成为一个被加载模块的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo'; }
export {foo};
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
// $ node --experimental-modules a.mjs
// b.mjs
// foo
// a.mjs
// bar

上述代码的执行过程:首先解析 a.mjs,引擎发现它加载了 b.mjs,因此会优先执行 b.mjs 再执行 a.mjs。下面执行 b.mjs,执行 b.mjs 的时候,已知它从 a.mjs 中输入了 foo 接口,此时不会去执行 a.mjs 认为这个接口已经存在了,继续往下执行。到第三行的时候,由于 foo 这个接口在 import {bar} from './b' 是已经有定义了(函数声明的提升作用)。所以打印出结果,执行完毕后输出 {foo} 接口后开始执行 a.mjs。接着 a.mjs 执行完毕。