You Don't Know JS: Scope & Closures (第一章:什么是Scope)

Content

  1. What is Scope?
  2. Lexical Scope
  3. Function Vs. Block Scope
  4. Hoisting
  5. Scope Closures

Appendix:

  1. Dynamic Scope
  2. Polyfilling Block Scope
  3. Lexical-this
  4. Thank You's



 

 

Chapter1: What is Scope?

 

 In fact, the ability to store values and pull values out of variables is what gives a program state.

 

背后的问题:

变量储存在哪?

当需要用到它们,程序怎样找到它们?

 

回答:

需要a well-defined set of rules: Scope.

 

但是Scope在哪里和如何得到设置get set?

 


 

Compiler Theory

JS是编译后的语言。

 

Compilation的几个大步骤:

  1. Tokenizing/Lexing(代币/类型分析): 把a string of characters分解为有意义的小块,被成为tokens
  2. Parsing: 把tokens转化进一个嵌套元素的tree结构。集体地代表程序的语法结构。
    • tree叫做Abstract Syntax Tree(提取句法树)
    • var a = 2;
      //被分解成:
      /*
      顶层: VariableDeclaration
        子节点 Identifier: a
      子节点 AssignmentExpression: =
        子节点  NumericLiteral: 2
      */

       

  3. Code-Generation: 把这个AST转化为可执行的代码。(根据不同语言,有区别)。
    • 转化为机器指令,目的是来创建一个变量a, 并储存一个value到a.

当然JS引擎更复杂。编译时间,在许多情况下,仅仅耗时微秒或更少。并立即在编译后执行代码。


 

 

Understanding Scope

学习理解Scope,可以比喻成,进程的一场谈话。

var a = '123'

 

 

The Cast 全体演员

  • Engine 响应从开始到结束编译,并执行JS程序。 
  • Compiler 编译器。处理所有脏活,包括parsing and code-generation.
  • Scope 收集和保存一个名单。名单上包括所有的变量identifiers,并强迫实施一个严格的规则:这些identifiers如何存取到 currently executing code。

 

Back & Forth

Engine看2个独立的声明。1个是在编译期间,编译器处理。另一个是在执行期间,引擎处理。

 

首先,编译器Tokening and Lexing,

然后,  Parsing 代码到AST树,

最后,  Code-Generation。(用到Scope和Engine的交互。)

  1. 编译器询问Scope"变量a是否存在于你的list中?"。
    • 如果是,继续;  如果否,编译器要求Scope去声明一个新的变量a
  2. 编译器生产代码用于Engine在后续执行去处理a = 2。
    1. Engine 跑起来,先询问Scope:你的当前list里面有变量a吗?
    2. 如果有,Engine使用这个变量, assignment.
    3. 如果没有,Engine去其他地方查看(查看嵌套的Scope部分)

总结:

编译器先声明变量,然后Engine在Scope中查询这个变量,如果发现就分配它。

 

首先Compiler Speak

然后Engine/Scope Conversation

 

Engine执行代码,是在编译器生产后的第2步,Engine查询变量a是否已被声明。这个查询是咨询Scope。

Engine执行的查询的类型(LHS和RHS),影响查询的结果outcome。

 

本例子:var a = '123';

Engine将执行一个“Left-hand Side”查询,查询内容是变量a。

还有"Right-hand Side",准确的描述是:非左侧查询。

Side是指分配符号an assignment operation的左右两边。

如: console.log(a); 就是非左侧查询。

 

其实LHS和RHS只是涉及是否assign的一种称呼。

"who's the target of the assignment (LHS)" and "who's the source of the assignment (RHS)".

 

(这两个概念有什么用??见后,ES6以后区别不明显了。)

 

 

 Engine和Scope的对话:

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

foo( 2 );

 

Engine: Scope,我有一个右侧查询, 引用foo,听说过它吗?

Scope: 当然,我有。Compiler在一秒前声明了它。它是函数。给你!

Engine: 太好了, 🙏。我执行foo.

 

Engine: Scope! 我有一个左侧查询,引用a,听说过它吗?

Scope:当然,我有。Compiler声明它作为一个参数给foo。给你!

Engine:谢谢。现在分配2给a

 

Engine:Scope不好意思,又打扰你了。 我有一个右侧查询, 引用console, 听说过它吗?

Scope:当然!它是Build-in, 给你!

Engine:太好了! 查找log(..),  ok,它是一个函数方法

Engine: Scope! 我又有一个右侧查询,引用a。 我记得它,这是一次2次核对。

Scope: 正确!Engine!它没有改变。给你!

Engine: Cool!传递a的value, 是2,放入log(..)

 

Quiz

function foo(a) {
    var b = a;
    return a + b;
}

var c = foo( 2 );

 

  1. Identify all the LHS look-ups (there are 3!).

     

    •  c = ..

    •  a = 2 (implicit param assignment) ,

    •  b = ..

  2. Identify all the RHS look-ups (there are 4!).

     

    • foo(2..,   

    • = a;,   

    • a + .. and .. + b

 

结论:LHS 3, RHS4


 

Nested Scope

Scope是一条法则: 用变量的识别符号name查询它们。

就像块/函数可以被嵌套在其他块/函数内一样,作用域也可以被嵌套在其他作用域内。

如果一个变量在当前作用域内找不到,Engine就会咨询下一个outer containing scope,并继续直到发现或到达最外层作用域。

 


 

Errors

  • ReferenceError: Scope resolution失败。
  • TypeError:   Scope resolution是成功的,但有非法/不可能行为存在试图反对这个结果。

 

为什么我们起了2个名字,LHS, RHS?

在变量还没有被声明的时候(或者说询问任何作用域都查找到),这2类look-ups查询的行为是不同的!

  • RHS右侧查询,在当前作用域和嵌套作用域都没找到后,会抛出❌ReferenceERror
  • 在ES5之前,没有严格模式,所以LHS没有找到一个变量后,当到达全局作用域后,会创建一个全局变量。
    • 严格模式下,则会报错❌ ReferenceError。
    • ⚠️:这个区别在ES5后,非严格模式也会报错了❌!!

 

  • 如果一个变量在RHS查询被发现,但你用它执行的某些操作(比如:分配一个非法的值),Engine会抛出一个错误❌,TypeError

 


  

Review(TL;DR)

涉及分配的就是LHS, 只涉及查询值的就是RHS

LHS:

  • = opertaor
  • 传递arguments给函数parameters

 

JS Engine首先在执行代码前编译代码,分割声明var a = 2;成2部分:

  1. var a, 在Scopen内声明它。这步在最开始执行。
  2. a = 2, 查询变量2,并分配value。

 

posted @ 2018-10-02 15:30  Mr-chen  阅读(220)  评论(0编辑  收藏  举报