JavaScript-编译与闭包
编译原理
尽管 JavaScript 经常被归类为“动态”或“解释执行”的语言,但实际上它是一门编译语言。JavaScript 引擎进行的编译步骤和传统编译语言非常相似,但有些地方可能比预想的要复杂。
传统编译流程:
分词/此法分析(Tokenizing/Lexing)
这个过程会将有字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如:
var a = 2
;这段程序通常会被分解成词法单元:var
、a
、=
、2
;空格是否会被当成词法单元,取决于空格在这门语言种是否具有意义。解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
var a = 2
的 AST 为:VariableDeclaration
--Identifier = a
--AssignmentExpression
----NumericLiteral = 2
代码生成
将 AST 转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等息息相关。简单来说就是将 AST 转换为一组机器指令,用来创建一个叫做 a 的变量(包括分配内存等),并将值 2 存储在 a 中。
JavaScript 的编译
JavaScript 的编译由 JavaScript 引擎来负责(包括执行)。编译通常由三个部分组成:
- 引擎:从头到尾负责整个 JavaScript 的编译以及执行;
- 编译器:负责语法分析以及代码生成;
- 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
在我们看来var a = 2
;这是一个普通的变量声明。而在 JavaScript 引擎看来这里有两个完全不同的声明:
var a
,编译器会寻找当前作用域中是否有同样的声明。如果有,则忽略该声明,并继续编译;否则它会在当前作用域(全局/函数作用域)的集合中声明一个新的变量,并命名为 a。- 接下来编译器会为引擎生成运行时所需的代码,这些代码用来处理赋值(
a = 2
)操作。引擎会在当前作用域中查找变量 a。如果能找到,则为其赋值;如果找不到,则继续向上查找(作用域链)。
由于编译的第一步操作会寻找所有的var
关键词声明,无论它在代码的什么位置,都会声明好。在代码真正运行时,所有声明都已经声明好了,哪怕它是在其他操作的下面,都可以直接进行。这就是var
关键词的声明提升。
a = 2;
console.log(a);
var a;
LHS 和 RHS
编译器在编译过程的第二步生成了代码,引擎执行它时,就会查找变量 a 来判断它是否已经声明过。但引擎如何进行查找,影响最终查找的结果。
LHS 和 RHS 分别对应的是左侧查找与右侧查找。左右两侧分别代表一个赋值操作的左侧和右侧。也就说,当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询。
例如:a = 2
,这里进行的就是 LHS 查询。这里不关心 a 的当前值,只想找到 a 并为其赋一个值。
而:console.log(a)
,这里进行的是 RHS 查询。因为这里需要取到 a 的值,而不是为其赋值。
“赋值操作的左侧和右侧”并不一定代表就是=
的左右两侧,赋值操作还有其他多种形式。因此,可以在概念上理解为“查询被赋值的目标(LHS)”以及”查询目标的值(RHS)“。
小测验:
寻找 LHS 查询(3处)以及 RHS 查询(4处)。
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
LHS:
var c = foo(...)
:为变量 c 赋值foo(2)
:传递参数时,为形参 a 赋值 2var b = a
:为变量 b 赋值
RHS:
var c = foo(...)
:查询foo()
var b = a
:(为变量 b 赋值时)取得 a 的值return a + b
:取得 a 与 b(两次)
异常
通过详细的了解异常可以准确的确定发生的问题所在。
在 LHS 查询时,如果到作用域顶部还没有查询到声明,则作用域会热心的帮我们(隐式)创建一个全局变量(非严格模式下)。
而在 RHS 查询时,如果在作用域顶部还没有查询到声明,就会抛出一个 ReferenceError 异常。
在严格模式下,LHS 如果没有找到声明,引擎会抛出一个和 RHS 类似的 ReferenceError 异常。
无论是 LHS 还是 RHS 都是查询一个引用,而没有查询到对应的引用时,就会得到(引用)ReferenceError 异常。
接下来,如果 RHS 查询到了一个变量,但是我们尝试对这个变量的值进行不合理的操作。例如对一个非函数进行函数调用,或者对对象中不存在的属性进行引用。那么引擎会抛出另外一个异常,叫做 TypeError。
闭包
闭包是基于词法作用域书写代码时所产生的自然结果。闭包的主要定义:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
JavaScript 使用的是词法作用域模型,另一种作用域模型是动态作用域。
仔细来看,闭包的主要定义有:
- 函数记住并可以访问所在的词法作用域
- 在当前词法作用域之外执行也能继续访问所在的词法作用域
来看一个例子:
function foo() {
const a = 123;
function bar() {
console.log(a);
}
bar();
}
foo();
这段代码看起来好像符合闭包的一部分定义,虽然bar()
函数并没有脱离当前的词法作用域执行。但是它依然记住了foo()
的词法作用域,并能访问。
它确实满足闭包定义的一部分(很重要的一部分),从技术上讲,也许是,但并不能完全断定这就是闭包。通常我们所见到的与认为闭包的情况就是满足所有定义的时候:
function foo() {
const a = 321;
function bar() {
console.log(a);
}
return bar;
}
// 同理
// foo()();
const baz = foo()
baz();
因为垃圾收集机制,当一个函数执行结束后,通常它的整个内部作用域会被销毁。当我们的foo()
函数执行结束后,看上去它的内容不会再被使用,所以很自然的考虑会被回收。
但闭包的神奇之处就在这里,它会阻止这一切的发生。当bar
被return
出去之后,在其词法作用域的外部依然能够访问foo()
的内部作用域。bar
依然持有对该作用域的引用,这个引用就叫作闭包。
这也是经常见到说闭包会影响性能的主要原因。某些情况下,它确实会影响到性能,例如过度多的返回本不需要的函数,甚至是嵌套。这会导致本不需要的作用域没有被回收。
常见的闭包
上述将一个函数return
出来的案例是最常见的闭包案例。但在我们的代码中,也有些其他非常常见的闭包。不过平时可能没有太过去注意它。
先来回顾一下定义:
无论通过何种手段将内部函数传递到词法作用域之外,它都会保留对改内部词法作用域的引用,无论在何处执行这个函数都会使其闭包。
function waitAMinute(msg: string) {
setTimeout(() => {
console.log(msg);
}, 1000);
}
waitAMinute('嘤嘤嘤');
function btnClick(selector: string, msg: string) {
$(selector).click(() => {
alert(msg);
});
}
btnClick('#btn_1', 'hah');
btnClick('#btn_2', 'got you');
JavaScript-编译与闭包的更多相关文章
- 让你分分钟学会Javascript中的闭包
Javascript中的闭包 前面的话: 闭包,是 javascript 中重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,你很难从定义去理解它 ...
- javascript作用域和闭包之我见
javascript作用域和闭包之我见 看了<你不知道的JavaScript(上卷)>的第一部分--作用域和闭包,感受颇深,遂写一篇读书笔记加深印象.路过的大牛欢迎指点,对这方面不懂的同学 ...
- Javascript中的闭包(转载)
前面的话: 闭包,是 javascript 中重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,你很难从定义去理解它.下面是作者从作用域链慢慢讲到 ...
- 狗日的Javascript中的闭包
前面的话: 闭包,是 javascript 中重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,你很难从定义去理解它.下面是作者从作用域链慢慢讲到 ...
- 深入理解javascript原型和闭包 (转)
该教程绕开了javascript的一些基本的语法知识,直接讲解javascript中最难理解的两个部分,也是和其他主流面向对象语言区别最大的两个部分--原型和闭包,当然,肯定少不了原型链和作用域链.帮 ...
- JavaScript葵花宝典之闭包
闭包,写过JS脚本的人对这个词一定不陌生,都说闭包是JS中最奇幻的一个知识点, 虽然在工作中,项目里经常都会用到~ 但是是不是你已经真正的对它足够的了解~~ 又或者是你代码中出现的闭包,并不是你刻 ...
- 深入理解javascript原型和闭包系列
从下面目录中可以看到,本系列有16篇文章,外加两篇后补的,一共18篇文章.写了半个月,从9月17号开始写的.每篇文章更新时,读者的反馈还是可以的,虽然不至于上头条,但是也算是中规中矩,有看的人,也有评 ...
- 深入理解javascript原型和闭包(1)——一切都是对象
“一切都是对象”这句话的重点在于如何去理解“对象”这个概念. ——当然,也不是所有的都是对象,值类型就不是对象. 首先咱们还是先看看javascript中一个常用的函数——typeof().typeo ...
- 深入理解javascript原型和闭包(2)——函数和对象的关系
上文(理解javascript原型和作用域系列(1)——一切都是对象)已经提到,函数就是对象的一种,因为通过instanceof函数可以判断. var fn = function () { }; co ...
- 深入理解javascript原型和闭包(3)——prototype原型
既typeof之后的另一位老朋友! prototype也是我们的老朋友,即使不了解的人,也应该都听过它的大名.如果它还是您的新朋友,我估计您也是javascript的新朋友. 在咱们的第一节(深入理解 ...
随机推荐
- 第11章 PADS功能使用技巧(1)-最全面
一.如何走蛇形线? 蛇形线是布线过程中常用的一种走线方式,其主要目的是为了调节延时满足系统时序设计要求,但是设计者应该有这样的认识:蛇形线会破坏信号质量,改变传输延时,布线时要尽量避免使用,因此一块P ...
- Duilib的双缓冲实现,附带GDI、WTL的双缓冲实现
前言: 闪烁问题,之前的经验是使用双缓冲,借此机会,把双缓冲的研究心得总结下. 双缓冲的含义: 缓冲这个词,相信大家都不陌生,Cache.主要是为了解决上下游(或者模块.或者系统)等性能不匹配问题.如 ...
- UnityWebRequest使用总结
using System.Collections; using UnityEngine; using UnityEngine.Networking; /// <summary> /// 网 ...
- 7.8、如何制作openstack镜像
1.创建openstack-linux镜像: 下载已经做好的的镜像:https://docs.openstack.org/image-guide/obtain-images.html#centos ( ...
- ORA-12560: 解决TNS:协议适配器错误
1)安装成功,但无法连接数据库 2)网上查找原因:32位的不能运行64位的oracle,而且不会有64位的版本 3)解决办法:大致是修改客户端数据库为32位的(此方法OK) (1)解压instantc ...
- FlowNet:simple / correlation 与 相关联操作
Flow Net : simple / correlation 与 相关联操作 上一篇文章中(还没来得及写),已经简单的讲解了光流是什么以及光流是如何求得的.同时介绍了几个光流领域的经典传统算法. ...
- ExtJs4学习(九)读写器reader、writer
Ext.data.reader.Json JSON Reader是用一个代理来读取服务端响应的JSON格式的返回值. 一般用于将结果加载成一个存储集-例如我们将创建一些如下东西: Ext.define ...
- 传统mvc platform与前后端分离项目smart 共用域名nginx配置
#server { # listen 80; # server_name 139.129.100.155 rjhaasz.cn; # rewrite ^(.*)$ https://$host$1 pe ...
- Redis数据库 常用的基本操作
一.redis数据库操作 redis数据库默认有16个数据库[0-15],默认进入的是0库. select 数据库名 :切换数据库. keys * :查看所有的键. expire 键 秒数:设置过 ...
- Luogu P4313 文理分科
link 最小割 双倍经验 这道题运用了最小割最常用的一种用法:集合划分. 因为源汇最小割即就是将源汇划分到不同的集合,那么最简单的应用就是最小代价划分集合了. 本题中,题意是将 \(n\cdot m ...