前面的话

  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作用域系列第一篇——内部原理的更多相关文章

  1. 深入理解javascript作用域系列第一篇

    前面的话 javascript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域.作用域貌似简单,实则复杂,由于作用域与this机制非常容易混淆,使得理解作用域的原 ...

  2. 深入理解javascript函数系列第一篇——函数概述

    × 目录 [1]定义 [2]返回值 [3]调用 前面的话 函数对任何一门语言来说都是一个核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即 ...

  3. 深入理解javascript作用域系列第二篇——词法作用域和动态作用域

    × 目录 [1]词法 [2]动态 前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极 ...

  4. 深入理解javascript作用域系列第二篇

    前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极易出错.这实际上是由两种作用域工作 ...

  5. 深入理解javascript函数系列第一篇

    前面的话 函数对任何一门语言来说都是核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即对象,程序可以随意操控它们.函数可以嵌套在其他函数中 ...

  6. 深入理解javascript对象系列第一篇——初识对象

    × 目录 [1]定义 [2]创建 [3]组成[4]引用[5]方法 前面的话 javascript中的难点是函数.对象和继承,前面已经介绍过函数系列.从本系列开始介绍对象部分,本文是该系列的第一篇——初 ...

  7. 深入理解javascript作用域系列第五篇——一张图理解执行环境和作用域

    × 目录 [1]图示 [2]概念 [3]说明[4]总结 前面的话 对于执行环境(execution context)和作用域(scope)并不容易区分,甚至很多人认为它们就是一回事,只是高程和犀牛书关 ...

  8. 深入理解javascript作用域系列第五篇

    前面的话 对于执行环境(execution context)和作用域(scope)并不容易区分,甚至很多人认为它们就是一回事,只是高程和犀牛书关于作用域的两种不同翻译而已.但实际上,它们并不相同,却相 ...

  9. 深入理解javascript作用域系列第三篇——声明提升(hoisting)

    × 目录 [1]变量 [2]函数 [3]优先 前面的话 一般认为,javascript代码在执行时是由上到下一行一行执行的.但实际上这并不完全正确,主要是因为声明提升的存在.本文是深入理解javasc ...

随机推荐

  1. http协议与网页优化泛谈

    最近公司开展职业技能培训,本人招录了部分内容写成博客,若有纰漏,欢迎拍砖. http简述: http又称超文本传输协议,是一种无状态&无记忆协议的运用层协议,处于tcp传输层协议之上,采用请求 ...

  2. 一个好用的C#类型转换器

    public static object ChangeType(object value, Type targetType) { if (targetType.IsGenericType && ...

  3. 关于Certificate、Provisioning Profile、App ID的介绍及其之间的关系

    1.概念介绍 如果你拥有一个开发者账户的话,在iOS Dev Center打开Certificates, Indentifiers & Profiles,你就可以看到如下的列表: Profil ...

  4. [置顶]PADS PCB功能使用技巧系列之NO.003- 如何统一修改元件标号字体?

    LAYOUT完毕后进行元件标号字体调整时,你是否试图用Select Document+Select All来选定所有标号?可结果却并不令人满意. (1)在Layout中,选择菜单栏Edit -> ...

  5. 高性能网站架构设计之缓存篇(4)- Redis 主从复制

    Redis 的主从复制配置非常容易,但我们先来了解一下它的一些特性. redis 使用异步复制.从 redis 2.8 开始,slave 也会周期性的告诉 master 现在的数据量.可能只是个机制, ...

  6. [转]单点登录SSO学习——CAS协议内容

    作者:anmaler 本文转自:http://blog.zhaojunling.me/p/24 CAS中文文档甚少,这篇文章对CAS接口参数有比较清楚的说明,排版也不错查阅舒适 在当前互联网产品中使用 ...

  7. 获取机器安装.NET版本的几种方式

    当调查应用程序问题时,通常需要先确认目标机器所安装的 .NET Framework 的版本.可以通过如下方式来确认版本号: 通过控制面板安装程序查询 通过查询注册表获取版本信息 通过查看安装目录获取版 ...

  8. 防止开发人员获取到敏感数据(SQL Server的数据加密简介)

    背景 有时候,我们还真的会碰到这样的需求:防止开发人员获取到敏感数据.也许你觉得很简单,把开发和运营分开不就可以了吗?是的,如果公司有专门的运营团队的话,但对于很多小公司来说,几个人的开发团队就兼顾了 ...

  9. Java中文编码小结

    Java中文编码小结 1. 只有 字符到字节 或者 字节到字符 的转换才存在编码转码; 2. Java String 采用 UTF-16 编码方式存储所有字符.unicode体系采用唯一的码点表示唯一 ...

  10. 关于Windows窗口框架

    我们知道Windows的窗口消息处理函数是C方式, 面向过程的, 所以窗口框架的基本任务就是将它转成面向对象的方式, 确切的说如何将消息处理函数第一参数HWND转成对象指针. 关于这个问题, 其实网上 ...