JS中的闭包(closure)

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
下面就是我的学习笔记,对于Javascript初学者应该是很有用的。

一.什么是闭包

JS中,在函数内部可以读取函数外部的变量

function outer(){
var localVal = 30;
return localVal;
}
outer();//30

但,在函数外部自然无法读取函数内的局部变量

function outer(){
var localVal = 30;
}
alert(localVal);//error

这里有个需要注意的地方,函数内部声明变量的时候,一定要使用var命令。如果不用的话,实际上是声明了一个全局变量。

function outer(){
localVal = 30;
return localVal;
}
outer();
alert(localVal);//30

以上的表述,是JS变量的作用域的知识,它包括全局变量和局部变量。

Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

function outer(){
var localVal = 30;
function inner(){
alert(localVal);
}

     return inner;
}
var func = outer();
func();//30

我们看到在上面的代码中,outer函数内又定义一个函数inner,outer函数的返回值是inner函数,inner函数把localVal alert出来。

我们可以看出以上代码的特点:函数嵌套函数,内部函数可以引用外部函数的参数和变量,参数和变量不会被垃圾回收机制收回。

代码中的inner函数,就是闭包。简单的说,闭包(closure)就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

在上面的代码中,函数inner被包含在函数outer内部,这时outer内部的所有局部变量,对inner都是可见的。但是inner内部的局部变量,对oute 是不可见的。这是Javascript语言特有的“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

补充--JS中的函数定义

JS中定义一个函数,最常用的就是函数声明和函数表达式

Js中的函数声明是指下面的形式:

function functionName(){  

}

函数表达式则是类似表达式那样来声明一个函数:

var functionName = function(){  

}

我们可以使用函数表达式创建一个函数并马上执行它,如:

(function() {
var a, b // local variables
// ... // and the code
})()

()();第一个括号里放一个无名的函数。

二者区别:js的解析器对函数声明与函数表达式并不是一视同仁地对待的。对于函数声明,js解析器会优先读取,确保在所有代码执行之前声明已经被解析,而函数表达式,如同定义其它基本类型的变量一样,只在执行到某一句时也会对其进行解析,所以在实际中,它们还是会有差异的,具体表现在,当使用函数声明的形式来定义函数时,可将调用语句写在函数声明之前,而后者,这样做的话会报错。

二.闭包的应用

使用闭包的好处:

-希望一个变量长期驻扎在内存当中;

-避免全局变量的污染;

-私有成员的存在

1.模块化代码

使用自执行的匿名函数来模拟块级作用域

(function(){
// 这里为块级作用域
})();

该方法经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数影响全局作用域。也可以减少如闭包这样的对内存的占用,由于匿名函数没有变量指向,执行完毕就可以立即销毁其作用域链。

示例:

var test = (function(){
var a= 1;
return function(){
a++;
alert(a);
}
})(); test();//
test();//

实现a的自加,不污染全局。

2.循环闭包

循环给每个li注册一个click事件,点击alert序号。代码如下:

var aLi = document.getElementByClassName("test");
function showAllNum( aLi ){
for( var i =0,len = aLi.length ;i<len;i++ ){
aLi[i].onclick = function(){
alert( i );//all are aLi.length!
}
}
}

点击后会一直弹出同一个值 aLi.length 而不是123。当点击之前,循环已经结束,i值为aLi.length。

利用闭包,建一个匿名函数,将每个i存在内存中,onclick函数用的时候提取出外部匿名函数的i值。代码如下:

var aLi = document.getElementByClassName("test");
function showAllNum( aLi ){
for( var i =0,len = aLi.length ;i<len;i++ ){
(function(i){
aLi[i].onclick = function(){
alert( i );
}
})(i);
}
}

或者:

function showAllNum( aLi ){
for( var i =0,len = aLi.length ;i<len;i++ ){
aLi[i].onclick = (function(i){
return function(){
alert( i );
}
})(i);
}
}

实现解释:

1.作用域链

2.闭包函数的赋值与运行

实际上只是通过函数的赋值表式方式付给了标签点击事件,并没有运行;当遍历完后,i变成标签组的长度,根据作用域的原理,向上找到for函数里的i,所以点击执行的时候都会弹出标签组的长度。闭包可以使变量长期驻扎在内存当中,我们在绑定事件的时候让它自执行一次,把每一次的变量存到内存中;点击执行的时候就会弹出对应本作用域i的序号。

3.封装

外部无法直接获取函数内的变量,可通过暴露的方法获取

var info = function(){
var _userId = 23492;
var _typeId = 'item'; function getUserId(){
alert(_userId);
} function getTypeId(){
alert(_typeId);
}
}; info.getUserId();//
info.getTypeId();//item info._userId//undefined
info._typeId//undefined

但是这种方式会使我们在每一次创建新对象的时候都会创建一个这种方法。使用原型来创建一个这种方法,避免每个实例都创建不同的方法。在这里不做深究(一般构造函数加属性,原型加方法)。

4.关于 this 对象

this 对象是在运行时基于函数的执行环境绑定的(匿名函数中具有全局性)(this:当前发生事件的元素),有时候在一些闭包的情况下就有点不那么明显了。

代码1:

var name = "The Window";
var obj = {
name : "The object",
getNameFunc : function(){
return function(){
return this.name;
}
}
}
alert( obj. getNameFunc()() )//The Window

代码2:

var name="The Window"
var obj = {
name : "The object", getNameFunc : function(){
var _this = this;
return function(){
return _this.name;
}
}
}
alert(object.getNameFunc()());//The object

javascript是动态(或者动态类型)语言,this关键字在执行的时候才能确定是谁。所以this永远指向调用者,即对‘调用对象‘者的引用。第一部分通过代码:执行代码object.getNameFunc()之后,它返回了一个新的函数,注意这个函数对象跟object不是一个了,可以理解为全局函数;它不在是object的属性或者方法,此时调用者是window,因此输出是 The Window。

第二部分,当执行函数object.getNameFunc()后返回的是:

function( )
{
return _this.name;
}

此时的_this=this。而this指向object,所以that指向object。他是对object的引用,所以输出My Object。

总结:关于js中的this,记住谁调用,this就指向谁;要访问闭包的this,要定义个变量缓存下来。一般喜欢var _this = this。

5.闭包在IE下内存泄露问题

IE9之前,JScript对象和COM对象使用不同的垃圾收集例程,那么闭包会引起一些问题。

创建一个闭包,而后闭包有创建一个循环引用,那么该元素将无法销毁。常见的就是dom获取的元素或数组的属性(或方法)再去调用自己属性等。例如:

function handler(){
var ele = document.getElementById("ele");
ele.onclick = function(){
alert(ele.id);
}
}

闭包会引用包含函数的整个活动对象,即是闭包不直接引用ele,活动对象依然会对其保存一个引用,那么设置null就可以断开保存的引用,释放内存。代码如下:

function handler(){
var ele = document.getElementById("ele");
var id = ele.id;
ele.onclick = function(){
alert(id);
}
ele = null;
}

当然还有其他方法,推荐此法。

三.闭包的原理

当某个函数第一次被调用时,会创建一个执行环境(execution context)及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[[Scope]])。然后,使用this、arguncmts 和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。

在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。来看下面的例子:

function compare(valael, value2){ 
    if (valuel < value2){ 
       return -1;
} else if (vaiuel > value2){
       return 1;
} else {
return 0;
}
} var result = compare(5, 10);

以上代码先定义了compare()函数,然后又在全局作用域中调用了它。当第一次调用compare()时,会创建一个包含this、arguments、valuel和value2的活动对象。全局执行环境的变量对象 (包含this、result和compare)在compare()执行环境的作用域链中则处于第二位。图展示了包含上述关系的compare()函数执行时的作用域链。

后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建compare()函数时,会创建一个预先包含全局变童对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调用compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。对于这个例子中compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。 但是,闭包的情况又有所不同。

function createComparisonFunction(propertyName) {
return function(object1, object2){
var valuel = objectl[propertyName];
var value2 = object2[propertyName];
if (valuel < value2){
return -1;
} else if (valuel > value2){
return 1;
} else {
return 0;
}
};
}

在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()涵数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。图展示了当下列代码执行时,包含函数与内部匿名函数的作用域链。

var compare = createComparisonFunction("name");

var result = compare({ name: "Nicholas" }, { naine: BGreg" });

在匿名函数从createComparisonFunction()中被返冋后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()中定义的所有变量。更重要的是,createCoir.parisonFunction() 函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁,例如:

var compareNames = createComparisonFunction("name");

//调用函数
var result = compareNames({ name: "Nicholas" ), { name:"Greg" }); //解除对匿名函数的引用(以便释放内存)
compareNanies = null;

首先,创建的比较函数被保存在变量coinpareNames中。而通过将compareNames设置为等于null解除该函数的引用,就等于通知垃圾问收例程将其清除。随着匿名函数的作用域链被销毁,其他作用域 (除r全局作用域)也都可以安全地销毁了。图 展示了调用conpareNamesO的过程中产生的作用域链之间的关系。

-------------------------------------------------------------------------------------------------------------------------------------

闭包无处不在,弄懂它很重要。

转载需注明转载字样,标注原作者和原博文地址。

JS中的闭包(closure)的更多相关文章

  1. 详解js中的闭包

    前言 在js中,闭包是一个很重要又相当不容易完全理解的要点,网上关于讲解闭包的文章非常多,但是并不是非常容易读懂,在这里以<javascript高级程序设计>里面的理论为基础.用拆分的方式 ...

  2. js中的闭包之我理解

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  3. 浅谈JS中的闭包

    浅谈JS中的闭包 在介绍闭包之前,我先介绍点JS的基础知识,下面的基础知识会充分的帮助你理解闭包.那么接下来先看下变量的作用域. 变量的作用域 变量共有两种,一种为全局变量,一种为局部变量.那么全局变 ...

  4. js中的“闭包”

    js中的“闭包” 姓名:闭包 官方概念:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. ( ⊙o⊙ )!!!这个也太尼玛官方了撒,作为菜鸟的 ...

  5. Js中的闭包原理

    要了解清楚js中的闭包制机,那么得先了解全局执行环境.块级执行环境.函数执行环境.变量对象.环境栈.作用域链.摧毁执行环境. 全局执行环境 全局执行环境指的是最外层的执行环境.在web中全局执行环境被 ...

  6. js中的闭包理解一

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  7. js中的闭包理解

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  8. javascript中的闭包closure详解

    目录 简介 函数中的函数 Closure闭包 使用闭包实现private方法 闭包的Scope Chain 闭包常见的问题 闭包性能的问题 总结 简介 闭包closure是javascript中一个非 ...

  9. 初识js中的闭包

    今天看了关于js闭包方面的文章,还是有些云里雾里,对于一个菜鸟来说,学习闭包确实有一定的难度,不说别的,能够在网上找到一篇优秀的是那样的不易. 当然之所以闭包难理解,个人觉得是基础知识掌握的不牢,因为 ...

随机推荐

  1. 优雅的启动、停止、重启你的SpringBoot项目

    前言 你是如何启动.关闭你的SpringBoot项目的?还是使用java -jar xxxx.jar启动? 还在用ps -ef找到你的pid去kill你的应用吗? 让我们来看看还有什么更加优雅的一键启 ...

  2. 精读《Scheduling in React》

    1. 引言 这次介绍的文章是 scheduling-in-react,简单来说就是 React 的调度系统,为了得到更顺滑的用户体验. 毕竟前端做到最后,都是体验优化,前端带给用户的价值核心就在于此. ...

  3. 补习系列(8)-springboot 单元测试之道

    目录 目标 一.About 单元测试 二.About Junit 三.SpringBoot-单元测试 项目依赖 测试样例 四.Mock测试 五.最后 目标 了解 单元测试的背景 了解如何 利用 spr ...

  4. git版本控制工具的使用

    目录 git版本管理工具使用 一丶Git的下载与安装 1.windows下的git的下载与安装 2.linux下的git安装 二丶常用命令 三丶Git仓库 1.配置仓库信息 2.仓库的创建于管理 四丶 ...

  5. Java开发知识之Java入门

    Java开发知识之Java入门 一丶了解JAVA的版本 JAVA 有三个版本 JAVA SE: 标准版,开发桌面跟商务应用程序 JAVA SE 包括了Java的核心类库,集合,IO 数据库连接 以及网 ...

  6. mysql数据库备份并且实现远程复制

    一.实现ssh 远程登陆 机器环境: 192.167.33.108 clent 用户:crawler 192.167.33.77 server 用户:crawler 1.客户端 生成密钥 /home/ ...

  7. [三]JavaIO之IO体系类整体设计思路 流的概念以及四大基础分类

    从本文开始,将正式进入JavaIO的简介 在继续javaIO系列的文章之前 可以过去看一下 本人博客上的设计模式中的 适配器模式和装饰器模式 这会对接下来的阅读大有帮助   本文是从逻辑上介绍整个的J ...

  8. Perl线程队列:Thread::Queue

    (Thread::Queue)队列数据结构(FIFO)是线程安全的,它保证了某些线程从一端写入数据,另一些线程从另一端读取数据.只要队列已经满了,写入操作就自动被阻塞直到有空间支持写操作,只要队列空了 ...

  9. Spring Cloud Alibaba基础教程:Nacos的数据持久化

    前情回顾: <Spring Cloud Alibaba基础教程:使用Nacos实现服务注册与发现> <Spring Cloud Alibaba基础教程:支持的几种服务消费方式> ...

  10. C#线程同步--线程通信

    问题抽象:当某个操作的执行必须依赖于另一个操作的完成时,需要有个机制来保证这种先后关系.线程通信方案:ManualResetEventSlim.ManualResetEvent.AutoResetEv ...