在描述闭包的实现与用途前,需要了解以下一些知识点。



  执行上下文(执行上下文环境) 


 console.log(a);     //Uncaught ReferenceError: a is not defined 

 console.log(b);    //undefined
var b; console.log(c); //undefined
var c = 10;

  看上面一个例子,当直接在控制台输出a时,提示a未定义,这个很容易就理解了。不过在"var b"和"var c = 10"执行前输出b和c的值时,得到的结果却是undefined。因为代码是一条一条执行的,在执行console.log()之前,"var b"和"var c = 10"必定是还未执行的。而结果却与a的输出结果不同,这是怎么一回事?

 第二种情况:

console.log(this);

输出:

第三种情况:

 console.log(f1);  
function f1(){};  //函数声明 console.log(f2);
var f2 = function (){};  //函数表达式

输出: 

  从这里可以看出来,在一段js代码拿过来真正一句一句运行之前,浏览器做了一些“准备工作”,其中就包括对变量的声明,而不是赋值,变量赋值是在赋值语句执行的时候进行的,还有函数的声明的赋值。而函数表达式类似与第一种情况中的var c = 10,两者结果也是相同的。当然还有this的赋值等等。

  下面总结一下浏览器在准备中做了哪些工作:

    变量、函数表达式 —— 变量声明,默认赋值为undefined;

    this —— 赋值

    函数声明 —— 赋值

  这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。

  以上的一切都是在全局环境中进行的。

  在函数中,除了以上数据之外,还会有其他数据。

 function f(x){
console.log(arguments);
console.log(x);
}
f(10);

输出:

  以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。

  从而我们可以看出来,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

  进一步来说,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。

总结一下:

  全局代码的上下文环境数据内容为:

普通变量(包括函数表达式),

如: var a = 10;

声明(默认赋值为undefined)

函数声明,

如: function fn() { }

赋值

this

赋值

  如果代码段是函数体,那么在此基础上需要附加:

参数

赋值

arguments

赋值

自由变量的取值作用域

赋值

  给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。



  在执行js代码时,会有数不清的函数调用次数,会产生许多个上下文环境。这么多上下文环境该如何管理,以及如何销毁而释放内存呢?这就引入了一个概念——执行上下文栈


  在这里插播一个概念 —— this

  在平时,this使用频率非常高,我们在很多情况下都会用到它。而无论在哪个位置获取this,都是有值的。在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。因为this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。

  接下来,分四种情况描述this的取值:

  情况1:构造函数

    所谓构造函数就是用来new对象的函数。其实严格来说,所有的函数都可以new一个对象,但是有些函数的定义是为了new一个对象,而有些函数则不是。另外注意,构造函数的函数名第一个字母大写(规则约定)。例如:Object、Array、Function等。

    如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象。这里有一个情况,当函数的调用不是new一个对象,而是直接调用函数时,其中的this的值为window。

    这里还有一个问题,在构造函数的prototype中,this代表着什么。其实不仅仅是构造函数的prototype,即便是在整个原型链中,this代表的也都是当前对象的值。

  情况2:函数作为对象的一个属性

    如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象。

  情况3:函数用call或者apply调用

    当一个函数被call和apply调用时,this的值就取传入的对象的值。 

  情况4:全局 & 调用普通函数

    在全局环境下,this永远是window,普通函数在调用时,其中的this也都是window。


下面继续学习上下文栈。

  执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。这其实就是一个压栈出栈的过程——执行上下文栈。如图:

  在上下文栈中,同一时间内只有一个活跃的上下文环境。

  当一个函数被调用时,将产生一个新的执行上下文环境,并被压入执行上下文栈中,原先的栈顶执行上下文环境进入非活跃状态,新压入的执行上下文环境则进入活动状态。

  当函数调用完成后,栈顶执行上下文环境被弹出栈,并被回收机制回收销毁,而新的栈顶上下文环境则进入活跃状态。

  不过这只是一个理想的运行情况,有时候执行上下文环境并不会说销毁就会被销毁的,这就是后面要介绍的——闭包。



   作用域 & 作用域链


  无论在哪个编程语言中,都有一个作用域的概念。而在javascript中,除了全局作用域之外,只有函数可以创建的作用域。作用域在函数定义时就已经确定了。而不是在函数调用时确定。

  所以,我们在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式。因为在函数中,如果声明变量时没用"var",则所声明的变量为全局变量。

  作用域是一个很抽象的概念,类似于一个“地盘”,如下图:

  作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。

  作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

  我们把作用域与上下文环境结合起来看,如下图:

  

  在上图中,我们可以看到,作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

  所以,如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。

  这里有一个概念——自由变量。所谓"自由变量",即在这一个作用域中调用的变量,并没有在这个作用域中声明,而是在其他作用域声明的变量,即称之为"自由变量"。

  而我们在这个作用域中调用自由变量时,究竟要到哪里去获取这个变量的值呢?有人会说,到这个作用域的父作用域调用。这个说法有点模糊,有时候会产生歧义。正确来说,应该是:要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”。其实这也就是所谓的"静态作用域"。

  上面描述的只是跨一步作用域去寻找。

  如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。

  这个一步一步“跨”的路线,我们称之为——作用域链。

  下面我们来用文字描述一下这个过程:(假设a为自由变量)  

    第一步,现在当前作用域查找a,如果有则获取并结束。如果没有则继续;

    第二步,如果当前作用域是全局作用域,则证明a未定义,结束;否则继续;

    第三步,(不是全局作用域,那就是函数作用域)将创建该函数的作用域作为当前作用域;

    第四步,跳转到第一步。



  接下来就是主菜 —— 闭包


   关于闭包,百度的解释是:闭包是可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。。。看起来有点云里雾里的。

  其实,我们只要了解应用的两种情况即可——函数作为返回值,函数作为参数传递。

  函数作为返回值:

 function fn(){
var a = 10; function bar(x){
if (X < a){
console.log(X);
}
} return bar;
}

  函数作为参数被传递:

 var
max = 10;
fn = function(X){
if (X > max){
console.log(X);
}
else{
console.log(max);
}
}; (function(f) {
var max = 100;
f(15);
})(fn);

  这里的输出结果为: 15。原因为取max的值时取的是创建这个函数的那个作用域中获取,而不是“父作用域”。所以取的值为10,而不是100。

  结合前面学过的,我们可以看看下面这张图:

  在图中我们可以看到,执行完第17行,fn()调用完成。按理说应该销毁掉fn()的执行上下文环境,但是这里不能这么做。注意,重点来了:因为执行fn()时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量max要引用fn作用域下的fn()上下文环境中的max。因此,这个max不能被销毁,销毁了之后bar函数中的max就找不到值了。因此,这里的fn()上下文环境不能被销毁,还依然存在与执行上下文栈中。

  执行到第20行,执行f1(15),即执行bar(15),创建bar(15)上下文环境,并将其设置为活动状态。

  执行bar(15)时,max是自由变量,需要向创建bar函数的作用域中查找,找到了max的值为10。而重点在于,创建bar函数是在执行fn()时创建的。fn()早就执行结束了,但是fn()执行上下文环境还存在与栈中,因此bar(15)时,max可以查找到。如果fn()上下文环境销毁了,那么max就找不到了。从而fn上下文环境依旧在栈中没有销毁。

  虽说闭包的使用增加了内存的消耗,而且使用不当有可能造成内存泄露的问题发生,不过其还是有很多的用途。譬如匿名自执行函数、匿名自执行函数、实现封装以及实现面向对象中的对象,模拟传统的对象语言都提供类的模板机制等等。

【javascript】Javascript闭包的更多相关文章

  1. 深入理解JavaScript的闭包特性如何给循环中的对象添加事件

    初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...

  2. JavaScript作用域闭包简述

    JavaScript作用域闭包简述 作用域 技术一般水平有限,有什么错的地方,望大家指正. 作用域就是变量起作用的范围.作用域包括全局作用域,函数作用域以块级作用域,ES6中的let和const可以形 ...

  3. JavaScript的闭包原理

    什么是js(JavaScript)的闭包原理,有什么作用? 一.定义 官方解释:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. 个人的理解是 ...

  4. Js(javaScript)的闭包原理

    问题?什么是js(javaScript)的闭包原理,有什么作用? 一.定义 官方解释:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分.  小编 ...

  5. 深入理解javascript的闭包

    闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域 ...

  6. 如何给循环中的对象添加事件--深入理解JavaScript的闭包特性

    初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...

  7. javascript,jquery(闭包概念)(转)

    偶尔听人说javascript闭包,让我联想起以前学编译原理和数字逻辑里讲的闭包,以前上课讲的闭包很难懂,而且含有递归的意思在里面,现在不想再查看里面的闭包概念. 但javascript我是经常要用, ...

  8. 理解Javascript 的闭包(closure)

    要理解闭包的概念先从变量的作用域说去 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之 ...

  9. 两个示例介绍JavaScript的闭包

    JavaScript的闭包有两个用途:一个是访问函数内部的变量:另一个是让变量的值在作用域内保持不变.函数是JavaScript 中唯一有作用域的对象,因此JavaScript的闭包依赖于函数实现,下 ...

  10. javascript 关于闭包的知识点

    javascript 关于闭包的认识 概念:闭包(closure)是函数对象与变量作用域链在某种形式上的关联,是一种对变量的获取机制. 所以要大致搞清三个东西:函数对象(function object ...

随机推荐

  1. Jessica's Reading Problem——POJ3320

    Jessica's Reading Problem——POJ3320 题目大意: Jessica 将面临考试,她只能临时抱佛脚的在短时间内将课本内的所有知识点过一轮,课本里面的P个知识点顺序混乱,而且 ...

  2. TSQL--可以在触发器中使用COMMIT吗?

    很多场景中,我们使用触发器来回滚一些不满足业务逻辑的修改,这没有问题,问题是我能在触发器中提交事务吗? 这个问题很小白,当也来测试一下 /*测试中创建三种表,对表 TB2 插入时触发触发器,在触发器中 ...

  3. Backup--备份相关的信息查看及小技巧

    --查看指定数据库当前最小 LSN DECLARE @database_name NVARCHAR( 200) SET @database_name ='DBName' SELECT  MIN (re ...

  4. ManualResetEvent 线程通信

    using System; using System.Threading; namespace ConsoleApp1 { class MyThread { Thread t = null; Manu ...

  5. Android Camera开发经验总结以及踩过的那些坑

    写在开头 需求方:上传试卷的时候,用户自己拍的照片有很多问题.如:不清晰.图片歪了.错误图片等.我们要是能够对拍摄照片进行识别处理就好了,能够裁切矫正就更好了,最好可以像二维码扫描一样,直接识别处理- ...

  6. pageadmin网站制作 如何修改和管理网站模板

    在使用pageadmin CMS 的同时,遇到问题可以参考官网帮助中心.1.网站模板目录地址/templates目录, 2.点击展开后,每个目录就是一个网站模板, 前端设计师制作的新版本都可以放到这个 ...

  7. Robot Framework连接MySQL数据库

    注:内容来自网络,整理之如下 Robot Framework连接mysql数据库需要: 1.安装databaselibrary.pymysql 通过cmd命令执行:pip install robotf ...

  8. kvm linux虚拟机在线扩展磁盘

    说明: 1) vmware ESXi虚拟化平台也支持这台在线扩展磁盘功能. 2) kvm虚拟机也支持在线扩展磁盘功能,在线扩展有特定的使用环境,主要用于不能随便停用的生产环境中. 3) 经过测试KVM ...

  9. nginx处理高并发请求强于apache

    ginx 不同于 Apache2 的一点就是,Nginx 采用单线程,非阻塞,异步 IO 的工作模型. Apache2 对于每一个请求,都会创建一个新进程或线程,会浪费很多内存和 CPU 时间,而 N ...

  10. MD5算法分析

    1. MD5是什么? MD5即Message-Digest Algorithm 5(消息摘要算法第五版)的简称,是当前计算机领域用于确保信息传输完整一致而广泛使用的散列算法之一(又译哈希算法.摘要算法 ...