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的新朋友. 在咱们的第一节(深入理解 ...
随机推荐
- Django-ORM-事务和锁
一.锁 行级锁 返回一个锁住行,值到事物结束的查询集. 所有匹配的行将被锁住,直到事物结束,这就意味着可以通过锁防止数据被其他事务修改. 一般情况下如果其他事务锁定了相关行,那么本查询将被阻塞直到锁被 ...
- 『心善渊』Selenium3.0基础 — 20、Selenium对Cookie的操作
目录 1.Cookie介绍 2.Session介绍 3.Cookie工作原理图解 4.Cookie内容参数说明 5.Selenium操作Cookie的API 6.Selenium操作Cookie的示例 ...
- POJ 1220 大数字的进制转换,偷下懒,用java
题意为进制转换,Java的大数类就像是作弊 import java.math.BigInteger; import java.util.Scanner; public class Main { pub ...
- 怎么用git将自己的源代码提交到git服务器上
在git服务器上新建仓库 在本地初始化本地仓库 初始化 git init 添加远程仓库地址 git remote add origin XXX.git 同步 git pull origin maste ...
- salesforce零基础学习(一百零五)Change Data Capture
本篇参考: https://developer.salesforce.com/docs/atlas.en-us.232.0.api_streaming.meta/api_streaming/using ...
- phpstorm之"Can not run PHP Code Sniffer"
前言 其实我是不太愿意写这种工具使用博客的,因为实在没有营养,只是有些简单问题,搜索一番,却始终找不到答案,遂以博客记录下来,希望后面的人,可以省去搜索之苦. 相信你搜到这篇博客,肯定是已经安装好了P ...
- 18 shell 重定向以及文件描述符
1.对重定向的理解 2.硬件设备和文件描述符 文件描述符到底是什么 3.Linux Shell 输出重定向 4.Linux Shell 输入重定向 5.结合Linux文件描述符谈重定向 6.Shell ...
- 22 shell组命令与子进程
1.组命令 2.子进程 2.1 什么是子进程 2.2 创建子进程 2.3 子进程总结 3.如何检测子shell与子进程 1.组命令 组命令,就是将多个命令划分为一组,或者看成一个整体. 用法 区别 S ...
- B站蹦了,关我A站什么事?
昨天的大瓜,B站蹦了,大伙都跳起来分析了一波异常原因,着实给大伙的秋招准备了一波热乎乎的素材!在大家都在关注 B站的时候, 我大A站终于要站起来了!!!经过多方网友的极力引流,我A站也蹦了- 紧急通知 ...
- cke编辑器插入&ZeroWidthSpace占位字符的问题记录
背景 本博文主要记录在使用cke编辑器时,遇到的一系列的问题 问题1:在执行某些业务操作后,编辑器会偶现在页面头部或者尾部插入&ZeroWidthSpace占位符(编辑器好像就爱干这事~) 解 ...