开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情
一、开始之前
在开始讨论作用域之前需要简单了解一下什么是作用域,以及作用域的规则是谁制定的,方便后续的解释。
-
首先解释一下什么是作用域?按个人理解来总结成一句话
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)
-
那么规则在哪设定的?
在编译过程中,是的,JavaScript 事实上是一门编译语言。任何 JavaScript 代码片段在执行前都要进行编译,大部分编译发生在执行前的几微妙时间内。
在传统的编译语言流程中,编译会分为三个步骤
-
分词/词法分析
详细介绍第一点,因为和我们将要讲到的词法作用域关系密切。将字符串分割成为有意义的代码块。如 "var a = 2;"将会被分割为 "var"、"a"、"="、"2"、";"
-
解析/语法分析
-
代码生成
-
var a = 2编译时遇到 var a 时会在当前作用域声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域里面查找该变量,如果找到则为其赋值。
-
var a = 2运行时在运行时会在作用域里面进行查找操作,具体分为两步 LHS 和 RHS 。可以简单理解为从赋值操作符的左侧查找和从赋值操作符的右侧查找。
- LHS 查找是试图查找到变量的容器本身 a
- RHS 查找这个变量原本的值,它的源值(可能有点抽象,我是这样理解的,在内存中寻找变量a开辟出来的存储空间的地址,从而方便进行赋值操作)
总结一下,LHS 通常是在声明变量时进行的,而 RHS 是在引用变量的时候进行的。如果 RHS 查询不到所需的变量,则会抛出 ReferenceError 异常。而 LHS 查询如果在全局作用域中都无法找到目标变量,则会在全局作用域中创建一个具有该名称的变量(注意是在非严格模式下)
-
作用域链,作用域的查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到找到第一个匹配的标识符为止。
二、词法作用域
好嘞,在简单了解作用域,编译和运行时的操作后。现在可以开始正题,讨论一下我们最常见的词法作用域了。
之前简单介绍过,编译中有一个工作阶段叫做词法话,词法作用域也就是定义在词法阶段的作用域。词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此编译过后代码会保持作用域不变。(大部分情况下)
function demo () {
var a = "demo"
demo2(() => {
console.log(a)
})
}
function demo2 (fn) {
var a = "demo2"
fn()
}
demo() // demo
以上代码最终输出 demo ,上面的函数各自拥有一个作用域,并且还有嵌套关系,可以根据词法作用域来画出如下的图。
总共有三个函数,蓝色的 demo 嵌套着一个紫色的匿名函数,红色的 demo2。在紫色圈内的作用域查不到变量a,会去上一级函数demo的作用域里寻找 a ,最终输出 demo。即使没有找到,也只会去全局作用域里面寻找 a ,至始至终都没有在demo2的作用域里面进行查找。
关键原因在于匿名函数定义时的位置,从而决定了其作用域
通俗的说,变量的作用域取决于你写的位置,并且绝大多数情况作用域都不会改变,除了欺骗词法,这一类只能等到运行时才能知道变量的作用域,会导致性能下降
欺骗词法作用域(不推荐)
1. eval 函数
JavaScript 中的eval(..)函数可以接收一个字符串作为参数,并将其中的内容视为在书写时就存在于程序中这个位置的代码。
即根据传入的字符串动态生成代码片段,在有 eval() 处引擎无法知道这串代码是什么,从而可能修改原本的词法作用域。(比如使一个本该出现在上一级的变量或者根本不存在的变量在当前作用域里面进行声明)
function foo(str,a){
eval( str );
console.log(a,b);
}
var b = 2
foo("var b = 3;",1) // 1,3
eval(..)可以在运行时期修改书写时期的词法作用域
2. with 函数
with 可以将一个没有或者有多个属性的对象处理为一个完全隔绝的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符
简单来说,with在接收一个对象的同时,也会生成一个全新的词法作用域。然后从当前新生成的词法作用域去进行查找,查找不到则会去上一级,若一直没查找到则会最终来到顶层,在这里声明创建没有找到的全局变量。
function foo(obj){
with(obj){
a=2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo(o1);
console.log(o1.a); //2
foo(o2);
console.log(o2.a); // undefine
console.log(a); // 2
这完全和对象里面的变量查找不一样,对象里面进行赋值操作时若没有查找到是会直接在对象内生成该变量,而不是接着去上一级查找。
3. 不推荐的原因
eval 和 with ,一个修改词法作用域,一个凭空创建词法作用域。如果在代码中出现了这两个,将会影响到引擎对标识符位置的判断,从而执行前无法预先得知所有变量和函数的定义位置。直接的影响到了 JavaScript 引擎在编译阶段的性能优化和代码的词法和静态分析。
前端学习中,如有不对,欢迎指出
参考书籍:《你不知道的JavaScript上卷》