ES6笔记总结

一. let/const:javascript

1. “暂时性死区”概念:在代码块内,使用let/const命令声明变量以前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。“暂时性死区”也意味着typeof再也不是一个百分之百安全的操做。html

2. 块做用域与函数声明:java

 
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());  // I am inside!
 

上面代码至关于如下代码:node

 
// ES5 环境
function f() { console.log('I am outside!'); }

(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}());
 

在 ES5 中运行,会获得“I am inside!”,由于在if内声明的函数f会被提高到函数头部。ES6 就彻底不同了,理论上会获得“I am outside!”。由于块级做用域内声明的函数相似于let,对做用域以外没有影响。可是,若是你真的在 ES6 浏览器中运行一下上面的代码,是会报错的。原来,若是改变了块级做用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻所以产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现能够不遵照上面的规定,有本身的行为方式es6

  • 容许在块级做用域内声明函数。
  • 函数声明相似于var,即会提高到全局做用域或函数做用域的头部。
  • 同时,函数声明还会提高到所在的块级做用域的头部。

根据这三条规则,在浏览器的 ES6 环境中,块级做用域内声明的函数,行为相似于var声明的变量。web

 
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function
 

上面的代码在符合 ES6 的浏览器中,都会报错,由于实际运行的是下面的代码:算法

 
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function
 

 3. const:const实际上保证的,并非变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,所以等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即老是指向另外一个固定的地址)。编程

4. 声明变量:json

ES6 一共有 6 种声明变量的方法:var, function, let, const, imort, class数组

5. var命令和function命令声明的全局变量,依旧是顶层对象的属性;另外一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性:

 
var a = 1;
// 若是在 Node 的 REPL 环境,能够写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined
 

二. 解构赋值:数组/对象/字符串/数组/布尔值

1. 只要某种数据结构具备 Iterator 接口,均可以采用数组形式的解构赋值(数组,Set,Generator等)。

2. 原则:解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。因为undefinednull没法转为对象,因此对它们进行解构赋值,都会报错

3. 

 
function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
 

 4. 解构赋值可使用圆括号的状况:只有一种:赋值语句的非模式部分,可使用圆括号。

[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确

上面三行语句均可以正确执行,由于首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。

三. 字符串扩展:

1. 模板字符串:

(1)模板字符串之中还能调用函数。若是大括号中的值不是字符串,将按照通常的规则转为字符串。好比,大括号中是一个对象,将默认调用对象的toString方法。(模板字符串的大括号内部,就是执行 JavaScript 代码)

function fn() {
  return "Hello World";
}

`foo ${fn()} bar`
// foo Hello World bar

(2)标签模板:紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。

alert`123`
// 等同于
alert(123)
let a = 5;
let b = 10;

tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

 四. 函数的扩展:

1. 函数的 length 属性:

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

上面代码中,length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。这是由于length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值之后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入length属性。

(function(...args) {}).length // 0

若是设置了默认值的参数不是尾参数,那么length属性也再也不计入后面的参数了。

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

 2. 做用域:

一旦设置了参数的默认值,函数进行声明初始化时,参数会造成一个单独的做用域(context)。等到初始化结束,这个做用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

 
var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2) // 2
 
var x = 1;
function foo(x, y = function() { x = 2; }) {
  x = 3;
  y();
  console.log(x);
}

foo() // 2
x // 1
 

 3. rest 参数:

  • rest 参数以后不能再有其余参数(即只能是最后一个参数),不然会报错。
// 正确的
function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)

// 报错
function f(a, ...b, c) {
  // ...
}
  • 函数的length属性,不包括 rest 参数。
(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

 4. 严格模式:只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,不然会报错。

函数执行的时候,先执行函数参数,而后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,可是参数却应该先于函数体执行。

// 报错
function doSomething(value = 070) {
  'use strict';
  return value;
}

 5. name 属性:

  • Function构造函数返回的函数实例,name属性的值为anonymous
(new Function).name // "anonymous"
  • bind返回的函数,name属性值会加上bound前缀。
function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

 6. 箭头函数:

注意点:

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不能够看成构造函数,也就是说,不可使用new命令,不然会抛出一个错误。

(3)不可使用arguments对象,该对象在函数体内不存在。若是要用,能够用 rest 参数代替。

(4)不可使用yield命令,所以箭头函数不能用做 Generator 函数。

上面四点中,第一点尤为值得注意。this对象的指向是可变的,可是在箭头函数中,它是固定的。

this指向的固定化,并非由于箭头函数内部有绑定this的机制,实际缘由是箭头函数根本没有本身的this,致使内部的this就是外层代码块的this。正是由于它没有this,因此也就不能用做构造函数。

 
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?

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
 

上面代码之中,只有一个this,就是函数foothis,因此t1t2t3都输出一样的结果。由于全部的内层函数都是箭头函数,都没有本身的this,它们的this其实都是最外层foo函数的this

除了this,如下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.target

 

因为箭头函数没有本身的this,因此固然也就不能用call()apply()bind()这些方法去改变this的指向。

(function() {
  return [
    (() => this.x).bind({ x: 'inner' })()
  ];
}).call({ x: 'outer' });
// ['outer']

 

注:不适合使用箭头函数的场合:

(1)定义函数的方法,且该方法内部包括this

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
}

上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,若是是普通函数,该方法内部的this指向cat;若是写成上面那样的箭头函数,使得this指向全局对象,所以不会获得预期结果。

(2)须要动态this的时候,也不该使用箭头函数。

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

上面代码运行时,点击按钮会报错,由于button的监听函数是一个箭头函数,致使里面的this就是全局对象。若是改为普通函数,this就会动态指向被点击的按钮对象。

7. 嵌套的箭头函数:

部署管道机制(pipeline)的例子,即前一个函数的输出是后一个函数的输入:

 
const pipeline = (...funcs) =>
  val => funcs.reduce((a, b) => b(a), val);

const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThenMult = pipeline(plus1, mult2);

addThenMult(5)
// 12
 

若是以为上面的写法可读性比较差,也能够采用下面的写法。

const plus1 = a => a + 1;
const mult2 = a => a * 2;

mult2(plus1(5))
// 12

 8. 尾调用优化:

尾调用:指某个函数的最后一步是调用另外一个函数。尾调用不必定出如今函数尾部,只要是最后一步操做便可。

 
// 如下都不属于尾调用

// 状况一
function f(x){
  let y = g(x);
  return y;
}

// 状况二
function f(x){
  return g(x) + 1;
}

// 状况三
function f(x){        
  g(x);
}

// 状况三至关于如下
function f(x){ g(x); return undefined; }
 
function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

上面代码中,函数mn都属于尾调用,由于它们都是函数f的最后一步操做。

9. 尾递归:

递归很是耗费内存,由于须要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来讲,因为只存在一个调用记录,因此永远不会发生"栈溢出"错误。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

上面代码是一个阶乘函数,计算n的阶乘,最多须要保存n个调用记录,复杂度 O(n) 。

若是改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

  • 尾递归优化:
function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120

上面代码中,参数 total 有默认值1,因此调用时不用提供这个值。

总结一下,递归本质上是一种循环操做。纯粹的函数式编程语言没有循环操做命令,全部的循环都用递归实现,这就是为何尾递归对这些语言极其重要。对于其余支持"尾调用优化"的语言(好比Lua,ES6),只须要知道循环能够用递归代替,而一旦使用递归,就最好使用尾递归。

五. 数组的扩展:

1. 扩展运算符:

扩展运算符(spread)是三个点(...)。它比如 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

 
console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
 

若是扩展运算符后面是一个空数组,则不产生任何效果。

扩展运算符若是放在括号中,JavaScript 引擎就会认为这是函数调用。若是这时不是函数调用,就会报错。

 
(...[1, 2])
// Uncaught SyntaxError: Unexpected number

console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number

console.log(...[1, 2])
// 1 2
 

 扩展运算符内部调用的是数据结构的 Iterator 接口,所以只要具备 Iterator 接口的对象,均可以使用扩展运算符,好比 Map 结构。

 
let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]
 

 2. Array.from():

(1) Array.from方法用于将两类对象转为真正的数组:相似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

(2) Array.from()的另外一个应用是,将字符串转为数组,而后返回字符串的长度。由于它能正确处理各类 Unicode 字符,能够避免 JavaScript 将大于\uFFFF的 Unicode 字符,算做两个字符的 bug。

3. Array.of():Array.of方法用于将一组值,转换为数组。Array.of老是返回参数值组成的数组。若是没有参数,就返回一个空数组。

4. copyWithin():

数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其余位置(会覆盖原有成员),而后返回当前数组。也就是说,使用这个方法,会修改当前数组。

 

Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三个参数。

  • target(必需):从该位置开始替换数据。若是为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。若是为负值,表示倒数。
  • end(可选):到该位置前中止读取数据,默认等于数组长度。若是为负值,表示倒数。

这三个参数都应该是数值,若是不是,会自动转为数值。

[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]

上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。

 5. find() / findIndex():

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,全部数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,而后返回该成员。若是没有符合条件的成员,则返回undefined

数组实例的findIndex方法的用法与find方法很是相似,返回第一个符合条件的数组成员的位置,若是全部成员都不符合条件,则返回-1

[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0

上面代码中,indexOf方法没法识别数组的NaN成员,可是findIndex方法能够借助Object.is方法作到。

6. fill():给定值,填充一个数组。接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

 
['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
 

7. entries() / keys() / values() :

ES6 提供三个新的方法——entries()keys()values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),能够用for...of循环进行遍历,惟一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

 
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"
 

若是不使用for...of循环,能够手动调用遍历器对象的next方法,进行遍历。

let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']

8. includes():Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法相似。第二个参数表示搜索的起始位置,默认为0

 
[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true
 

indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,因此要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会致使对NaN的误判。

下面代码用来检查当前环境是否支持该方法,若是不支持,部署一个简易的替代版本。

const contains = (() =>
  Array.prototype.includes
    ? (arr, value) => arr.includes(value)
    : (arr, value) => arr.some(el => el === value)
)();
contains(['foo', 'bar'], 'baz'); // => false

另外,Map 和 Set 数据结构有一个has方法,须要注意与includes区分。

  • Map 结构的has方法,是用来查找键名的,好比Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
  • Set 结构的has方法,是用来查找值的,好比Set.prototype.has(value)WeakSet.prototype.has(value)

9. flat() / flatMap():

数组的成员有时仍是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

flat()默认只会“拉平”一层,若是想要“拉平”多层的嵌套数组,能够将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。若是无论有多少层嵌套,都要转成一维数组,能够用Infinity关键字做为参数。

 
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

[1, [2, [3]]].flat(Infinity) // [1, 2, 3]
 

flatMap()方法对原数组的每一个成员执行一个函数(至关于执行Array.prototype.map()),而后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。flatMap()只能展开一层数组。

// 至关于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

flatMap()方法的参数是一个遍历函数,该函数能够接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。还能够有第二个参数,用来绑定遍历函数里面的this

arr.flatMap(function callback(currentValue[, index[, array]]) {
  // ...
}[, thisArg])

10. 数组的空位:

数组的空位指,数组的某一个位置没有任何值。好比,Array构造函数返回的数组都是空位。空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符能够说明这一点。

0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false

ES5 对空位的处理,已经很不一致了,大多数状况下会忽略空位。

  • forEach()filter()reduce()every() 和some()都会跳过空位。
  • map()会跳过空位,但会保留这个值
  • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。
 
// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1

// filter方法
['a',,'b'].filter(x => true) // ['a','b']

// every方法
[,'a'].every(x => x==='a') // true

// reduce方法
[1,,2].reduce((x,y) => x+y) // 3

// some方法
[,'a'].some(x => x !== 'a') // false

// map方法
[,'a'].map(x => 1) // [,1]

// join方法
[,'a',undefined,null].join('#') // "#a##"

// toString方法
[,'a',undefined,null].toString() // ",a,,"
 

ES6 则是明确将空位转为undefined。好比 Array.from():

Array.from(['a',,'b'])
// [ "a", undefined, "b" ]

但不是全部 ES6 的方法对于空位的处理规则都是一致的,因此建议避免出现空位。

六. 对象的扩展:

1. 表达式还能够用于定义方法名。

let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};

obj.hello() // hi

注意,属性名表达式与简洁表示法,不能同时使用,会报错。

 
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
 

注意,属性名表达式若是是一个对象,默认状况下会自动将对象转为字符串[object Object],这一点要特别当心。

 
const keyA = {a: 1};
const keyB = {b: 2};

const myObject = {
  [keyA]: 'valueA',
  [keyB]: 'valueB'
};

myObject // Object {[object Object]: "valueB"}
 

2. 若是对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的getset属性上面,返回值是方法名前加上getset

 
const obj = {
  get foo() {},
  set foo(x) {}
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
 

若是对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
 

ES6 规定,全部 Class 的原型的方法都是不可枚举的。总的来讲,操做中引入继承的属性会让问题复杂化,大多数时候,咱们只关心对象自身的属性。因此,尽可能不要用for...in循环,而用Object.keys()代替。

3. 属性的遍历:for...in / Object.keys(obj) / Object.getOwnPropertyNames(obj) / Object.getOwnPropertySymblos(obj) / Reflect.ownKeys(obj):

以上的 5 种方法遍历对象的键名,都遵照一样的属性遍历的次序规则。

  • 首先遍历全部数值键,按照数值升序排列。
  • 其次遍历全部字符串键,按照加入时间升序排列。
  • 最后遍历全部 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]

附:Reflect.ownKeys返回一个数组,包含对象自身的全部键名,无论键名是 Symbol 或字符串,也无论是否可枚举。

4. super 关键字:super指向当前对象的原型对象

 
const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
 

注意:super必须用在对象的方法之中,只有对象方法的简写法可让 JavaScript 引擎确认,定义的是对象的方法。

JavaScript 引擎内部,super.foo 等同于 Object.getPrototypeOf(this).foo(属性)或 Object.getPrototypeOf(this).foo.call(this)(方法)。

5. 对象的解构赋值:

对象的解构赋值用于从一个对象取值,至关于将目标对象自身的全部可遍历的(enumerable)、但还没有被读取的属性,分配到指定的对象上面。全部的键和它们的值,都会拷贝到新对象上面。

因为解构赋值要求等号右边是一个对象,因此若是等号右边是undefinednull,就会报错,由于它们没法转为对象。

let { x, y, ...z } = null; // 运行时错误
let { x, y, ...z } = undefined; // 运行时错误

 另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性。

与数组的扩展运算符同样,对象的扩展运算符后面能够跟表达式。

const obj = {
  ...(x > 1 ? {a: 1} : {}),
  b: 2,
};

 扩展运算符的参数对象之中,若是有取值函数get,这个函数是会执行的。

 
// 并不会抛出错误,由于 x 属性只是被定义,但没执行
let aWithXGetter = {
  ...a,
  get x() {
    throw new Error('not throw yet');
  }
};

// 会抛出错误,由于 x 属性被执行了
let runtimeError = {
  ...a,
  ...{
    get x() {
      throw new Error('throw now');
    }
  }
};
 

 6. Object.is():比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。不一样之处只有两个:一是+0不等于-0,二是NaN等于自身。

 
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
 
 
Object.defineProperty(Object, 'is', {
  value: function(x, y) {
    if (x === y) {
      // 针对+0 不等于 -0的状况
      return x !== 0 || 1 / x === 1 / y;
    }
    // 针对NaN的状况
    return x !== x && y !== y;
  },
  configurable: true,
  enumerable: false,
  writable: true
});
 

7. Object.assign():用于对象的合并,将源对象(source)的全部可枚举属性,复制到目标对象(target)。

因为undefinednull没法转成对象,因此若是它们做为首参数,就会报错。

Object.assign(undefined) // 报错
Object.assign(null) // 报错

let obj = {a: 1}; Object.assign(obj, undefined) === obj // true Object.assign(obj, null) === obj // true

Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。

附:

Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)。

Object.create()方法建立一个新对象,使用现有的对象来提供新建立的对象的__proto__。 

8. Object.fromEntries():

Object.fromEntries()方法是 Object.entries()的逆操做,用于将一个键值对数组转为对象。该方法的主要目的,是将键值对的数据结构还原为对象,所以特别适合将 Map 结构转为对象。

 
// 例一 const entries = new Map([ ['foo', 'bar'], ['baz', 42] ]); Object.fromEntries(entries) // { foo: "bar", baz: 42 }  // 例二 const map = new Map().set('foo', true).set('bar', false); Object.fromEntries(map) // { foo: true, bar: false }
 

该方法的一个用处是配合URLSearchParams对象,将查询字符串转为对象。

Object.fromEntries(new URLSearchParams('foo=bar&baz=qux')) // { foo: "bar", baz: "qux" }

附:URLSearchParams

 七. Symbol:新的原始数据类型Symbol,表示独一无二的值。

1. Symbol 函数前不能使用 new 命令,不然会报错。这是由于生成的 Symbol 是一个原始类型的值,不是对象。也就是说,因为 Symbol 值不是对象,因此不能添加属性。基本上,它是一种相似于字符串的数据类型。

2. 属性名的遍历:

  Symbol 做为属性名,该属性不会出如今for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。可是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,能够获取指定对象的全部 Symbol 属性名。

 
const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]
 

 因为以 Symbol 值做为名称的属性,不会被常规方法遍历获得。咱们能够利用这个特性,为对象定义一些非私有的、但又但愿只用于内部的方法。

 
let size = Symbol('size');

class Collection {
  constructor() {
    this[size] = 0;
  }

  add(item) {
    this[this[size]] = item;
    this[size]++;
  }

  static sizeOf(instance) {
    return instance[size];
  }
}

let x = new Collection();
Collection.sizeOf(x) // 0

x.add('foo');
Collection.sizeOf(x) // 1

Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]
 

上面代码中,对象xsize属性是一个 Symbol 值,因此Object.keys(x)Object.getOwnPropertyNames(x)都没法获取它。这就形成了一种非私有的内部方法的效果。

3. Symbol.for() / Symbol.keyFor():

Symbol.for():接受一个字符串做为参数,而后搜索有没有以该参数做为名称的 Symbol 值。若是有,就返回这个 Symbol 值,不然就新建并返回一个以该字符串为名称的 Symbol 值。

Symbol.keyFor():返回一个已登记的 Symbol 类型值的 key。

 
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

// 不一样于 Symbol()
Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false
 
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

 4. 内置的 Symbol 值(11个):

Symbol.hasInstance / Symbol.isConcatSpreadable / Symbol.species / Symbol.match等

例:对象的Symbol.hasInstance属性,指向一个内部方法。当其余对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。好比,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)

 
class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  }
}

[1, 2, 3] instanceof new MyClass() // true
 

八. Set 和 Map 数据结构:

1. Set:Set自己是一个构造函数,用来生成 Set 数据结构。

Set 函数能够接受一个数组(或者具备 iterable 接口的其余数据结构)做为参数,用来初始化,且 Set 结构不会添加剧复的值。

 
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5

// 例三
const set = new Set(document.querySelectorAll('div'));
set.size // 56

// 相似于
const set = new Set();
document
 .querySelectorAll('div')
 .forEach(div => set.add(div));
set.size // 56

[...new Set('ababbc')].join('')
// "abc"
 

向 Set 加入值的时候,不会发生类型转换,Set 内部判断两个值是否不一样,使用的算法叫作“Same-value-zero equality”,它相似于精确相等运算符(===),主要的区别是NaN等于自身。

2. Set 实例的属性和方法:

属性:size 至关于数组的 length;

方法:操做方法(用于操做数据)和遍历方法(用于遍历成员)。

  操做方法:

  • add(value):添加某个值,返回 Set 结构自己。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除全部成员,没有返回值。

  遍历方法:(Set的遍历顺序就是插入顺序)

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每一个成员
 
let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
 

因为 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),keys方法和values方法的行为彻底一致。

 
// Set 结构的实例默承认遍历,它的默认遍历器生成函数就是它的values方法 =》这意味着,能够省略values方法,直接用for...of循环遍历 Set
Set.prototype[Symbol.iterator] === Set.prototype.values
// true

let set = new Set(['red', 'green', 'blue']);

for (let x of set) {
  console.log(x);
}
// red
// green
// blue
 

 3. WeakSet:结构与 Set 相似,也是不重复的值的集合,但成员只能是对象,而不能是其余类型的值;WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,若是其余对象都再也不引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

=》WeakSet 不能遍历,是由于成员都是弱引用,随时可能消失,遍历机制没法保证成员的存在

 
// 做为构造函数,WeakSet 能够接受一个数组或相似数组的对象做为参数。(实际上,任何具备 Iterable 接口的对象,均可以做为 WeakSet 的参数。)该数组的全部成员,都会自动成为 WeakSet 实例对象的成员。

const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
 

  一些方法:

  • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

4. Map:键值对集合,区别于普通 Object 的一点就是,该键能够是任意类型,好比对象;

(1) 不只仅是数组,任何具备 Iterator 接口、且每一个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)均可以看成 Map 构造函数的参数。这就是说,Set 和 Map 均可以用来生成新的 Map。

(2) 若是对同一个键屡次赋值,后面的值将覆盖前面的值。但只有对同一个对象的引用,Map 结构才将其视为同一个键。

// set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不同的,所以get方法没法读取该键,返回undefined
const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

(3) 一些属性/方法:

size属性:返回 Map 结构的成员总数;

set(key, value):set方法设置键名key对应的键值为value,而后返回整个 Map 结构(所以可链式写法)。若是key已经有值,则键值会被更新,不然就新生成该键;

get(key):读取key对应的键值,若是找不到key,返回 undefined;

has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中;

delete(key):删除某个键,返回true。若是删除失败,返回false;

clear():清除全部成员,没有返回值;

keys():返回键名的遍历器;

values():返回键值的遍历器;

entries():返回全部成员的遍历器;

forEach():遍历 Map 的全部成员;

注:Map 的遍历顺序就是插入顺序; Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

(4) Map 转为数组:使用扩展运算符(...),转为数组后才可使用数组相关 map() / filter() 方法;

5. WeakMap:结构与 Map 结构相似,也是用于生成键值对的集合,2 点区别:WeakMap只接受对象做为键名(null除外),不接受其余类型的值做为键名;WeakMap的键名所指向的对象,不计入垃圾回收机制。

基本上,若是你要往对象上添加数据,又不想干扰垃圾回收机制,就可使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。

注:WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

 
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};

wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}
 

上面代码中,键值obj是正常引用。因此,即便在 WeakMap 外部消除了obj的引用,WeakMap 内部的引用依然存在。

一些方法:WeakMap只有四个方法可用:get()set()has()delete()

  • WeakMap 的用途:WeakMap 应用的典型场合就是 DOM 节点做为键名;另外一个用处是部署私有属性。
 
// Countdown类的两个内部属性_counter和_action,是实例的弱引用,因此若是删除实例,它们也就随之消失,不会形成内存泄漏
const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    let counter = _counter.get(this);
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}

const c = new Countdown(2, () => console.log('DONE'));

c.dec()
 

 九. Proxy:

1. Proxy 能够理解成,在目标对象以前架设一层“拦截”,外界对该对象的访问,都必须先经过这层拦截,所以提供了一种机制,能够对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操做,能够译为“代理器”。

Proxy 对象的全部用法,都是下面这种形式,不一样的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

var proxy new Proxy(target, handler);

 
var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    // target:目标对象;key:key-value中的key值;receiver:当前proxy代理对象;
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});
 
 
// 如下为上述代码运行后结果
obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2
 
 
var handler = {
  get: function(target, name) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },
 // 参数:目标对象、目标对象的上下文对象和目标对象的参数数组
  apply: function(target, thisBinding, args) {
    return args[0];
  },
  // 参数:target 目标对象;args:构造函数的参数对象;newTarget:创造实例对象时,new命令做用的构造函数
  construct: function(target, args) {
    return {value: args[1]};
  }
};

var fproxy = new Proxy(function(x, y) {
  return x + y;
}, handler);

fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true
 

2. Proxy 的一些方法:

具体参数及使用方法:http://es6.ruanyifeng.com/#docs/proxy

 proxy对象getReceiver属性是由对象提供的,receiver指向proxy对象。

 若是一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,不然经过 Proxy 对象访问(get)该属性会报错。

 
const target = Object.defineProperties({}, {
  foo: {
    value: 123,
    writable: false,
    configurable: false
  },
});

const handler = {
  get(target, propKey) {
    return 'abc';
  }
};

const proxy = new Proxy(target, handler);

proxy.foo
// TypeError: Invariant check failed
 

若是目标对象自身的某个属性,不可写且不可配置,那么set方法将不起做用,不是报错。

严格模式下,set代理若是没有返回true,就会报错(也就是若是没有return true就会报错)。

3. this 问题:

Proxy 代理的状况下,目标对象内部的this关键字会指向 Proxy 代理。this指向变化会致使 Proxy 没法代理目标对象。

十. Reflect:

1. Reflect对象与Proxy对象同样,也是 ES6 为了操做对象而提供的新 API。

设计目的

(1) 将Object对象的一些明显属于语言内部的方法(好比Object.defineProperty),放到Reflect对象上;

(2) 修改某些Object方法的返回结果,让其变得更合理;好比,Object.defineProperty(obj, name, desc)在没法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false;

(3) 让Object操做都变成函数行为。某些Object操做是命令式,好比name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为;

(4) Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法;

2. 静态方法:

 3. 使用 Proxy 实现观察者模式:

观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。

 
// 思路是函数返回一个原始对象的 Proxy 代理,拦截赋值操做,触发充当观察者的各个函数
const queuedObservers = new Set(); const observe = fn => queuedObservers.add(fn); const observable = obj => new Proxy(obj, {set}); function set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); queuedObservers.forEach(observer => observer()); return result; }observable

上面代码中,先定义了一个Set集合,全部观察者函数都放进这个集合。而后,observable函数返回原始对象的代理,拦截赋值操做。拦截函数set之中,会自动执行全部观察者。

 十一. Promise 对象:

1. Promise: Promise 对象是一个构造函数;

2. Promise 对象的特色:

(1)对象的状态不受外界影响。Promise对象表明一个异步操做,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败);

(2)一旦状态改变,就不会再变,任什么时候候均可以获得这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种状况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型).

3. 使用:

 
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
 

执行顺序:Promise 新建后就会当即执行。

 
let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved
 

上面代码中,Promise 新建后当即执行,因此首先输出的是Promise。而后,then方法指定的回调函数,将在当前脚本全部同步任务执行完才会执行,因此resolved最后输出。

 
new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1
 

上面代码中,调用resolve(1)之后,后面的console.log(2)仍是会执行,而且会首先打印出来。这是由于当即 resolved 的 Promise 是在本轮事件循环的末尾执行,老是晚于本轮循环的同步任务。

通常来讲,调用resolvereject之后,Promise 的使命就完成了,后继操做应该放到then方法里面,而不该该直接写在resolvereject的后面。因此,最好在它们前面加上return语句,这样就不会有意外。

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})

4. Promise.prototype.then():

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成之后,会将返回结果做为参数,传入第二个回调函数。

5. “Promise 会吃掉错误”:Promise 内部的错误(任何报错)不会影响到 Promise 外部的代码;

6. Promise.prototype.finally():finally方法用于指定无论 Promise 对象最后状态如何,都会执行的操做。

7. Promise.all():用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

Promise.all方法接受一个数组做为参数,p1p2p3都是 Promise 实例,若是不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数能够不是数组,但必须具备 Iterator 接口,且返回的每一个成员都是 Promise 实例。)

p的状态由p1p2p3决定,分红两种状况(且):

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

:若是做为参数的 Promise 实例,本身定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法,以下:

 
const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
 

上面代码中,p1resolvedp2首先会rejected,可是p2有本身的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的其实是这个实例。该实例执行完catch方法后,也会变成resolved,致使Promise.all()方法参数里面的两个实例都会resolved,所以会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。若是p2没有本身的catch方法,就会调用Promise.all()catch方法。

8. Promise.race():Promise.race方法一样是将多个 Promise 实例,包装成一个新的 Promise 实例。区别在于:只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变(或)。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

9. Promise.resolve():

当即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。

 
setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three
 

上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是当即执行,所以最早输出。

10. Promise.reject():Promise.reject()方法的参数,会原封不动地做为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

 
const thenable = {
  then(resolve, reject) {
    reject('出错了');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true
 

上面代码中,Promise.reject方法的参数是一个thenable对象,执行之后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。

11. Promise.try():Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。

十二. Iterator 和 for...of 循环:

1. 一个数据结构只要部署了Symbol.iterator属性,就被视为具备 iterator 接口,就能够用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

for...of循环可使用的范围包括数组、Set 和 Map 结构、某些相似数组的对象(好比arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

 2. 原生具有 Iterator 接口的数据结构以下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

 3. 遍历器(Iterator)就是这样一种机制。它是一种接口,为各类不一样的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就能够完成遍历操做(即依次处理该数据结构的全部成员)。

Iterator 的遍历过程是这样的。

(1)建立一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,能够将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来讲,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

十三. Generator函数

1. 语法上,首先能够把Generator 函数理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,仍是一个遍历器对象生成函数。返回的遍历器对象,能够依次遍历 Generator 函数内部的每个状态。

形式上,Generator 函数是一个普通函数,可是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不一样的内部状态(yield在英语里的意思就是“产出”)。

 
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
 

而后,Generator 函数的调用方法与普通函数同样,也是在函数名后面加上一对圆括号。不一样的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法能够恢复执行。

 
hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }
 

总结一下,调用 Generator 函数,返回一个遍历器对象,表明 Generator 函数的内部指针。之后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

2. yield 表达式:

因为 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,因此其实提供了一种能够暂停执行的函数。yield表达式就是暂停标志。

遍历器对象的next方法的运行逻辑以下。

(1)遇到yield表达式,就暂停执行后面的操做,并将紧跟在yield后面的那个表达式的值,做为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)若是没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,做为返回的对象的value属性值。

(4)若是该函数没有return语句,则返回的对象的value属性值为undefined

须要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,所以等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

 3. for...of循环:

for...of循环能够自动遍历 Generator 函数运行时生成的Iterator对象,且此时再也不须要调用next方法。

 
function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5
 

这里须要注意,一旦next方法的返回对象的done属性为truefor...of循环就会停止,且不包含该返回对象,因此上面代码的return语句返回的6,不包括在for...of循环之中。

4. yield*:yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。

任何数据结构只要有 Iterator 接口,就能够被yield*遍历。

5. 做为对象属性的 Generator 函数:

若是一个对象的属性是 Generator 函数,能够简写成下面的形式。

 
let obj = {
  * myGeneratorMethod() {
    ···
  }
};
等同于
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};
 

6. generator 的 this:

Generator 函数老是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。可是,若是把g看成普通的构造函数,并不会生效,由于g返回的老是遍历器对象,而不是this对象。

Generator 函数也不能跟new命令一块儿用,会报错。

如何绑定 this:用call:

 
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* 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
 

7. 含义:

(1)状态机:

 
var clock = function* () {
  while (true) {
    console.log('Tick!');
    yield;
    console.log('Tock!');
    yield;
  }
};

等同于

var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}
 

(2)协程:

一个线程(或函数)执行到一半,能够暂停执行,将执行权交给另外一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种能够并行执行、交换执行权的线程(或函数),就称为协程

在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。

因为 JavaScript 是单线程语言,只能保持一个调用栈。引入协程之后,每一个任务能够保持本身的调用栈。这样作的最大好处,就是抛出错误的时候,能够找到原始的调用栈。不至于像异步操做的回调函数那样,一旦出错,原始的调用栈早就结束。

Generator 函数是 ES6 对协程的实现,但属于不彻底实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。若是是彻底执行的协程,任何函数均可以让暂停的协程继续执行。

(3)上下文:

JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前全部的变量和对象。而后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此造成一个上下文环境的堆栈(context stack)。

这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,而后再执行完成它下层的上下文,直至全部代码执行完成,堆栈清空。

Generator 函数不是这样,它执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,可是并不消失,里面的全部变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会从新加入调用栈,冻结的变量和对象恢复执行。

 
function* gen() {
  yield 1;
  return 2;
}

let g = gen();

console.log(
  g.next().value,
  g.next().value,
);
 

上面代码中,第一次执行g.next()时,Generator 函数gen的上下文会加入堆栈,即开始运行gen内部的代码。等遇到yield 1时,gen上下文退出堆栈,内部状态冻结。第二次执行g.next()时,gen上下文从新加入堆栈,变成当前的上下文,从新恢复执行。

8. 应用:

(1)异步操做的同步化表达:

Generator 函数的暂停执行的效果,意味着能够把异步操做写在yield表达式里面,等到调用next方法时再日后执行。这实际上等同于不须要写回调函数了,由于异步操做的后续操做能够放在yield表达式下面,反正要等到调用next方法时再执行。因此,Generator 函数的一个重要实际意义就是用来处理异步操做,改写回调函数。

(2)控制流管理:

 
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

或者

Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function (value4) {
    // Do something with value4
  }, function (error) {
    // Handle any error from step1 through step4
  })
  .done();

都可写为:

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

scheduler(longRunningTask(initialValue));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // 若是Generator函数未结束,就继续调用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}
 

注意,上面这种作法,只适合同步操做,即全部的task都必须是同步的,不能有异步操做。由于这里的代码一获得返回值,就继续往下执行,没有判断异步操做什么时候完成。

(3)部署 Iterator 接口:

利用 Generator 函数,能够在任意对象上部署 Iterator 接口。

 
function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7
 

(4)做为数据结构:

Generator 能够看做是一个数组结构,由于 Generator 函数能够返回一系列的值,这意味着它能够对任意表达式,提供相似数组的接口。

好比:

 
function doStuff() {
  return [
    fs.readFile.bind(null, 'hello.txt'),
    fs.readFile.bind(null, 'world.txt'),
    fs.readFile.bind(null, 'and-such.txt')
  ];
}

for (task of doStuff()) {
  // task是一个函数,能够像回调函数那样使用它
}
 

 9. 

调用 Generator 函数,会返回一个内部指针(即遍历器)g。这是 Generator 函数不一样于普通函数的另外一个地方,即执行它不会返回结果,返回的是指针对象。调用指针gnext方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的yield语句,上例是执行到x + 2为止。

换言之,next方法的做用是分阶段执行Generator函数。每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)。value属性是yield语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示 Generator 函数是否执行完毕,便是否还有下一个阶段。

10. Thunk 函数:

(1)参数的求值策略:

 
var x = 1;

function f(m) {
  return m * 2;
}

f(x + 5)
 

上面代码先定义函数f,而后向它传入表达式x + 5。请问,这个表达式应该什么时候求值?

"传值调用"(call by value),即在进入函数体以前,就计算x + 5的值(等于 6),再将这个值传入函数f。C 语言就采用这种策略。

“传名调用”(call by name),即直接将表达式x + 5传入函数体,只在用到它的时候求值。Haskell 语言采用这种策略。

(2)编译器的“传名调用”实现,每每是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫作 Thunk 函数。

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不一样。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数做为参数的单参数函数。

任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器:

 
// ES5版本
var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  };
};

// ES6版本
const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};
 

 十四. Async 函数:

1. async 函数就是 Generator 函数的语法糖。async函数就是将 Generator 函数的星号(*)替换成 async,将yield替换成 await。

2. async函数的返回值是 Promise 对象。进一步说,async函数彻底能够看做多个异步操做,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

3. 用法:async函数返回一个 Promise 对象,可使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操做完成,再接着执行函数体内后面的语句。

 
async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});
 

 4. async 函数的实现原理:async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

 
async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}
 

 5. 异步遍历器:Async Iterator,为异步操做提供原生的遍历器接口,即valuedone这两个属性都是异步产生。

(1)异步遍历器的最大的语法特色,就是调用遍历器的 next方法,返回的是一个 Promise 对象。

对象的异步遍历器接口,部署在Symbol.asyncIterator属性上面。无论是什么样的对象,只要它的Symbol.asyncIterator属性有值,就表示应该对它进行异步遍历。

(2)for await ... of:for...of循环用于遍历同步的 Iterator 接口。新引入的for await...of循环,则是用于遍历异步的 Iterator 接口。

(3)异步 Generator 函数出现之后,JavaScript 就有了四种函数形式:普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不一样之处。基本上,若是是一系列按照顺序执行的异步操做(好比读取文件,而后写入新内容,再存入硬盘),可使用 async 函数;若是是一系列产生相同数据结构的异步操做(好比一行一行读取文件),可使用异步 Generator 函数。

十五. Class:

1. ES5 与 Class:

 
// ES5:

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);

// Class 方式:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
 

2. 静态方法:类至关于实例的原型,全部在类中定义的方法,都会被实例继承。若是在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接经过类来调用,这就称为“静态方法”。

 
class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
 

Foo类的classMethod方法前有static关键字,代表该方法是一个静态方法,能够直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。若是在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

注意,若是静态方法包含this关键字,这个this指的是类,而不是实例。

父类的静态方法,能够被子类继承。静态方法也是能够从super对象上调用的。

 
class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"
 

3. 实例属性的新写法:

 
class IncreasingCounter {
  constructor() {
    this._count = 0;
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

// 也能够

class IncreasingCounter {
  _count = 0;
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}
 

4. 静态属性:

 
// 老写法
class Foo {
  // ...
}
Foo.prop = 1;

// 新写法
class Foo {
  static prop = 1;
}
 

5. new target 属性:

new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性通常用在构造函数之中,返回new命令做用于的那个构造函数。若是构造函数不是经过new命令或Reflect.construct()调用的,new.target会返回undefined,所以这个属性能够用来肯定构造函数是怎么调用的。

 
function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

// 另外一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错
 

上面代码确保构造函数只能经过new命令调用。

Class 内部调用new.target,返回当前 Class。

须要注意的是,子类继承父类时,new.target会返回子类。

利用这个特色,能够写出不能独立使用、必须继承后才能使用的类。

 
class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape();  // 报错
var y = new Rectangle(3, 4);  // 正确
 

 6. 继承:

(1)Class 经过extends关键字实现继承;

super关键字,表示父类的构造函数,用来新建父类的this对象(super 既能够当函数使用(表明调用父类的构造函数),也能够当对象使用(在普通方法中,指向父类的原型对象;在静态方法中,指向父类))。

子类必须在constructor方法中调用super方法,不然新建实例时会报错。这是由于子类本身的this对象,必须先经过父类的构造函数完成塑造,获得与父类一样的实例属性和方法,而后再对其进行加工,加上子类本身的实例属性和方法。若是不调用super方法,子类就得不到this对象。

 
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()
  }
}
 

(2)Object.getPrototypeOf方法能够用来从子类上获取父类,能够用这个方法判断,一个类是否继承了另外一个类。

Object.getPrototypeOf(ColorPoint) === Point
// true

 (3)mixin:

多个类的接口“混入”(mix in)另外一个类:

 
function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // 拷贝实例属性
      }
    }
  }

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷贝静态属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }

  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);
    }
  }
}

// 使用
class DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}
 

 十六. Module:

1. 编译时加载:下面代码的实质是从fs模块加载 3 个方法,其余方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 能够在编译时就完成模块加载。

// ES6模块
import { stat, exists, readFile } from 'fs';

2. ES6 模块的好处:

  • 再也不须要UMD模块格式了,未来服务器和浏览器都会支持 ES6 模块格式。目前,经过各类工具库,其实已经作到了这一点。
  • 未来浏览器的新 API 就能用模块格式提供,再也不必须作成全局变量或者navigator对象的属性。
  • 再也不须要对象做为命名空间(好比Math对象),将来这些功能能够经过模块提供。
  • 因为 ES6 模块是编译时加载,使得静态分析成为可能。进一步拓宽 JavaScript 的语法,好比引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

3. 模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其余模块提供的功能。

4. export命令能够出如今模块的任何位置,只要处于模块顶层就能够。若是处于块级做用域内,就会报错,import命令也是如此。这是由于处于条件代码块之中,就无法作静态优化了,违背了 ES6 模块的设计初衷。

function foo() {
  export default 'bar' // SyntaxError
}
foo()

5. import命令具备提高效果,会提高到整个模块的头部,首先执行。

6. export default 命令:

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,所以export default命令只能使用一次。因此,import命令后面才不用加大括号,由于只可能惟一对应export default命令。

本质上,export default就是输出一个叫作default的变量或方法,而后系统容许你为它取任意名字。因此,下面的写法是有效的。

 
// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同于
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
 

7. import():返回一个 Promise 对象。

import()函数能够用在任何地方,不只仅是模块,非模块的脚本也可使用。它是运行时执行,也就是说,何时运行到这一句,就会加载指定的模块,因此能够按需加载/条件加载。另外,import()函数与所加载的模块没有静态链接关系,这点也是与import语句不相同。import()相似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

8. 加载规则:浏览器加载 ES6 模块,也使用<script>标签,可是要加入type="module"属性。

<script type="module" src="./foo.js"></script>

上面代码在网页中插入一个模块foo.js,因为type属性设为module,因此浏览器知道这是一个 ES6 模块。

浏览器对于带有type="module"<script>,都是异步加载,不会形成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

若是网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

<script>标签的async属性也能够打开,这时只要加载完成,渲染引擎就会中断渲染当即执行。执行完成后,再恢复渲染。

一旦使用了async属性,<script type="module">就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。

ES6 模块也容许内嵌在网页中,语法行为与加载外部脚本彻底一致。

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

对于外部的模块脚本(上例是foo.js),有几点须要注意。

  • 代码是在模块做用域之中运行,而不是在全局做用域运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,无论有没有声明use strict
  • 模块之中,可使用import命令加载其余模块(.js后缀不可省略,须要提供绝对 URL 或相对 URL),也可使用export命令输出对外接口。
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无心义的。
  • 同一个模块若是加载屡次,将只执行一次。

利用顶层的this等于undefined这个语法点,能够侦测当前代码是否在 ES6 模块之中。

9. ES6 模块与 CommonJS 模块的差别:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

第一个,CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

第二个差别是由于 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

10. Node 加载:

Node 对 ES6 模块的处理比较麻烦,由于它有本身的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将二者分开,ES6 模块和 CommonJS 采用各自的加载方案。

Node 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。require命令不能加载.mjs文件,会报错,只有import命令才能够加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import

若是模块名不含路径,那么import命令会去node_modules目录寻找这个模块。

若是脚本文件省略了后缀名,好比import './foo',Node 会依次尝试四个后缀名:./foo.mjs./foo.js./foo.json./foo.node。若是这些脚本文件都不存在,Node 就会去加载./foo/package.jsonmain字段指定的脚本。若是./foo/package.json不存在或者没有main字段,那么就会依次加载./foo/index.mjs./foo/index.js./foo/index.json./foo/index.node。若是以上四个文件仍是都不存在,就会抛出错误。

最后,Node 的import命令是异步加载,这一点与浏览器的处理方法相同。

ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块。

 11. SystemJS:

它是一个垫片库(polyfill),能够在浏览器内加载 ES6 模块、AMD 模块和 CommonJS 模块,将其转为 ES5 格式。它在后台调用的是 Google 的 Traceur 转码器。

System.import使用异步加载,返回一个 Promise 对象。

十七. 编程风格:

1. 全局常量和线程安全:

letconst之间,建议优先使用const,尤为是在全局环境,不该该设置变量,只应设置常量。

const优于let有几个缘由。一个是const能够提醒阅读程序的人,这个变量不该该改变;另外一个是const比较符合函数式编程思想,运算不改变值,只是新建值,并且这样也有利于未来的分布式运算;最后一个缘由是 JavaScript 编译器会对const进行优化,因此多使用const,有利于提升程序的运行效率,letconst的本质区别,实际上是编译器内部的处理不一样。

全部的函数都应该设置为常量。

2. 不要在函数体内使用 arguments 变量,使用 rest 运算符(...)代替。由于 rest 运算符显式代表你想要获取参数,并且 arguments 是一个相似数组的对象,而 rest 运算符能够提供一个真正的数组。

 
// bad
function concatenateAll() {
  const args = Array.prototype.slice.call(arguments);
  return args.join('');
}

// good
function concatenateAll(...args) {
  return args.join('');
}
 

3. 使用默认值语法设置函数参数的默认值。

 
// bad
function handleThings(opts) {
  opts = opts || {};
}

// good
function handleThings(opts = {}) {
  // ...
}
 

4. 使用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];
  }
}
 

 十八. 修饰器:

修饰器(Decorator)函数,用来修改类的行为。

修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类。

 
@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true
 

上面代码中,@testable就是一个修饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestabletestable函数的参数targetMyTestableClass类自己。

十九. ArrayBuffer:

1.

ArrayBuffer对象、TypedArray视图和DataView视图是 JavaScript 操做二进制数据的一个接口。它们都是以数组的语法处理二进制数据,因此统称为二进制数组。

这个接口的原始设计目的,与 WebGL 项目有关。所谓 WebGL,就是指浏览器与显卡之间的通讯接口,为了知足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通讯必须是二进制的,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将很是耗时。这时要是存在一种机制,能够像 C 语言那样,直接操做字节,将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提高。

二进制数组就是在这种背景下诞生的。它容许开发者以数组下标的形式,直接操做内存,大大加强了 JavaScript 处理二进制数据的能力,使得开发者有可能经过 JavaScript 与操做系统的原生接口进行二进制通讯。

二进制数组由三类对象组成:

(1)ArrayBuffer对象:表明内存之中的一段二进制数据,能够经过“视图”进行操做。“视图”部署了数组接口,这意味着,能够用数组的方法操做内存。

(2)TypedArray视图:共包括 9 种类型的视图,好比Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等。

(3)DataView视图:能够自定义复合格式的视图,好比第一个字节是 Uint8(无符号 8 位整数)、第2、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还能够自定义字节序。

简单说,ArrayBuffer对象表明原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。

注意,二进制数组并非真正的数组,而是相似数组的对象。

2. TypedArray 视图:

ArrayBuffer对象做为内存区域,能够存放多种类型的数据。同一段内存,不一样数据有不一样的解读方式,这就叫作“视图”(view)。ArrayBuffer有两种视图,一种是TypedArray视图,另外一种是DataView视图。前者的数组成员都是同一个数据类型,后者的数组成员能够是不一样的数据类型。

目前,TypedArray视图一共包括 9 种类型,每一种视图都是一种构造函数。

  • Int8Array:8 位有符号整数,长度 1 个字节。
  • Uint8Array:8 位无符号整数,长度 1 个字节。
  • Uint8ClampedArray:8 位无符号整数,长度 1 个字节,溢出处理不一样。
  • Int16Array:16 位有符号整数,长度 2 个字节。
  • Uint16Array:16 位无符号整数,长度 2 个字节。
  • Int32Array:32 位有符号整数,长度 4 个字节。
  • Uint32Array:32 位无符号整数,长度 4 个字节。
  • Float32Array:32 位浮点数,长度 4 个字节。
  • Float64Array:64 位浮点数,长度 8 个字节。

这 9 个构造函数生成的数组,统称为TypedArray视图。它们很像普通数组,都有length属性,都能用方括号运算符([])获取单个元素,全部数组的方法,在它们上面都能使用。普通数组与 TypedArray 数组的差别主要在如下方面。

  • TypedArray 数组的全部成员,都是同一种类型。
  • TypedArray 数组的成员是连续的,不会有空位。
  • TypedArray 数组成员的默认值为 0。好比,new Array(10)返回一个普通数组,里面没有任何成员,只是 10 个空位;new Uint8Array(10)返回一个 TypedArray 数组,里面 10 个成员都是 0。
  • TypedArray 数组只是一层视图,自己不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。

参考博客:http://www.javashuo.com/article/p-gbmhwmbf-hm.html

相关文章
相关标签/搜索