es-next-1

ES6 简介

语法提案的批准流程

任何人都可以向标准委员会(又称 TC39 委员会)提案,要求修改语言标准。

一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。

一个提案只要能进入 Stage 2,就差不多肯定会包括在以后的正式标准里面。ECMAScript 当前的所有提案,可以在 TC39 的官方网站 GitHub.com/tc39/ecma262 查看。

ECMAScript 的历史

ES6 从开始制定到最后发布,整整用了 15 年。

前面提到,ECMAScript 1.0 是 1997 年发布的,接下来的两年,连续发布了 ECMAScript 2.0(1998 年 6 月)和 ECMAScript 3.0(1999 年 12 月)。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学 3.0 版的语法。

部署进度

各大浏览器的最新版本,对 ES6 的支持可以查看 kangax.github.io/compat-table/es6/。随着时间的推移,支持度已经越来越高了,超过 90% 的 ES6 语法特性都实现了。

Node 是 JavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看 Node 已经实现的 ES6 特性。

  // Linux & Mac
  $ node --v8-options | grep harmony
  
  // Windows
  $ node --v8-options | findstr harmony

Let 命令和 Const 命令

Let 命令

ES6 新增了 let 命令,用来声明变量。它的用法类似于 var,但是所声明的变量,只在 let 命令所在的代码块内有效。

  {
    let a = 10;
    var b = 1;
  }

  a // ReferenceError: a is not defined.
  b // 1

不存在变量提升

var 命令会发生“变量提升”现象,即变量可以在声明之前使用,值为 undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

为了纠正这种现象,let 命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

  // var 的情况
  console.log(foo); // 输出undefined
  var foo = 2;

  // let 的情况
  console.log(bar); // 报错ReferenceError
  let bar = 2;

暂时性死区

只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代码中,存在全局变量 tmp,但是块级作用域内 let 又声明了一个局部变量 tmp,导致后者绑定这个块级作用域,所以在 let 声明变量前,对 tmp 赋值会报错。

ES6 明确规定,如果区块中存在 letconst 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

  if (true) {
    // TDZ开始
    tmp = 'abc'; // ReferenceError
    console.log(tmp); // ReferenceError

    let tmp; // TDZ结束
    console.log(tmp); // undefined

    tmp = 123;
    console.log(tmp); // 123
  }

“暂时性死区”也意味着 typeof 不再是一个百分之百安全的操作。

  typeof x; // ReferenceError
  let x;

作为比较,如果一个变量根本没有被声明,使用 typeof 反而不会报错。

  typeof undeclared_variable // "undefined"

所以,在没有 let 之前,typeof 运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。

有些“死区”比较隐蔽

  function bar(x = y, y = 2) {
    return [x, y];
  }
  
  bar(); // 报错

上面代码中,调用 bar 函数之所以报错(某些实现可能不报错),是因为参数 x 默认值等于另一个参数 y,而此时 y 还没有声明,属于“死区”。如果 y 的默认值是 x,就不会报错,因为此时 x 已经声明了。

  function bar(x = 2, y = x) {
    return [x, y];
  }
  bar(); // [2, 2]

另外,下面的代码也会报错,与 var 的行为不同。

// 不报错
var x = x;

// 报错
let x = x;
// ReferenceError: x is not defined

上面代码报错,也是因为暂时性死区。使用 let 声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量 x 的声明语句还没有执行完成前,就去取 x 的值,导致报错”x 未定义“。

ES6 规定暂时性死区和 letconst 语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

不允许重复声明

let 不允许在相同作用域内,重复声明同一个变量。

  // 报错
  function func() {
    let a = 10;
    var a = 1;
  }

  // 报错
  function func() {
    let a = 10;
    let a = 1;
  }

因此,不能在函数内部重新声明参数。

  function func(arg) {
    let arg; // 报错
  }
  
  function func(arg) {
    {
      let arg; // 不报错
    }
  }

函数的形参,也可以理解为在函数作用域内,使用 let 声明一个局部变量,所以也不允许在函数作用域内,重复声明与函数形参同名的变量。

块级作用域

为什么需要块级作用域?

ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

第一种场景,内层变量可能会覆盖外层变量。

  var tmp = new Date();

  function f() {
    console.log(tmp);
    if (false) {
      var tmp = 'hello world';
    }
  }

  f(); // undefined

上面代码的原意是,if 代码块的外部使用外层的 tmp 变量,内部使用内层的 tmp 变量。但是,函数 f 执行后,输出结果为 undefined,原因在于变量提升,导致内层的 tmp 变量覆盖了外层的 tmp 变量。

第二种场景,用来计数的循环变量泄露为全局变量。

  var s = 'hello';

  for (var i = 0; i < s.length; i++) {
    console.log(s[i]);
  }

  console.log(i); // 5

上面代码中,变量 i 只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

ES6 的块级作用域

块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。

  // IIFE 写法
  (function () {
    var tmp = ...;
    ...
  }());

  // 块级作用域写法 只有 let、const 支持
  {
    let tmp = ...;
    ...
  }

块级作用域与函数声明

ES6 规定了可以在块级作用域使用函数声明,表现和 let const 一致,不会提升到外部。但是浏览器为了兼容旧的代码,不一定会支持

考虑到环境导致的行为差异太大,应该 避免 在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

块级作用域必须有大括号

// 第一种写法,报错
if (true) let x = 1;

// 第二种写法,不报错
if (true) {
  let x = 1;
}

上面代码中,第一种写法没有大括号,所以不存在块级作用域,而 let 只能出现在当前作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立。

函数声明也是如此,严格模式下,函数只能声明在当前作用域的顶层。

// 不报错
'use strict';
if (true) {
  function f() {}
}

// 报错
'use strict';
if (true)
  function f() {}

Const 命令

基本用法

const 声明一个只读的常量。一旦声明,常量的值就不能改变。这意味着,const 一旦声明变量,就必须立即初始化,不能留到以后赋值。

  const PI = 3.1415;
  PI // 3.1415

  PI = 3;
  // TypeError: Assignment to constant variable.

对于 const 来说,只声明不赋值,就会报错。

  const foo;
  // SyntaxError: Missing initializer in const declaration

const 的作用域与 let 命令相同:只在声明所在的块级作用域内有效。

  if (true) {
    const MAX = 5;
  }

  MAX // Uncaught ReferenceError: MAX is not defined

const 命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

  if (true) {
    console.log(MAX); // ReferenceError
    const MAX = 5;
  }

const 声明的常量,也与 let 一样不可重复声明。

  var message = "Hello!";
  let age = 25;
  
  // 以下两行都会报错
  const message = "Goodbye!";
  const age = 30;

本质

const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个 内存地址 所保存的 数据不得改动

对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。

但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的 数据结构 是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

  const foo = {};

  // 为 foo 添加一个属性,可以成功
  foo.prop = 123;
  foo.prop // 123

  // 将 foo 指向另一个对象,就会报错
  foo = {}; // TypeError: "foo" is read-only

上面代码中,常量 foo 储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把 foo 指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

  const a = [];
  a.push('Hello'); // 可执行
  a.length = 0;    // 可执行
  a = ['Dave'];    // 报错

上面代码中,常量 a 是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给 a,就会报错。

es-object

ES6 声明变量的六种方法

ES5 只有两种声明变量的方法:var 命令和 function 命令。ES6 除了添加 letconst 命令,后面章节还会提到,另外两种声明变量的方法:import 命令和 class 命令。所以,ES6 一共有 6 种声明变量的方法。

循环中的 Let 和 Const

For 循环

  var a = [];
  for (let i = 0; i < 10; i++) {
    a[i] = function () {
      console.log(i);
    };
  }
  a[6](); // 6

问题在于,上面讲了 let 不提升,不能重复声明,不能绑定全局作用域等等特性,可是为什么在这里就能正确打印出 i 值呢?

如果是不重复声明,在循环第二次的时候,又用 let 声明了 i,应该报错呀,就算因为某种原因,重复声明不报错,一遍一遍迭代,i 的值最终还是应该是 3 呀,还有人说 for 循环的设置循环变量的那部分是一个单独的作用域,就比如:

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

这个例子是对的,如果我们把 let 改成 var 呢?

for (var i = 0; i < 3; i++) {
  var i = 'abc'
  console.log(i);
}
// abc
// 只打印了一个,因为内部的声明覆盖了i,NaN < 3 返回 false,循环结束

为什么结果就不一样了呢,如果有单独的作用域,结果应该是相同的呀……

如果要追究这个问题,就要抛弃掉之前所讲的这些特性!这是因为 let 声明在循环内部的行为是标准中专门定义的,不一定就与 let 的不提升特性有关,其实,在早期的 let 实现中就不包含这一行为。

我们会发现,在 for 循环中使用 let 和 var,底层会使用不同的处理方式。

那么当使用 let 的时候底层到底是怎么做的呢?

简单的来说,就是在 for (let i = 0; i < 3; i++) 中,即圆括号之内建立一个隐藏的作用域。

然后 每次迭代循环时都创建一个新变量,并以之前迭代中同名变量的值将其初始化。这样对于下面这样一段代码

var funcs = [];
for (let i = 0; i < 3; i++) {
    funcs[i] = function () {
        console.log(i);
    };
}
funcs[0](); // 0

相当于

  // 伪代码
  (let i = 0) {
    funcs[0] = function() {
      console.log(i)
    };
  }

  (let i = 1) {
    funcs[1] = function() {
      console.log(i)
    };
  }

  (let i = 2) {
    funcs[2] = function() {
      console.log(i)
    };
  };

当执行函数的时候,根据词法作用域就可以找到正确的值,其实你也可以理解为 let 声明模仿了闭包的做法来简化循环过程。

注意

For in 循环

那下面的结果是什么呢?

  var funcs = [], object = {a: 1, b: 1, c: 1};
  for (var key in object) {
      funcs.push(function(){
          console.log(key)
      });
  }

  funcs[0]()

结果是 'c';

那如果把 var 改成 let 或者 const 呢?

使用 let,结果自然会是 'a',const 呢? 报错还是 'a'?

结果是正确打印 'a',这是因为在 for in 循环中,每次迭代不会修改已有的绑定,而是会创建一个新的绑定

for of 循环 类似

var funcs = [], arr = [1,2,3];
for (var key of arr) {
    funcs.push(function(){
        console.log(key)
    });
}

funcs[0]() // 3

var funcs = [], arr = [1,2,3];
for (let key of arr) {
    funcs.push(function(){
        console.log(key)
    });
}

funcs[0]() // 1

Babel

在 Babel 中是如何编译 let 和 const 的呢?我们来看看编译后的代码:

let value = 1;

编译为:

var value = 1;

们可以看到 Babel 直接将 let 编译成了 var,如果是这样的话,那么我们来写个例子:

if (false) {
    let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined

如果还是直接编译成 var,打印的结果肯定是 undefined,然而 Babel 很聪明,它编译成了:

if (false) {
    var _value = 1;
}
console.log(value);

我们再写个直观的例子:

let value = 1;
{
    let value = 2;
}
value = 3;

编译成:

  var value = 1;
  {
      var _value = 2;
  }
  value = 3;

本质是一样的,就是改变量名,使内外层的变量名称不一样。

那像 const 的修改值时报错,以及重复声明报错怎么实现的呢?

其实就是在编译的时候直接给你报错……

循环中的 Let 声明

  var funcs = [];
  for (let i = 0; i < 10; i++) {
      funcs[i] = function () {
          console.log(i);
      };
  }
  funcs[0](); // 0

Babel 巧妙的编译成了:

  var funcs = [];

  var _loop = function _loop(i) {
      funcs[i] = function () {
          console.log(i);
      };
  };

  for (var i = 0; i < 10; i++) {
      _loop(i);
  }
  funcs[0](); // 0

利用闭包

顶层对象的属性

顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象。ES5 之中,顶层对象的属性与全局变量是等价的。

  window.a = 1;
  a // 1

  a = 2;
  window.a // 2

顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题

ES6 为了改变这一点,一方面规定,为了保持兼容性,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

  var a = 1;
  // 如果在 Node 的 REPL 环境,可以写成 global.a
  // 或者采用通用方法,写成 this.a
  window.a // 1

  let b = 1;
  window.b // undefined

上面代码中,全局变量 avar 命令声明,所以它是顶层对象的属性;全局变量 blet 命令声明,所以它不是顶层对象的属性,返回 undefined

let 和 const 定义的变量不会成为全局对象的属性,因为他们在块级作用域生效,而 ES6 全部基于模块

看下面一个例子,像这种情况只有 max 是 let 声明的,其他都是省略了 var 声明而成为全局属性

  let max = min = curMax = curMin =  null
  // 这样写即可
  let max,min,curMax,curMin

Global 对象

很难找到一种方法,可以在所有情况下,都取到顶层对象。现在有一个提案,在语言标准的层面,引入 global 作为顶层对象。也就是说,在所有环境下,global 都是存在的,都可以从它拿到顶层对象。

变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

解构赋值会沿着原型链 @@@

数组的解构赋值

基本用法

let [a, b, c] = [1, 2, 3];

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

如果解构不成功,变量的值就等于 undefined

let [foo] = [];
let [bar, foo] = [1];

不完全解构

即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

不可遍历结构

如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。

事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。原生具有 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

默认值

解构赋值允许指定默认值。

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

注意,ES6 内部使用严格相等运算符(\===),判断一个位置是否有值。所以,只有当一个数组成员 严格等于 undefined,默认值才会生效。

let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null

上面代码中,如果一个数组成员是 null,默认值就不会生效,因为 null 不严格等于 undefined

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

对象的解构赋值

解构不仅可以用于数组,还可以用于对象。

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

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

如果变量名与属性名不一致,必须写成下面这样。

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

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。

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

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。 @@@

let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined

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

注意

嵌套结构的解构

与数组一样,解构也可以用于嵌套结构的对象。

    let obj = {
      p: [
        'Hello',
        { y: 'World' }
      ]
    };

    let { p: [x, { y }] } = obj;
    x // "Hello"
    y // "World"

注意,这时 p模式,不是变量,因此不会被赋值。如果 p 也要作为变量赋值,可以写成下面这样。

  let obj = {
    p: [
      'Hello',
      { y: 'World' }
    ]
  };
  
  let { p, p: [x, { y }] } = obj;
  x // "Hello"
  y // "World"
  p // ["Hello", {y: "World"}]

类似的

对象的解构也可以指定默认值。

默认值生效的条件是,对象的属性值严格等于 undefined

如果解构失败,变量的值等于 undefined

注意

如果要将一个已经声明的变量用于解构赋值,必须非常小心。

  // 错误的写法
  let x;
  {x} = {x: 1};
  // SyntaxError: syntax error

上面代码的写法会报错,因为 JavaScript 引擎会将 {x} 理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

// 正确的写法
let x;
({x} = {x: 1});

;([referLine,currentLine] = [currentLine,referLine])

上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。

运用

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

let { log, sin, cos } = Math;

上面代码将 Math 对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。

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

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

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

搭配扩展运算符

见扩展运算符一章

字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

  const [a, b, c, d, e] = 'hello';
  a // "h"
  b // "e"
  c // "l"
  d // "l"
  e // "o"

类似数组的对象都有一个 length 属性,因此还可以对这个属性解构赋值。

  let {length : len} = 'hello';
  len // 5

数值、布尔值、null、undefined 解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先 转为对象

  let {toString: s} = 123;
  s === Number.prototype.toString // true

  let {toString: s} = true;
  s === Boolean.prototype.toString // true

上面代码中,数值和布尔值的包装对象都有 toString 属性,因此变量 s 都能取到值。

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值,都会报错。

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

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

function add([x, y]){
  return x + y;
}

add([1, 2]); // 3

上面代码中,函数 add 的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量 xy。对于函数内部的代码来说,它们能感受到的参数就是 xy

下面是另一个例子。

  [[1, 2], [3, 4]].map(([a, b]) => a + b);
  // [ 3, 7 ]

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

  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]

上面代码中,函数 move 的参数是一个对象,通过对这个对象进行解构,得到变量 xy 的值。如果解构失败,xy 等于默认值。

注意

用途

变量的解构赋值用途很多。

交换变量的值

  let x = 1;
  let y = 2;
  
  [x, y] = [y, x];
  
  // 临时变量
  var _ref = [y, x];
  x = _ref[0];
  y = _ref[1];

上面代码交换变量 xy 的值,这样的写法不仅简洁,而且易读,语义非常清晰。

从函数返回多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

  // 返回一个数组
  
  function example() {
    return [1, 2, 3];
  }
  let [a, b, c] = example();
  
  // 返回一个对象
  
  function example() {
    return {
      foo: 1,
      bar: 2
    };
  }
  let { foo, bar } = example();
  
  // 临时变量
  var _example = example(),
      foo = _example.foo,
      bar = _example.bar;

函数参数的定义

解构赋值可以方便地将一组参数与变量名对应起来。

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

提取 JSON 数据

解构赋值对提取 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]

上面代码可以快速提取 JSON 数据的值。

函数参数的默认值

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

指定参数的默认值,就避免了在函数体内部再写 var foo = config.foo || 'default foo'; 这样的语句。

遍历 Map 结构

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

  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

如果只想获取键名,或者只想获取键值,可以写成下面这样。

  // 获取键名
  for (let [key] of map) {
    // ...
  }
  
  // 获取键值
  for (let [,value] of map) {
    // ...
  }

输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

  const { SourceMapConsumer, SourceNode } = require("source-map");

扩展语法

Not a Operator

https://stackoverflow.com/questions/44934828/is-it-spread-syntax-or-the-spread-operator

语法

扩展语法(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的 参数序列隐式遍历

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

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

该运算符主要用于函数调用。

function push(array, ...items) {
  array.push(...items);
}

function add(x, y) {
  return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42

注意

扩展运算符后面还可以放置表达式。

  const arr = [
    ...(x > 0 ? ['a'] : []),
    'b',
  ];

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

  [...[], 1]
  // [1]

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

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

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

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

上面前两种情况都会报错,因为扩展运算符所在的括号不是函数调用,而第三种情况 console.log(...[1, 2]) 就不会报错,因为这时是函数调用。

替代函数的 Apply 方法

由于扩展运算符可以展开数组,所以不再需要 apply 方法,将数组转为函数的参数了。

  // ES5 的写法
  function f(x, y, z) {
    // ...
  }
  var args = [0, 1, 2];
  f.apply(null, args);

  // ES6的写法
  function f(x, y, z) {
    // ...
  }
  let args = [0, 1, 2];
  f(...args);

下面是扩展运算符取代 apply 方法的一个实际的例子,应用 Math.max 方法,简化求出一个数组最大元素的写法。

  // ES5 的写法
  Math.max.apply(null, [14, 3, 77])

  // ES6 的写法
  Math.max(...[14, 3, 77])

  // 等同于
  Math.max(14, 3, 77);

上面代码中,由于 JavaScript 不提供求 数组最大元素的函数,所以只能套用 Math.max 函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用 Math.max 了。

另一个例子是通过 push 函数,将一个数组添加到另一个数组的尾部。

ES5 写法中,push 方法的参数不能是数组,所以只好通过 apply 方法变通使用 push 方法。有了扩展运算符,就可以直接将数组传入 push 方法。

下面是另外一个例子。

  // ES5
  new (Date.bind.apply(Date, [null, 2015, 1, 1]))
  // ES6
  new Date(...[2015, 1, 1]);

扩展运算符的应用

复制数组

合并数组

扩展运算符提供了数组合并的新写法。

  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' ]
  // babel
  [].concat(arr1, arr2, arr3);

不过,这两种方法都是浅拷贝,使用的时候需要注意。

上面代码中,a3a4 是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了原数组的成员,会同步反映到新数组。

与解构赋值结合

扩展运算符可以与解构赋值结合起来,用于生成数组

  // 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   // []

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

  const [...butLast, last] = [1, 2, 3, 4, 5];
  // 报错
  
  const [first, ...middle, last] = [1, 2, 3, 4, 5];
  // 报错

字符串

扩展运算符还可以将字符串转为真正的数组。

实现了 Iterator 接口的对象、Map 和 Set 结构,Generator 函数

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

《数组的扩展》一章中,已经介绍过扩展运算符(...)。ES2018 将这个运算符引入了对象。

对象的扩展运算符

ES2018 之后也可以用于对象

主要的应用场景是 解构赋值和浅拷贝

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

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

由于解构赋值要求等号右边是一个对象,所以如果等号右边是 undefinednull,就会报错,因为它们无法转为对象。

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

解构赋值必须是最后一个参数,否则会报错。

let { ...x, y, z } = someObject; // 句法错误
let { x, ...y, ...z } = someObject; // 句法错误

注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2

上面代码中,x 是解构赋值所在的对象,拷贝了对象 obja 属性。a 属性引用了一个对象,修改这个对象的值,会影响到解构赋值对它的引用。

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

let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined

上面代码中,对象 o3 复制了 o2,但是只复制了 o2 自身的属性,没有复制它的原型对象 o1 的属性。

简化默认配置对象

const commonOptions = {
  value: "function myScript(){return 100;}\n",
  mode:  "javascript",
  theme:'material',
  lineNumbers: true,
  lineWrapping:true,
  foldGutter: true,
  gutters:["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
}

const editor = CodeMirror(cm_container.current,commonOptions)
const receiver  = CodeMirror(cm_receiver.current,{
  ...commonOptions,
  readOnly:true
})

以前的方法是通过 Object.assign({},commonOptions,restOptions)

条件扩展

const obj = {
    a: 123,
}

const foo = {
    b: 123,
    ...(obj.a && obj)
}

总结 @@@

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的 参数序列隐式遍历,虽然是参数列表,但包一层 [] 又可以轻松的转换为数组。ES2018 之后也可以用于对象

数值的扩展

Math 对象的扩展

ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。

指数运算符

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

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

这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。 #

  // 相当于 2 ** (3 ** 2)
  2 ** 3 ** 2
  // 512

上面代码中,首先计算的是第二个指数运算符,而不是第一个。

指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。

  let a = 1.5;
  a **= 2;
  // 等同于 a = a * a;

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

注意,V8 引擎的指数运算符与 Math.pow 的实现不相同,对于特别大的运算结果,两者会有细微的差异。

Math.pow(99, 99)
// 3.697296376497263e+197

99 ** 99
// 3.697296376497268e+197

上面代码中,两个运算结果的最后一位有效数字是有差异的。

Symbol 2023-05-06

概述

参数

注意

作为属性名的 Symbol

用途

实例:消除魔术字符串

属性名的遍历

Symbol.for(),Symbol.keyFor()

实例:模块的 Singleton 模式

内置的 Symbol 值

Symbol.hasInstance

Symbol.isConcatSpreadable

Symbol.species

Symbol.match

Symbol.replace

Symbol.split

Symbol.iterator

Symbol.toPrimitive

Symbol.toStringTag

Symbol.unscopables

Set 和 Map 数据结构

Set

基本用法

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

Set 本身是一个 构造函数,用来生成 Set 数据结构。

const s = new 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
// 上面代码通过add()方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。

Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来 初始化。 @@@

// 例一
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

// 上面代码中,例一和例二都是Set函数接受数组作为参数,例三是接受类似数组的对象作为参数。

Set 实例的属性和方法

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

四个 操作方法

四个遍历方法

foreach

遍历的应用

WeakSet

含义

语法

WeakSet 结构的三个方法。

Map

含义和基本用法

 let map = new Map();
 
 map.set(-0, 123);
 map.get(+0) // 123
 
 map.set(true, 1);
 map.set('true', 2);
 map.get(true) // 1
 
 map.set(undefined, 3);
 map.set(null, 4);
 map.get(undefined) // 3
 
 map.set(NaN, 123);
 map.get(NaN) // 123

实例的属性和操作方法

size 属性

set(key, value)

get(key)

has(key)

delete(key)

clear()

遍历方法

Map 结构原生提供三个 遍历器生成函数 和一个 遍历方法

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

Map 转为数组

数组 转为 Map

Map 转为对象

对象转为 Map

Map 转为 JSON

JSON 转为 Map

WeakMap

含义

WeakMap 的语法

WeakMap 的示例

WeakMap 的用途

FAQ

#faq/js

使用闭包实现每隔一秒打印 1,2,3,4

link 一下

// 使用闭包实现
for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}

// 使用 let 块级作用域

for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}