你不可不知的ES6

本文为阮一峰大神的《ECMAScript 6 入门》的个人版提纯!

babel

babel负责将JS高级语法转义,使之能被各种浏览器所执行。其使用步骤如下:

  1. 编写配置文件.babelrc
    此文件存于根目录,基本格式如下:
{
  "presets": [],
  "plugins": []
}
  1. 选定转码规则
    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
  1. 将这些规则加入.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 ${basket.count} items
   in your basket, ${basket.onSale}
  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,然后声明了变量yy的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。如果将var x = 3var去除,函数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()代替,为了规避掉继承的属性
  • 属性的遍历
  1. for...in,循环遍历对象自身的和继承的可枚举属性(不含 Symbol属性)。
  2. Object.keys(obj),返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol属性)的键名。
  3. Object.getOwnPropertyNames(obj),返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
  4. Object.getOwnPropertySymbols(obj),返回一个数组,包含对象自身的所有 Symbol 属性的键名。
  5. 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()
  • 深拷贝
  1. JSON.parse(JSON.stringify(initalObj))
  2. 递归
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 个内置函数
    1 Symbol.hasInstance
    2 Symbol.isConcatSpreadable
    3 Symbol.species
    4 Symbol.match
    5 Symbol.replace
    6 Symbol.search
    7 Symbol.split
    8 Symbol.iterator
    9 Symbol.toPrimitive
    10 Symbol.toStringTag
    11 Symbol.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对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法
  • 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false
  • Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete 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的遍历操作
    1 keys():返回键名的遍历器,keys方法和values方法的行为完全一致
    2 values():返回键值的遍历器,keys方法和values方法的行为完全一致
    3 entries():返回键值对的遍历器
    4 forEach():使用回调函数遍历每个成员
  • WeakSetSet类似,成员不重复,但只能是对象。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构造函数的参数
    1 size 属性
    2 set(key, value)
    3 get(key)
    4 has(key),返回一个布尔值
    5 delete(key),返回true
    6 clear(),清除所有对象,无返回值
  • 遍历方法,Map 的遍历顺序就是插入顺序
    1 keys():返回键名的遍历器。
    2 values():返回键值的遍历器。
    3 entries():返回所有成员的遍历器。
    4 forEach():遍历 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:
    1 Array
    2 Map
    3 Set
    4 String
    5 TypedArray
    6 函数的arguments对象
    7 NodeList对象

Promise

  • 一个保存异步结果的容器
  • 两种状态:pedding => resolvedpedding => 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

  1. 同步代码直接执行;
  2. 异步代码的回调函数先放在异步队列中(将要执行的时候放入)
  3. 待同步代码执行完毕后,轮询执行异步队列的函数。

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、rejectthen、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 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJSAMD 模块,都只能在运行时确定这些东西。

// 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命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
  • import命令是编译阶段执行的,在代码运行之前,因此其有提升的效果。
  • 可以用星号*指定一个对象,所有输出值都加载在这个对象上面

整体加载的写法如下:

// 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
    importexport default导出的模块时,可以任意指定这个模块导出的方法,这时import命令后面,不使用大括号。一个模块只有一个默认输出。
  • export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。
// 错误
export default var a = 1
  • exportimport 的复合写法:
export { foo, bar } from 'my_module'

// 可以简单理解为
import { foo, bar } from 'my_module'
export { foo, bar }

写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

浏览器异步加载



上面代码中,

ES6 模块与 CommonJS 模块的差异

  1. 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 模块输入的变量是活的,完全反应其所在模块内部的变化。

  1. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

node中的importexport

  • Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。
  • ES6 模块之中,顶层的this指向undefinedCommonJS模块的顶层this指向当前模块,这是两者的一个重大差异。
  • ES6 模块加载 CommonJS 模块时,CommonJS模块的输出都定义在module.exports这个属性上面。Node 的import命令加载 CommonJS模块,Node 会自动将module.exports属性,当作模块的默认输出,即等同于export default xxx
  • ES6 模块加载 CommonJS 模块,不能使用require命令,而要使用import()函数。ES6 模块的所有输出接口,会成为输入对象的属性。

循环加载

  1. CommonJS 模块的循环加载:
    CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
    CommonJS 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
  2. 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,传递引用

内存泄漏

  1. 全局变量
a = 10;
//未声明对象。
global.b = 11;
//全局变量引用

这种比较简单的原因,全局变量直接挂在 root 对象上,不会被清除掉。

  1. 闭包
function out() {
  const bigData = new Buffer(100);
  inner = function () {
    void bigData;
  }
}

inner 直接挂在了 root 上,从而导致内存泄漏(bigData 不会释放)。

  1. 事件监听
    对同一个事件重复监听,忘记移除(removeListener),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现。
  2. 缓存
    在使用缓存的时候,得清楚缓存的对象的多少,如果缓存对象非常多,得做限制最大缓存数量处理。还有就是非常占用 CPU 的代码也会导致内存泄漏,服务器在运行的时候,如果有高 CPU 的同步代码,因为Node.js 是单线程的,所以不能处理处理请求,请求堆积导致内存占用过高。

你可能感兴趣的:(你不可不知的ES6)