深入理解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);//2
}
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);// 4
在代码片段中,作用域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);//1
【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作用域系列第一篇——内部原理
× 目录 [1]编译 [2]执行 [3]查询[4]嵌套[5]异常[6]原理 前面的话 javascript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域.作用域 ...
- 深入理解javascript函数系列第一篇——函数概述
× 目录 [1]定义 [2]返回值 [3]调用 前面的话 函数对任何一门语言来说都是一个核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即 ...
- 深入理解javascript作用域系列第二篇——词法作用域和动态作用域
× 目录 [1]词法 [2]动态 前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极 ...
- 深入理解javascript作用域系列第二篇
前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极易出错.这实际上是由两种作用域工作 ...
- 深入理解javascript函数系列第一篇
前面的话 函数对任何一门语言来说都是核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即对象,程序可以随意操控它们.函数可以嵌套在其他函数中 ...
- 深入理解javascript对象系列第一篇——初识对象
× 目录 [1]定义 [2]创建 [3]组成[4]引用[5]方法 前面的话 javascript中的难点是函数.对象和继承,前面已经介绍过函数系列.从本系列开始介绍对象部分,本文是该系列的第一篇——初 ...
- 深入理解javascript作用域系列第三篇——声明提升(hoisting)
× 目录 [1]变量 [2]函数 [3]优先 前面的话 一般认为,javascript代码在执行时是由上到下一行一行执行的.但实际上这并不完全正确,主要是因为声明提升的存在.本文是深入理解javasc ...
- 深入理解javascript作用域系列第三篇
前面的话 一般认为,javascript代码在执行时是由上到下一行一行执行的.但实际上这并不完全正确,主要是因为声明提升的存在.本文是深入理解javascript作用域系列第三篇——声明提升(hois ...
- 深入理解javascript作用域系列第五篇——一张图理解执行环境和作用域
× 目录 [1]图示 [2]概念 [3]说明[4]总结 前面的话 对于执行环境(execution context)和作用域(scope)并不容易区分,甚至很多人认为它们就是一回事,只是高程和犀牛书关 ...
随机推荐
- CF821 B. Okabe and Banana Trees 简单数学
Link 题意:给出一条直线,在直线上取一点,其垂直x,y轴作成一个,求矩阵中所有包含的点的x,y坐标之和的最大值. 思路:对于一个任意一点我们计算公式,对于任意一点$(x, y)$,有$(x+y)^ ...
- ZOJ 3774 Fibonacci的K次方和
In mathematics, Fibonacci numbers or Fibonacci series or Fibonacci sequence are the numbers of the f ...
- How to Evaluate Machine Learning Models, Part 4: Hyperparameter Tuning
How to Evaluate Machine Learning Models, Part 4: Hyperparameter Tuning In the realm of machine learn ...
- 你知道吗?31种 CSS 选择器的应用
选择器(selector)是CSS中很重要的概念,所有HTML语言中的标记都是通过不同的CSS选择器进行控制的.用户只需要通过选择器对不同的HTML标签进行控制,并赋予各种样式声明,即可实现各种效果. ...
- TED_Topic2:My desperate journey with a human smuggler
My desperate journey with a human smuggler By Barat Ali Batoor When I was a child there was a toy wh ...
- 【Atcoder】AGC 020 B - Ice Rink Game 递推
[题意]n个人进行游戏,每轮只保留最大的a[i]倍数的人,最后一轮过后剩余2人,求最小和最大的n,或-1.n<=10^5. [算法]递推||二分 [题解]令L(i),R(i)表示第i轮过后的最小 ...
- JavaScript:自动生成博文目录导航
感谢 孤傲苍狼 分享了 自动生成博文目录的方法,本文仅作存档使用. 图 1:效果预览 CSS 样式 #TOCbar{ font-size:12px; text-align:left; position ...
- NYOJ 1012 RMQ with Shifts (线段树)
题目链接 In the traditional RMQ (Range Minimum Query) problem, we have a static array A. Then for each q ...
- Django之组合搜索组件(二)--另附simple_tag的创建使用方法
这次的代码为Django之组合搜索组件(一)的改版,实现的结果和(一)相同,不同的是,这次运用simple_tag方式,使.html程序简化 所以现在就开始编程吧! 首先想使用simple_tag方法 ...
- ueditor和thinkphp框架整合修改版
基于tp官网上的一篇文章修改的 因为tp中所有目录其实都是性对于入口文件的 在原来的基础上略做修改后 已经做到 无论项目放在www下的任何位置 图片在编辑器中回填后都能正常显示! http://fi ...