一、编译过程
常见编译性语言,在程序代码执行之前会经历三个步骤,称为编译。
步骤一:分词或者词法分析
将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元。
例子: 
var a = 2;

这一句通常被分解成为下面这些词法单元:var 、a 、 = 、2、; 。
 
步骤二:解析或者语法分析
将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree, AST)
例子:
var 、a 、 = 、2、;  会生成类似与下面的语法树: 
 

步骤三:代码生成

将 抽象语法树 (AST)转换为可执行代码。

然而对于解释型语言(例如JavaScript)来说,通过词法分析和语法分析得到语法树,没有生成可执行文件的这一过程,就可以开始解释执行了。

对于 var a = 2; 进行处理的时候,会有 引擎、编译器、还有作用域的参与。
引擎:从头到尾负责整个 Javascript 程序的编译及执行过程。
编译器:负责语法分析及代码生成等。
作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符(变量)的访问权限。

他们是这样合作的:
首先编译器会进行如下处理:
1、var a,编译器会从作用域中寻找是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会自动忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a 。
2、接下来编译器会为引擎生成运行时所需的代码,这些代码用来处理 a = 2 这个赋值操作。引擎运行时会首先从作用域中查找 当前作用域集合中是否存在 变量 a。如果有,引擎就会使用这个变量。如果没有,引擎就会继续向上一级作用域集合中查找改变量。

然后 如果引擎最终找到了 变量 a,就赋值 2 给它。如果没有找到,就会抛出一个异常。

总结:
变量的赋值操作分两步完成:第一步 由编译器在作用域中声明一个变量(前提是之前没有声明过),第二步 是在运行时引擎会在作用域中查找该变量,如果可以找到,就对其赋值。

二、作用域
1、RL 查询
在上一部分我们说到了,引擎会对变量 a 进行查找。而查找分为两种,一是 LHS(Left-Hand-Side) 查询,二是 RHS(Right-Hand-Side) 查询。
LHS 查询:试图找到变量的容器本身,从而可以对其赋值。也就是查找 变量 a 。
RHS 查询:查找某个变量的值。查找变量 a 的值,即 2。
例子:

console.log(a); // 这里对 a 是一个 RHS 查询,找到 a 的值,并 console.log 出来。
a = 2; // 这里对 a 是一个 LHS 查询,找到 变量 a,并对其赋值为 2 。
function foo(a){
console.log(a); //
} foo(2); // 这里首先对 foo() 函数调用,执行 RHS 查询,即找到 foo 函数,然后 执行了 a = 2 的传参赋值,这里首先执行 LHS 查询 找到 a 并赋值为 2,然后 console.log(a) 执行了 RHS 查询。

这里还要说一下 作用域的嵌套:当一个块或函数嵌套在另一个块或函数中时,就发生了所用的嵌套。遍历查找嵌套作用域,是首先从当前作用域中查找变量,如果找不到,就像上一级继续查找,当抵达全局作用域时,无论找到还是没有找到,查找都将结束。
 
如果 RHS 查找在所有嵌套作用域中都没有找到所需变量,引擎就会抛出 ReferenceError。如果找到了所需变量,但你想要进行不合理的操作,比如对非函数类型的值进行调用等,引擎就会抛出 TypeError 。
如果 LHS 查找在顶层全局作用域中都没有找到所需变量,如果是在非严格模式下,全局作用域会创建一个具有该名称的变量,并将其返回给引擎,如果是在严格模式下,引擎就会抛出 ReferenceError。
 
ReferenceError 和 TypeError 是比较常见的异常,你需要知道它们的不同,对你排除程序问题有很大帮助。
 
2、词法作用域
由你在写代码时将变量和块作用域写在哪里来决定的。
例子: 
function foo(a){
var b = a*2;
function bar(c){
console.log(a,b,c);
} bar(b*3);
}
foo(2); // 2,4,12

在这段代码中有三层作用域,如图所示嵌套:

作用域1:包含着全局作用域,其中有标识符:foo.
作用域2:包含着 foo 所创建的作用域,其中有三个标识符:a、 b、 bar。
作用域3:包含着 bar 所创建的作用域,其中有标识符:c。
 
在查找变量时,作用域查找会在找到第一个匹配的标识符时停止。而且它只查找一级标识符,比如a 、b、c,而对于 foo.bar.baz ,词法作用域只会查找 foo 标识符,找到这个变量之后,对象属性访问规则会分别接管对 bar 和 baz 属性的访问。
 
这里还要说一点,全局变量会自动成为全局对象的属性,所以可以间接的通过全局对象属性的引用来对其进行访问。
window.a 

 
3、提升
变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
举个例子:
当你看到 var a = 2; 时,可能会认为这是一个声明,但实际上 Javascript 会将其看成两个声明:var a ; 和 a = 2;并且在不同阶段执行。var a 是在编译阶段进行的,而 a = 2 会被留在原地等待执行阶段。
这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面,这个过程就叫做变量提升。
 
例子: 
foo();

function foo(){
console.log(a);
var a = 2;
}

学了上面的知识,你应该可以猜到 foo() 可以正常执行,而 console.log(a) 会打出 undefined; 原因是当把提升应用到上面代码,代码就相当于 下面的形式:

function foo() {
var a;
console.log(a);
a = 2;
} foo();

对于变量提升要注意另外两个知识点:
1、函数声明会被提升,而函数表达式却不会被提升。
区分函数声明和函数表达式最简单的方式是看  function 关键字出现在声明中的位置。如果 function 时声明中的第一个词,那么就是函数声明,否则就是一个函数表达式。
例子:
函数声明: 
function foo() {
var a;
console.log(a);
a = 2;
}

函数表达式:

var foo = function() {
var a;
console.log(a);
a = 2;
} (function foo(){
var a;
console.log(a);
a = 2;
})();

那么对于提升,来看个例子:

foo(); // 报TypeError错误

var foo = function() {
var a;
console.log(a);
a = 2;
}

这段代码相当于

var foo;
foo(); // 此时 foo 为 undefined,而我们尝试对它进行函数式调用,属于不合理操作,报 TypeError 错误。
foo = function() {
var a;
console.log(a);
a = 2;
}

 
2、函数会被优先提升,然后是变量。
例子: 
foo();  // 1
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}

会输出 1 为不是 2,这段代码提升之后相当于:

function foo(){
console.log(1);
}
foo();
foo = function(){
console.log(2);
}

注意,var foo 尽管出现在 function foo() 之前,但它是重复的声明,因为函数声明会被提升到普通变量之前。重复的  var 声明会被忽略,但出现在后面的函数声明却会覆盖前面的。
例子: 
foo(); //

function foo(){
console.log(1);
}
var foo = function(){
console.log(2)
} function foo(){
console.log(3)
}

 
三、闭包
所谓  闭包:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
例子:
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
} var baz = foo();
baz(); //

例子中,通过调用 baz 来调用 foo 内部的 bar , bar 在自己定义的词法作用域以外的地方执行,在 foo 执行之后,通常会期待 foo 的整个内部作用域被销毁,因为引擎的垃圾回收器会释放不再使用的内存空间。看上去 foo 不再被使用,所以很自然的考虑到对其进行回收,然而闭包就是阻止这样的事情发生,事实上内部作用域依然存在,没有被回收,因为 bar 依然在使用该作用域。
bar 拥有 涵盖 foo 内部作用域的闭包,使得该作用域能够一直存活,以供 bar 在之后任何时间进行引用。
bar 依然持有对该作用域的引用,而这个引用就叫作 闭包。
bar 在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的作用域。
 
在举个例子: 
function wait(message){
setTimeout(function timer(){
console.log(message)
},1000)
} wait("hi");

timer 具有 涵盖 wait 作用域的闭包,保有对 message 的引用。
wait 执行 1s 后,它的内部作用域不会消失,timer 依然保有 wait 作用域的闭包,所以 可以获得 message 并 console.log,这就是闭包。
 
也就是说,如果将函数作为第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。像定时器、事件监听器、Ajax 请求、跨窗口通信或者任何异步任务中,只要使用了回调函数,实际上就是在使用闭包。
例子: 
function foo(){
var a = 2;
function baz(){
console.log(a); //
}
bar(baz);
} function bar(fn){
fn(); //闭包
}

内部函数 baz 传递给 bar,当在 bar 中调用 baz时,就可以访问到他定义时所在的作用域中的变量,console.log 出 a。 
 
例子: 
for (var i=0;i<=5;i++){
setTimeout(function timer(){
console.log(i)
},i*1000)
}

我们预期输出数次1-5,每秒一次,每次一个。
然而 真正的 输出结果却是 五个 6。
原因是 timer 在 循环结束之后即 i 等于 6 时, 才执行,就算你将 setTimeout 的时间 设为 0 ,回调函数也会在循环结束之后才执行。
那么我们应该怎么解决呢?
我们希望每次循环时,timer 都会给自己捕获一个 i 的副本,然而根据作用域的原理,实际情况却是 尽管 循环的 5 个函数是在各个迭代中被分别定义的,但是它们都封闭在一个共享的全局作用域中,因此实际上只有一个 i,所有函数共享一个 i 的引用。如果将延迟函数的回调重复定义5次,不使用循环,那它同这段代码完全等价的。
所以解决办法就是 循环的过程中每个迭代都需要一个闭包作用域。而 立即执行函数 正好可以做到这一点。 
for (var i=0;i<=5;i++){
(function(i){
setTimeout(function timer(){
console.log(i)
},i*1000)
})(i)
}

在循环中使用 立即执行函数会为每个迭代生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代的内部,每个迭代都会含有一个正确的 i 等待 console。 
 
问题解决。
 
现在 你应该真正的明白 作用域和闭包了,找点题做吧,加深一下印象,不然你还会回来的。
 
 
学习并感谢
《你不知道的JavaScript》上卷  (炒鸡推荐大家看)

【 js 基础 】【读书笔记】作用域和闭包的更多相关文章

  1. JS基础知识笔记

    2020-04-15 JS基础知识笔记 // new Boolean()传入的值与if判断一样 var test=new Boolean(); console.log(test); // false ...

  2. JS基础篇之作用域、执行上下文、this、闭包

    前言:JS 的作用域.执行上下文.this.闭包是老生常谈的话题,也是新手比较懵懂的知识点.当然即便你作为老手,也未必真的能理解透彻这些概念. 一.作用域和执行上下文 作用域: js中的作用域是词法作 ...

  3. 【js】【读书笔记】廖雪峰的js教程读书笔记

    最近在看廖雪峰的js教程,重温了下js基础,记下一些笔记,好记性不如烂笔头嘛 编写代码尽量使用严格模式 use strict JavaScript引擎是一个事件驱动的执行引擎,代码总是以单线程执行 执 ...

  4. handlebars.js基础学习笔记

    最近在帮学校做个课程网站,就有人推荐用jquery+ajax+handlebars做网站前端,刚接触发现挺高大上的,于是就把一些基础学习笔记记录下来啦. 1.引用文件: jquery.js文件下载:h ...

  5. JS三座大山再学习 ---- 作用域和闭包

    本文已发布在西瓜君的个人博客,原文传送门 作用域 JS中有两种作用域:全局作用域|局部作用域 栗子1 console.log(name); //undefined var name = '波妞'; v ...

  6. 我不知道的js(一)作用域与闭包

    作用域与闭包 作用域 什么是作用域 作用域就是一套规则,它负责解决(1)将变量存在哪儿?(2)如何找到变量?的问题 作用域工作的前提 谁赋予了作用域的权利?--js引擎 传统编译语言编译的过程 分词/ ...

  7. JS基础知识(作用域/垃圾管理)

    1.js没有块级作用域 if (true) { var color = “blue”; } alert(color); //”blue” for (var i=0; i < 10; i++){ ...

  8. 说说循环与闭包——《你不知道的JS》读书笔记(一)

    什么是闭包 <你不知道的JS>里有对闭包的定义:"当函数可以记住并访问所在的词法作用域,即使函数是在当前作用域之外执行,这就产生了闭包." 讲闭包是啥的太多了...就一 ...

  9. js高程读书笔记(第4章--变量、作用域和内存)

    JavaScript变量松散类型的本质,决定了它只是在特定时间用于保存特定值的一个名字而已.由于不存在定义某个变量必须要保存何总数据类型值的规则,变量的值及其数据类型可以在脚本的生命周期内改变. 1. ...

  10. 两万字Vue.js基础学习笔记

    Vue.js学习笔记 目录 Vue.js学习笔记 ES6语法 1.不一样的变量声明:const和let 2.模板字符串 3.箭头函数(Arrow Functions) 4. 函数的参数默认值 5.Sp ...

随机推荐

  1. python 数据类型一 (重点是字符串的各种操作)

    一.python基本数据类型 1,int,整数,主要用来进行数学运算 2,bool,布尔类型,判断真假,True,False 3,str,字符串,可以保存少量数据并进行相应的操作(未来使用频率最高的一 ...

  2. PHP正则表达式函数的替代函数

    1,preg_split()函数将字符串按照某元素分割,分割后结果以数组方式返回. php中explode()可以实现此功能.array explode(string $pattern,string ...

  3. Spring Cloud断路器Hystrix

    在微服务架构中,存在着那么多的服务单元,若一个单元出现故障,就会因依赖关系形成故障蔓延,最终导致整个系统的瘫痪,这样的架构相较传统架构就更加的不稳定.为了解决这样的问题,因此产生了断路器模式. 什么是 ...

  4. Spring Boot中使用Redis数据库

    引入依赖 Spring Boot提供的数据访问框架Spring Data Redis基于Jedis.可以通过引入spring-boot-starter-redis来配置依赖关系. <depend ...

  5. mysql之视图,存储过程,触发器,事务

    视图 视图是一个虚拟表(非真实存在),其本质是[根据SQL语句获取动态的数据集,并为其命名],用户使用时只需使用[名称]即可获取结果集,可以将该结果集当做表来使用. 使用视图我们可以把查询过程中的临时 ...

  6. 文件压缩小项目haffman压缩

    文件压缩的原理: 文件压缩总体可以分为有损压缩和无损压缩两类,有损压缩是指对mp3等格式的文件,忽略一些无关紧要的信息,只保留一些关键的信息,但并不因此影响用户对于这些mp3格式文件的体验度,无损压缩 ...

  7. 【LeetCode】200. 岛屿的个数

    题目 给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量.一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的.你可以假设网格的四个边均被水包围. 示例 1:输 ...

  8. windows 系统安装git的方法

    windows 系统安装git的方法 msysgit是Windows版的Git,从https://git-for-windows.github.io下载 安装默认步骤,一步步安装即可 安装完成后,在开 ...

  9. MVC3学习:利用mvc3+ajax实现删除记录

    首先根据模板生成list视图,上面就会有一个delete的链接,但是模板自带的这种删除,需要另外再打开一个删除页,再进行删除.我们可以利用ajax来改写,实现在当前页删除. 在视图上面,将原来的 @H ...

  10. (转)python协程2:yield from 从入门到精通

    原文:http://blog.gusibi.com/post/python-coroutine-yield-from/ https://mp.weixin.qq.com/s?__biz=MzAwNjI ...