# 为什么会有作用域?
存储和访问变量的值的能力,将状态带给了程序,因此程序能执行复杂的任务。因此需要设计一套存储和查找变量的规则,这套规则就被称为作用域。
# 作用域的概念
作用域是存储和查找变量的规则
# 词法作用域和动态作用域
词法作用域和动态作用域是作用域的两种主要工作模型
词法作用域就是定义在词法阶段的作用域,由变量和块作用域写在哪里决定的。也就是说函数的作用域在函数定义的时候就决定了。
动态作用域是在函数调用的时候才决定的,是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)不允许读取。
# 提升
- 函数和变量的声明,会在代码执行前被提前处理。具体是引擎在编译阶段就会找到这些声明,并用合适的作用域将他们关联起来。也就会说作用域和作用域链在编译阶段已经被处理好了(上下文的创建)
- 函数会被优先提升,然后是变量。如果同名且函数声明在前,则同名变量会被视为重复声明,且忽略
- 函数表达式不会被提升
- 箭头函数没有自己的上下文,没有arguments,也不存在变量提升
# 闭包
能够访问其他函数内部变量的函数,被称为闭包。目的是局部数据的共享。
当函数记住并访问所在词法作用域时,即使在当前词法作用域外执行,就产生了闭包。
一个标准的闭包是:一组嵌套函数,内部函数访问了包装函数的变量等。然后内部函数的引用被return出去(或者SetTimeout,挂载在全局等),在其他作用域被执行。
应用场景: 模拟私有属性或者静态变量、模拟模块化、柯里化、防抖和节流、Vue收集依赖、Redux、发布订阅模式
缺点: 早期浏览器不能被垃圾回收导致内存泄漏(ie9,现代浏览器没这个问题)、烧内存
# 作用域链
当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。实体上查找是在变量对象上延伸查找。
# 其他概念
LHS RHS
引擎查找变量的方式。RHS 是 retrieve his source value,可理解成赋值操作的源头。LHS即赋值操作的目标。
ReferenceError 同作用域判别失败相关。TypeError 则代表作用域判别成功了,但是对结果的操作是不合法的。
作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套
作用域屏蔽
在多层嵌套作用域中,可以定义同名的标识符。但作用域查找会在找到第一个匹配的标识符时停止,这叫遮蔽效应。
访问被同名遮蔽的全局可以Window.a
欺骗词法
词法作用域完全由函数所声明的位置来定义,但怎么进行修改或欺骗这种规则呢?
eval:传入一个包含声明的代码字符串,可以修改已经存在的词法作用域。 with:将一个对象的引用当做作用域处理,将对象的属性当做作用域的标识符处理,从而创建了一个新的词法作用域。
这两机制的副作用是引擎无法在编译时对作用域查找进行优化。