《你不知道的JavaScript上卷》笔记(1)

  • 作用域是什么

存储变量,并且可以方便的找到这些变量的一套规则被称为作用域

作用域是根据名称查找变量的一套规则


1.1 编译原理

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”

  • 分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元。

  • 解析/语法分析(Parsing)

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。

  • 代码生成

将 AST 转换为可执行代码的过程称被称为代码生成。

对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。


1.2 理解作用域

1.2.1 演员表

  • 引擎

从头到尾负责整个 JavaScript 程序的编译及执行过程。

  • 编译器

引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。

  • 作用域

引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

1.2.2 对话

当你看见 var a = 2; 这段程序时,很可能认为这是一句声明。事实上,引擎认为这里有两个完全不同的声明,一个由编译器在编译时处理,另一个则由引擎在运行时处理。

变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

1.2.3 LHS查询与RHS查询

编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量 a 来判断它是否已声明过。查找的过程由作用域进行协助,但是引擎执行怎样的查找,会影响最终的查找结果。

(LHS查询)赋值操作的目标是谁:当变量出现在赋值操作的左侧时进行 LHS 查询,LHS 查询则是试图找到变量的容器本身,从而可以对其赋值。

a = 2  实际上我们并不关心当前的值是什么,只是想要为 =2 这个赋值操作找到一个目标

(RHS查询)谁是赋值操作的源头:当变量出现在赋值操作的右侧时进行 RHS 查询,RHS 查询与简单地查找某个变量的值别无二致。

console.log( a )  需要查找并取得 a 的值,这样才能将值传递给 console.log(..)。


1.3 作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用 域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。

遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。


1.4 异常

为什么区分 LHS 和 RHS 是一件重要的事情?

因为在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行 为是不一样的。

不成功的RHS引用会导致抛出 ReferenceError 异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出ReferenceError异常(严格模式下)。


  • 词法作用域

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,我们会对这种作用域进行深入讨论。另外一种叫作动态作用域,仍有一些编程语言在使用(比如 Bash 脚本、Perl 中的一些模式等)。

2.1 词法阶段

大部分标准语言编译器的第一个工作阶段叫作词法化

词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。

查找

作用域查找会在找到第一个匹配的标识符时停止。

在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应, 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。


2.2 欺骗词法(不要使用它们)

欺骗词法作用域会导致性能下降。

如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”(也可以说欺骗)词法作用域呢?

2.2.1 eval

JavaScript中的 eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。

function foo(str, a) 
{ 
    eval( str ); // 欺骗! 
    console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

eval(..) 调用中的 "var b = 3;" 这段代码会被当作本来就在那里一样来处理。这段代码实际上在 foo(..) 内部创建了一个变量 b,并遮蔽了外部(全局)作用域中的同名变量。当 console.log(..) 被执行时,会在 foo(..) 的内部同时找到 a 和 b,但是永远也无法找到外部的 b。因此会输出“1, 3”而不是正常情况下会输出的“1, 2”。

在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。

JavaScript中还有其他一些功能效果和eval(..)很相似。setTimeout(..)和setInterval(..) 的第一个参数可以是字符串,字符串的内容可以被解释为一段动态生成的函数代码。这些功能已经过时且并不被提倡。不要使用它们!

new Function(..) 函数的行为也很类似,最后一个参数可以接受代码字符串,并将其转化为动态生成的函数(前面的参数是这个新生成的函数的形参)。这种构建函数的语法比eval(..) 略微安全一些,但也要尽量避免使用。

2.2.2 with

JavaScript 中另一个难以掌握(并且现在也不推荐使用)的用来欺骗词法作用域的功能是with 关键字。with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

var obj = { 
    a: 1,
    b: 2,
    c: 3 
};
// 单调乏味的重复 "obj" 
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式 
with (obj) {
     a = 3;
     b = 4;
     c = 5;
}
但实际上这不仅仅是为了方便地访问对象属性。考虑如下代码:

猜你喜欢

转载自blog.csdn.net/qq_40417810/article/details/82938933