[JS] You Don't Know JavaScript [Scope & Closures] -

前言

上面两章节简单的介绍了一下什么是JS与特性,而本章节开始会正式进入到JS中,而一开始我们先从作用域开始介绍,什么是作用域?作用域就是一个变数的生存範围,一旦超出了这么範围就无法存取到这个变数,定义了变量在哪里存活与我们能在哪里找到他,这就是作用域。

Compiling Code

作用域主要是在编译的时候定义的,所以需要了解JS编译与执行之间的关係。

在一般的编译器中,编译一般分为三个阶段 :
1.Tokenizing/Lexing : 将程式码分解为对JS有意义的token,举个例子,若有一段程式码var a = 2;那们它将会被分解为vara=2;,至于空格则取决于是否有意义而选择性的转换。
2.Parsing : 获取所有的token并将他们转换为AST(Avstract Syntax Tree)。举例来若将var a = 2;转换为AST,可以从最顶端的节点VariableDeclaration开始,它底下有两个子节点,一个是代表着变量a的identifier与代表拥有数值的AssignmentExpression,而代表有数值节点下有一个NumericLiteral的子节点代表数值(2)。
http://img2.58codes.com/2024/20124767pPkGK2hJNp.png
3.Code Generation : 执行AST并将它转换为可以执行得代码,这个转换会因为不同语言而有不同的结果,以JS来说它会对var a = 2;这个程式码转换成一组机械指令,将创建一个称为a的变量并将2赋予给这个变量。

由于JS与大部分语言一样它不会再build的时期就提前编译,而这个动作必须发收生在执行代码前几ms的时间,为了确保拥有最快的性能,JS引擎就使用了各种技巧(JIT,lazy compile...)这些就不再此处进行讨论。


为了简单的描述JS对于程式的处理,可以简单的分为解析/编译然后是执行,虽然JS没有明确的要求进行编译,但是它对于程式实质上的行为却是需要编译后才能执行的。

我们以下面三个例子来证明这点 :

var greeting = 'Hello';console.log(greeting);greeting = .'Hi'; //SyntaxError: unexpected token .

上面的程式中"Hello"并不会被输出,它掷出了一个SymtaxError,因为在Hi前面有一个非预期的token,以上面的例子中,若JS是由上而下一行一行的编译的话,那么应该会先输出"Hello"后才发生错误才对,但事实上JS引擎能够知道在第三行中有一个syntaxError,所以JS引擎会在执行前先解析整个程式。


console.log("Howdy");saySomething("Hello","Hi");// Uncaught SyntaxError: Duplicate parameter name not// allowed in this contextfunction saySomething(greeting,greeting) {    "use strict";    console.log(greeting);}

上面的例子中也会掷出一个SyntaxError,因为我们在saySomething这个函数中使用了严格模式,而严格模式禁止function的参数使用一样的名子,虽然抛出的错误并不是语法错误,但是在严格模式下这个错误会在执行前被掷出并被当成early error

而这个例子再次证明了JS会在执行代码前对程式完全的解析,因为不这么做的话它就不会知道function中有两个一样名子的参数与funciton中使用的是严格模式。


function saySomething() {    var greeting = "Hello";    {        greeting = "Howdy";  // error comes from here        let greeting = "Hi";        console.log(greeting);    }}saySomething();// ReferenceError: Cannot access 'greeting' before// initialization

上面了例子中,因为在let宣告greeting之前就使用了这个变量(let不会hoasting)所以导致了ReferenceError,这个例子也证明了JS是需要先将全部程式编译后才会执行。


Compiler Speak

在了解了JS引擎处理程式的两个阶段后,我们再回到JS引擎是如何识别便量并确定它在程式中的使用範围。

为了更深入地理解需要了解更多的编译器术语,这边会介绍LHS查询(Left-hand Side)RHS查询(Right-hand Side),简单来说当一个变量出现在赋值的左边时会进行LHS查询,当一个变量出现在赋值操作的右手边会进行RHS查询,说得更準确一点:

RHS(source)是简单地查询某个变量的值。LHS(target)是试着找到变量容器以便它可以赋值。
var students = [    { id: 14, name: "Kyle" },    { id: 73, name: "Suzy" },    { id: 112, name: "Frank" },    { id: 6, name: "Sarah" }];function getStudentName(studentID) {    for (let student of students) {        if (student.id == studentID) {            return student.name;        }    }}var nextStudent = getStudentName(73);console.log(nextStudent); //Suzy

Targets

我们先找到上面例子中的Targets。

students = [...]

这个很显然是一个Target,因为students被赋予了一个阵列的值,它与nextStudent = getStudentName(73)一样都是赋值操作。

for(let student of students)

这里是一个比较难发现的Target,这句程式的意思是将students这个阵列迭代给student这个变数,所以也是一个赋值(只是不太明显)。

getStudentName(73);

这里也是一个隐性的Target,因为它将73这个数值赋予给getStudentName这个function的参数。

Source

for(let student of students)

上面提到student是属于被赋值的target,那么students这个阵列便是给予数值的source

if(student.id == studentID)

在这边的student与studentID都是属于source reference。

return student.name

这边的student也是属于source,因为它提供了可以retuen的值。


在举个简单的例子:

function foo(a){    console.log(a);}foo(2);

上面的例子既有LHS也有RHS,我们仔细地对它进行分析 :

当程式调用了foo(...),代表着函数调用要求一个指向foo的RHS,它代表着去查询foo的值并将结果交给引擎。当值2做作为参数传递给foo时,2被赋予给了参数a,这边使用了隐含的参数赋值,所以进行了一个LHS。而当结果被传递给console.log(...)时,它会对console这个物件进行一个RHS查询,查询是否有一个log的mathod。最后当值2被传入log(...)时,因为2被赋予给了log的参数,所以也进行了一次LHS。

Nested Scope

作用域是通过标示福明成查询变量的规则,但是通常会有多余一个作用域的问题需要考虑,就像一个程式嵌套在另一个程式码区域或函数中,作用域也会被嵌套在其他作用域中,所以如果再直接作用域找不到需要的变量,则会往外层作用域寻找,如此继续找到最外层作用域(全域作用域)。

举个例子 :

function foo(a){    console.log(a + b);}var b = 2;foo(2); //4

以上面的例子来说,当JS引擎在直接作用域(函式foo中)找不到b这个变量,那么它就会访问外层的作用域(全域作用域)看是否有b这个变数可以让它进行RHS。

遍历嵌套作用域的规则很简单,JS引擎从当前执行的作用域开始查找变量,如果没有找到就向上走一级继续查找,如此类推。如果到了最外层(全局作用域),那么查找就会停止无论它是否找到了变量。


Errors

为什么需要了解LHS与RHS?
因为在一个变量还没被宣告的情况下,这两种类型的查询做了完全不同的行为。

function foo(a){    console.log(a + b);    b = a;}foo(2);
当b的第一次RHS查询发生时,因为它是一个没有被宣告过的变量,所以在作用域中找不到它,因此如果在所有的作用域中都找不到b这个变量的时候,JS引擎会抛出一个ReferenceError。相比之下若JS引擎在做LHS查询,虽然达到了顶层作用域都没有找到这个变量,若是没有在严格模式(strict)下,那么就会在全局作用域中创建一个同名的新变量。

而若一个RHS查询到了需要的变量,但是却对这个变量做这个值不可能做到的事,比如将这个非函数的值当作函数执行,或是引用nullundefined,那们JS引擎就会抛出种类错误TypeError

所以ReferenceError是对于作用域解析失败,而TypeError则是作用域解析成功了但是对对这个解析成功的变量做非法/不可能的操作。

参考文献 :
You Don'y Know JavaScript
You Don'y Know JavaScript 2nd


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章