class作为一个语法糖,确实更符合了一般的面向对象编程语言的形式。但是包括类的继承的部分,还要关联上es5的原型之类的写法,再加上this的变化,感觉有一点搞脑子。编程风格中的建议也很实用。至于ArrayBuffer、最新提案、装饰器的部分,等实际遇到了再详细了解。
Class 的基本语法
ES6 的class可以看作只是一个语法糖。ES6 的类,完全可以看作构造函数的另一种写法。
简介
类的由来
JavaScript 语言中,生成实例对象的传统方法是通过构造函数。这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大。
1 2 3 4 5 6 7 8 9 10
| function Point(x, y) { this.x = x; this.y = y; }
Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; };
var p = new Point(1, 2);
|
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
1 2 3 4 5 6 7 8 9 10
| class Point { constructor(x, y) { this.x = x; this.y = y; }
toString() { return '(' + this.x + ', ' + this.y + ')'; } }
|
ES6 的类,完全可以看作构造函数的另一种写法。
1 2 3 4 5 6 7 8
| class Bar { doStuff() { console.log('stuff'); } }
var b = new Bar(); b.doStuff()
|
- 类的所有方法都定义在类的prototype属性上面。在类的实例上面调用方法,其实就是调用原型上的方法。
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
| class Point { constructor() { }
toString() { }
toValue() { } }
Point.prototype = { constructor() {}, toString() {}, toValue() {}, };
class Point { constructor(){ } }
Object.assign(Point.prototype, { toString(){}, toValue(){} });
|
- 类的内部所有定义的方法,都是不可枚举的(non-enumerable)。这一点与 ES5 的行为不一致。
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
| class Point { constructor(x, y) { }
toString() { } }
Object.keys(Point.prototype)
Object.getOwnPropertyNames(Point.prototype)
var Point = function (x, y) { };
Point.prototype.toString = function() { };
Object.keys(Point.prototype)
Object.getOwnPropertyNames(Point.prototype)
|
constructor 方法
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
1 2 3 4 5 6 7
| class Point { }
class Point { constructor() {} }
|
constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
1 2 3 4 5 6 7 8 9
| class Foo { constructor() { return Object.create(null); } }
new Foo() instanceof Foo
|
类的实例
类必须使用new调用,否则会报错。
与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Point {
constructor(x, y) { this.x = x; this.y = y; }
toString() { return '(' + this.x + ', ' + this.y + ')'; }
}
var point = new Point(2, 3);
point.toString()
point.hasOwnProperty('x') point.hasOwnProperty('y') point.hasOwnProperty('toString') point.__proto__.hasOwnProperty('toString')
|
与 ES5 一样,类的所有实例共享一个原型对象。
1 2 3 4 5
| var p1 = new Point(2,3); var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
|
可以通过实例的proto属性为“类”添加方法。这意味着,使用实例的proto属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。
取值函数(getter)和存值函数(setter)
与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class MyClass { constructor() { } get prop() { return 'getter'; } set prop(value) { console.log('setter: '+value); } }
let inst = new MyClass();
inst.prop = 123;
inst.prop
|
存值函数和取值函数是设置在属性的 Descriptor 对象上的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class CustomHTMLElement { constructor(element) { this.element = element; }
get html() { return this.element.innerHTML; }
set html(value) { this.element.innerHTML = value; } }
var descriptor = Object.getOwnPropertyDescriptor( CustomHTMLElement.prototype, "html" );
"get" in descriptor "set" in descriptor
|
属性表达式
类的属性名,可以采用表达式。
1 2 3 4 5 6 7 8 9 10 11
| let methodName = 'getArea';
class Square { constructor(length) { }
[methodName]() { } }
|
Class 表达式
与函数一样,类也可以使用表达式的形式定义。
1 2 3 4 5 6
| const MyClass = class Me { getClassName() { return Me.name; } };
|
采用 Class 表达式,可以写出立即执行的 Class。
1 2 3 4 5 6 7 8 9 10 11
| let person = new class { constructor(name) { this.name = name; }
sayName() { console.log(this.name); } }('张三');
person.sayName();
|
注意点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Foo { constructor(...args) { this.args = args; } * [Symbol.iterator]() { for (let arg of this.args) { yield arg; } } }
for (let x of new Foo('hello', 'world')) { console.log(x); }
|
类的方法内部如果含有this,它默认指向类的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Logger { printName(name = 'there') { this.print(`Hello ${name}`); }
print(text) { console.log(text); } }
const logger = new Logger(); const { printName } = logger; printName();
|
如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到该方法而报错。
在构造方法中绑定this,这样就不会找不到print方法了。
1 2 3 4 5 6 7
| class Logger { constructor() { this.printName = this.printName.bind(this); }
}
|
使用箭头函数。箭头函数内部的this总是指向定义时所在的对象。
1 2 3 4 5 6 7 8
| class Obj { constructor() { this.getThis = () => this; } }
const myObj = new Obj(); myObj.getThis() === myObj
|
- 使用Proxy,获取方法的时候,自动绑定this。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function selfish (target) { const cache = new WeakMap(); const handler = { get (target, key) { const value = Reflect.get(target, key); if (typeof value !== 'function') { return value; } if (!cache.has(value)) { cache.set(value, value.bind(target)); } return cache.get(value); } }; const proxy = new Proxy(target, handler); return proxy; }
const logger = selfish(new Logger());
|
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
1 2 3 4 5 6 7 8 9 10 11
| class Foo { static classMethod() { return 'hello'; } }
Foo.classMethod()
var foo = new Foo(); foo.classMethod()
|
如果静态方法包含this关键字,这个this指的是类,而不是实例。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Foo { static bar() { this.baz(); } static baz() { console.log('hello'); } baz() { console.log('world'); } }
Foo.bar()
|
父类的静态方法,可以被子类继承。
静态方法也是可以从super对象上调用的。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Foo { static classMethod() { return 'hello'; } }
class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; } }
Bar.classMethod()
|
实例属性的新写法
实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class IncreasingCounter { constructor() { this._count = 0; } }
class IncreasingCounter { _count = 0; }
|
静态属性
静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。
1 2 3 4 5 6 7 8 9 10
| class Foo { } Foo.prop = 1;
class Foo { static prop = 1; }
|
私有方法和私有属性
现有的解决方案
私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。
一种做法是在命名上加以区别。私有属性明明前加_,但是这种做法是不保险的,因为外部还是可以调用。
另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。
1 2 3 4 5 6 7 8 9 10 11
| class Widget { foo (baz) { bar.call(this, baz); }
}
function bar(baz) { return this.snaf = baz; }
|
还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。
私有属性的提案
目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。
new.target 属性
new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。
Class 内部调用new.target,返回当前 Class。
子类继承父类时,new.target会返回子类。
利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Shape { constructor() { if (new.target === Shape) { throw new Error('本类不能实例化'); } } }
class Rectangle extends Shape { constructor(length, width) { super(); } }
var x = new Shape(); var y = new Rectangle(3, 4);
|
Class 的继承
简介
Class 可以通过extends关键字实现继承
1 2 3 4 5 6 7 8 9 10
| class ColorPoint extends Point { constructor(x, y, color) { super(x, y); this.color = color; }
toString() { return this.color + ' ' + super.toString(); } }
|
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
1 2 3 4 5 6 7 8 9
| class ColorPoint extends Point { }
class ColorPoint extends Point { constructor(...args) { super(...args); } }
|
另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Point { constructor(x, y) { this.x = x; this.y = y; } }
class ColorPoint extends Point { constructor(x, y, color) { this.color = color; super(x, y); this.color = color; } }
|
实例对象cp同时是ColorPoint和Point两个类的实例
1 2 3 4
| let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint cp instanceof Point
|
父类的静态方法,也会被子类继承。
Object.getPrototypeOf()
Object.getPrototypeOf方法可以用来从子类上获取父类。
1 2 3 4
|
Object.getPrototypeOf(ColorPoint) === Point
|
super 关键字
super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
- super作为函数调用时,代表父类的构造函数。作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。
1 2 3 4 5 6 7 8 9
| class A {}
class B extends A { constructor() { super(); } }
|
在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class A { p() { return 2; } }
class B extends A { constructor() { super(); console.log(super.p()); } }
let b = new B();
|
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
| 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);
var child = new Child(); child.myMethod(2);
|
使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
1 2 3 4 5 6 7 8
| class A {}
class B extends A { constructor() { super(); console.log(super); } }
|
最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。
1 2 3 4 5 6 7
| var obj = { toString() { return "MyObject: " + super.toString(); } };
obj.toString();
|
类的 prototype 属性和proto属性
Class 作为构造函数的语法糖,同时有prototype属性和proto属性,因此同时存在两条继承链。
(1)子类的proto属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的proto属性,表示方法的继承,总是指向父类的prototype属性。
1 2 3 4 5 6 7 8
| class A { }
class B extends A { }
B.__proto__ === A B.prototype.__proto__ === A.prototype
|
extends关键字后面可以跟多种类型的值。
原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。
1 2 3 4 5 6 7 8 9 10 11 12
| class MyArray extends Array { constructor(...args) { super(...args); } }
var arr = new MyArray(); arr[0] = 12; arr.length
arr.length = 0; arr[0]
|
extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。
Mixin 模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。
1 2 3 4 5 6 7
| const a = { a: 'a' }; const b = { b: 'b' }; const c = {...a, ...b};
|
下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。
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
| function mix(...mixins) { class Mix { constructor() { for (let mixin of mixins) { copyProperties(this, new mixin()); } } }
for (let mixin of mixins) { copyProperties(Mix, mixin); copyProperties(Mix.prototype, mixin.prototype); }
return Mix; }
function copyProperties(target, source) { for (let key of Reflect.ownKeys(source)) { if ( key !== 'constructor' && key !== 'prototype' && key !== 'name' ) { let desc = Object.getOwnPropertyDescriptor(source, key); Object.defineProperty(target, key, desc); } } }
|
Module 的语法
概述
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
1 2
| import { stat, exists, readFile } from 'fs';
|
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
ES6 模块有以下好处。
静态加载使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。
将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上”use strict”;。
严格模式主要有以下限制。
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用with语句
- 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
- eval不会在它的外层作用域引入变量
- eval和arguments不能被重新赋值
- arguments不会自动反映函数参数的变化
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局对象
- 不能使用fn.caller和fn.arguments获取函数调用的堆栈
- 增加了保留字(比如protected、static和interface)
export 命令
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量
1 2 3 4 5 6 7 8 9 10 11 12 13
| export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958;
var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958;
export { firstName, lastName, year };
|
export命令除了输出变量,还可以输出函数或类(class)。
可以使用as关键字重命名。
1 2 3 4 5 6 7 8
| function v1() { ... } function v2() { ... }
export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion };
|
export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export var m = 1;
var m = 1; export {m};
var n = 1; export {n as m};
export function f() {};
function f() {} export {f};
|
export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。
import 命令
1 2 3 4 5
| import { firstName, lastName, year } from './profile.js';
import { lastName as surname } from './profile.js';
|
import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。如果a是一个对象,改写a的属性是允许的。
1 2 3
| import {a} from './xxx.js'
a = {};
|
import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
import命令具有提升效果,会提升到整个模块的头部,首先执行。
import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
import语句会执行所加载的模块,因此可以有下面的写法。
如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。也就是说,import语句是 Singleton 模式。
模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。
export default 命令
用export default命令,为模块指定默认输出。
1 2 3 4 5 6 7 8
| export default function () { console.log('foo'); }
import customName from './export-default'; customName();
|
使用export default时,对应的import语句不需要使用大括号;不使用export default时,对应的import语句需要使用大括号。
一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。
本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。
1 2 3 4 5 6 7 8 9 10 11 12
| function add(x, y) { return x * y; } export {add as default};
import { default as foo } from 'modules';
|
正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。可以直接将一个值写在export default之后。
1 2 3 4 5 6 7 8 9
| var a = 1; export default a;
export default var a = 1;
export default 42;
|
有了export default命令,输入模块时就非常直观了,以输入 lodash 模块为例。
1 2 3 4 5
| import _ from 'lodash';
import _, { each, forEach } from 'lodash';
|
export 与 import 的复合写法
1 2 3 4 5 6 7 8 9 10 11
| export { foo, bar } from 'my_module';
import { foo, bar } from 'my_module'; export { foo, bar };
export { es6 as default } from './someModule';
import { es6 } from './someModule'; export default es6;
|
模块的继承
export *命令会忽略模块的default方法
模块之间也可以继承。
跨模块常量
const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export const A = 1; export const B = 3; export const C = 4;
import * as constants from './constants'; console.log(constants.A); console.log(constants.B);
import {A, B} from './constants'; console.log(A); console.log(B);
|
如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。然后,将这些文件输出的常量,合并在index.js里面。使用的时候,直接加载index.js就可以了。
import()
因为require是运行时加载模块,import命令无法取代require的动态加载功能。有一个提案,建议引入import()函数,完成动态加载。
编程风格
下面的内容主要参考了 Airbnb 公司的 JavaScript 风格规范。
块级作用域
字符串
静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
解构赋值
- 使用数组成员对变量赋值时,优先使用解构赋值。
- 函数的参数如果是对象的成员,优先使用解构赋值。
- 如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。
对象
- 单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
- 对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。
1 2 3 4 5 6 7 8 9 10 11
| const a = {}; a.x = 3;
const a = {}; Object.assign(a, { x: 3 });
const a = { x: null }; a.x = 3;
|
- 如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。
1 2 3 4 5 6 7 8 9 10 11 12 13
| const obj = { id: 5, name: 'San Francisco', }; obj[getKey('enabled')] = true;
const obj = { id: 5, name: 'San Francisco', [getKey('enabled')]: true, };
|
- 对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| var ref = 'some value';
const atom = { ref: ref,
value: 1,
addValue: function (value) { return atom.value + value; }, };
const atom = { ref,
value: 1,
addValue(value) { return atom.value + value; }, };
|
数组
- 使用扩展运算符(…)拷贝数组。
- 使用 Array.from 方法,将类似数组的对象转为数组。
函数
1 2 3
| (() => { console.log('Welcome to the Internet.'); })();
|
1 2 3 4 5 6 7
| function divide(a, b, option = false ) { }
function divide(a, b, { option = false } = {}) { }
|
- 不要在函数体内使用 arguments 变量,使用 rest 运算符(…)代替。
1 2 3 4 5 6 7 8 9 10
| function concatenateAll() { const args = Array.prototype.slice.call(arguments); return args.join(''); }
function concatenateAll(...args) { return args.join(''); }
|
1 2 3 4 5 6 7 8 9
| function handleThings(opts) { opts = opts || {}; }
function handleThings(opts = {}) { }
|
Map 结构
注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
Class
- 总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。
- 使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。
模块
首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import取代require。
使用export取代module.exports。
如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default,export default与普通的export不要同时使用。
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
如果模块默认输出一个函数,函数名的首字母应该小写。
如果模块默认输出一个对象,对象名的首字母应该大写。
ESLint 的使用
ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。
- 然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件。
1 2
| $ npm i -g eslint-config-airbnb $ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
|
- 最后,在项目的根目录下新建一个.eslintrc文件,配置 ESLint。
1 2 3
| { "extends": "eslint-config-airbnb" }
|
读懂 ECMAScript 规格
异步遍历器
ES2018 引入了“异步遍历器”(Async Iterator),为异步操作提供原生的遍历器接口,即value和done这两个属性都是异步产生。
异步遍历器的最大的语法特点,就是调用遍历器的next方法,返回的是一个 Promise 对象。因此,可以把它放在await命令后面。
1 2 3 4 5
| asyncIterator .next() .then( ({ value, done }) => );
|
for await…of
新引入的for await…of循环,则是用于遍历异步的 Iterator 接口。for await…of循环也可以用于同步遍历器。
异步 Generator 函数
异步 Generator 函数的作用,是返回一个异步遍历器对象。
在语法上,异步 Generator 函数就是async函数与 Generator 函数的结合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function* map(iterable, func) { const iter = iterable[Symbol.iterator](); while (true) { const {value, done} = iter.next(); if (done) break; yield func(value); } }
async function* map(iterable, func) { const iter = iterable[Symbol.asyncIterator](); while (true) { const {value, done} = await iter.next(); if (done) break; yield func(value); } }
|
yield* 语句
yield*语句也可以跟一个异步遍历器。
1 2 3 4 5 6 7 8 9 10
| async function* gen1() { yield 'a'; yield 'b'; return 2; }
async function* gen2() { const result = yield* gen1(); }
|
ArrayBuffer
二进制数组由三类对象组成。
(1)ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。
(2)TypedArray视图:共包括 9 种类型的视图,比如Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等。
(3)DataView视图:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。
简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。