二、词法作用域 (学习笔记)—— 《你不知道的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上卷》

注:以上所有的文字、代码都是本人一个字一个字敲上去的,图片也是一张一张画出来的,转载请注明出处,谢谢!

原文地址:https://www.cnblogs.com/lwl0812/p/9792162.html