> For the complete documentation index, see [llms.txt](https://chanshiyu.gitbook.io/blog/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://chanshiyu.gitbook.io/blog/shu-zhai/es6-biao-zhun-ru-men/01-bian-liang-sheng-ming-yu-jie-gou-fu-zhi.md).

# 01 变量声明与解构赋值

柏林已经来了命令，阿尔萨斯和洛林的学校只许教 ES6 了...他转身朝着黑板，拿起一支粉笔，使出全身的力量，写了两个大字：“ES6 万岁！”（《最后一课》）。

阮一峰的《ES6 标准入门》第二版和第三版都有购入，第二版是去年买的实体书，当初大略翻了一遍，今年第三版又出世了，在原来的基础上新增了不少内容，是时候重拾书本学习了。

> Any application that can be written in JavaScript will eventually be written in JavaScript. --Jeff Atwood

## let 和 const

### 基本用法

let 和 const 声明变量的三大特性：**不存在变量提升、暂时性死区、不允许重复声明**。

#### 不存在变量提升

let 声明的变量一定要在声明后使用，否则便会报错。

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

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

#### 暂时性死区

ES6 规定，如果区块中存在 let 和 const 命令，则这个区块对这些命令声明的变量从一开始就形成封闭作用域。这在语法上称为“暂时性死区”（temporal dead zone，简称 TDZ）。

```javascript
var tmp = 123
if (true) {
  // TDZ 开始
  tmp = 'abc' // ReferenceError
  let tmp
  // TDZ 结束
  console.log(tmp) // undefined
}
```

**“暂时性死区”也意味着 typeof 不再是一个百分之百安全的操作**，变量用 let 声明的话，那么在声明之前引用会报错。作为比较，如果一个变量根本没有被声明，使用 typeof 反而不会报错。

```javascript
typeof x // ReferenceError
let x

typeof undeclared_variable // "undefined"
```

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

#### 不允许重复声明

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

```javascript
function foo(arg) {
  let arg // 报错
}

function bar(arg) {
  {
    let arg // 不报错
  }
}
```

#### 扩展

扩展 1：const 实际上保证的是变量指向的那个内存地址不得改动。如果真的想将对象冻结，应该使用 `Object.freeze` 方法。

```javascript
var constantize = (obj) => {
  Object.freeze(obj)
  Object.keys(obj).forEach((key, i) => {
    if (typeof obj[key] === 'object') {
      constantize(obj[key])
    }
  })
}
```

扩展 2：ES5 只有两种声明变量的方法：使用 var 命令和 function 命令。ES6 除了添加了 let 和 const 命令，还有另外两种声明变量的方法：import 命令和 class 命令。所以，**ES6 一共有 6 种声明变量的方法**。

### 块级作用域

ES5 只有全局作用域和函数作用域，没有块级作用域，容易出现变量覆盖和变量泄露的问题。

ES6 引入了块级作用域，明确允许在块级作用域之中声明函数。ES6 规定：**在块级作用域之中，函数声明语句的行为类似于 let，在块级作用域之外不可引用**。

块级作用域的出现，实际上使得获得广泛应用的立即执行匿名函数（IIFE）不再必要了。

但是由于这条规则会对旧代码产生很大影响。为了减轻因此产生的不兼容问题，浏览器可以不遵守这条规则，所以尽量避免在块级作用域内声明函数。

### 顶层对象的属性

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

ES6 为了改变这一点，一方面规定，为了保持兼容性，var 命令和 function 命令声明的全局变量依旧是顶层对象的属性；另一方面规定，let 命令、const 命令、class 命令声明的全局变量不属于顶层对象的属性。

```javascript
var a = 1
window.a // 1

let b = 1
window.b // undefined
```

### global 对象

ES5 的顶层对象在各种实现中是不统一的。

* 在浏览器中，顶层对象是 window，但 Node 和 Web Worker 没有 window。
* 在浏览器和 Web Worker 中，self 也指向顶层对象，但是 Node 没有 self。
* 在 Node 中，顶层对象是 global，但其他环境都不支持。

同一段代码为了能够在各种环境中都取到顶层对象，目前一般是使用 this 变量，但是也有局限性。

* 在全局环境中，this 会返回顶层对象。但是在 Node 模块和 ES6 模块中，this 返回的是当前模块。
* 对于函数中的 this，如果函数不是作为对象的方法运行，而是单纯作为函数运行，this 会指向顶层对象。但是严格模式下，this 会返回 undefined。
* 不管是严格模式，还是普通模式，`new Function('returnthis')()` 总会返回全局对象。但是，如果浏览器用了 CSP（Content Security Policy，内容安全政策），那么 eval、new Function 这些方法都可能无法使用。

综上所述，很难找到一种方法可以在所有情况下都取到顶层对象。以下是两种勉强可以使用的方法：

```javascript
// 方法一
typeof window !== 'undefined' ? window : typeof process === 'object' && typeof require === 'function' && typeof global === 'object' ? global : this

// 方法二
const getGlobal = function () {
  if (typeof self !== 'undefined') {
    return self
  }
  if (typeof window !== 'undefined') {
    return window
  }
  if (typeof global !== 'undefined') {
    return global
  }
  throw new Error('unable to locate global object')
}
```

## 变量的解构赋值

### 数组的解构赋值

解构（Destructuring）属于“模式匹配”，只要等号两边的模式相同，左边的变量就会被赋予对应的值。

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

// 不完全解构
let [a, b] = [1, 2, 3]
a // 1
b // 2
```

对于 Set 结构，也可以使用数组的解构赋值。

```javascript
let [x, y, z] = new Set(['a', 'b', 'c'])
x // "a"
```

事实上，只要某种数据结构具有 Iterator 接口，都可以采用数组形式的解构赋值。

```javascript
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
```

解构赋值允许指定默认值。ES6 内部使用严格相等运算符（===）判断一个位置是否有值。所以，**如果一个数组成员不严格等于 undefined，默认值是不会生效的**。

```javascript
let [x = 1] = [undefined]
x // 1

let [y = 1] = [null]
y // null
```

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

```javascript
// x 可以取值，函数不会执行
function f() {
  console.log('aaa')
}
let [x = f()] = [1]
```

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

```javascript
let [x = 1, y = x] = [] // x=1; y=1
```

### 对象的解构赋值

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

```javascript
let { foo: baz } = { foo: 'aaa', bar: 'bbb' }
baz // 'aaa'
```

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

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

```javascript
let obj = { p: ['Hello', { y: 'World' }] }
let {
  p: [x, { y }],
} = obj
```

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

```javascript
let {
  p,
  p: [x, { y }],
} = obj
```

同数组解构一样，对象解构也可以使用默认值，默认值生效的条件是，对象的属性值严格等于 undefined。

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

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

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

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

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

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

### 字符串的解构赋值

对字符串解构时，字符串会被转换为类数组对象。

```javascript
const [a, b, c, d, e] = 'hello'
a // 'h'
b // 'e'
```

### 数值和布尔值的解构赋值

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

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

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

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

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

### 函数参数的解构赋值

```javascript
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]
```

### 圆括号问题

解构赋值时，对于编译器来说，一个式子到底是模式还是表达式，没有办法从一开始就知道，必须解析到（或解析不到）等号才能知道。

ES6 规定：**只要有可能导致解构的歧义，就不得使用圆括号**。

#### 不能使用圆括号的情况

1. 变量声明语句

```javascript
// 全部报错
let [(a)] = [1]
let {x: (c)} = {}
let ({x: c}) = {}
let {(x: c)} = {}
let {(x): c} = {}
let { o: ({ p: p }) } = { o: { p: 2 } }
```

上面 6 个语句都会报错，因为它们都是变量声明语句，模式不能使用圆括号。

1. 函数参数

函数参数也属于变量声明，因此不能使用圆括号。

```javascript
// 报错
function foo([(z)]) { return z }
```

1. 赋值语句的模式

```javascript
// 全部报错
;({ p: a }) = { p: 42 }
;([a]) = [5]
```

上面的代码将整个模式放在圆括号之中，导致报错。

```javascript
// 报错
;[{ p: a }, { x: c }] = [{}, {}]
```

上面的代码将一部分模式放在圆括号之中，导致报错。

#### 可以使用圆括号的情况

可以使用圆括号的情况只有一种：赋值语句的非模式部分可以使用圆括号。

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

上面 3 行语句都可以正确执行，因为它们都是赋值语句，而不是声明语句，另外它们的圆括号都不属于模式的一部分：

* 第 1 行语句中，模式是取数组的第 1 个成员，跟圆括号无关；
* 第 2 行语句中，模式是 p 而不是 d；
* 第 3 行语句与第 1 行语句的性质一致。

### 扩展

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

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