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

    package com.sprucetec.tms.utils; /** * AjaxReturn * * @author Yinqiang Du * @date 2016/7/12 */import ...

  2. Python大法之告别脚本小子系列—各类URL采集器编写

    本文作者:i春秋签约作家——阿甫哥哥 系列文章专辑:https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid ...

  3. Android动画Animation简单示例

    Animation是Android给我们提供的一个可以实现动画效果的API,利用Animation我们可以实现一系列的动画效果,比如缩放动画,透明度动画,旋转动画,位移动画,布局动画,帧动画等等.An ...

  4. NVIDIA GRID 和 NICE DCV 技术用于实现 Linux 和 Windows® 图形加速虚拟桌面

    NVIDIA GRID 和 NICE DCV 技术用于实现 Linux 和 Windows® 图形加速虚拟桌面. NICE DCV: 满足 LINUX 和 WINDOWS 的远程 3D 通过 NICE ...

  5. 10-02 Java 形式参数和返回值的问题深入研究,链式编程

    形式参数和返回值的问题: 1:形式参数和返回值的问题(理解) (1)形式参数: 类名:需要该类的对象 抽象类名:需要该类的子类对象 接口名:需要该接口的实现类对象 (2)返回值类型: 类名:返回的是该 ...

  6. CDH 版本子节点启动问题

    今天下午整整为了启动一个节点瞎忙活一下午,惨痛的教训还是记录下来吧,毕竟付出了代价.事情原委,一个同事在一台机器上占用了大量内存训练CTR点击率模型,而这台机器上部署了分布式Hadoop的一个data ...

  7. sql server always on安装

    always on 是sql server 服务器的数据同步备份容灾工具, 集中了故障转移群集.数据库镜像和日志传送等功能. 环境: window server 2012 sql server 201 ...

  8. mybatis 关联映射

    一对一 创建数据表 CREATE TABLE `tb_card` ( `id` int NOT NULL AUTO_INCREMENT , `code` varchar() NULL , PRIMAR ...

  9. MySql中插入乱码问题解决

    今天在使用Java写入数据库时候,发现Insert语句和Update语句在执行过后,数据库中中文显示的是“??”,经过一番查阅,其中关键的问题在于编码格式是否统一. 其中创建表时候,每个关键字的格式都 ...

  10. Centos7下安装redis实战(单机版以及集群)

    一.背景 因项目需要,要引入redis做缓存,就在centos7下亲自安装了一遍redis,刚好趁着这个机会就来把redis的概念以及单机版和集群版redis安装步骤记录下来,在此和大家一起分享. 二 ...