一、编译过程
常见编译性语言,在程序代码执行之前会经历三个步骤,称为编译。
步骤一:分词或者词法分析
将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元。
例子: 
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. intellij 引入本地库并war打包

    一.引入本地库 1.File -> Project Structure -> Libraries,点击+,新增本地lib库. 2.File -> Project Structure ...

  2. centos docker 安装笔记

    安装epel rpm -ivh http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm rpm --import ...

  3. 用开源 ASP.NET MVC 程序 Bonobo Git Server 搭建 Git 服务器(转)

    用开源 ASP.NET MVC 程序 Bonobo Git Server 搭建 Git 服务器   现在不用Git,都不好意思说自己是程序员. 当你想用Git,而源代码服务器是Windows系统时,你 ...

  4. rabbitmq系列五 之远程过程调用(RPC)

    1.远程过程调用(RPC) 在第二篇教程中我们介绍了如何使用工作队列(work queue)在多个工作者(woker)中间分发耗时的任务. 可是如果我们需要将一个函数运行在远程计算机上并且等待从那儿获 ...

  5. 3DMax——室内设计:墙体+吊顶

    1.导入CAD平面图 2.将导入的平面图全部选中→颜色设置为其他颜色→设置为组(设置为组,是为了后期选材质方便) 3.选中图形,选择移动工具,输入坐标为0,右键选择冻结当前选择 4.右键“角度捕捉切换 ...

  6. android设备不识别awk命令,缺少busybox

    android设备不识别awk命令,缺少busybox 一.什么是BusyBox ? BusyBox 是标准 Linux 工具的一个单个可执行实现.BusyBox 包含了一些简单的工具,例如 cat ...

  7. odoo开发笔记-自定义发送邮件模板

    1. 首先激活开发者模式 2. 点击设置 - Email - 模板 - “选择你需要修改的模板” 我们以销售模块-报价单 邮件模板为例 来说明. quote order 原先默认模板,发出的邮件显示效 ...

  8. Hadoop环境搭建及wordcount程序

    目的: 前期学习了一些机器学习基本算法,实际企业应用中算法是核心,运行的环境和数据处理的平台是基础. 手段: 搭建简易hadoop集群(由于机器限制在自己的笔记本上通过虚拟机搭建) 一.基础环境介绍 ...

  9. Java 多线程学习笔记:wait、notify、notifyAll的阻塞和恢复

    前言:昨天尝试用Java自行实现生产者消费者问题(Producer-Consumer Problem),在coding时,使用到了Condition的await和signalAll方法,然后顺便想起了 ...

  10. Docker概念学习系列之虚拟化(系统虚拟化和容器虚拟化)

    不多说,直接上干货! 虚拟化定义: 虚拟化是一种资源管理技术,是将计算机的各种实体资源,如服务器.网络.内存及存储等,予以抽象.转换后呈现出来,打破实体结构间的不可切割的障碍,使用户可以比原本的配置更 ...