# 为什么会有作用域?

存储和访问变量的值的能力,将状态带给了程序,因此程序能执行复杂的任务。因此需要设计一套存储和查找变量的规则,这套规则就被称为作用域。

# 作用域的概念

作用域是存储和查找变量的规则

# 词法作用域和动态作用域

词法作用域和动态作用域是作用域的两种主要工作模型

词法作用域就是定义在词法阶段的作用域,由变量和块作用域写在哪里决定的。也就是说函数的作用域在函数定义的时候就决定了。

动态作用域是在函数调用的时候才决定的,是this的表亲。

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

// 结果是 1

# 作用域分类

作用域分类 全局作用域 函数作用域 块作用域
对应变量 全局作用域 局部变量 块级变量
如何形成 默认 函数 let/const与{}、try catch、with
  • var:有全局作用域和函数作用域,没有块作用域,有变量提升。在全局声明时创建window 对象的属性;能重复声明
  • let、const:全局作用域、局部作用域、块作用域,有暂存性死区; let在for循环中每循环一次就会重新声明一次(因为let有块级作用域)

暂存性死区:

var的变量提升不同,通过 let 声明的变量直到它们的定义被执行时才初始化。在变量初始化前访问该变量会导致 ReferenceError: Cannot access 'a' before initialization 。该变量处在一个自块顶部到初始化处理的“暂存死区”中。尽管 a 发生了变量提升,但是在初始化赋值前(before initialization)不允许读取。

# 提升

  1. 函数和变量的声明,会在代码执行前被提前处理。具体是引擎在编译阶段就会找到这些声明,并用合适的作用域将他们关联起来。也就会说作用域和作用域链在编译阶段已经被处理好了(上下文的创建)
  2. 函数会被优先提升,然后是变量。如果同名且函数声明在前,则同名变量会被视为重复声明,且忽略
  3. 函数表达式不会被提升
  4. 箭头函数没有自己的上下文,没有arguments,也不存在变量提升

# 闭包

能够访问其他函数内部变量的函数,被称为闭包。目的是局部数据的共享

当函数记住并访问所在词法作用域时,即使在当前词法作用域外执行,就产生了闭包。

一个标准的闭包是:一组嵌套函数,内部函数访问了包装函数的变量等。然后内部函数的引用被return出去(或者SetTimeout,挂载在全局等),在其他作用域被执行。

应用场景: 模拟私有属性或者静态变量、模拟模块化、柯里化、防抖和节流、Vue收集依赖、Redux、发布订阅模式

缺点: 早期浏览器不能被垃圾回收导致内存泄漏(ie9,现代浏览器没这个问题)、烧内存

# 作用域链

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。实体上查找是在变量对象上延伸查找。

# 其他概念

LHS RHS

引擎查找变量的方式。RHS 是 retrieve his source value,可理解成赋值操作的源头。LHS即赋值操作的目标。

ReferenceError 同作用域判别失败相关。TypeError 则代表作用域判别成功了,但是对结果的操作是不合法的。

作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套

作用域屏蔽

在多层嵌套作用域中,可以定义同名的标识符。但作用域查找会在找到第一个匹配的标识符时停止,这叫遮蔽效应。

访问被同名遮蔽的全局可以Window.a

欺骗词法

词法作用域完全由函数所声明的位置来定义,但怎么进行修改或欺骗这种规则呢?

eval:传入一个包含声明的代码字符串,可以修改已经存在的词法作用域。 with:将一个对象的引用当做作用域处理,将对象的属性当做作用域的标识符处理,从而创建了一个新的词法作用域。

这两机制的副作用是引擎无法在编译时对作用域查找进行优化。