原文:你不知道的js系列

什么是作用域(Scope)?

作用域 是这样一组规则——它定义了如何存放变量,以及程序如何找到之前定义的变量。

编译器原理

JavaScript 通常被归类为动态语言或者解释型语言,但实际上它是编译型语言。它不是像其它传统的编译型语言一样预先编译好,编译后也不能在各种系统上兼容。

但无论如何,JS 引擎采取和传统编译器相同的步骤,只不过以一种更不易被人意识到的负责的方式。

传统编译型语言的处理过程:

1. 分词/词法分析(Tokenizing/Lexing)

将一连串字符分割成有意义的片段,称为 token。比如 var a = 2; 可能会被分成几个 token :var,a,=,2,和;空格也有可能被保留为一个 token,取决于这个空格符是否有意义。

注:词法分析可以理解为基于有状态分析规则进行的分词,比如 a 是一个独立的 token 还是 其它 token 的一部分。

2. 解析 (Parsing)

将一个 token 序列转换成一个抽象语法树(Abstract Syntax Tree),代表整个程序的语法结构。

比如 var a = 2;可能就会有一个最上层的结点叫做变量声明,然后一个子结点标识符 代表 a,另一个子结点为赋值表达式,表达式下面又有一个子结点叫做数字字面量,值为 2。

3. 代码生成 (Code-Generation)

将一个抽象语法树转换成可执行代码。这部分的实现很大程度上依赖目标语言和平台。

所以抛开具体细节,我们只需要知道,通过某种方式,将上一步解析 var a = 2;得到的 AST转化成一系列机器指令,这些指令真正意义上创建了变量 a (在内存中)并赋给一个值。

而 JavaScript 引擎比这些步骤还要更加复杂。比如,在解析和代码生成的过程中,有对执行性能的优化以及去除冗余元素等。

首先,JavaScript 引擎没有足够的时间去做代码优化,因为 JavaScript 编译器没有预先构建的步骤。

对于 JavaScript 来说,很多情况下,在运行之前只有几微妙的时间去编译。

简单来说,任何 JavaScript 代码在执行之前都会先经过编译,然后立即执行。

理解作用域 (Scope)

我们可以将编译的过程理解为一场对话,编译对象为 var a = a;

出场角色:

1.引擎——负责代码的编译和执行

2.编译器——引擎的一个朋友,负责分析语法和生成代码

3.作用域——引擎的另一个好朋友,收集和保存声明过的标识符(变量),以及严格规定了正在执行的代码如何对这些变量进行访问。

台前幕后:

当你看到代码 var a = 2;时,你很可能会把它看作一条语句,但引擎不是这么认为的。事实上,引擎看到的是两条语句,一个是编译器在编译的时候处理的,一个是在引擎执行代码期间才会处理的。

编译器做的第一件事情是分词,生成语法树。

接下来你可能假设编译器会生成类似于这样的代码:给变量分配一个内存区域,标记为 a,然后把值 2 放入 这个变量里。

事实上编译器是这样做的:

1. 遇到 var a,编译器会问作用域,是否有变量 a 已经存在于当前的作用域集合,如果有,编译器就会忽略这个声明,否则,编译器会让作用域在当前的集合中新增一个变量a。

2. 编译器生成可执行代码,让引擎去执行,处理 a = 2 这个赋值语句。执行过程中,这段代码会首先问作用域,当前的集合范围内是否存在一个变量 a 可以访问,如果有,引擎会使用这个变量,如果没有,就会向上查找(嵌套作用域)

如果引擎最终可以找到这个变量,就给它赋值为 2,如果没有,引擎就会举手大喊“有个错误”

总结:一个变量赋值有两个动作,首选编译器声明变量(如果在当前范围内没有被声明过),其次,引擎查找这个变量,如果找到就赋值给这个变量。

编译器说

我们需要一些术语来进一步理解。

当引擎执行编译器在第 2 步生成的代码时,它必须去查找是否这个变量已经被声明,这个查找就是去问作用域,但是查询的方式会影响查询的结果。

在这个例子中,引擎将进行 “LHS” 查询变量 a ,另外一种查询为 “RHS”。

LHS 代表 左边,RHS 是右边,这个左右是对于赋值操作符来说的。

当一个变量出现在赋值操作符的左边时,就会进行 LHS 查找,反之,则是 RHS 查找。

一个 RHS 是不太好区分的,因为它仅仅是查询变量的值,而 LHS 查询就是查找这个变量本身,然后可以进行赋值。

从这方面看,RHS 并不是真的在赋值操作右边的意思,它仅仅意味着不在左边。你也可以认为 RHS 是去取某个值。

比如:

console.log(a);

这里的 a 就是一个 RHS 引用,因为 a 没有被赋值,我们只是把 a 里的值取出来传给了console.log()

a = 2;

这个 a 就是一个 LHS 引用,因为这里我们不关心当前 a 的值是什么,我们只想把 2 赋值给它

注:LHS 和 RHS 这里的左右并不一定是字面代表的赋值操作符的左右,赋值也会以其它方式进行。所以 LHS 为赋值的目标,RHS 为赋值的源。

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

这里 foo(a) 中的 foo 就是一个 RHS 引用,而 foo(2) 在执行之后,把值 2 赋值给了参数 a,所有这里 a 是一个 LHS 引用。

在 console.log(a) 中,a 又是一个 RHS 引用,得到的值传给 console.log(),console 是一个 RHS 引用,然后检查这个对象是否有 log() 方法。

最后,概念化描述一下,当值 2 通过 a 传入 log() 时,a 发生了一个 LHS/RHS 交换,在 log 方法的内部实现中,我们可以假设它有参数,第一个参数是一个 LHS 引用,然后赋值为2 。

注:你可能会将函数声明理解为普通的变量声明和赋值,例如 var foo 以及 foo = function(a){... ,也就会认为这里包含一次 LHS 查询。

然而,这个细小但很重要的区别是,编译器在代码生成的时候处理声明和值的定义,也就是说在引擎执行代码的时候,没必要有把一个函数赋值给变量 foo 这个操作,因此,把一个函数声明看作是一个 LHS 查询赋值是不太合适的。

引擎/作用域 对话

还是上面的代码,想象一组对话:

引擎:嘿!作用域,我有一个 foo 的 RHS 引用,你听过它吗?

作用域:呀 我见过,编译器刚刚声明了它,它是一个函数,给你。

引擎:太棒了 谢谢!我要执行 foo 了

引擎:嘿!作用域,我有一个 对 a 的 LHS 引用,你听过吗?

作用域:呀 我见过的,编译器刚刚声明了它作为 foo 的形参,给你啦

引擎:再次感谢啦!现在可以把 2 赋值给 a 了

引擎:嘿!作用域,我有一个 对 console 的 RHS 引用,你听过吗?

作用域:没问题,它是个内置对象,给你

引擎:完美!查找 log(),找到!太好了,这是个函数

引擎:嘿!作用域,你帮我查一下对 a 的 RHS 引用,我记得它,但是想再次确认一下。

作用域:你是对的!还是那个 a ,给你

引擎:好了!现在把 a 的值,也就是 2 ,传给 log()

……

测试:

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

找出其中的 LHS 查询(3个)

找出其中的 RHS 查询(4个)

注:在小结中查看答案

嵌套作用域

作用域是一组定义如何通过标识符名称查询变量的规则。通常程序中不只有一个作用域。

就像一个代码块或者函数可以被嵌套在另一个代码块或者函数中,作用域也可以嵌套别的作用域。

所以如果一个变量在当前的作用域没有被找到的话,引擎就会查找包含这个作用域的外部作用域,直到找到这个变量或者一直达到最外层(也就是全局作用域)。

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

在这段代码中,对 b 的 RHS 引用在 foo 内部无法解决,但是在全局作用域中存在。

引擎在查找到最外层的全局作用域时就会停止,无论是否找到到变量。

建筑比喻

这栋建筑代表程序的嵌套作用域规则集合。第一层是代表你现在正则执行的作用域。建筑顶层是全局作用域。

你要查找当前的楼层来解析 LHS 和 RHS 引用。如果没找到,就坐电梯去上一层找,然后再上一层,……

错误

为什么区别 LHS 和 RHS 是重要的?

因为如果变量没有被声明时,这两种查找的行为是不一样的。

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

当对 b 进行 RHS 查找时,没有找到,这个时候就做 未声明 变量,引擎会抛出 ReferenceError 类型的错误。

相反,如果引擎进行 LHS 查找的时候,直到全局作用域都没有查询成功,如果程序不是在严格模式下,全局作用域会创建一个变量,然后返回给引擎。

但在严格模式下,引擎会和 RHS 的情况一样,抛出 ReferenceError 类型的错误。

如果一个 RHS 引用的变量被查找到,但是你想做一些不可能的操作,比如把一个非函数的值当作函数去调用,或者引用值为 null 或 undefined 的变量的属性,这时引擎就会抛出 TypeError 类型的错误。

ReferenceError 是作用域解析失败相关的错误,而 TypeError 表示作用域查找没有问题,但是对于值的操作非法/不可能的操作。

总结

作用域的概念

LHS 查询和 RHS 查询

与作用域相关的赋值可以发生在赋值语句或者调用函数传参时

LHS 查询失败在严格模式和非严格模式下表现不同,在严格模式下抛出 ReferenceError 错误。

测试题答案

1. c = ..

  a = 2

  b = ..

2. foo(2..

   = a;

  a + ..

  .. + b

原文地址 https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch1.md

你不知道的JS之作用域和闭包(一)什么是作用域?的更多相关文章

  1. 你不知道的JS之作用域和闭包(五)作用域闭包

    原文:你不知道的js系列 一个简单粗暴的定义 闭包就是即使一个函数在它所在的词法作用域外部被执行,这个函数依然可以访问这个作用域. 比如: function foo() { var a = 2; fu ...

  2. 说说循环与闭包——《你不知道的JS》读书笔记(一)

    什么是闭包 <你不知道的JS>里有对闭包的定义:"当函数可以记住并访问所在的词法作用域,即使函数是在当前作用域之外执行,这就产生了闭包." 讲闭包是啥的太多了...就一 ...

  3. 《你不知道的JavaScript》第一部分:作用域和闭包

    第1章 作用域是什么 抛出问题:程序中的变量存储在哪里?程序需要时,如何找到它们? 设计 作用域 的目的:为了更好地存储和访问变量. 作用域:根据名称查找变量的一套规则,用于确定在何处以及如何查找变量 ...

  4. 【 js 基础 】作用域和闭包

    一.编译过程 常见编译性语言,在程序代码执行之前会经历三个步骤,称为编译. 步骤一:分词或者词法分析 将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元. 例子:  var a = 2 ...

  5. 【 js 基础 】【读书笔记】作用域和闭包

    一.编译过程 常见编译性语言,在程序代码执行之前会经历三个步骤,称为编译. 步骤一:分词或者词法分析 将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元. 例子:  var a = 2 ...

  6. JS的作用域和闭包

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

  7. 《你必须知道的javascript(上)》- 1.作用域和闭包

    1 作用域是什么 1.1 编译原理 分词/词法分析(Tokenizing/Lexing) 将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token). 解析/语 ...

  8. 前端知识体系:JavaScript基础-作用域和闭包-闭包的实现原理和作用以及堆栈溢出和内存泄漏原理和相应解决办法

    闭包的实现原理和作用 闭包: 有权访问另一个函数作用域中的变量的函数. 创建闭包的常见方式就是,在一个函数中创建另一个函数. 闭包的作用: 访问函数内部变量.保持函数在环境中一直存在,不会被垃圾回收机 ...

  9. JavaScript之作用域和闭包

    一.作用域 作用域共有两种主要的工作模型:第一种是最为普遍的,被大多数编程语言所采用的词法作用域,另外一种叫作动态作用域: JavaScript所采用的作用域模式是词法作用域. 1.词法作用域 词法作 ...

  10. 你不知道的JS之作用域和闭包 附录

     原文:你不知道的js系列 A 动态作用域 动态作用域 是和 JavaScript中的词法作用域 对立的概念. 动态作用域和 JavaScript 中的另外一个机制 (this)很相似. 词法作用域是 ...

随机推荐

  1. bounding box的简单理解

    1. 小吐槽 OverFeat是我看的第一篇深度学习目标检测paper,因为它是第一次用深度学习来做定位.目标检测问题.可是,很难懂...那个bounding box写得也太简单了吧.虽然,很努力地想 ...

  2. Linux系统(虚拟机)安装禅道

    1.查看linux系统版本 uname -a 2.禅道下载:http://www.zentao.net/download.html,找到要下载的版本,点击进入各平台下载: 3.将下载好的安装包上传到l ...

  3. TP5多模块开发

    一般的thinkphp框架一般都是单模块开发的,但有时候我们可能需要进行多模块开发,例如添加个后台管理的模块.这次给人讲课,在Tp多模块开发的配置上翻车,感觉很有必要总结下,话不多说,直接上干货. 总 ...

  4. 定时任务框架Quartz-(一)Quartz入门与Demo搭建

    注:本文来源于:是Guava不是瓜娃  <定时任务框架Quartz-(一)Quartz入门与Demo搭建> 一.什么是Quartz 什么是Quartz? Quartz是OpenSympho ...

  5. 洛谷 P1045 & [NOIP2003普及组] 麦森数

    题目链接 https://www.luogu.org/problemnew/show/P1045 题目大意 本题目的主要意思就是给定一个p,求2p-1的位数和后500位数. 解题思路 首先看一下数据范 ...

  6. java ReentrantLock结合条件队列 实现生产者-消费者模式 以及ReentratLock和Synchronized对比

    package reentrantlock; import java.util.ArrayList; public class ProviderAndConsumerTest { static Pro ...

  7. Photoshop 操作

    本文主要记录在工作过程中使用ps的一些快捷键或操作顺序 1.ctrl+H:取消标尺 2.ctrl+D:取消选区 3.看矩形尺寸:选中矩形图层 >窗口 >属性(w:宽  H:高) 4.看图层 ...

  8. springboot 错误求解决

    最近再学习springboot这个好东西,结果给当成白老鼠了,我使用的是idea 2018  来测试  一个简单的界面跳转  ,结果报错了,在网上搜了好半天没搜到相应的解决方案,很头疼,希望哪位大神能 ...

  9. 【转】vscode调试运行c#详细操作过程

    [转]vscode调试运行c#详细操作过程 主要命令: //路径跳转cd //新建项目dotnet new console -o 路径 //运行dotnet run //用于发布exe<Runt ...

  10. java servlet的域对象

    在进行网络编程中的项目时 经常用到的域对象主要包括以下三种: 1. ServletContext  作用范围比较大 代码如下: //一个请求代码: ServletContext sc = reques ...