[TOC]

ES6 入门教程 - ECMAScript 6入门

https://es6.ruanyifeng.com/

let 和 const 命令

块级作用域

let 实际上为 JavaScript 新增了块级作用域。
ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
const 声明一个只读的常量。一旦声明,常量的值就不能改变。
const 的作用域与 let 命令相同:只在声明所在的块级作用域内有效。

顶层对象的属性

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

变量的解构赋值

数组的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

function* fibs() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5

上面代码中,fibs 是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。
带有默认值的情况:
ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于 undefined ,默认值才会生效。
[x=2] = [undefined] // x==2
[x=2] = [3] // x==3

对象的解构赋值

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。

let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。

由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

上面代码对数组进行对象解构。数组arr的0键对应的值是1,[arr.length - 1]就是2键,对应的值是3。方括号这种写法,属于“属性名表达式”(参见《对象的扩展》一章)。

函数解构

函数参数的解构也可以使用默认值。

用途

交换变量的值

let x = 1;
let y = 2;

[x, y] = [y, x];

从函数返回多个值

// 返回一个数组

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

函数参数的定义

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

提取 JSON 数据

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

函数参数的默认值

jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
} = {}) {
  // ... do stuff
};

看下面这个,参数后面的 ={} ,({x = 0, y = 0} = {}) 是传参作用,{} 是函数参数的默认值—空对象,如果参数传入是对象,则运行左边{x = 0, y = 0},如果没有传入参数 move(),会运行右边的默认值—空对象 {}。

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

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

// 
function move({x = 0, y = 0} = {x:9}) {
  return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]   
move(); // [9, 0]   // 如果没有传入参数 move(),会运行右边的值 {x:9}

// 所以={} 有两个作用 1.是给对象{x = 0, y = 0}默认值 2.传参应是个对象

遍历 Map 结构

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world

任何部署了 Iterator 接口的对象,都可以用 for…of 循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

字符串的新增方法

includes(), startsWith(), endsWith()

传统上,JavaScript 只有 indexOf 方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

这三个方法都支持第二个参数,表示开始搜索的位置。

let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

使用第二个参数n时,endsWith 的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。

函数的扩展

函数参数的默认值

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

function log(x, y = 'World') {   // 参数变量是默认声明的,所以不能用let或const再次声明
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

使用参数默认值时,函数不能有同名参数。
参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101
// 每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100

作用域 !!!

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。
看例子:

var x = 1;

function f(x, y = x) {  // 参数y的默认值等于变量x
  console.log(y);
}

f(2) // 2

调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。

let x = 1;

function f(y = x) { // 变量x本身没有定义,所以指向外层的全局变量x
  let x = 2;
  console.log(y);
}

f() // 1  外层的全局变量x  

// 如果此时,全局变量x不存在,就会报错。

上面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。

var x = 1;

function foo(x = x) {  // 实际执行的是 let x = x  暂时性死区 报错
  // ...
}

foo() // ReferenceError: Cannot access 'x' before initialization

上面代码中,参数 x = x 形成一个单独作用域。实际执行的是 let x = x ,由于暂时性死区的原因,这行代码会报错。

更复杂的例子

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

foo() // 3
x // 1

image.png
image.png

箭头函数

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

上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数)后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。
image.png
箭头函数实际上可以让this指向固定化,绑定this使得它不再可变,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面

var handler = {
  id: '123456',

  init: function() {
    document.addEventListener('click',
      event => this.doSomething(event.type), false); // 这个this,总是指向handler对象
  },

  doSomething: function(type) {
    console.log('Handling ' + type  + ' for ' + this.id); // 此时this指向document对象
  }
};

上面代码的init() 方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。如果回调函数是普通函数,那么运行this.doSomething()这一行会报错,因为此时this指向document对象。
总之,箭头函数根本没有自己的this,导致内部的this就是外层代码块的this

例二

请问下面的代码之中,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的指向只有一个,就是函数foo的this,这是因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。所以不管怎么嵌套,t1、t2、t3都输出同样的结果。如果这个例子的所有内层函数都写成普通函数,那么每个函数的this都指向运行时所在的不同对象。

不适用场合

由于箭头函数使得this从“动态”变成“静态”,下面两个场合不应该使用箭头函数。
第一个场合是定义对象的方法,且该方法内部包括this。

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;// 对象不构成单独的作用域,jumps箭头函数定义时的作用域就是全局作用域。
  }
}
// 如果是普通函数,该方法内部的this指向cat;
// 如果写成上面那样的箭头函数,使得this指向全局对象
globalThis.s = 21;

const obj = {
  s: 42,
  m: () => console.log(this.s) //  this指向全局对象
};

obj.m() // 21

// 这个例子中,obj.m()使用箭头函数定义。
// JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给obj.m,
// 这导致箭头函数内部的this指向全局对象,所以obj.m()输出的是全局空间的21,
// 而不是对象内部的42。

对象的属性建议使用传统的写法定义,不要用箭头函数定义。
第二个场合是需要动态this的时候,也不应使用箭头函数。

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');   // this是全局对象
});

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

数组的扩展

扩展运算符

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

扩展运算符的应用

复制数组

const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

合并数组

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
// 这两种方法都是浅拷贝 使用的时候需要注意

与解构赋值结合

// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list

const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest  // []

const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

字符串

[...'hello']
// [ "h", "e", "l", "l", "o" ]

实现了 Iterator 接口的对象

任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组

let nodeList = document.querySelectorAll('div');
let array = [...nodeList];

上面代码中,querySelectorAll方法返回的是一个NodeList对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator 。

Map 和 Set 结构,Generator 函数

Array.from

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
只要是部署了 Iterator 接口的数据结构,Array.from都能将其转为数组。

Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']

let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

Array.of()

Array.of()方法用于将一组值,转换为数组。

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1

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号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]

// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]

// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}

// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]

// 对于没有部署 TypedArray 的 copyWithin 方法的平台
// 需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]

find() 和 findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。

[1, 4, -5, 10].find((n) => n < 0)
// -5

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

fill()

entries(),keys() 和 values()

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

includes()

flat(),flatMap()

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

at()

数组的空位

ES6 则是明确将空位转为undefined。

对象的扩展

属性的简洁表示法

super 关键字

ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo; // 通过super.foo引用了原型对象proto的foo属性
  }
};

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

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

const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};

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

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"  // super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj

对象的扩展运算符

对象的扩展运算符(…)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}

{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}

扩展运算符可以用于合并两个对象

let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);

对象的新增方法

Object.is()

Object.assign()

Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

const target = { a: 1 };

const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

proto属性,Object.setPrototypeOf(),Object.getPrototypeOf()

Object.setPrototypeOf()

用来设置一个对象的原型对象(prototype)。它是 ES6 正式推荐的设置原型对象的方法。

// 格式
Object.setPrototypeOf(object, prototype)
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);

proto.y = 20;
proto.z = 40;

obj.x // 10
obj.y // 20
obj.z // 40

// 上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。

运算符的扩展

指数运算符

ES2016 新增了一个指数运算符(**)。

2 ** 2 // 4
2 ** 3 // 8

// 多个指数运算符连用时,是从最右边开始计算的。
// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

let b = 4;
b **= 3;
// 等同于 b = b * b * b;

链判断运算符

Null 判断运算符

ES2020 引入了一个新的 Null 判断运算符??。它的行为类似||,但是只有运算符左侧的值为null或undefined时,才会返回右侧的值。

const headerText = response.settings.headerText ?? 'Hello, world!'; // 默认值只有在左侧属性值为null或undefined时,才会生效

Symbol

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。保证每个属性的名字都是独一无二的,从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)

Set 和 Map 数据结构

Set

基本用法

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

const s = new Set();   // Set本身是一个构造函数,用来生成 Set 数据结构。

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4  结果表明 Set 结构不会添加重复的值。
// 可用于 去除数组重复成员
[...new Set(array)]
// 去除字符串里面的重复字符。
[...new Set('ababbc')].join('')
// "abc"

Set 实例的属性和方法

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。
    const items = new Set([1, 2, 3, 4, 5]);
    const array = Array.from(items);   // Array.from方法可以将 Set 结构转为数组。
    

    遍历的应用

    扩展运算符(…)内部使用for…of循环,所以也可以用于 Set 结构。
    let set = new Set(['red', 'green', 'blue']);
    let arr = [...set];
    // ['red', 'green', 'blue']
    
    扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。
    let arr = [3, 5, 2, 2, 5, 5];
    let unique = [...new Set(arr)];
    // [3, 5, 2]
    
    数组的map和filter方法也可以间接用于 Set ```javascript let set = new Set([1, 2, 3]); set = new Set([…set].map(x => x * 2)); // 返回Set结构:{2, 4, 6}

let set = new Set([1, 2, 3, 4, 5]); set = new Set([…set].filter(x => (x % 2) == 0)); // 返回Set结构:{2, 4}

<a name="MCjpV"></a>
## WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。<br />首先,WeakSet 的成员只能是对象,而不能是其他类型的值。<br />WeakSet 结构有以下三个方法。

- **WeakSet.prototype.add(value)**:向 WeakSet 实例添加一个新成员。
- **WeakSet.prototype.delete(value)**:清除 WeakSet 实例的指定成员。
- **WeakSet.prototype.has(value)**:返回一个布尔值,表示某个值是否在 
<a name="kNgmN"></a>
## Map
ES6 提供了 Map 数据结构。它类似于对象,也是**键值对的集合**,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
```javascript
const map = new Map();

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

// 表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此get方法无法读取该键,返回undefined

Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

与其他数据结构的互相转换

Map 转为数组

Map 转为数组最方便的方法,就是使用扩展运算符(…)

const myMap = new Map()
  .set(true, 7)
  .set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]

数组 转为 Map

将数组传入 Map 构造函数,就可以转为 Map。

new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])

Map 转为对象

如果所有 Map 的键都是字符串,它可以无损地转为对象。

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

对象转为 Map

对象转为 Map 可以通过Object.entries()

let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));

Map 转为 JSON

JSON 转为 Map

Proxy…看不懂

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler); 

// new Proxy()表示生成一个Proxy实例,
// target参数表示所要拦截的目标对象,
// handler参数也是一个对象,用来定制拦截行为。

Reflect…看不懂

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。

Promise 对象

Promise 的含义

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

基本用法

ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
let promise = new Promise(function(resolve, reject) {
  console.log('Promise');  // 首先输出的是Promise
  resolve();
});

promise.then(function() {
  console.log('resolved.');   // then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行
});

console.log('Hi!');

// Promise
// Hi!
// resolved

上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。
下面是异步加载图片的例子。

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

Iterator 和 for…of 循环

Iterator(遍历器)的概念

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set.
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。

原生具备 Iterator 接口的数据结构如下

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

    调用 Iterator 接口的场合

    解构赋值

    对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。 ```javascript let set = new Set().add(‘a’).add(‘b’).add(‘c’);

let [x,y] = set; // x=’a’; y=’b’

let [first, …rest] = set; // first=’a’; rest=[‘b’,’c’];

<a name="TmWll"></a>
### 扩展运算符
```javascript
// 例一
var str = 'hello';
[...str] //  ['h','e','l','l','o']

// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']

只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组

yield*

yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

字符串的 Iterator 接口

Generator 函数的语法

简介

基本概念

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)

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

var hw = helloWorldGenerator();

// 调用 Generator 函数后,该函数并不执行,
// 返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)

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 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

yield 表达式

yield表达式就是暂停标志.
Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。

function* f() {
  console.log('执行了!')
}

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);

// Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

Generator 函数的异步应用

异步编程对 JavaScript 语言太重要。JavaScript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可。

传统方法

ES6 诞生以前,异步编程的方法,大概有下面四种。

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象

Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。

基本概念

异步

所谓”异步”,简单说就是一个任务不是连续完成的.
相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务。

回调函数

JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。回调函数的英语名字callback,直译过来就是”重新调用“。
读取文件进行处理,是这样写的。

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log(data);
});

// readFile函数的第三个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执行。

Promise

多重嵌套,多个异步操作形成了强耦合——“回调函数地狱”(callback hell)
Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用 Promise,连续读取多个文件,写法如下。

var readFile = require('fs-readfile-promise');   // 使用了fs-readfile-promise模块 返回一个 Promise 版本的readFile函数

readFile(fileA)
.then(function (data) {   // Promise 提供then方法加载回调函数
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {  // catch方法捕捉执行过程中抛出的错误
  console.log(err);
});

Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。

Generator 函数

协程

协程有点像函数,又有点像线程。它的运行流程大致如下。

  • 第一步,协程A开始执行。
  • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
  • 第三步,(一段时间后)协程B交还执行权。
  • 第四步,协程A恢复执行。

上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。
举例来说,读取文件的协程写法如下。

function* asyncJob() {  // 函数asyncJob是一个协程
  // ...其他代码
  var f = yield readFile(fileA);   // yield命令, 表示执行到此处,执行权将交给其他协程。是异步两个阶段的分界线
  // ...其他代码
}

协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

协程的 Generator 函数实现

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。Generator 函数的执行方法如下。

function* gen(x) {
  var y = yield x + 2;
  return y;
}

var g = gen(1);  // 调用 Generator 函数,会返回一个内部指针(即遍历器)g; 执行Generator 函数不会返回结果,返回的是指针对象
g.next() // { value: 3, done: false }  每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性); value属性是yield语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
g.next() // { value: undefined, done: true }

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

Generator 函数的数据交换和错误处理

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换错误处理机制
next返回值的 value 属性,是 Generator 函数向外输出数据;next方法还可以接受参数,向 Generator 函数体内输入数据。

function* gen(x){
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }   // 第一个next方法的value属性,返回表达式x + 2的值3
g.next(2) // { value: 2, done: true } 
// 第二个next方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量y接收

异步任务的封装

var fetch = require('node-fetch');  

function* gen(){   // Generator 函数封装了一个异步操作
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);  // 先读取一个远程接口  Fetch模块返回的是一个 Promise 对象
  console.log(result.bio);  // 然后从 JSON 格式的数据解析信息
}

// 执行这段代码的方法如下

var g = gen();  // 首先执行 Generator 函数,获取遍历器对象
var result = g.next();  // 使用next方法,执行异步任务的第一阶段 yield fetch(url)

result.value.then(function(data){  // Fetch模块返回的是一个 Promise 对象, 要用then方法调用下一个next方法
  return data.json();
}).then(function(data){   
  g.next(data);
});

Thunk 函数

Thunk 函数是自动执行 Generator 函数的一种方法。

参数的求值策略

Thunk 函数的含义

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

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

f(x + 5);

// 等同于

var thunk = function () {  // 函数 f 的参数x + 5被一个函数替换了。凡是用到原参数的地方,对Thunk函数求值即可。
  return x + 5;
};

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

JavaScript 语言的 Thunk 函数

?

Generator 函数的流程管理

async 函数(不懂。。。。

ES2017 标准引入了 async 函数使得异步操作变得更加方便
async 函数是什么?一句话,它就是 Generator 函数的语法糖

例子:一个 Generator 函数,依次读取两个文件

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

//上面代码的函数gen可以写成 async 函数,就是下面这样

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

// 一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

基本用法

不懂。。。。

Class 的基本语法

Class 的继承

简介

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)  super关键字,它在这里表示父类的构造函数,用来新建父类的this对象
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。

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

super 关键字

第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数.

class A {}

class B extends A {
  constructor() {
    super();  // 子类B的构造函数之中的super(),代表调用父类的构造函数  必须的,否则 JavaScript 引擎会报错
  }
}

// super虽然代表了父类A的构造函数,但是返回的是子类B的实例,
// 即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。
class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();  // new.target指向当前正在执行的函数; super()内部的this指向的是B  
  }
}
new A() // A
new B() // B

作为函数时,super() 只能用在子类的构造函数之中,用在其他地方就会报错。
第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2 ;   将super当作一个对象使用  super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()
  }
}

let b = new B();

类的 prototype 属性和proto属性

Class 作为构造函数的语法糖,同时有prototype属性和proto属性,因此同时存在两条继承链。
(1)子类的proto属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的proto属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

ES6 允许继承原生构造函数定义子类,这是 ES5 无法做到的,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。

const a = {
  a: 'a'
};
const b = {
  b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}   // c对象是a对象和b对象的合成,具有两者的接口。

Module 的语法

概述

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

严格模式

ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this。

export 命令

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

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,  // export输出的变量就是本来的名字,但是可以使用as关键字重命名。
  v2 as streamV2,
  v2 as streamLatestVersion
};

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

// 正确
export function f() {};

// 正确
function f() {}
export {f};

import 命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

// main.js
import { firstName, lastName, year } from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

由于import是静态执行,所以不能使用表达式和变量

模块的整体加载

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

import * as circle from './circle'; 

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

export default 命令

模块指定默认输出

// export-default.js  一个模块文件, 它的默认输出是一个函数
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';  // 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
customName(); // 'foo'

Module 的加载实现

浏览器加载

传统方法

defer 与async 的区别是:defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer 是“渲染完再执行”,async 是“下载完就执行”。另外,如果有多个defer 脚本,会按照它们在页面出现的顺序加载,而多个async 脚本是不能保证加载顺序的。

加载规则

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

// 由于type属性设为module,所以浏览器知道这是一个 ES6 模块

浏览器对于带有type=”module”的