深入理解javascript作用域系列第一篇——内部原理
前面的话
javascript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域。作用域貌似简单,实则复杂,由于作用域与this机制非常容易混淆,使得理解作用域的原理更为重要。本文是深入理解javascript作用域系列的第一篇——内部原理
内部原理分成编译、执行、查询、嵌套和异常五个部分进行介绍,最后以一个实例过程对原理进行完整说明
编译
以var a = 2;为例,说明javascript的内部编译过程,主要包括以下三步:
【1】分词(tokenizing)
把由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)
var a = 2;被分解成为下面这些词法单元:var、a、=、2、;。这些词法单元组成了一个词法单元流数组
// 词法分析后的结果
[
"var" : "keyword",
"a" : "identifier",
"=" : "assignment",
"2" : "integer",
";" : "eos" (end of statement)
]
【2】解析(parsing)
把词法单元流数组转换成一个由元素逐级嵌套所组成的代表程序语法结构的树,这个树被称为“抽象语法树” (Abstract Syntax Tree, AST)
var a = 2;的抽象语法树中有一个叫VariableDeclaration的顶级节点,接下来是一个叫Identifier(它的值是a)的子节点,以及一个叫AssignmentExpression的子节点,且该节点有一个叫Numericliteral(它的值是2)的子节点
{
operation: "=",
left: {
keyword: "var",
right: "a"
}
right: "2"
}
【3】代码生成
将AST转换为可执行代码的过程被称为代码生成
var a=2;的抽象语法树转为一组机器指令,用来创建一个叫作a的变量(包括分配内存等),并将值2储存在a中
实际上,javascript引擎的编译过程要复杂得多,包括大量优化操作,上面的三个步骤是编译过程的基本概述
任何代码片段在执行前都要进行编译,大部分情况下编译发生在代码执行前的几微秒。javascript编译器首先会对var a=2;这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它
执行
简而言之,编译过程就是编译器把程序分解成词法单元(token),然后把词法单元解析成语法树(AST),再把语法树变成机器指令等待执行的过程
实际上,代码进行编译,还要执行。下面仍然以var a = 2;为例,深入说明编译和执行过程
【1】编译
1、编译器查找作用域是否已经有一个名称为a的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a
2、编译器将var a = 2;这个代码片段编译成用于执行的机器指令
[注意]依据编译器的编译原理,javascript中的重复声明是合法的
//test在作用域中首次出现,所以声明新变量,并将20赋值给test
var test = 20;
//test在作用域中已经存在,直接使用,将20的赋值替换成30
var test = 30;
【2】执行
1、引擎运行时会首先查询作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量
2、如果引擎最终找到了变量a,就会将2赋值给它。否则引擎会抛出一个异常
查询
在引擎执行的第一步操作中,对变量a进行了查询,这种查询叫做LHS查询。实际上,引擎查询共分为两种:LHS查询和RHS查询
从字面意思去理解,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询
更准确地讲,RHS查询与简单地查找某个变量的值没什么区别,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值
function foo(a){
console.log(a);//
}
foo( 2 );
这段代码中,总共包括4个查询,分别是:
1、foo(...)对foo进行了RHS引用
2、函数传参a = 2对a进行了LHS引用
3、console.log(...)对console对象进行了RHS引用,并检查其是否有一个log的方法
4、console.log(a)对a进行了RHS引用,并把得到的值传给了console.log(...)
嵌套
在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止
function foo(a){
console.log( a + b ) ;
}
var b = 2;
foo(2);//
在代码片段中,作用域foo()函数嵌套在全局作用域中。引擎首先在foo()函数的作用域中查找变量b,并尝试对其进行RHS引用,没有找到;接着,引擎在全局作用域中查找b,成功找到后,对其进行RHS引用,将2赋值给b
异常
为什么区分LHS和RHS是一件重要的事情?因为在变量还没有声明(在任何作用域中都无法找到变量)的情况下,这两种查询的行为不一样
RHS
【1】如果RHS查询失败,引擎会抛出ReferenceError(引用错误)异常
//对b进行RHS查询时,无法找到该变量。也就是说,这是一个“未声明”的变量
function foo(a){
a = b;
}
foo();//ReferenceError: b is not defined
【2】如果RHS查询找到了一个变量,但尝试对变量的值进行不合理操作,比如对一个非函数类型值进行函数调用,或者引用null或undefined中的属性,引擎会抛出另外一种类型异常:TypeError(类型错误)异常
function foo(){
var b = 0;
b();
}
foo();//TypeError: b is not a function
LHS
【1】当引擎执行LHS查询时,如果无法找到变量,全局作用域会创建一个具有该名称的变量,并将其返还给引擎
function foo(){
a = 1;
}
foo();
console.log(a);//
【2】如果在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError异常
function foo(){
'use strict';
a = 1;
}
foo();
console.log(a);//ReferenceError: a is not defined
原理
function foo(a){
console.log(a);
}
foo(2);
以上面这个代码片段来说明作用域的内部原理,分为以下几步:
【1】引擎需要为foo(...)函数进行RHS引用,在全局作用域中查找foo。成功找到并执行
【2】引擎需要进行foo函数的传参a=2,为a进行LHS引用,在foo函数作用域中查找a。成功找到,并把2赋值给a
【3】引擎需要执行console.log(...),为console对象进行RHS引用,在foo函数作用域中查找console对象。由于console是个内置对象,被成功找到
【4】引擎在console对象中查找log(...)方法,成功找到
【5】引擎需要执行console.log(a),对a进行RHS引用,在foo函数作用域中查找a,成功找到并执行
【6】于是,引擎把a的值,也就是2传到console.log(...)中
【7】最终,控制台输出2
深入理解javascript作用域系列第一篇——内部原理的更多相关文章
- 深入理解javascript作用域系列第一篇
前面的话 javascript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域.作用域貌似简单,实则复杂,由于作用域与this机制非常容易混淆,使得理解作用域的原 ...
- 深入理解javascript函数系列第一篇——函数概述
× 目录 [1]定义 [2]返回值 [3]调用 前面的话 函数对任何一门语言来说都是一个核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即 ...
- 深入理解javascript作用域系列第二篇——词法作用域和动态作用域
× 目录 [1]词法 [2]动态 前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极 ...
- 深入理解javascript作用域系列第二篇
前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极易出错.这实际上是由两种作用域工作 ...
- 深入理解javascript函数系列第一篇
前面的话 函数对任何一门语言来说都是核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即对象,程序可以随意操控它们.函数可以嵌套在其他函数中 ...
- 深入理解javascript对象系列第一篇——初识对象
× 目录 [1]定义 [2]创建 [3]组成[4]引用[5]方法 前面的话 javascript中的难点是函数.对象和继承,前面已经介绍过函数系列.从本系列开始介绍对象部分,本文是该系列的第一篇——初 ...
- 深入理解javascript作用域系列第五篇——一张图理解执行环境和作用域
× 目录 [1]图示 [2]概念 [3]说明[4]总结 前面的话 对于执行环境(execution context)和作用域(scope)并不容易区分,甚至很多人认为它们就是一回事,只是高程和犀牛书关 ...
- 深入理解javascript作用域系列第五篇
前面的话 对于执行环境(execution context)和作用域(scope)并不容易区分,甚至很多人认为它们就是一回事,只是高程和犀牛书关于作用域的两种不同翻译而已.但实际上,它们并不相同,却相 ...
- 深入理解javascript作用域系列第三篇——声明提升(hoisting)
× 目录 [1]变量 [2]函数 [3]优先 前面的话 一般认为,javascript代码在执行时是由上到下一行一行执行的.但实际上这并不完全正确,主要是因为声明提升的存在.本文是深入理解javasc ...
随机推荐
- 《LINUX内核设计与实现》读书笔记之第五章
第五章——系统调用 5.1 与内核通信 1.为用户空间提供一种硬件的抽象接口 2.保证系统稳定和安全 3.除异常和陷入,是内核唯一的合法入口. API.POSIX和C库 关于Unix接口设计:提供机制 ...
- Bootstrap 栅格系统
1.简介 Bootstrap内置了一套响应式.移动设备优先的流式栅格系统,随着屏幕设备或视口(viewport)尺寸的增加,系统会自动分为最多12列.它包含了易于使用的预定义classe,还有强大的m ...
- ubuntu安装日文分词软件MeCab及其Python插件
1.从官网上下载三个包: http://taku910.github.io/mecab/ mecab-0.996 mecab-0.996.tar.gz mecab-ipadic-2.7.0-20070 ...
- 回调函数(callback)
参考维基百科链接: 中文:http://zh.wikipedia.org/wiki/%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0 英文:http://en.wikipedi ...
- HashSet和HapMap取distinct value
public class TestHashSetAndHashMap { private final int setNum=5000; @Test public void doTest(){ List ...
- 关于echart横轴颜色 纵轴颜色 以及文本颜色的修改
xAxis : [ { type : 'category', boundaryGap : false, data : ['周一','周二','周三','周四','周五','周六','周日'], axi ...
- NancyFx开发-Razor视图using外部Dll解决方案
问题:NancyFx框架 Razor视图无法using 其他项目DLL 解决方案: Nancyfx框架Razor需要在web.config 中声明要引入的命名空间(包括system这些) &l ...
- 创建动态WCF服务(无配置文件)
public class WCFServer { ServiceHost host = null; public WCFServer(string addressurl, string tcpurl, ...
- android知识杂记(二)
记录项目中的android零碎知识点,用以备忘. AsyncQueryHandler 继承与handler,可以用于处理增删改(ContentProvider提供的数据) 例如:query = new ...
- windows10的激活及SQL Server 2008 的安装
在我第一次安装的SQL Server的时候,由于我的电脑是windows8专业版未激活的系统,当时安装的时候,首先出现提示说我电脑缺少了某一个插件,于是乎就下了安装.但是后来又是安装不上,各种问题一一 ...