ECMAScript 2015标准给JavaScript语言带来最大改变应该是增加了类(Class
)和模块(Module
)机制。在早期的JavaScript语言中,我们会使用原型链(prototype
)实现基于对象的继承并使用new
关键字来创建新对象。ES6中引入了类的概念,虽然ES6中的类只是一个语法糖,但它让原型继续语法结构更加清晰,也更加接近面向对象编程的写法。在ES6之前主要有,主要有CommonJS和AMD两种模块规范,但这两种规范都由开源社区制定,而ES6中引入了模块(Module)体系,从语言层在实现了模块机制,为JavaScript开发大型的、复杂的项目扫清了障碍。
1. 类(Class
)
传统面向对象语言可以基于类或接口实现继承,而JavaScript不同,只能基于原型实现继承。为了弥补这一缺点,ES6 中新增了类,通过class
关键字,你可以实现传统面向对象的继承。class可以认为是对象的模板,它只是一种语法糖,其本质还是原型链继承。
1.1 类定义
类实际上是一个特殊的函数。函数有函数声明
和函数表达式
两种,同样类定义也有类声明
和类表达式
两种。
类声明
类声明的语法结构如下:
// 类语法结构 class name [extends] { // 类体 }
我们可以像下面这样定义一个Polygon
类:
class Polygon { constructor(height, width) { this.height = height; this.width = width; } }
在定义函数时,会有函数变量的提升,我们可以先引用函数然后在定义函数。而类声明不同,必须先定义类然后再引用,否则会引发ReferenceError异常:
var p = new Polygon(); // ReferenceError class Polygon {}
类表达式
类表达式语法结构如下:
// 类表达式语法结构 var MyClass = class [className] [extends] { // 类体 };
在类表达式中,类名是可有可无的。如果定义了类名,则该类名只有在类体内部才能访问到:
// 匿名的 var Polygon = class { constructor(height, width) { this.height = height; this.width = width; } }; // 命名的 var Polygon = class Polygon { constructor(height, width) { this.height = height; this.width = width; } };
类表达式和类声明一样,也不会有变量提升的现象。
1.2 类体与类方法定义
在定义类时,类的成员要定义在一对花括号{}
中,花括号中的代码和花括号本身构成了类体
。
类成员包括类构造器和类方法,而类方法又分为静态方法和实例方法。
严格模式
类体中的代码都强制在严格模式中执行。
构造器
构造器即构造函数,是一个特殊的类方法,用于创建和初始化对象。一个类只能拥有一个名为constructor
的方法,该方法即为构造器。未实现该方法时,会抛出SyntaxError异常。
在子类的构造器中可以使用super
关键字调用父类的构造器。
方法定义
定义类时,支持ES6新增的对象方法简写形式:
class Polygon { constructor(height, width) { this.height = height; this.width = width; } get area() { return this.calcArea() } calcArea() { return this.height * this.width; } } const square = new Polygon(10, 10); // 100 console.log(square.area);
静态方法
静态方法是指那些不需要对类进行实例化,通过类名就可以直接访问的方法,但静态方法不能在对象实例中调用。静态方法经常用来作为工具函数调用。定义类时,可以通过static
关键字用来定义静态方法。
class Point { constructor(x, y) { this.x = x; this.y = y; } static distance(a, b) { const dx = a.x - b.x; const dy = a.y - b.y; return Math.sqrt(dx*dx + dy*dy); } } const p1 = new Point(5, 5); const p2 = new Point(10, 10); console.log(Point.distance(p1, p2));
1.3 extends
与子类定义
在传统的面向对象语言中,可通过继承来实现类复用。在ES6规范的类定义中,可以通过extends
关键字在类声明或类表达式中来创建一个继承了某个类的子类。
class Animal { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a noise.'); } } class Dog extends Animal { speak() { console.log(this.name + ' barks.'); } } var d = new Dog('Mitzie'); // 'Mitzie barks.' d.speak();
extends
关键字同样适用于传统的原型继承的"类"
中:
function Animal (name) { this.name = name; } Animal.prototype.speak = function () { console.log(this.name + ' makes a noise.'); } class Dog extends Animal { speak() { super.speak(); console.log(this.name + ' barks.'); } } var d = new Dog('Mitzie'); d.speak();
但是,extends
关键字不能用于非构造对象中。如果要创建的类继承自某个一般对象(非构造对象),要使用Object.setPrototypeOf()
:
var Animal = { speak() { console.log(this.name + ' makes a noise.'); } }; class Dog { constructor(name) { this.name = name; } speak() { super.speak(); console.log(this.name + ' barks.'); } } Object.setPrototypeOf(Dog.prototype, Animal); var d = new Dog('Mitzie'); d.speak();
1.4 Symbol.species
与子构造器重写
Symbol.species
属性用于返回创建派生类对象的构造函数。
如,现在MyArray
继承自Array
对象,在调用.map()
等实例方法时,我们希望Array
的实例方法而不是返回MyArray
的实例方法。这时可以使用Symbol.species
重写子类的构造方法:
class MyArray extends Array { // 重写 species 为父 Array 的构造器 static get [Symbol.species]() { return Array; } } var a = new MyArray(1,2,3); var mapped = a.map(x => x * x); console.log(mapped instanceof MyArray); // false console.log(mapped instanceof Array); // true
1.5 使用super
引用父类
通过super
关键字,可以调用其父类的构造器或者类方法:
class Cat { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a noise.'); } } class Lion extends Cat { speak() { super.speak(); console.log(this.name + ' roars.'); } }
1.6 Mix-ins
多类混入
一个 ECMAScript 类只能有一个父类,所以想要实现多重继承的是不可能的,子承的只能类继父类提供的功能。
抽象子类或Mix-ins
是类的模板,可以通过将父类作为输入且将其子类作为输出的函数来实现Mix-ins
:
var calculatorMixin = Base => class extends Base { calc() { } }; var randomizerMixin = Base => class extends Base { randomize() { } };
使用Mix-ins
的类可以像下面这样写:
class Foo { } class Bar extends calculatorMixin(randomizerMixin(Foo)) { }
2. 模块(Module
)
在开发大型、复杂应用时,必不可少的就是模块化。通过模块机制,可以将一个大型的程序拆分成多个小型的模块,再通过一定的规则将多个子模块组装起来。
在ES6 之前,主要有开源社区制定的CommonJS
和AMD
两种模块规划。而在,ES6中通过import
和export
两处关键字在语言面实现了模块规范。
2.1 ES6
的模块机制
ECMAScript 2015
基于export
和import
,定义了模块的导出和导入规范,从而在语言标准层面实现了模块机制。该标准的目标是创建一种能够兼容CommoneJS
和AMD
两标准的规范,即可以像CommoneJS
一样语法简洁、使用单一的接口且支持循环依赖,又可以像AMD
支持异步加载和可配置的模块加载机制。
该模块规范由以下两部分组成:
- 声明语法(定义引入与导出)
- 编程式加载接口(API):用于配置如何加载模块和按条件加载模块
ES6
的模块规范有以下特点:
- 简洁的语法。语法将比
CommoneJS
更简单,只使用export
和import
实现模块的导出和导入- 使用
export
关键字定义导出对象,这个关键字可以无限次使用 - 使用
import
关键字引入导入对象,这个关键字可导入任意数量的模块
- 使用
- 模块结构可以做静态分析。这使得在编译时就能确定模块的依赖关系,以及输入和输出的变量
- 模块支持异步加载
- 为加载模块提供编程支持,可以按需加载
- 比
CommonJS
更优秀的循环依赖处理
2.2 export
与模块导出
export
语法声明用于导出函数、对象、指定文件(或模块)的原始值。export
有两种模块导出方式:命名式导出(名称导出)和定义式导出(默认导出),命名式导出每个模块可以多个,而默认导出每个模块仅一个。
export
可能会有以下几种形式的语法结构:
export { name1, name2, …, nameN }; export { variable1 as name1, variable2 as name2, …, nameN }; export let name1, name2, …, nameN; // 也可以是 var export let name1 = …, name2 = …, …, nameN; // 也可以是 var, const export default expression; export default function (…) { … } // 也可以是 class, function* export default function name1(…) { … } // 也可以是 class, function* export { name1 as default, … }; export * from …; export { name1, name2, …, nameN } from …; export { import1 as name1, import2 as name2, …, nameN } from …;
命名式导出
模块可以通过export
前缀关键词声明导出对象,导出对象可以是多个。这些导出对象用名称进行区分,称之为命名式导出
。
export { myFunction }; // 导出一个已定义的函数 export const foo = Math.sqrt(2); // 导出一个常量
我们可以使用*
和from
关键字来实现的模块的继承:
export * from 'article';
导出成员与重命名
模块导出时,可以指定模块的导出成员。导出成员可以认为是类中的公有对象,而非导出成员可以认为是类中的私有对象:
var name = 'IT笔录'; var domain = 'http://itbilu.com'; export {name, domain};
模块导出时,可以使用as
关键字对导出成员进行重命名:
var name = 'IT笔录'; var domain = 'http://itbilu.com'; export {name as siteName, domain};
默认导出
默认导出
也被称做定义式导出
。命名式导出可以导出多个值,但在在import
引用时,也要使用相同的名称来引用相应的值。而默认导出每个导出只有一个单一值,这个输出可以是一个函数、类或其它类型的值,这样在模块import
导入时也会很容易引用。
export default function() {}; // 可以导出一个函数 export default class(){}; // 也可以出一个类
默认导出可以理解为另一种形式的命名导出,默认导出可以认为是使用了default
名称的命名导出。
下面两种导出方式是等价的:
const D = 123; export default D; export { D as default };
2.3 import
与模块导入
import
语法声明用于从已导出的模块、脚本中导入函数、对象、指定文件(或模块)的原始值。import
模块导入与export
模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和定义式导入(默认导入)。
import
可能会有以下几种形式的导入语法:
import defaultMember from "module-name"; import * as name from "module-name"; import { member } from "module-name"; import { member as alias } from "module-name"; import { member1 , member2 } from "module-name"; import { member1 , member2 as alias2 , [...] } from "module-name"; import defaultMember, { member [ , [...] ] } from "module-name"; import defaultMember, * as name from "module-name"; import "module-name";
命名式导入
与导出命名类似,在模块导入时也可以指定导入模块的名称,并将这些成员插入到当作用域中。导入时,可以导入单个成员或多个成员:
import {myMember} from "my-module"; import {foo, bar} from "my-module";
通过*
符号,可以导入模块中的全部属性和方法。当导入模块的全部导出内容时,就是将导出模块(如:'my-module.js'
)所有的导出内容绑定,并插入到当前模块('myModule')的作用域中:
import * as myModule from "my-module";
导入成员与重命名
导入模块对象时,也可以使用as
对导入成员重命名,以方便在当前模块内使用:
import {reallyReallyLongModuleMemberName as shortName} from "my-module";
导入多个成员时,同样可以使用别名:
import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";
默认导入
在模块导出时,可能会存在默认导出。同样的,在导入时可以使用import
指令导入这些默认值。
直接导入默认值:
import myDefault from "my-module";
也可以在命名空间导入和名称导入中,同时使用默认导入:
import myDefault, * as myModule from "my-module"; // myModule 做为命名空间使用
或
import myDefault, {foo, bar} from "my-module"; // 指定成员导入