一、Class
1.与传统构造方法对比
传统构造方法:
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 Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var p = new Point(1, 2);
constructor方法,这就是构造方法,而this关键字则代表实例对象。使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
2.Class表达式
示例:
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
需要注意的是,这个类的名字是MyClass而不是Me,Me只在Class的内部代码可用,指代当前类。
如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式:
const MyClass = class { /* ... */ };
立即执行的Class:
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('张三');
person.sayName(); // "张三"
3.私有方法
私有方法是常见需求,但ES6不提供,只能通过变通方法模拟实现。
- 一种做法是在命名上加以区别:在私有方法名前加下划线,表示是私有方法。
- 另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。以下示例中,bar()方法就是一个私有方法。
class Widget {
foo (baz) {
bar.call(this, baz);
}
}
function bar(baz) {
return this.snaf = baz;
}
- 还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
foo(baz) {// 公有方法
this[bar](baz);
}
[bar](baz) {// 私有方法
return this[snaf] = baz;
}
};
4.this的指向
类的方法内部如果含有this,它默认指向类的实例。
但是包含this的方法不能直接使用,否则会报错,解决方法如下:
- 在构造方法中绑定this。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
- 使用箭头函数。
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
};
}
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
- 使用Proxy,获取方法的时候,自动绑定this。
5.严格模式
类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。
6.name属性
name属性总是返回紧跟在class关键字后面的类名。
7.class的继承
1)extends关键字
示例:
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
ColorPoint的父类是Point类。
extends关键字后面可以跟多种类型的值。
class B extends A {}
上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。
三种特殊情况:
- 子类继承Object类
- 不存在任何继承
- 子类继承null
2)super关键字
super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。
super这个关键字,既可以当作函数使用,也可以当作对象使用:
- 作为函数调用时,代表父类的构造函数。
- super作为对象时,指向父类的原型对象。
3)类的prototype属性和__proto__属性
Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链:
(1) 子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2) 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
实例的__proto__属性:子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。
4)Object.getPrototypeOf()
可以用来从子类上获取父类。
Object.getPrototypeOf(ColorPoint) === Point
// true
5)原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些:
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
以前,这些原生构造函数是无法继承的,因为子类无法获得原生构造函数的内部属性。
ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。
下面是一个继承Array的例子:
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
注意,继承Object的子类,有一个行为差异,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为ES6改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6规定Object构造函数会忽略参数。
8.Class的取值函数(getter)和存值函数(setter)
与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
示例:
class MyClass {
constructor() {}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
存值函数和取值函数是设置在属性的descriptor对象上的。
9.Class的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
父类的静态方法,可以被子类继承。
静态方法也是可以从super对象上调用的。
10.Class的静态属性和实例属性
静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性。
写法:
class Foo {}
Foo.prop = 1;
Foo.prop // 1
上面的写法为Foo类定义了一个静态属性prop。目前,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性。
ES7写法:
ES7有一个静态属性的提案,目前Babel转码器支持。这个提案对实例属性和静态属性,都规定了新的写法:
- 类的实例属性:类的实例属性可以用等式,写入类的定义之中。
class MyClass {
myProp = 42;
constructor() {
console.log(this.myProp); // 42
}
}
为了可读性的目的,对于那些在constructor里面已经定义的实例属性,新写法允许直接列出。
class ReactCounter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
state;
}
- 类的静态属性:类的静态属性只要在上面的实例属性写法前面,加上static关键字就可以了。
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myProp); // 42
}
}
11.new.target属性
new是从构造函数生成实例的命令。ES6为new命令引入了一个new.target属性,(在构造函数中)返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。
Class内部调用new.target,返回当前Class。子类继承父类时,new.target会返回子类。
12.Mixin模式的实现
Mixin模式指的是,将多个类的接口“混入”(mix in)另一个类。
二、ES6 修饰器
1.类的修饰
修饰器(Decorator)是一个函数,用来修改类的行为。这是ES7的一个提案,目前Babel转码器已经支持。
修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。
基本上,修饰器的行为就是下面这样:
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
修饰器本质就是编译时执行的函数。
修饰器函数的第一个参数,就是所要修饰的目标类。
2.方法的修饰
示例:
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
此时,修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。
修饰器有注释的作用。从下面代码中,我们一眼就能看出,Person类是可测试的,而name方法是只读和不可枚举的:
@testable
class Person {
@readonly
@nonenumerable
name() { return `${this.first} ${this.last}` }
}
如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。
除了注释,修饰器还能用来类型检查。
3.为什么修饰器不能用于函数?
修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。
4.core-decorators.js
core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。
(1)@autobind
autobind修饰器使得方法中的this对象,绑定原始对象。
(2)@readonly
readonly修饰器使得属性或方法不可写。
(3)@override
override修饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。
(4)@suppressWarnings
suppressWarnings修饰器抑制decorated修饰器导致的console.warn()调用。但是,异步代码发出的调用除外。
4.使用修饰器实现自动发布事件
我们可以使用修饰器,使得对象的方法被调用时,自动发出一个事件。
可使用Postal.js事件“发布/订阅”库。
5.Mixin
在修饰器的基础上,可以实现Mixin模式。所谓Mixin模式,就是对象继承的一种替代方案,中文译为“混入”(mix in),意为在一个对象之中混入另外一个对象的方法。
下面,我们部署一个通用脚本mixins.js,将mixin写成一个修饰器:
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
};
}
然后,就可以使用上面这个修饰器,为类“混入”各种方法。
import { mixins } from './mixins';
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // "foo"
6.Trait
Trait也是一种修饰器,效果与Mixin类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。
traits-decorator第三方模块,这个模块提供的traits修饰器,不仅可以接受对象,还可以接受ES6类作为参数。
Trait不允许“混入”同名方法。
7.Babel转码器的支持
目前,Babel转码器已经支持Decorator。
首先,安装babel-core和babel-preset-stage-0。
然后,设置配置文件.babelrc。
{
"plugins": ["transform-decorators"]
}
这时,Babel就可以对Decorator转码了。
三、Module
1.简介
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
2.优点
- 由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
- 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
- 将来浏览器的新 API 就能用模块格式提供,不再必要做成全局变量或者navigator对象的属性。
- 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
3.严格模式
ES6 的模块自动采用严格模式。
严格模式主要有以下限制:
变量必须声明后再使用
函数的参数不能有同名属性,否则报错
不能使用with语句
不能对只读属性赋值,否则报错
不能使用前缀0表示八进制数,否则报错
不能删除不可删除的属性,否则报错
不能删除变量delete prop,会报错,只能删除属性delete global[prop]
eval不会在它的外层作用域引入变量
eval和arguments不能被重新赋值
arguments不会自动反映函数参数的变化
不能使用arguments.callee
不能使用arguments.caller
禁止this指向全局对象
不能使用fn.caller和fn.arguments获取函数调用的堆栈
增加了保留字(比如protected、static和interface)
4.export和import
1)export 命令
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
正确的写法:
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
2)import 命令
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。
import命令具有提升效果,会提升到整个模块的头部,首先执行。本质是,import命令是编译阶段执行的,在代码运行之前。
如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。
3)export default 命令
为模块指定默认输出。
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字,这时import命令后面,不使用大括号。
一个模块只能有一个默认输出,因此export default命令只能使用一次。
本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。
4)export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。
export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, boo};
5)模块的整体加载
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
5.模块的继承
例如,有一个circleplus模块,继承了circle模块:
// circleplus.js
export * from 'circle';//继承
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
6.ES6模块加载的实质
ES6模块输出的是值的引用。
ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个动态的只读引用。等到真的需要用到时,再到模块里面去取值,换句话说,ES6的输入有点像Unix系统的“符号连接”,原始值变了,import输入的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
7.浏览器的模块加载
浏览器使用 ES6 模块的语法如下。
<script type="module" src="foo.js"></script>
浏览器对于带有type="module"的
对于外部的模块脚本(上例是foo.js),有几点需要注意。
- 该脚本自动采用严格模块。
- 该脚本内部的顶层变量,都只在该脚本内部有效,外部不可见。
- 该脚本内部的顶层的this关键字,返回undefined,而不是指向window。
8.循环加载
“循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。
通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。
但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖b,b依赖c,c又依赖a这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。
对于JavaScript语言来说,目前最常见的两种模块格式CommonJS和ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。
1)CommonJS模块的加载原理
CommonJS的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
2)CommonJS模块的循环加载
CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
CommonJS输入的是被输出值的拷贝,不是引用。
另外,由于CommonJS模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。
3)ES6模块的循环加载
ES6模块是动态引用,如果使用import从一个模块加载变量(即import foo from ‘foo’),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
9.import()动态加载
动态加载:require到底加载哪一个模块,只有运行时才知道。import语句做不到这一点。
因此,有一个提案,建议引入import()函数,完成动态加载。
import(specifier)
上面代码中,import函数的参数specifier,指定所要加载的模块的位置。import语句能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。
import()返回一个 Promise 对象。
import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。
10.ES6模块的转码
除了Babel可以用来转码之外,还有以下两个方法,也可以用来转码。
- ES6 module transpiler
- SystemJS