JavaScript系列----作用域链和闭包
1.作用域链
1.1.什么是作用域
谈起作用域链,我们就不得不从作用域开始谈起。因为所谓的作用域链就是由多个作用域组成的。那么, 什么是作用域呢?
1.1.1作用域是一个函数在执行时期的执行环境。
每一个函数在执行的时候都有着其特有的执行环境,ECMAScript标准规定,在javascript中只有函数才拥有作用域。换句话,也就是说,JS中不存在块级作用域。比如下面这样:
function getA() {
if (false) {
var a = 1;
}
console.log(a); //undefined
}
getA();
function getB() {
console.log(b);
}
getB(); // ReferenceError: b is not defined
上面的两段代码,区别在于 :getA()函数中,有变量a的声明,而getB()函数中没有变量b的声明。
另外还有一点,关于作用域中的声明提前。
1.1.2.作用域中声明提前
在上面的getA()函数中,或许你还存在着疑惑,为什么a="undefined"呢,具体原因就是因为作用域中的声明提前:所以getA()函数和下面的写法是等价的:
function getA(){
var a;
if(false){
a=1
};
console.log(a);
}
既然提到变量的声明提前,那么只需要搞清楚三个问题即可:
1.什么是变量
2.什么是变量声明
3.声明提前到什么时候。
什么是变量?
变量包括两种,普通变量和函数变量。
- 普通变量:凡是用var标识的都是普通变量。比如下面 :
var x=1;
var object={};
var getA=function(){}; //以上三种均是普通变量,但是这三个等式都具有赋值操作。所以,要分清楚声明和赋值。声明是指 var x; 赋值是指 x=1; - 函数变量:函数变量特指的是下面的这种,fun就是一个函数变量。
function fun(){} ;// 这是指函数变量. 函数变量一般也说成函数声明。类似下面这样,不是函数声明,而是函数表达式
var getA=function(){} //这是函数表达式
var getA=function fun(){}; //这也是函数表达式,不存在函数声明。关于函数声明和函数表达式的区别,详情见javascript系列---函数篇第二部分
什么是变量声明?
变量有普通变量和函数变量,所以变量的声明就有普通变量声明和函数变量声明。
- 普通变量声明
var x=1; //声明+赋值
var object={}; //声明+赋值上面的两个变量执行的时候总是这样的
var x = undefined; //声明
var object = undefined; //声明
x = 1; //赋值
object = {}; //赋值关于声明和赋值,请注意,声明是在函数第一行代码执行之前就已经完成,而赋值是在函数执行时期才开始赋值。所以,声明总是存在于赋值之前。而且,普通变量的声明时期总是等于undefined.
- 函数变量声明
函数变量声明指的是下面这样的:function getA(){}; //函数声明
声明提前到什么时候?
所有变量的声明,在函数内部第一行代码开始执行的时候就已经完成。-----声明的顺序见1.2作用域的组成
1.2.作用域的组成
函数的作用域,也就是函数的执行环境,所以函数作用域内肯定保存着函数内部声明的所有的变量。
一个函数在执行时所用到的变量无外乎来源于下面三种:
1.函数的参数----来源于函数内部的作用域
2.在函数内部声明的变量(普通变量和函数变量)----也来源于函数内部作用域
3.来源于函数的外部作用域的变量,放在1.3中讲。
比如下面这样:
var x = 1;
function add(num) () {
var y = 1;
return x + num + y; //x来源于外部作用域,num来源于参数(参数也属于内部作用域),y来源于内部作用域。
}
那么一个函数的作用域到底是什么呢?
在一个函数被调用的时候,函数的作用域才会存在。此时,在函数还没有开始执行的时候,开始创建函数的作用域:
函数作用域的创建步骤:
1. 函数形参的声明。
2.函数变量的声明
3.普通变量的声明。
4.函数内部的this指针赋值
......函数内部代码开始执行!
所以,在这里也解释了,为什么说函数被调用时,声明提前,在创建函数作用域的时候就会先声明各种变量。
关于变量的声明,这里有几点需要强调
1.函数形参在声明的时候已经指定其形参的值。
function add(num) {
var num;
console.log(num); //1
}
add(1);
2.在第二步函数变量的生命中,函数变量会覆盖以前声明过的同名声明。
function add(num1, fun2) {
function fun2() {
var x = 2;
}
console.log(typeof num1); //function
console.log(fun2.toString()) //functon fun2(){ var x=2;}
}
add(function () {
}, function () {
var x = 1
});
3. 在第三步中,普通变量的声明,不会覆盖以前的同名参数
function add(fun,num) {
var fun,num;
console.log(typeof fun) //function
console.log(num); //1
}
add(function(){},1);
在所有的声明结束后,函数才开始执行代码!!!
1.3.作用域链的组成
在JS中,函数的可以允许嵌套的。即,在一个函数的内部声明另一个函数
类似这样:
function A(){
var a=1;
function B(){ //在A函数内部,声明了函数B,这就是所谓的函数嵌套。
var b=2;
}
}
对于A来说,A函数在执行的时候,会创建其A函数的作用域, 那么函数B在创建的时候,会引用A的作用域,类似下面这样

函数B在执行的时候,其作用域类似于下面这样:

从上面的两幅图中可以看出,函数B在执行的时候,是会引用函数A的作用域的。所以,像这种函数作用域的嵌套就组成了所谓的函数作用域链。当在自身作用域内找不到该变量的时候,会沿着作用域链逐步向上查找,若在全局作用域内部仍找不到该变量,则会抛出异常。
2.什么是闭包
闭包的概念:有权访问另一个作用域的函数。
这句话就告诉我们,第一,闭包是一个函数。第二,闭包是一个能够访问另一个函数作用域。
那么,类似下面这样,
function A(){
var a=1;
function B(){ //闭包函数,函数b能够访问函数a的作用域。所以,像类似这么样的函数,我们就称为闭包
}
}
所以,创建闭包的方式就是在一个函数的内部,创建另外一个函数。那么,当外部函数被调用的时候,内部函数也就随着创建,这样就形成了闭包。比如下面。
var fun = undefined;
function a() {
var a = 1;
fun = function () {
}
}
3.闭包所引起的问题
其实,理解什么是闭包并不难,难的是闭包很容易引起各种各样的问题。
3.1.变量污染
看下面的这道例题:
var funB,
funC;
(function() {
var a = 1;
funB = function () {
a = a + 1;
console.log(a);
}
funC = function () {
a = a + 1;
console.log(a);
}
}());
funB(); //2
funC(); //3.
对于 funB和funC两个闭包函数,无论是哪个函数在运行的时候,都会改变匿名函数中变量a的值,这种情况就会污染了a变量。
两个函数的在运行的时候作用域如下图:

这这幅图中,变量a可以被函数funB和funC改变,就相当于外部作用域链上的变量对内部作用域来说都是静态的变量,这样,就很容易造成变量的污染。还有一道最经典的关于闭包的例题:
var array = [
];
for (var i = 0; i < 10; i++) {
var fun = function () {
console.log(i);
}
array.push(fun);
}
var index = array.length;
while (index > 0) {
array[--index]();
} //输出结果 全是10;
想这种类似问题产生的根源就在于,没有注意到外部作用域链上的所有变量均是静态的。
所以,为了解决这种变量的污染问题---而引入的闭包的另外一种使用方式。
那种它是如何解决这种变量污染的呢? 思想就是: 既然外部作用域链上的变量时静态的,那么将外部作用域链上的变量拷贝到内部作用域不就可以啦!! 具体怎么拷贝,当然是通过函数传参的形式啊。
以第一道例题为例:
var funB,funC;
(function () {
var a = 1;
(function () {
funB = function () {
a = a + 1;
console.log(a);
}
}(a));
(function (a) {
funC = function () {
a = a + 1;
console.log(a);
}
}(a));
}());
funB()||funC(); //输出结果全是2 另外也没有改变作用域链上a的值。
在函数执行时,内存的结构如图所示:

由图中内存结构示意图可见,为了解决闭包的这种变量污染的问题,而加了一层函数嵌套(通过匿名函数自执行),这种方式延长了闭包函数的作用域链。
3.2.内存泄露
内存泄露其实严格来说,就是内存溢出了,所谓的内存溢出,当时就是内存空间不够用了啊。
那么,闭包为什么会引起内存泄露呢?
var fun = undefined;
function A() {
var a = 1;
fun = function () {
}
}
看上面的例题,只要函数fun存在,那么函数A中的变量a就会一直存在。也就是说,函数A的作用域一直得不到释放,函数A的作用域链也不能得到释放。如果,作用域链上没有很多的变量,这种牺牲还可有可无,但是如果牵扯到DOM操作呢?
var element = document.getElementById('myButton');
(function () {
var myDiv = document.getElementById('myDiv')
element.onclick = function () {
//处理程序
}
}())
像这样,变量myDiv如果是一个占用内存很大的DOM....如果持续这么下去,内存空间岂不是一直得不到释放。久而久之,变引起了内存泄露(也是就内存空间不足)。
JavaScript系列----作用域链和闭包的更多相关文章
- 个人理解的javascript作用域链与闭包
闭包引入的前提个人理解是为从外部读取局部变量,正常情况下,这是办不到的.简单的闭包举例如下: function f1(){ n=100; function f2(){ alert(n); } retu ...
- Javascript的作用域、作用域链以及闭包
一.javascript中的作用域 ①全局变量-函数体外部进行声明 ②局部变量-函数体内部进行声明 1)函数级作用域 javascript语言中局部变量不同于C#.Java等高级语言,在这些高级语言内 ...
- 【进阶2-2期】JavaScript深入之从作用域链理解闭包(转)
这是我在公众号(高级前端进阶)看到的文章,现在做笔记 https://github.com/yygmind/blog/issues/18 红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一 ...
- 《浏览器工作原理与实践》<10>作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?
在上一篇文章中我们讲到了什么是作用域,以及 ES6 是如何通过变量环境和词法环境来同时支持变量提升和块级作用域,在最后我们也提到了如何通过词法环境和变量环境来查找变量,这其中就涉及到作用域链的概念. ...
- 前端高质量知识(四)-JS详细图解作用域链与闭包
攻克闭包难题 初学JavaScript的时候,我在学习闭包上,走了很多弯路.而这次重新回过头来对基础知识进行梳理,要讲清楚闭包,也是一个非常大的挑战. 闭包有多重要?如果你是初入前端的朋友,我没有办法 ...
- 初探JavaScript(四)——作用域链和声明提前
前言:最近恰逢毕业季,千千万万的学生党开始步入社会,告别象牙塔似的学校生活.往往在人生的各个拐点的时候,情感丰富,感触颇深,各种对过去的美好的总结,对未来的展望.与此同时,也让诸多的老“园”工看完这些 ...
- 理解JavaScript的作用域链
上一篇文章中介绍了Execution Context中的三个重要部分:VO/AO,scope chain和this,并详细的介绍了VO/AO在JavaScript代码执行中的表现. 本文就看看Exec ...
- 在chrome开发者工具中观察函数调用栈、作用域链与闭包
在chrome开发者工具中观察函数调用栈.作用域链与闭包 在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量 ...
- 在chrome开发者工具中观察函数调用栈、作用域链、闭包
在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量对象,闭包,this等关键信息的变化.因此,断点调试对于快 ...
随机推荐
- 剑指offer(纪念版) 面试题3:二维数组中的查找
题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序.请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数. 第一种方法题目说不可行 ...
- Linux之不得不说的init(Linux启动级别的含义 init 0-6)
init 0:关机: init 1:单用户模式(只root进行维护): init 2:多用户 init 3:完全多用户 init 4:安全模式 init 5:图形化 init 6:重启 可以在/etc ...
- JavaScript 实现发布消息后,距离当前时间的实现
某条消息发布后,距离当前时间多久的时间显示 //显示发布时间的函数 function pastTime(_createTime) { //var createTime = _createTime.su ...
- jQuery ajax的提交
1.利用jQuery中的aja提交数据,首先引入jQuery中的文件 2.jquery.form.js下载地址:http://vdisk.weibo.com/s/thY_x31gX0M-p?categ ...
- Python自学笔记-关于切片(来自廖雪峰的官网Python3)
感觉廖雪峰的官网http://www.liaoxuefeng.com/里面的教程不错,所以学习一下,把需要复习的摘抄一下. 以下内容主要为了自己复习用,详细内容请登录廖雪峰的官网查看. 切片 L[0: ...
- C# 如何添加Word文本和图片超链接
超链接简单来讲就是内容链接,通过设置超链接可以实现对象与网页.站点之间的连接.链接目标可以是网页.图片.邮件地址.文件夹或者是应用程序.设置链接的对象可以是文本或者图片. 在以下内容中,我将介绍如何用 ...
- C#-WinForm 串口通信
//C# 的串口通信,是采用serialPort控件,下面是对serialPort控件(也是串口通信必备信息)的配置如下代码: serialPort1.PortName = commcomboBox1 ...
- 深刻理解反射(Reflection)
最近公司在搞自动化测试,由于版权问题,无法用 '录制脚本' 进行,也就没法用 VS 自带的 UITest 框架(蛋疼), 所以只能开源的 FlaUI 框架来搞了.其中不可避免的涉及到反射的应用,但自己 ...
- ovs2.7 在系统重启后,再次使用时提示数据库无法连接的问题。
问题现象如下,ovs开始安装后,对ovs的操作是正常的,但是,现在系统重启后,OVS的操作第一条命令就失败,如下: 问题解决方法: 参考 http://blog.csdn.net/xyq54/art ...
- Django创建通用视图函数
想在我们有两个视图: def thinkingview(request): user = request.user if request.method == 'GET': return render( ...