你不知道的JS系列【1】- 什么是作用域
几乎所有的编程语言都能够储存变量,并且能在之后对这个变量值进行访问或修改,正是储存和访问变量的能力将状态带给了程序,那么,这些变量储存在哪里呢?程序需要时又是如何找到他们?这些问题说明需要一套设计良好的规则来储存变量,并且之后可以方便的找到这些变量,这套规则被称为作用域。
1、了解编译原理
尽管将JS归类为“动态”或“解释执行”脚本语言,但事实上它是一门编译语言。但是与传统编译语言不同的是,它不是提前编译的,编译结果也不能在分布式系统中进行移植。JS引擎进行编译的步骤与传统的语言非常相似,程序中一段源代码在执行之前会经历三个步骤,统称为“编译”。
- 分词/词法分析
这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元。例如,考虑程序
var a = 2;
。这段程序通常会被分解成 为下面这些词法单元:var、a、=、2 、;
。
- 解析/语法分析
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
var a = 2;
的抽象语法树中可能会有一个叫作VariableDeclaration的顶级节点,接下来是一个叫作 Identifier(它的值是 a)的子节点,以及一个叫作 AssignmentExpression 的子节点。AssignmentExpression 节点有一个叫作 NumericLiteral(它的值是 2)的子节点。
- 代码生成
AST转换为可执行代码的过程称被称为代码生成,简单来说就是有某种方法可以将
var a = 2;
的AST转化为一组机器指 令,用来创建一个叫作a的变量(包括分配内存等),并将一个值储存在a中。
编译流程如下图所示:
JS引擎比传统的编译语言编译器复杂很多,在语法分析和代码生成阶段有特定的步骤来对性能进行优化,大部分情况下编译发生在代码之前的前几微秒,在讨论作用域背后,js引擎用了各种办法来保证性能最佳。
Tips:我们平时在写JS代码的时候,一个语句结尾要加分号(;),便于JS编译器编译。
2、理解作用域
我们先了解JS编译过程中几个名词,JS引擎,编译器,作用域。
2.1.名词介绍
- JS引擎:从头到尾负责整个JS程序编译过程。
- 编译器:负责语法分析及代码生成等。
- JS引擎:负责收集并维护由所有声明的标识符(变量)组成的一系列查 询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
2.2.变量赋值
对于var a=2;
这段代码,我们认为这就是申明一个为变量a且初始值为2,实际上,JS引擎认为这里有两个完全不同的申明,一个由编译器在编译时处理,另一个则由引擎在运行时处理。
处理过程分为两步:
1.遇到var a
,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。
2.接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2
这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。
如果引擎最终找到a变量,就会将2赋值给它。否则就抛出异常。
Tips:声明提前(hoist)-JS引擎在创建变量时,会将该变量提升到当前作用域的最前面。
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
2.3.LHS查询&RHS查询
编译器在编译过程中的第二步生成了代码,引擎在执行时,会通过查找变量a来判断它是否已经声明过。当变量出现在赋值操作的左侧时进行LHS查询,当变量出现在右侧时进行RHS查询。
console.log(a); //对a的引用时RHS引用,这里没有对a赋予任何值,需要查找a的值。
a=2; //对a的引用是LHS引用,因为这里不关心a的值等于多少,只想为 =2 这个赋值操作找到一个目标(变量a);
LHS和RHS的含义是“赋值操作的左侧或右侧”并不一定意味着就是“= 赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“去找到XX变量的值,谁是赋值操作的源头(RHS)”。
3、作用域嵌套
作用域是根据名称查找变量的一套规则。实际情况中,通常需要同时顾及几个作用域。 当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用 域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。
参考以下代码:
var name='peer';
function sayHello(){
alert('hello '+ name)
}
sayHello();
// 对name的RHS引用无法在函数sayHello完成,但是可以在上一级作用域中完成。
把作用域比喻成一个建筑如下图所示:
LHS和RHS引用都会在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼,如果还是没有找到就继续向上,以此类推。一旦抵达顶层(全局作用域),可能找到了你所需的变量,也可能没找到,但无论如何查找过程都将停止。
4、总结
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。LHS和RHS查询都会在当前执行作用域中开始,如果有需要就会向上级作用域继续查找目标标识符,这样每次上升一级作用域,最后抵达全局作用域,无论找到或没找到都将停止。不成功的RHS 引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),掌握这些基本作用域知识能使我们更深入理解JS引擎的编译过程来编写更高性能的代码。
参考资料:
《你不知道的JavaScript》
你不知道的JS系列【1】- 什么是作用域的更多相关文章
- 你不知道的 JS (系列丛书) - 第二版
你不知道的 JS (系列丛书) - 第二版 You Don't Know JS (book series) - 2nd Edition https://github.com/learning-js-b ...
- 你不知道的JS之作用域和闭包 附录
原文:你不知道的js系列 A 动态作用域 动态作用域 是和 JavaScript中的词法作用域 对立的概念. 动态作用域和 JavaScript 中的另外一个机制 (this)很相似. 词法作用域是 ...
- 你不知道的JS之作用域和闭包(五)作用域闭包
原文:你不知道的js系列 一个简单粗暴的定义 闭包就是即使一个函数在它所在的词法作用域外部被执行,这个函数依然可以访问这个作用域. 比如: function foo() { var a = 2; fu ...
- 你不知道的JS之作用域和闭包(四)(声明)提升
原文:你不知道的js系列 先有鸡还是先有蛋? 如下代码: a = 2; var a; console.log( a ); 很多开发者可能会认为结果会输出 undefined,因为 var a 在 a ...
- 你不知道的JS之作用域和闭包(三)函数 vs. 块级作用域
原文:你不知道的js系列 在第(二)节中提到的,标识符在作用域中声明,这些作用域就像是一个容器,一个嵌套一个,这个嵌套关系是在代码编写时定义的. 那么到底是什么产生了一个新的作用域,只有函数能做到 ...
- 你不知道的JS之作用域和闭包(二)词法作用域
原文:你不知道的js系列 词法作用域(Lexical Scope) Lex time 一个标准的编译器的第一个阶段就是分词(token化) 词法作用域就是在词法分析时定义的作用域.换句话说,词法作用域 ...
- 你不知道的JS之作用域和闭包(一)什么是作用域?
原文:你不知道的js系列 什么是作用域(Scope)? 作用域 是这样一组规则——它定义了如何存放变量,以及程序如何找到之前定义的变量. 编译器原理 JavaScript 通常被归类为动态语言或者解释 ...
- 你不知道的JS之 this 和对象原型(一)this 是什么
原文:你不知道的js系列 JavaScript 的 this 机制并没有那么复杂 为什么会有 this? 在如何使用 this 之前,我们要搞清楚一个问题,为什么要使用 this. 下面的代码尝试去 ...
- 翻译连载 | 第 10 章:异步的函数式(上)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇
原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...
随机推荐
- Ubuntu php安装xdebug
1.安装xdebug扩展: sudo apt-get install php-xdebug 2.找到扩展的路径: 3.编辑php.ini文件,末尾加入,保存退出: [xdebug] zend_exte ...
- java源码解析
String深入解析 String具有不变性的原因: String被final修饰,它不可能被继承,也就是任何对String的操作方法,都不会被继承覆写 String中保存数据的是一个char数组的v ...
- Go语言系列开发之延迟调用和作用域
Hello,各位小伙伴大家好,我是小栈君,最近一段时间我们将继续分享关于go语言基础系列,当然后期小栈君已经在筹划关于java.Python,数据分析.人工智能和大数据等相关系列文章.希望能和大家一起 ...
- 面试必问的Synchronized知道这些就可以了
Synchronized关键字算是Java的元老级锁了,一开始它撑起了Java的同步任务,其用法简单粗暴容易上手.但是有些与它相关的知识点还是需要我们开发者去深入掌握的.比如,我们都知道通过Synch ...
- 详解 Redis 内存管理机制和实现
Redis是一个基于内存的键值数据库,其内存管理是非常重要的.本文内存管理的内容包括:过期键的懒性删除和过期删除以及内存溢出控制策略. 最大内存限制 Redis使用 maxmemory 参数限制最大可 ...
- 使用 Hexo,Material Theme 以及 Github Pages 搭建个人博客
准备条件 Node.js npm Git GitHub账号 开始搭建 hexo init Blog cd Blog npm install hexo-deployer-git --save npm i ...
- Dubbo配置完全外部化实践,使用动态配置中心的注意事项
问题描述 近期开发项目,将Dubbo的配置全部外部化到动态配置中心.这里配置中心我使用的是Apollo. @Configuration public class DubboConfig { @Bean ...
- 实用脚本awk
非常实用的awk 有时候需要去服务器下载几个日志 日志太多,翻滚起来很麻烦,操作又慢又复杂. 可以使用这个下载最新的两个文件 ls -lt | head -3 | awk -F ' ' '{if(NR ...
- List<model>需要根据特定字段求差集的实现
list对象不能直接使用Except等封装好的函数,因为内存地址不一样(还有一些数虽然主数据一致但是update/create信息也不一致,对,我碰到的需求就是这么难受 TOT) 这时候我们的需求很多 ...
- Nexus安装(Windows)
1. nexus下载 官网下载:https://www.sonatype.com/download-oss-sonatype 网盘下载:https://pan.baidu.com/s/1CXOW7Lv ...