这是 ES6 学习笔记记录 Part3-3
学习并节选自 阮一峰 - ECMAScript 6 入门
仅供个人学习记录,原著请访问链接
JavaScript 语言中,生成实例对象的传统方法是通过构造函数:
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);
上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大。ES6 提供了更接近传统语言的写法,引入了 Class
(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
上面的代码用 ES6 的class
改写,就是下面这样:
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
// constructor方法是构造方法
// 而this关键字则代表实例对象
// Point类除了构造方法,还定义了一个toString方法
// 注意,定义“类”的方法的时候,前面不需要加上function这个关键字
// 另外,方法之间不需要逗号分隔,加了会报错
使用的时候,也是直接对类使用new
命令,跟构造函数的用法完全一致:
class Bar {
doStuff() {
console.log('stuff');
}
}
var b = new Bar();
b.doStuff() // "stuff"
事实上,类的所有方法都定义在类的prototype
属性上面。在类的实例上面调用方法,其实就是调用原型上的方法。另外,类的内部所有定义的方法,都是不可枚举的。
类和模块的内部,默认就是严格模式,只有严格模式可用。
考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。
constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加:
class Point {
}
// 等同于
class Point {
constructor() {}
}
constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象:
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
类必须使用new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行。
生成类的实例对象的写法,与 ES5 完全一样,也是使用new
命令:
class Point {
// ...
}
// 报错
var point = Point(2, 3);
// 正确
var point = new Point(2, 3);
实例的属性除非显式定义在其本身(即定义在this
对象上),否则都是定义在原型上(即定义在class
上):
//定义类
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() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
与 ES5 一样,类的所有实例共享一个原型对象。这也意味着,可以通过实例的__proto__
属性为“类”添加方法。
与函数一样,类也可以使用表达式的形式定义:
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
// 需要注意的是,这个类的名字是 MyClass
// 而不是 Me
// Me只在 Class 的内部代码可用,指代当前类。
类不存在变量提升(hoist),这一点与 ES5 完全不同。
new Foo(); // ReferenceError
class Foo {}
上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义:
{
let Foo = class {};
class Bar extends Foo {
}
}
私有是常见需求,但 ES6 不提供,只能通过变通方法模拟实现。
类的方法内部如果含有this
,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错:
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
// printName方法中的this,默认指向Logger类的实例
// 但是,如果将这个方法提取出来单独使用
// this会指向该方法运行时所在的环境
// 因为找不到print方法而导致报错。
由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class
继承,包括name
属性:
class Point {}
Point.name // "Point"
name
属性总是返回紧跟在class
关键字后面的类名。
与 ES5 一样,在“类”的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为:
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
如果某个方法之前加上星号(*
),就表示该方法是一个 Generator 函数:
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);
}
// hello
// world
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”:
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
注意,如果静态方法包含this
关键字,这个this
指的是类,而不是实例。
父类的静态方法,可以被子类继承。静态方法也是可以从super
对象上调用的。
类的实例属性可以用等式,写入类的定义之中:
class MyClass {
myProp = 42;
constructor() {
console.log(this.myProp); // 42
}
}
以前,我们定义实例属性,只能写在类的constructor
方法里面。有了新的写法以后,可以不在constructor
方法里面定义。这种写法比以前更清晰。
类的静态属性只要在上面的实例属性写法前面,加上static
关键字就可以了:
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
同样的,这个新写法大大方便了静态属性的表达。
new
是从构造函数生成实例对象的命令。ES6 为new
命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new
命令作用于的那个构造函数。如果构造函数不是通过new
命令调用的,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
Class 可以通过extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多:
class Point {
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
super
关键字,它在这里表示父类的构造函数,用来新建父类的this
对象。子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。如果不调用super
方法,子类就得不到this
对象。
如果子类没有定义constructor
方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor
方法。
Object.getPrototypeOf
方法可以用来从子类上获取父类:
Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用这个方法判断,一个类是否继承了另一个类。
super
这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
第一种情况,super
作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super
函数。
第二种情况,super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些:
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
ES6 允许继承原生构造函数定义子类,而 ES5 则不可以。
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix.prototype, mixin); // 拷贝实例属性
copyProperties(Mix.prototype, Reflect.getPrototypeOf(mixin)); // 拷贝原型属性
}
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);
}
}
}
// 上面代码的mix函数,可以将多个对象合成为一个类
// 使用的时候,只要继承这个类即可
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。
基本上,修饰器的行为就是下面这样:
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
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
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export
关键字输出该变量。下面是一个 JS 文件,里面使用export
命令输出变量。
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
上面代码保存了用户信息。ES6 将其视为一个模块,里面用export
命令对外部输出了三个变量。
export
的写法,还有另外一种:
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
// 应该优先考虑使用这种写法
export
命令除了输出变量,还可以输出函数或类(class
)。
通常情况下,export
输出的变量就是本来的名字,但是可以使用as
关键字重命名:
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
// 使用as关键字,重命名了函数v1和v2的对外接口
// 重命名后,v2可以用不同的名字输出两次
需要特别注意的是,export
命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 报错
export 1;
// 报错
var m = 1;
export m;
// 上面两种写法都会报错,因为没有提供对外的接口
// 第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1
// 1只是一个值,不是接口
// 正确的写法是下面这样
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
另外,export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
最后,export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,import
命令也是如此。
使用export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块:
// main.js
import {firstName, lastName, year} from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
上面代码的import
命令,用于加载profile.js
文件,并从中输入变量。import
命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js
)对外接口的名称相同。
如果想为输入的变量重新取一个名字,import
命令要使用as
关键字,将输入的变量重命名:
import { lastName as surname } from './profile.js';
import
命令输入的变量都是只读的,因为它的本质是输入接口。
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
import {myMethod} from 'util';
注意,import
命令具有提升效果,会提升到整个模块的头部,首先执行。
最后,import
语句会执行所加载的模块,因此可以有下面的写法。
import 'lodash';
上面代码仅仅执行lodash
模块,但是不输入任何值。
如果多次重复执行同一句import
语句,那么只会执行一次,而不会执行多次:
import 'lodash';
import 'lodash';
// 上面代码加载了两次lodash,但是只会执行一次
import语句是 Singleton 模式:
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
通过 Babel 转码,CommonJS 模块的
require
命令和 ES6 模块的import
命令,可以写在同一个模块里面,但是最好不要这样做。
除了指定加载某个输出值,还可以使用整体加载,即用星号(*
)指定一个对象,所有输出值都加载在这个对象上面。
下面是一个circle.js
文件:
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
现在,加载这个模块。
// main.js
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
上面写法是逐一指定要加载的方法,整体加载的写法如下:
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default
命令,为模块指定默认输出:
// export-default.js
export default function () {
console.log('foo');
}
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字:
// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代码的import
命令,可以用任意名称指向export-default.js
输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import
命令后面,不使用大括号。
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以,import
命令后面才不用加大括号,因为只可能唯一对应export default
命令。
本质上,export default
就是输出一个叫做default
的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。
如果在一个模块之中,先输入后输出同一个模块,import
语句可以与export
语句写在一起:
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
上面代码中,export
和import
语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
const
声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件)可以采用下面的写法:
// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
建议不再使用var
命令,而是使用let
命令取代。let
完全可以取代var
,因为两者语义相同,而且let
没有副作用。
在let
和const
之间,建议优先使用const
,尤其是在全局环境,不应该设置变量,只应设置常量。
// bad
var a = 1, b = 2, c = 3;
// good
const a = 1;
const b = 2;
const c = 3;
// best
const [a, b, c] = [1, 2, 3];
静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号:
// bad
const a = "foobar";
const b = 'foo' + a + 'bar';
// acceptable
const c = `foobar`;
// good
const a = 'foobar';
const b = `foo${a}bar`;
使用数组成员对变量赋值时,优先使用解构赋值:
const arr = [1, 2, 3, 4];
// bad
const first = arr[0];
const second = arr[1];
// good
const [first, second] = arr;
函数的参数如果是对象的成员,优先使用解构赋值:
// bad
function getFullName(user) {
const firstName = user.firstName;
const lastName = user.lastName;
}
// good
function getFullName(obj) {
const { firstName, lastName } = obj;
}
// best
function getFullName({ firstName, lastName }) {
}
如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序:
// bad
function processInput(input) {
return [left, right, top, bottom];
}
// good
function processInput(input) {
return { left, right, top, bottom };
}
const { left, right } = processInput(input);
单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾:
// bad
const a = { k1: v1, k2: v2, };
const b = {
k1: v1,
k2: v2
};
// good
const a = { k1: v1, k2: v2 };
const b = {
k1: v1,
k2: v2,
};
对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign
方法:
// bad
const a = {};
a.x = 3;
// if reshape unavoidable
const a = {};
Object.assign(a, { x: 3 });
// good
const a = { x: null };
a.x = 3;
如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义:
// bad
const obj = {
id: 5,
name: 'San Francisco',
};
obj[getKey('enabled')] = true;
// good
const obj = {
id: 5,
name: 'San Francisco',
[getKey('enabled')]: true,
};
对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写:
var ref = 'some value';
// bad
const atom = {
ref: ref,
value: 1,
addValue: function (value) {
return atom.value + value;
},
};
// good
const atom = {
ref,
value: 1,
addValue(value) {
return atom.value + value;
},
};
使用扩展运算符(…)拷贝数组:
// bad
const len = items.length;
const itemsCopy = [];
let i;
for (i = 0; i < len; i++) {
itemsCopy[i] = items[i];
}
// good
const itemsCopy = [...items];
使用 Array.from 方法,将类似数组的对象转为数组:
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);
立即执行函数可以写成箭头函数的形式:
(() => {
console.log('Welcome to the Internet.');
})();
那些需要使用函数表达式的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this
:
// bad
[1, 2, 3].map(function (x) {
return x * x;
});
// good
[1, 2, 3].map((x) => {
return x * x;
});
// best
[1, 2, 3].map(x => x * x);
箭头函数取代Function.prototype.bind
,不应再用 self/_this/that
绑定 this
:
// bad
const self = this;
const boundMethod = function(...params) {
return method.apply(self, params);
}
// acceptable
const boundMethod = method.bind(this);
// best
const boundMethod = (...params) => method.apply(this, params);
简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。
所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数:
// bad
function divide(a, b, option = false ) {
}
// good
function divide(a, b, { option = false } = {}) {
}
不要在函数体内使用 arguments
变量,使用 rest
运算符(...
)代替。:
// bad
function concatenateAll() {
const args = Array.prototype.slice.call(arguments);
return args.join('');
}
// good
function concatenateAll(...args) {
return args.join('');
}
使用默认值语法设置函数参数的默认值。
// bad
function handleThings(opts) {
opts = opts || {};
}
// good
function handleThings(opts = {}) {
// ...
}
注意区分 Object
和 Map
,只有模拟现实世界的实体对象时,才使用 Object
。如果只是需要key: value
的数据结构,使用 Map
结构。因为 Map
有内建的遍历机制:
let map = new Map(arr);
for (let key of map.keys()) {
console.log(key);
}
for (let value of map.values()) {
console.log(value);
}
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
总是用 Class
,取代需要 prototype
的操作:
// bad
function Queue(contents = []) {
this._queue = [...contents];
}
Queue.prototype.pop = function() {
const value = this._queue[0];
this._queue.splice(0, 1);
return value;
}
// good
class Queue {
constructor(contents = []) {
this._queue = [...contents];
}
pop() {
const value = this._queue[0];
this._queue.splice(0, 1);
return value;
}
}
使用extends
实现继承,不会有破坏instanceof
运算的危险:
// bad
const inherits = require('inherits');
function PeekableQueue(contents) {
Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek = function() {
return this._queue[0];
}
// good
class PeekableQueue extends Queue {
peek() {
return this._queue[0];
}
}
Module
语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import
取代require
:
// bad
const moduleA = require('moduleA');
const func1 = moduleA.func1;
const func2 = moduleA.func2;
// good
import { func1, func2 } from 'moduleA';
使用export
取代module.exports
:
// commonJS的写法
var React = require('react');
var Breadcrumbs = React.createClass({
render() {
return <nav />;
}
});
module.exports = Breadcrumbs;
// ES6的写法
import React from 'react';
class Breadcrumbs extends React.Component {
render() {
return <nav />;
}
};
export default Breadcrumbs;
如果模块只有一个输出值,就使用export default
,如果模块有多个输出值,就不使用export default
,export default
与普通的export
不要同时使用。
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default):
// bad
import * as myObject from './importModule';
// good
import myObject from './importModule';
如果模块默认输出一个函数,函数名的首字母应该小写:
function makeStyleGuide() {
}
export default makeStyleGuide;
如果模块默认输出一个对象,对象名的首字母应该大写:
const StyleGuide = {
es6: {
}
};
export default StyleGuide;
ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。