二、词法作用域 (学习笔记)—— 《你不知道的JavaScript》
词法作用域
作用域工作模型:
- 词法作用域(大多数编程语言采用)
- 动态作用域
词法阶段
大部分标准语言编译器的第一个工作阶段就是词法化。
词法化的过程:会对源代码中的代码进行检查,如果是有状态的解析过程,还会赋予单词语义。
词法作用域:就是定义在词法阶段的作用域。在写代码时,将变量和块作用域写在哪里决定的。
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2);
上面demo有3个逐级嵌套的作用域。便于理解,可以看成是逐级嵌套的气泡。
① 包含整个全局作用域,其中只有一个标识符:foo
② 包含 foo 创建的作用域,其中有三个标识符:a b bar
③ 包含 bar 创建的作用域,其中只有一个标识符:c
作用域气泡由其对应的作用域块代码写在哪里决定,是逐级包含的关系。
查找
作用域气泡的结构和互相之间的关系给引擎提供了足够的位置信息,引擎通过这些信息来查找标识符的位置。
在上一个代码片段中, 引擎执行 console.log(..) 声明, 并查找 a、 b 和 c 三个变量的引
用。可参考下图。
作用域查找,会在找到第一个匹配的标识符时终止。
遮蔽效应:在多层嵌套的作用域中定义多个同名的标识符。(内部标识符会“遮蔽”外部标识符)
抛开遮蔽效应,作用域查找,始终从运行时所处的最内部作用域开始查找,逐级向上进行,直到找到第一个匹配的标识符。
被同名变量遮蔽的全局变量可以通过 window.a 来访问。
词法作用域只会查找一级标识符。如果引用 foo.bar.baz,只会查找 foo, 找到这个变量,对象属性访问规则会接管对 bar baz 属性的访问。
欺骗词法
词法作用域完全由写代码时函数定义的位置来定义,如何在运行时修改(欺骗)?
有两种机制:
eval
还是先来看个栗子吧!
function foo(str, a) {
eval(str); // 欺骗引擎 var b = 3; 原本就在这里
console.log(a, b);
}
var b = 2;
foo('var b = 3', 1); // 1 3
可以看到,通过 eval(str),将原本不在 foo 中的的 var b = 3; 欺骗成书写时就代码就在那了,以此修改了词法作用域。
在严格模式中,eval()在运行时有自己的词法作用域,其中的声明无法修改所在的作用域。
// eval 严格模式
function foo(str) {
'use strict';
eval(str);
console.log(a);
}
foo('var a = 3'); // Uncaught ReferenceError: a is not defined
with
先来一个 demo
function foo(obj) {
with(obj) {
a = 2;
}
}
var obj1 = { // obj1 有 a 属性
a: 1
}
foo(obj1);
console.log(obj1.a); // 2
var obj2 = { // obj2 没有 a 属性
b: 1
}
foo(obj2);
console.log(obj2.a); // undefined
console.log(a); // 2,a 被挂在到全局作用域
with 声明会根据传入的对象凭空创建一个全新的词法作用域。
传入 obj2 时,为什么 a 被挂在到全局作用域,可以按下图来理解:
注意:使用 eval() 和 with() 会有性能问题。
性能
性能肯定是不好的,可参考下面图片,具体的文字解说可参考《你不知道的JavaScript上卷》
注:以上所有的文字、代码都是本人一个字一个字敲上去的,图片也是一张一张画出来的,转载请注明出处,谢谢!