工作模型
作用域主要有两种工作模型:
-
词法作用域(主要讲解)
-
动态作用域(Bash等)
词法阶段
基础回顾
在上一章中我们简单讲述了 JavaScript 的编译过程,其中第一个阶段叫词法化。
要理解词法作用域,需要先掌握词法化的概念:
即把一段 or 一行代码拆分成一个个单词,这些单词能为后来编译器进行词法分析并转换成机器指令。
而词法作用域就是定义在这个词法阶段的作用域。
原文中描述如下:
换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。
比如下面这段代码:
js
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3)
}
foo(2);
这里可以拆解成三层嵌套作用域。
第一层:
js
function foo(a) {
// ...
}
foo(2)
全局作用域,只有一个标识符foo。
第二层:
js
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
foo所创建的作用域,包含a、bar、b三个标识符。
这里包含a是因为在foo(a)和foo(2)中,隐式有一个a = 2
的赋值过程。
第三层:
js
console.log(a, b, c);
是bar所创建的作用域,只有标识符c。
查找
作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎利用这些信息来查找标识符的位置。
查找的顺序是从内向外的。
查找开始的起点是我们 用到了标识符的位置。
比如console.log(a, b, c)
对吧,那么引擎则需要分别找到a、b、c的值才能打印出来,所以引擎会从console.log
所在的当前作用域开始查找,然后一层一层往外找,直到找到所有a、b、c的值结束。
要是找不到咋办?找不到就抛出异常呗。
值得注意的是,作用域查找会在找到第一个匹配的标识符时停止。
比如:
js
var a = 1;
function fn() {
var a = 3;
console.log(a);
console.log(window.a); // 1
}
fn(); // 3
代码段很简单,答案也显而易见。
但要让你清晰地说出代码执行过程呢?
首先声明了变量 a 和函数 fn,然后最后一行调用了 fn 函数,执行内部函数体。
看console.log(a)
,那么引擎便会开始查找当前作用域中是否存在 a 这个变量。
在当前作用域( fn 所创建的作用域)中有一行var a = 3;
,可以找到 a,并把 a 的值3打印出来。
在此查找过程中,触及不到全局作用域下的 a(也就是值为 1 的那个 a )。
但我们能通过全局 window 对象直接访问值为 1 的那个 a。
函数的词法作用域只由函数 被声明时所处的位置 所决定。
欺骗词法
eval
MDN释义如下:
eval()
函数会将传入的字符串当做 JavaScript 代码进行执行。
其实相当于能够动态插入代码的一种函数。
比如:
js
function foo(str, a) {
eval(str);
console.log(a, b);
}
var b = 2;
foo("var b = 3;", 1); // 1 3
相当于;
js
function foo(str, a) {
var b = 3; // 这里相当于把传入 str 当作 JavaScript 执行
console.log(a, b); // 遮盖全局的 b
}
var b = 2;
foo("var b = 3;", 1); // 1 3
我感觉写 eval 和直接把var b = 3;
写在那边没什么区别。事实证明,确实没什么卵用。
所以一般情况下写 eval 其实反而会影响代码的性能。
with
一种能够重复引用同一个对象中的多个属性的快捷方式。
直接show case理解:
js
var obj = {
a: 1,
b: 2,
c: 3
};
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 相当于,可以用如下方式简化
with(obj) {
a = 2;
b = 3;
c = 4;
}
看似方便,但使用 with 的缺陷也很明显,看下面这段代码:
js
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); // undefined
console.log(a); // 2 --- 这里是 a 泄露到全局了
o1 在传给 with 的时候,o1 对象中有 a 这个属性,所以能够正常修改 a 的值。
但当我们把 o2 传给 with 时,发现 o2 中并没有 a 这个属性。
所以执行到 a = 2
的时候,自动创建了一个全局变量 a。
但是在 o2 的作用域下,a 的值仍然还是 undefined。
因为:
with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法作用域。
性能问题
咱就是说,介绍这几个东西实际意义和用途不大。
因为写项目代码的时候写这玩意儿一点不规范。
我觉得真正的意义大概在于,你可能哪天接管了屎一样的代码,在看到上面的类似用法时,能明白你的前辈写这代码是想干嘛,有什么缺陷,然后思考怎么改正它🥲。
或者解决历史遗留问题。
eval:运行时修改作用域
with:运行时创建新的作用域
JavaScript 引擎会在编译阶段进行数项的性能优化。
其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
如果遇到了 eval 或者 with,引擎不能确定 eval 会接收什么代码,也不能明确传递给 with 用来创建新词法作用域的对象的内容到底是什么。
所以性能会有所下降。
总结
本章描述了词法阶段、词法作用域等概念。
词法作用域意味着作用域是由书写代码时函数声明的位置决定的。
编译的词法分析阶段基本能够知道全部标识符在哪里,以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。
还理解了两种欺骗词法作用域的机制,eval 和 with,这两者都存在对性能方面较大的副作用,会抵消代码中所作的其他优化,因此我们在写代码时,需要尽可能避免这样的写法。
今天的内容就 Done 啦~ see you tomorrow!