# 作用域

## 作用域

负责收集维护所有变量的一系列查询，并实施一套严格的规则，确保代码对这些变量的访问权限

对于 `var a = 0` 这一操作，可以分解成如下：

* 如果变量 a 在作用域中不存在，**编译器**就在作用域中声明一个（若存在就忽略不会再声明一个，**注意只是不再在作用域中声明一个，不要和赋值搞混**）
* **在运行时**引擎在作用域查找该变量，并且为它赋值

> 也就是说，var a = 0 是拆分成编译和运行时两个部分的
>
> 编译是在运行时之前先运行的（虽然时间很短）

## LHS RHS 查询

### LHS

可以理解为找到变量容器的本身，**从而可以对其赋值**

### RHS

可以理解为 Retrieve his source value （取到他的源值），**来获取变量的值**

```javascript
function foo(n) {	// 这里其实有个隐式赋值 n = 2，对 n 做了一次 LHS
    console.log(n)	// 对 n 做了一次 RHS，并且对 console 也做了一次 RHS
}

foo(2)	// 对 foo 做了一次 RHS，来找到函数
```

### 查询错误

对于还未声明的变量，进行 LHS 和 RHS 查询抛出的异常不同：

* RHS 在查询失败时，会抛出 ReferenceError

  > RHS 查询成功后，但如果对变量的值进行不合理的操作的时候
  >
  > 如访问 null 的属性，会抛出 TypeError

  ```javascript
  var a = b	// RHS 查询失败，ReferenceError: b is not defined
  ```
* LHS 在查询失败时，**在非严格模式下**，会创建一个具有该名称的变量

  **具体来说，如果在顶层都无法找到变量，就会在全局作用域内创建一个变量**

  > 在严格模式下，LHS 查询失败时也会抛出 ReferenceError

  ```javascript
  "use strict"

  a = 0	// 在严格模式下 LHS 抛出异常：Uncaught ReferenceError ReferenceError: a is not defined
  ```

> ReferenceError 表示作用域判别失败；TypeError 表示作用域判别成功，但是对结果的操作是不合理的。

## 词法作用域

**JavaScript 和大部分语言一样，使用的都是词法作用域。**&#x5C3D;管 this 可以模拟类似动态作用域的行为（后文展开）。

词法作用域即：定义在词法阶段的作用域。换言之，**他是由你在写代码时将变量放置的位置所决定的**。（大部分如此，例外在后面展开）

### 查找

作用域查找会在**第一个匹配**时停止。

此外，如果访问 `foo.bar.baz` 只会对第一个标识符 `foo` 进行查找，查询成功后，`bar`, `baz` 由对象属性访问规则接管

#### 遮蔽

如果多个层级存在同名变量，内部标识符会遮蔽外部标识符。例如：

```javascript
var a = 0

function foo() {
    var a = 1
    console.log(a)        // 1
    console.log(window.a) // 0
}

foo()
```

可以使用 `window` 来“越级”访问变量 a。避免了 a 被内部作用域遮蔽。

### 改变词法作用域

可以通过一些技巧来在运行时改变词法作用域

#### eval

eval 执行了传入的 str，覆盖了全局变量 a：

```javascript
var a = 0

function foo(str) {
    eval(str)
    console.log(a)  // 1
}

foo('var a = 1')
```

在严格模式下，eval 会有自己的作用域：

```javascript
var a = 0

function foo(str) {
    "use strict"
    eval(str)
    console.log(a)  // 0
}

foo('var a = 1')    
```

#### with

在 with 中可以产生一个 LHS，就可以在全局变量中创建一个变量：

```javascript
function foo(obj) {
    with (obj) {
        a = 0
        b = 1 //  LHS 自动在全局创建了一个变量 b，并为他赋值 1
    }
}

var obj = {
    a: undefined
}

foo(obj)

console.log(obj.a) // 0
console.log(b)     // 1
```

## 函数作用域

### 函数声明 vs 函数表达式

一种最简单的区分函数声明和函数表达式的方式，就是看 `function` 关键字是否出现在声明中的第一个词。如果是，那就是函数声明。

他们之间的区别就是：**他们的名称标识符会被绑定在哪里**：

```javascript
// 绑定在全局作用域
function foo() {}     // 函数声明

// 绑定在自身的函数作用域中
(function foo() {})   // 函数表达式
```

### 具名函数 vs 匿名函数

> 函数声明不允许使用匿名函数

相比具名函数，匿名函数的缺陷是：

* 在调用栈上不能显示名称，不利于调试
* 不能通过函数名递归地调用自己
* 可读性较差

下面的这种写法也是允许的，因此你也可以使用具名函数：

```javascript
setTimeout(function foo() {
    console.log('bar')
}, 0)
```

### IIFE 立即执行函数

> IIFE 也可以接受具名函数

```javascript
var a = 0;

(function() {
    var a = 1;
    console.log(a);  // 1
})();

console.log(a);      // 0
```

当然，还可以传入参数：

```javascript
var a = 0;

(function(global) {
    console.log(global.a)  // 0
})(window)
```

#### 避免 undefined 被覆盖问题

有时候 undefined 关键词可能会被污染，因此可以使用 IIFE 来解决。IIFE 接受一个 undefined 参数，但在对应的位置不要传入他：

```javascript
// JavaScript 允许下面的写法
var undefined = true;

(function(undefined) {
    console.log(undefined);    // undefined
})();                          // 不要传入参数

console.log(undefined);        // true
```

## 块作用域

JavaScript 虽然会创建函数作用域，但有些情况下并不会创建块级作用域 `{ }`&#x20;

```javascript
if (false) {
    var foo = 'bar'  // if 语句中的 foo 其实是在全局作用域中
}

console.log(foo)     // undefined
```

或者一个更常见的例子，在 `for` 语句中：

```javascript
// 变量 i 其实是创建在全局作用域中的
for (var i = 0; i < 10; i++) {}

console.log(i)      // 10
```

### 创建块级作用域的方式

在上面的例子中，并不会创建块级作用域。但是在另一些情况下，可以创建：

* with
* catch 块

#### catch 块

`catch` 语句可以创建一个块级作用域，因此可以利用这个技巧来创建一个块级作用域：

```javascript
try {
    throw 0
} catch (foo) {
    console.log(foo)  // 0
}

console.log(foo)      // ReferrenceError
```

## let & const

在 ES6 中，使用 `let` 关键词创建的变量会**将该变量劫持在所在的块作用域中**：

```javascript
if (false) {
    let foo = 'bar'  // foo 在块级作用域中
}

console.log(foo)     // ReferenceError
```

### 显式的创建块级作用域

上面的例子中，其实是隐式的创建了一个块级作用域。

你可以用 `{ }` 来显示的创建一个块级作用域。那么 `let` 就会把变量劫持在内部：

```javascript
{
    let foo = 'bar'
}
```

### for 循环

使用 `let` 来代替 `var` 创建循环变量即可解决作用域泄露问题：

```javascript
for (let i = 0; i < 10; i++) {}
```

`let` 关键字不仅将变量 `i` **绑定在了循环块内部**，并且在**每次迭代时进行重新绑定**

> 每次都是用上一次循环结束时的值进行重新赋值

```javascript
{
    let i
    for (i = 0; i < 5; i++) {
        let j = i
        console.log(j)  // 0 1 2 3 4
    }
}
```

## 变量提升

* 定义发生在编译阶段
* 赋值发生在运行时

**每个作用域都会做变量提升，但不会提升到外部**，比如函数作用域的变量只会提升到自己作用域的前部

函数声明会被提升，但是函数表达式并不会。参考下面例子：

```javascript
foo()   // TypeError

var foo = function() {
    console.log('bar')
}
```

上面的代码变量 `foo` 被提升到了头部，但是声明并没有。因此在第一行对一个 undefined 进行函数调用时，会抛出 TypeError。

而一个函数声明是可以被提升的：

```javascript
foo()   // bar

function foo() {
    console.log('bar')
}
```

### 函数优先

变量和函数都会被提升，**但函数的优先级更高**

```javascript
foo()   // 0

var foo 

// 函数声明会被提升到最前
function foo() {
    console.log(0)
}

foo = function() {
    console.log(1)
}
```

上面的代码其实等价于：

```javascript
function foo() {
    console.log(0)
}

var foo // 重复的声明会被忽略

foo()

foo = function() {
    console.log(1)
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.hong97.ltd/javascript/zuo-yong-yu-bi-bao.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
