本文为阮一峰大神的《ECMAScript 6 入门》的个人版提纯!
babel
babel负责将JS高级语法转义,使之能被各种浏览器所执行。其使用步骤如下:
- 编写配置文件
.babelrc
此文件存于根目录,基本格式如下:
{
"presets": [],
"plugins": []
}
- 选定转码规则
presets
字段设定转码规则,官方提供以下的规则集:
# 最新转码规则
$ npm install --save-dev babel-preset-latest
# react 转码规则
$ npm install --save-dev babel-preset-react
# 不同阶段语法提案的转码规则(共有4个阶段),选装一个
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3
- 将这些规则加入
.babelrc
:
{
"presets": [
"latest",
"react",
"stage-2"
],
"plugins": []
}
命令行转码babel-cli
其使用方法如下:
// 安装
$ npm install --save-dev babel-cli
//改写package.json
{
// ...
"devDependencies": {
"babel-cli": "^6.0.0"
},
"scripts": {
"build": "babel src -d lib"
},
}
//转码命令
$ npm run build
babel-register
babel-register
模块改写require
命令,为它加上一个钩子。此后,每当使用require加载.js
、.jsx
、.es
和.es6
后缀名的文件,就会先用 Babel 进行转码。由于它是实时转码,所以只适合在开发环境使用。
//自动对index.js转码
require("babel-register")
require("./index.js")
- 如果某些代码需要调用 Babel 的 API 进行转码,就要使用
babel-core
模块。 - Babel 默认只转换新的 JavaScript 句法,而不转换新的 API。举例来说,ES6 在Array对象上新增了Array.from方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用
babel-polyfill
。
let和const
-
let
:创造了块级作用域,每个“块”中,变量不允许重名,不同的“块”中,同名变量不会相互污染;先声明后调用。 -
*do
表达式使块级作用域产生返回值
let x = do {
let t = f()
t * t + 1
}
-
const
:保存的地址(指针)不得修改,值可以修改;
var a = 1
window.a // 1
let b = 1
window.b // undefined
解构复制
- 数组、对象、字符串的解构复制
let [a, b, c] = [1, 2, 3];
let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
let {length : len} = 'hello';
len // 5
- 如果
=
右边不是可遍历解构(数组等),即不具有Iterator接口,则不可解构复制! - 提取JSON数据
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
}
let { id, status, data: number } = jsonData;
- 函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
}) {
// ... do stuff
}//避免了let a = this.a || ’a‘这种写法
- 通过
for...of
遍历Map
for (let [key, value] of map) {}
for (let [key] of map) {}//获取键名
for (let [value] of map) {}//获取键值
字符串的扩展
- ES6 为字符串添加了遍历器接口,使得字符串可以被
for...of
循环遍历。
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
-
includes()
,startsWith()
,endsWith()
let s = 'Hello world!'
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
-
repeat()
、padStart()
、padEnd()
- 模板
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`)//反引号(`)和$标识
函数的扩展
- 新增函数参数的默认值,且为单独作用域。一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
上面代码中,函数f
调用时,参数y = x
形成一个单独的作用域。这个作用域里面,变量x
本身没有定义,所以指向外层的全局变量x
。函数调用时,函数体内部的局部变量x
影响不到默认值变量x
。
如果此时,全局变量x
不存在,就会报错。
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo() // 3
x // 1
var x = 1;
function foo(x, y = function() { x = 2; }) {
x = 3;//指向第一个变量x
y();
console.log(x);
}
foo() // 2
x // 1
上面代码中,函数foo
的参数形成一个单独作用域。这个作用域里面,首先声明了变量x
,然后声明了变量y
,y
的默认值是一个匿名函数。这个匿名函数内部的变量x
,指向同一个作用域的第一个参数x
。函数foo
内部又声明了一个内部变量x
,该变量与第一个参数x
由于不是同一个作用域,所以不是同一个变量,因此执行y
后,内部变量x
和外部全局变量x
的值都没变。如果将var x = 3
的var
去除,函数foo
的内部变量x
就指向第一个参数x
,与匿名函数内部的x
是一致的,所以最后输出的就是2
,而外层的全局变量x
依然不受影响
-
rest
参数(形式为...变量名),用于获取不确定个数的全部参数,舍弃arguments
对象(arguments
为类数组的对象,不是真正的数组)
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
-
=>
箭头函数可以让this
指向固定。函数体内的this
对象,就是定义时所在的对象(与父作用域共享this
上下文,使用时即寻找父作用域this
),而不是运行时所在的对象。this
对象的指向是可变的,但是在箭头函数中,它是固定的。
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
上面代码中,箭头函数的this
绑定定义时所在的作用域(即Timer
函数),后面的普通函数的this
指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1
被更新了 3 次,而timer.s2
一次都没更新。
this
指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数;也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
-
::
,用于函数绑定,该运算符会自动将左边的对象,作为上下文环境(即this
对象),绑定到右边的函数上面。其结果是对象,可以链式绑定。 - 尾调用,指某个函数的最后一步是调用另一个函数。
function f(x){
return g(x);
}
- 尾调用优化,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧。
数组的扩展
-
...
将一个数组转为用逗号分隔的参数序列,即用于展开数组。
扩展运算符内部调用的是Iterator
接口,因此只要具有Iterator
接口的对象,都可以使用扩展运算符(Map,Generator等)
const arr = [
...(x > 0 ? ['a'] : []),//如果扩展运算符后面是一个空数组,则不产生任何效果。
'b',
]
- 复制数组
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。
const a1 = [1, 2];
// 写法一
const a2 = [...a1]
// 写法二
const [...a2] = a1
- 合并数组
[1, 2, ...more]
var arr1 = ['a', 'b']
var arr2 = ['c']
var arr3 = ['d', 'e']
[...arr1, ...arr2, ...arr3]
-
Array.from()
将类似数组的对象(任何有length属性的对象,如DOM
操作返回的NodeList
集合,以及函数内部的arguments
对象)和可遍历的对象转化为Array
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function (p) {
console.log(p);
})
// arguments对象
function foo() {
var args = Array.from(arguments);
}
-
Array.of()
将一组值,转换为数组 -
copyWithin()
在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。 -
find()
用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true
的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
-
findIndex()
返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
-
fill()
用于初始化数组 -
entries()
keys()
values()
用于遍历数组
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
-
includes()
取代indexOf()
对象的扩展
- 快速写法
const foo = 'bar'
const baz = {foo}
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo}
- 函数的
name
属性,返回函数名 -
Object.is()
,用于比较两个值是否相等 -
Object.assign(target, source1, source2)
将源对象(source
)的所有可枚举属性,复制到目标对象(target
),后面的覆盖前面的同名属性 -
Object.assign()
浅拷贝,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
const obj1 = {a: {b: 1}}
const obj2 = Object.assign({}, obj1)
obj1.a.b = 2
obj2.a.b // 2
-
Object.assign()
对于数组的处理,是将其看成对象
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
-
Object.assign()
为对象添加属性和方法
class Point {
constructor(x, y) {
Object.assign(this, {x, y})
}
}
Object.assign(SomeClass.prototype, {someMethod(arg1, arg2) {},
anotherMethod() {}
})
-
Object.assign({}, origin)
克隆对象,合并对象 - 尽量不要用
for...in
循环,而用Object.keys()
代替,为了规避掉继承的属性 - 属性的遍历
-
for...in
,循环遍历对象自身的和继承的可枚举属性(不含Symbol
属性)。 -
Object.keys(obj)
,返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol
属性)的键名。 -
Object.getOwnPropertyNames(obj)
,返回一个数组,包含对象自身的所有属性(不含Symbol
属性,但是包括不可枚举属性)的键名。 -
Object.getOwnPropertySymbols(obj)
,返回一个数组,包含对象自身的所有Symbol
属性的键名。 -
Reflect.ownKeys(obj)
,返回一个数组,包含对象自身的所有键名,不管键名是Symbol
或字符串,也不管是否可枚举。
//首先遍历所有数值键,按照数值升序排列。
//其次遍历所有字符串键,按照加入时间升序排列。
//最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
-
Object.setPrototypeOf()
用于设置一个对象的prototype
对象,返回参数对象本身 -
Object.setPrototypeOf()
用于读取一个对象的原型对象 -
super
关键字,指向当前对象的原型对象
const proto = {
foo: 'hello'
}
const obj = {
find() {
return super.foo;
}
}
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
-
Object.keys()
,Object.values()
,Object.entries()
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 }
for (let key of keys(obj)) {
console.log(key); // 'a', 'b', 'c'
}
for (let value of values(obj)) {
console.log(value); // 1, 2, 3
}
for (let [key, value] of entries(obj)) {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
- 对象的解构赋值,用
...
,用于从一个对象取值,相当于将所有可遍历的、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面,也可用于生成新对象,但是拷贝的是这个值的引用,不是新副本,与Object.assign()相同
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 },等同于Object.assign()
- 深拷贝
JSON.parse(JSON.stringify(initalObj))
- 递归
function deepClone(initalObj) {
var obj = {};
for (var i in initalObj) {
var prop = initalObj[i];
// 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
if(prop === obj) {
continue;
}
if (typeof prop === 'object') {
obj[i] = (prop.constructor === Array) ? [] : {};
arguments.callee(prop, obj[i]);
} else {
obj[i] = prop;
}
}
return obj;
}
Symbol
- 一种防止属性名冲突的机制,其表示独一无二的值,是一种类似于字符串的数据类型
-
Symbol
值可以显式转为字符串 - 每一个
Symbol
值都是不相等的,这意味着Symbol
值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖 -
Symbol
值作为对象属性名时,不能用点运算符(点运算符后面总是字符串) -
Object.getOwnPropertySymbols
,返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。 - 使用时需要放在
[ ]
中
et s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123)
-
Symbol
作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。 -
Reflect.ownKeys
,可以返回所有类型的键名,包括常规键名和 Symbol 键名。 -
Symbol.for()
,接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol
值。如果有,就返回这个Symbol
值,否则就新建并返回一个以该字符串为名称的Symbol
值。 -
Symbol.keyFor()
,返回一个已登记的Symbol
类型值的key
- Symbol的11 个内置函数
1Symbol.hasInstance
2Symbol.isConcatSpreadable
3Symbol.species
4Symbol.match
5Symbol.replace
6Symbol.search
7Symbol.split
8Symbol.iterator
9Symbol.toPrimitive
10Symbol.toStringTag
11Symbol.unscopables
Proxy
- Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
-
var proxy = new Proxy(target, handler)
,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为 - 要使得
Proxy
起作用,必须针对Proxy
实例进行操作,而不是针对目标对象进行操作
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
- Proxy 实例也可以作为其他对象的原型对象
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
- 13种拦截操作
Reflect
- 将
Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。也就是说,从Reflect
对象上可以拿到语言内部的方法 - 修改某些
Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
- 让
Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为 - Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为
- 13个静态方法一一对应proxy的13种拦截行为
Set 和 Map
- Set类似于数组,成员无重复,即可以用来数组去重
// 去除数组的重复成员
[...new Set(array)]
-
add(value)
:添加某个值,返回Set
结构本身 -
delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功 -
has(value)
:返回一个布尔值,表示该值是否为Set
的成员 -
clear()
:清除所有成员,没有返回值 -
Array.from
方法可以将Set
结构转为数组 - Set的遍历操作
1keys()
:返回键名的遍历器,keys方法和values方法的行为完全一致
2values()
:返回键值的遍历器,keys方法和values方法的行为完全一致
3entries()
:返回键值对的遍历器
4forEach()
:使用回调函数遍历每个成员 -
WeakSet
与Set
类似,成员不重复,但只能是对象。WeakSet
适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在WeakSet
里面的引用就会自动消失;WeakSet
的成员是不适合引用的,因为它会随时消失。另外,由于WeakSet
内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定WeakSet
不可遍历。 - Map,类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
- Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
- 作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
- 任何具有
Iterator
接口、且每个成员都是一个双元素的数组
的数据结构(详见《Iterator》一章)都可以当作Map
构造函数的参数
1size
属性
2set(key, value)
3get(key)
4has(key)
,返回一个布尔值
5delete(key)
,返回true
6clear()
,清除所有对象,无返回值 - 遍历方法,Map 的遍历顺序就是插入顺序
1keys()
:返回键名的遍历器。
2values()
:返回键值的遍历器。
3entries()
:返回所有成员的遍历器。
4forEach()
:遍历 Map 的所有成员。 - Map 结构转为数组结构,比较快速的方法是使用扩展运算符
...
- WeakMap 的用途,我们将这个状态作为键值放在 WeakMap 里,对应的键名就是myElement。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
Iterator
- 为各种数据结构提供一个统一的访问的接口
- 数据结构成员按某种次序排列
- ES6创造了
for...of
,Iterator供其消费,这种数据结构为可遍历的(Symbol.iterator
属性,其本身是个遍历器生成函数,执行后返回遍历器) - 其本质是指针,每次调用返回
{value:*****;done: *****}
原生具备Iterator:
1Array
2Map
3Set
4String
5TypedArray
6 函数的arguments
对象
7NodeList
对象
Promise
- 一个保存异步结果的容器
- 两种状态:
pedding => resolved
;pedding => rejected
,发生这两种情况时状态立刻固化,回调函数立刻执行,与事件监听不同 - 如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署
Promise
更好的选择。 - Promise实例
const promise = new Promise((resolve, reject) => {
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
})
-
Promise
新建后立刻执行,then
回调本轮循环最后执行,晚于本轮事件循环的同步任务 - 下面代码中,p1是一个
Promise
,3秒之后变为rejected
。p2的状态在1秒之后改变,resolve
方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,导致触发catch方法指定的回调函数
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
- then()方法,返回的是一个新的Promise实例,可以手工指定(
return
)需要返回的新的Promise实例,若不指定,则默认返回上一个Promise实例 - catch()方法,Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获,并且在下一轮循环中抛出
- Promise.all()方法
- Promise.race()方法
- Promise.resolve()方法,将现有对象转为Promise对象
1 参数是一个Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例
2 参数是一个thenable对象,Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法
3 参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为resolved。
4 不带有任何参数
Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的Promise对象。所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。 - done()
- finally()
- Promise.try(),统一同步异步写法,且让同步的同步执行,异步的异步执行,bluebird提供了这个方法
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
Generator
- Generator 函数是一个状态机,封装了多个内部状态。
- 执行 Generator 函数会返回一个遍历器对象,其可以依次遍历 Generator 函数内部的每一个状态。
- 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,代表 Generator 函数的内部指针,也就是遍历器对象(Iterator Object),指向该函数的内部状态。必须调用遍历器对象的next方法,使得指针移向下一个状态。
- yield表达式就是暂停标志
- 任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1
yield 2
yield 3
}
[...myIterable] // [1, 2, 3]
- next()方法,可以带一个参数,该参数就会被当作上一个yield表达式的返回值。而yield表达式本身没有返回值,或者说总是返回undefined
- Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为
- 由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数
- for...of可以直接遍历Generator,且此时不再需要调用next方法。
- 原生对象加上加上遍历器接口就可以用for...of遍历
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
- Generator.prototype.throw(),可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
- 一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
- Generator.prototype.return(),可以返回给定的值,并且终结遍历 Generator 函数。
- next()、throw()、return() 的共同点:
1 next()是将yield表达式替换成一个值。(undefined)
2 throw()是将yield表达式替换成一个throw语句。
3 return()是将yield表达式替换成一个return语句。 - yield* 表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
- 如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象
- 任何数据结构只要有 Iterator 接口,就可以被yield*遍历
let read = (function* () {
yield 'hello'
yield* 'hello'
})()
read.next().value // "hello"
read.next().value // "h"
- yield*命令可以很方便地取出嵌套数组的所有成员
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
- 让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
- Generator 函数是 ES6 对协程的实现,可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表示式交换控制权。
- 利用 Generator 函数,可以在任意对象上部署 Iterator 接口
- Generator 可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。
- 协程
1 第一步,协程A开始执行
2 第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
3 第三步,(一段时间后)协程B交还执行权。
4 第四步,协程A恢复执行。
function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
上面代码的函数asyncJob是一个协程,它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。
- Thunk 函数的含义,编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。在JS中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);//函数的2次调用
- Thunk的意义在于Generator的自动执行,通过回调函数
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);//Thunk函数
}
next()
}
function* g() {
// ...
}
run(g)
- co,co函数返回一个Promise对象,因此可以用then方法添加回调函数,用于Generator的自动执行,使用 co 的前提条件是,yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co。
function run(gen){
var iterator = gen()
function next(data){
var result = iterator.next(data)
if (result.done) return result.value
result.value.then(function(data){//Promise
next(data)
});
}
next()
}
run(gen)
- co处理并发的异步操作,要把并发的操作都放在数组或对象里面,跟在yield语句后面。
// 数组的写法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
// 对象的写法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
}
console.log(res)
}).catch(onerror)
async
- 内置执行器,Generator 函数需要next(),或CO
- await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
async function timeout(ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
}
asyncPrint('hello world', 50)
- 返回值是 Promise,可以用Then()
- async函数内部return语句返回的值,会成为then方法回调函数的参数
- async函数执行立刻返回Promise,其状态必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
- 前一个异步操作失败,也不会中断后面的异步操作,有下面两种操作。
async function f() {
try {
await Promise.reject('出错了')
} catch(e) {
}
return await Promise.resolve('hello world')
}
f()
.then(v => console.log(v))
// hello world
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world
- 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()])
// 写法二
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise
- async 函数的实现原理,将 Generator 函数和自动执行器,包装在一个函数里。
- for await...of,用于遍历异步的Iterator 接口
单线程和异步
浏览器只能做一件事——DOM渲染,为了避免DOM重复渲染,浏览器只提供了一个线程,即是一个单线程实例,类似于强计算、重IO的操作将使线程阻塞,视觉效果即为“卡住了”,因此就有了异步的出现。
异步是解决单线程的唯一方案,但是对于coder而言,callback函数执行顺序并不可控(定时器函数、Ajax请求等),callback函数不容易模块化且可读性十分差。异步的原理如下:
event-loop
- 同步代码直接执行;
- 异步代码的回调函数先放在异步队列中(将要执行的时候放入)
- 待同步代码执行完毕后,轮询执行异步队列的函数。
JQuery Deferred
//Deferred使用方法
function waitHandle(){
var dtd = $.Deferred()//生成Deferred对象实例
var wait = function( dtd ){
//对dtd对象深加工,即异步操作
//异步操作后
if(success){
dtd.resolve()
} else {
dtd.reject()
}
return dtd
}
return wait( dtd )//将深加工后的dtd对象返回
}
//调用
var w = waitHandle()
w
.then(func1, func2)//成功和失败的回调
.then(func3, func4)//成功和失败的回调
dtd的API分为两类:resolve、reject
和then、done、fail
,由于这两类方法分别表示因果,因此需要一定的手段将其强制分离使用,但是上文的dtd对象并不能够(可以直接使用dtd.reject()
修改resolve
状态),因此有了Promise对象,其仅仅提供then等结果方法,避免了外部暴力修改dtd状态的可能。将上文代码段进行如下修改:
......
return dtd.promise()//返回promise对象
}
return wait( dtd )
}
......
class
- 用以取代prototype,构造方法为constructor方法,也用new生成对象实例,实际上为ES5的语法糖
class A {
}
typeof A //"function"
A === A.prototype.constructor //true
a.__proto__ === A.prototype //true
- 类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。
class Point {
constructor(){
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
})
- constructor(),类的默认方法,通过new命令生成对象实例时,自动调用该方法,默认返回实例对象(即this),指定return返回另外一个对象
- 与 ES5 一样,类的所有实例共享一个原型对象
- Class 表达式,下面为一个立即执行的类的实例
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name)
}
}('张三');
person.sayName(); // "张三"
- 私有方法
1 通过把方法移到模块外部
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
2 利用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;
}
// ...
};
- this 的指向,默认指向类的实例,单独使用会引起上下文混乱,可以通过在构造函数中使用bind(this)、剪头函数和proxy绑定this指向
- Class 的静态方法,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,如果静态方法包含this关键字,这个this指的是类,而不是实例。
- Class 的静态属性和实例属性
class MyClass {
(static) myProp = 42;
constructor() {
console.log(this.myProp); // 42
}
}
myProp就是MyClass的实例属性(加上static为静态属性)。在MyClass的实例上,可以读取这个属性,不用必须写在constructor()中,在React中可读性更强
class ReactCounter extends React.Component {
state = {
count: 0
};
}
- new.target 属性,ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。
- extends,对象继承
- super,用来创建父类的this,子类必须在constructor()方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象
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作为函数,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。但是super内部的this指的是子类实例
class A {}
class B extends A {
constructor() {
super();
}
}
- super作为对象,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2,A,指向A.prototype
}
}
let b = new B()
- this指向子类,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性
- ES6 的继承机制是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
- 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。
- Object.getPrototypeOf(),用来从子类上获取父类
- prototype属性和proto属性这两条继承链。
- 1 子类的proto属性,表示构造函数的继承,总是指向父类。
- 2 子类prototype属性的proto属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
- 实例的 proto 属性,通过子类实例的proto.proto属性,可以修改父类实例的行为
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"
- 原生构造函数(用于生成数据结构)的继承
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
ES6先构造父类的this,然后通过子类的this继承父类的行为,这样可以生成构造函数的子类 - Mixin 模式的实现,指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口
JS构造函数
function A(x, y){
this.x = x
this.y = y
}//JS构造函数的写法
A.prototype.add = function(){
return this.x + this.y
}//通过prototype对象扩展构造函数的方法
var a = new A(1, 2)//同样通过new来生成对象实例
继承通过prototype
来实现
function A(){}
function B(){}
B.prototype = new A()
var b = new B() //B继承A
Zepto中的原型源码
(function(window){
var zepto = {}
Z.prototype = $.fn
$.fn = {//通过这个来扩展原型方法
css: function(){},
html: function(){},
......
}
function Z(dom, selector){
var i, len = dom? dom.length: 0
for(i=0; i < len; i++){
this[i] = dom[i]
}
this.length = len
this.selector = selector || ''
}
zepto.Z = function(dom, selector){
return new Z(dom, selector)
}
zepto.init = function(selector){
var slice = Array.prototype.slice
var dom = slice.call(document.quertSelectorAll(selector))//将类数组转为数组
return zepto.Z(dom, selector)
}
var $ = function(selector){
return zepto.init(selector)
}
window.$ = $
})(window)//自执行函数避免全局变量污染
JQuery中的原型实现与Zepto十分形似。
(function(window){
var jQuery = function(selector){
return new jQuery.fn.init(selector)
}
jQuery.fn = {}
var init = jQuery.fn.init = function(selector){
var slice = Array.prototype.slice
var dom = slice.call(document.quertSelectorAll(selector))
var i, len = dom? dom.length: 0
for(i=0; i < len; i++){
this[i] = dom[i]
}
this.length = len
this.selector = selector || ''
}
init.prototype = jQuery.fn
jQuery.fn = {
css: function(){},
html: function(){},
......
}
window.$ = jQuery
})(window)//自执行函数避免全局变量污染
call(),apply()和bind()
- call()和apply()用来改变this,就是调用函数,让它在指定的上下文执行,这样,函数可以访问的作用域就会改变。
- Function对象的方法
- bind()也用于上下文绑定,其新创建一个函数,然后把它的上下文绑定到bind()括号中的参数上,然后将它返回——bind后函数不会执行,而只是返回一个改变了上下文的函数副本,而call和apply是直接执行函数。
if (!function() {}.bind) {
Function.prototype.bind = function(context) {
var self = this
, args = Array.prototype.slice.call(arguments);
return function() {
return self.apply(context, args.slice(1));
}
};
}
装饰器
- 是一个函数,用来修改类的行为,这个函数的第一个参数,就是所要修饰的目标类。
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
上面代码中,@testable就是一个装饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable。testable函数的参数target是MyTestableClass类本身。
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
上面代码中,装饰器testable可以接受参数,这就等于可以修改装饰器的行为
- 装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。
- 可以修饰类的属性
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}//装饰器readonly用来修饰“类”的name方法
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
装饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,即类的实例(这不同于类的修饰,那种情况时target参数指的是类本身);第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。
装饰器的应用场景之一:路由
当项目过大时,API也随着变得更加复杂和难以维护,这时,可以根据应用场景拆分路由,使得路由和应用场景一一对应。
Module
ES6之前,为了应付大型项目的开发,社区制定了对应浏览器端的ADM(CMD)
标准,对应服务器端的CommonJS
标准。
// CommonJS模块
let { stat, exists, readFile } = require('fs')
// 等同于
let _fs = require('fs')
let stat = _fs.stat
let exists = _fs.exists
let readfile = _fs.readfile
上面代码的实质是整体加载fs模块(即加载fs
的所有方法),生成一个对象(_fs
),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
直到ES6出现后,ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS
和 AMD
模块,都只能在运行时确定这些东西。
// ES6模块
import { stat, exists, readFile } from 'fs'
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS
模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
export
-
export
的写法
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
-
export
还可以输出类和方法
export function multiply(x, y) {
return x * y;
}
-
export
输出的变量可以使用as
关键字重命名 - 接口名与模块内部变量之间需要建立一一对应的关系
// 写法一
export var m = 1
// 写法二
var m = 1
export {m}
// 写法三
var n = 1
export {n as m}
import
-
import
写法
// main.js
import {firstName, lastName, year} from './profile.js'
大括号里面的变量名,必须与被导入模块profile.js
对外接口的名称相同。
-
import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。 -
impor
t命令是编译阶段执行的,在代码运行之前,因此其有提升的效果。 - 可以用星号
*
指定一个对象,所有输出值都加载在这个对象上面
整体加载的写法如下:
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
模块整体加载所在的那个对象(上例是circle
),应该是可以静态分析的,所以不允许运行时改变。
-
export default
当import
由export default
导出的模块时,可以任意指定这个模块导出的方法,这时import
命令后面,不使用大括号。一个模块只有一个默认输出。 -
export default
命令其实只是输出一个叫做default
的变量,所以它后面不能跟变量声明语句。
// 错误
export default var a = 1
-
export
与import
的复合写法:
export { foo, bar } from 'my_module'
// 可以简单理解为
import { foo, bar } from 'my_module'
export { foo, bar }
写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
浏览器异步加载
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
上面代码中,<script>
标签打开defer
或async
属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
defer
与async
的区别是:defer
要等到整个页面在内存中正常渲染结束(DOM
结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。
浏览器加载 ES6 模块,也使用<script>
标签,但是要加入type="module"
属性。
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
ES6
模块与CommonJS
模块的差异
-
CommonJS
模块输出的是一个值的拷贝,ES6
模块输出的是值的引用。
CommonJS
模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值:
// lib.js
var counter = 3
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
}
// main.js
var mod = require('./lib')
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
},//输出的counter属性实际上是一个取值器函数
incCounter: incCounter,
}
JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import
有点像 Unix 系统的“符号连接”,原始值变了,import
加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块,ES6 模块输入的变量是活的,完全反应其所在模块内部的变化。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
node中的
import
与export
- Node 对 ES6 模块的处理比较麻烦,因为它有自己的
CommonJS
模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。 - ES6 模块之中,顶层的
this
指向undefined
;CommonJS
模块的顶层this
指向当前模块,这是两者的一个重大差异。 - ES6 模块加载
CommonJS
模块时,CommonJS
模块的输出都定义在module.exports
这个属性上面。Node 的import
命令加载CommonJS
模块,Node 会自动将module.exports
属性,当作模块的默认输出,即等同于export default xxx
。 - ES6 模块加载
CommonJS
模块,不能使用require
命令,而要使用import()
函数。ES6 模块的所有输出接口,会成为输入对象的属性。
循环加载
- CommonJS 模块的循环加载:
CommonJS 的一个模块,就是一个脚本文件。require
命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。以后需要用到这个模块的时候,就会到exports
属性上面取值。即使再次执行require
命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
CommonJS 模块的重要特性是加载时执行,即脚本代码在require
的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。 - ES6 模块的循环加载:
ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量import foo from 'foo'
,那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值,通常可通过函数的返回值解决,因为函数具有提升效果(函数表达式并没有提升效果)。
附录
JS中不常用方法集合
reduce()方法
此为JS高阶方法,为累加器函数,对数组中的元素从左至右分别作用于callbackfn,并返回一个“加工”后的元素。
array1.reduce(callbackfn[, initialValue])
模块化总结
模块化是现代前端架构的方向。从模块化概念诞生之初,AMD首先成为标准(通过require.js库,淘宝开源过CMD);后来紧接着出现了各种前端打包工具,使得后端的common.js标准(一个文件就是一个模块,拥有单独的作用域;普通方式定义的变量、函数、对象都属于该模块内;通过require来加载模块;通过exports和module.exports来暴露模块中的内容)可以被前端所用;直到ES6标准的出现,node端基本支持,但由于浏览器的支持有限,所以需要使用babel
来进行语法转化才可以放心大胆使用。
模块化基本语法如下:
//util.js中:
export default { a: 100}
export function fn1() { alert('1')}
export function fn2() { alert('2')}
import util from './util.js'
import {fn1, fn2} from './util.js'
目前比较火的模块化工具当属webpack,它的功能异常强大,但下面将介绍另一种打包工具,React和Vue都是由它打包的:
rollup
rollup虽然功能单一,但是其把打包做到极致(代码更少、体积更小),因此利于继承和扩展,而webpack则功能强大。
类型判断
作用域
引用传递
- JS中对象是引用传递,非对象(Undefined,Null,Boolean,Number,String)是值传递,但是通过将非对象进行包装,也可以进行引用传递
- Boolean,Number,String有各自的包装对象
null == undefined
#true,传值
[1] == [2]
#false,传递引用
内存泄漏
- 全局变量
a = 10;
//未声明对象。
global.b = 11;
//全局变量引用
这种比较简单的原因,全局变量直接挂在 root 对象上,不会被清除掉。
- 闭包
function out() {
const bigData = new Buffer(100);
inner = function () {
void bigData;
}
}
inner 直接挂在了 root 上,从而导致内存泄漏(bigData 不会释放)。
- 事件监听
对同一个事件重复监听,忘记移除(removeListener),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现。 - 缓存
在使用缓存的时候,得清楚缓存的对象的多少,如果缓存对象非常多,得做限制最大缓存数量处理。还有就是非常占用 CPU 的代码也会导致内存泄漏,服务器在运行的时候,如果有高 CPU 的同步代码,因为Node.js 是单线程的,所以不能处理处理请求,请求堆积导致内存占用过高。