【JavaScript】闭包,了解一下?

闭包概念

官方:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
通俗:闭包就是能够读取其他函数内部变量的函数。

一、变量的作用域

要理解闭包,首先必须理解Javascript特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。
另一方面,在函数外部自然无法读取函数内的局部变量。
这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

二、如何从外部读取局部变量?

我们有时候需要得到函数内的局部变量。但是,正常情况下这是办不到的,只有通过变通方法才能实现。
那就是在函数的内部,再定义一个函数

function f1(){
  n=999;
  function f2(){
    alert(n); // 999
  }
}

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1 就是不可见的。这是由于Javascript语言特有的“链式作用域”结构(chain scope)决定的,子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

  function f1(){
    n=999;
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999

function foo(){
  var local = 1
  function bar(){
    local++
    return local
  }
  return bar
}

var func = foo()
func()

这里面确实有闭包,local 变量和 bar 函数就组成了一个闭包(Closure)。

为什么要函数套函数呢?
是因为需要局部变量,所以才把 local 放在一个函数里,如果不把 local 放在一个函数里,local 就是一个全局变量了,达不到使用闭包的目的——隐藏变量。

有些人看到“闭包”这个名字,就一定觉得要用什么包起来才行。其实这是翻译问题,闭包的原文是 Closure,跟包没有任何关系。

三、闭包的运行机制

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

我们知道,this对象是在运行时基于函数的执行环境绑定的,在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性。每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments.内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
不过可以通过把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问到该对象了。
如下所示:

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

这里在定义匿名函数之前,我们把this对象赋值给了一个名叫that的变量,而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声明的一个变量。即使在函数返回之后,that也仍然引用着object,所以调用object.getNameFunc()()就返回了"My Object"。(this和arguments也存在同样的问题,如果想访问作用域中的arguments对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。)

四、闭包的用途

闭包可以用在许多地方。它的最大用处有三个,

  1. 读取函数内部的变量
  2. 让变量的值始终保持在内存中
  3. 用来“间接访问变量”(隐藏变量)

怎么来理解呢?下面给出两个例子。

function f1(){
  var n=999;
  AD*D=function(){n+=1}
  function f2(){
    alert(n);
  }
  return f2;
}
var result=f1();
result(); // 999
ADD();
result(); // 1000
  • 读取函数内部的变量

在这段代码中,result实际上就是闭包f2函数。

  • 让变量的值始终保持在内存中

result一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?
原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

注意:“ADD=function(){n+=1}”这一行,首先在ADD前面没有使用var关键字,因此 ADD是一个全局变量,而不是局部变量。其次,ADD的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以ADD相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

  • 间接访问变量

假设我们在写交易项目中,关于“还剩多少钱”的代码。
如果不用闭包,你可以直接用一个全局变量:

window.money = 500 // 还有五百块

这样看起来很不安全,万一不小心把这个值改了怎么办。所以我们不能让别人“直接访问”这个变量,所以要用到局部变量。
但是用局部变量别人又访问不到,怎么办呢?
暴露一个访问器(函数),让别人可以“间接访问”。

代码如下:

!function(){
  var money = 500
  window.addmoney = function(){
    money += 10
  }
  window.submoney = function(){
    money -= 10
  }
}()

那么在其他的 JS 文件,就可以使用 window.addmoney() 来增加钱包,使用 window.submoney() 来扣除余额。

五、使用闭包的注意点

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题;

  2. 在IE中可能导致内存泄露。IE 有 bug,在我们使用完闭包之后,IE依然回收不了闭包里面引用的变量。
    解决方法:在退出函数之前,将不使用的局部变量全部删除。

  3. 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(public method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

例1

function sayHello(name) 
{
 var text = 'Hello ' + name;
 var sayAlert = function() { console.log(text); }
 sayAlert();
}
sayHello("Bob") // 输出"Hello Bob"

在sayHello()函数中定义并调用了sayAlert()函数;sayAlert()作为内层函数,可以访问外层函数sayHello()中的text变量。
例2

function sayHello2(name) 
{
 var text = 'Hello ' + name; // 局部变量
 var sayAlert = function() { console.log(text); }
 return sayAlert;
}
var say2 = sayHello2("Jane");
say2(); // 输出"Hello Jane"

例3

function buildList(list) {
 var result = [];
 for(var i = 0; i < list.length; i++) {
    var item = 'item' + list[i];
    result.push( 
        function() {
            console.log(item + ' ' + list[i]);
        } 
     );
 }
 return result;
}
var fnlist = buildList([1,2,3]);
for (var j = 0; j < fnlist.length; j++) {
    fnlist[j](); 
}

得到的结果:连续输出3个"item3 undefined"
解析:通过执行buildList函数,返回了一个result,那么这个result存放的是3个匿名函数。然而这三个匿名函数其实就是三个闭包,因为它可以访问到父函数的局部变量。所以闭包内的保留的i是最终的值为3.所以list[3]肯定是undefined. item变量值为item3.
改成如下代码:

function buildList(list) {
 var result = [];
 for(var i = 0; i < list.length; i++) {
    var item = 'item' + list[i];
    result.push( 
        (function(i) {
            console.log(item + ' ' + list[i]);
        })(i)
     );
 }
 return result;
}
var fnlist = buildList([1,2,3]);

得到的结果:
item1 1
item2 2
item3 3
解释:这儿虽然传递了一个数组进去,但是返回的是三个自执行的函数。
例4

function newClosure(someNum, someRef) 
{
 var anArray = [1,2,3];
 var num = someNum;
 var ref = someRef;
 return function(x) 
 {
 num += x;
 anArray.push(num);
 console.log('num: ' + num + "; " + 'anArray ' + anArray.toString() + "; " + 'ref.someVar ' + ref.someVar);
 }
}
closure1 = newClosure(40, {someVar: "closure 1"}); 
closure2 = newClosure(1000, {someVar: "closure 2"}); 
closure1(5); // 打印"num: 45; anArray 1,2,3,45; ref.someVar closure 1"
closure2(-10); // 打印"num: 990; anArray 1,2,3,990; ref.someVar closure 2"

每次调用newClosure()都会创建独立的闭包,它们的局部变量num与ref的值并不相同。
例5

function sayAlice() 
{
 var sayAlert = function() { console.log(alice); }
 var alice = 'Hello Alice';
 return sayAlert;
}
var sayAlice2 = sayAlice();
sayAlice2(); // 输出"Hello Alice"

alice变量在sayAlert函数之后定义,这并未影响代码执行。因为返回函数sayAlice2所指向的闭包会包含sayAlice()函数中的所有局部变量,这自然包括了alice变量,因此可以正常打印”Hello Alice”。
例6

function setupSomeGlobals() {
 var num = 666;
 gAlertNumber = function() { console.log(num); }
 gIncreaseNumber = function() { num++; }
 gSetNumber = function(x) { num = x; }
}
setupSomeGlobals();
gAlertNumber(); // 输出666
gIncreaseNumber();
gAlertNumber(); // 输出667
gSetNumber(5);
gAlertNumber(); // 输出5

解释:首先gAlertNumber,gIncreaseNumber,gSetNumber是三个全局变量,并且其三个值都是匿名函数,然而这三个匿名函数本身都是闭包。他们操作的num都是保存在内存中的同一个num,所有会得出上面的结果。
下面看一个dom操作,使用闭包的例子:

// 这个代码是错误的,因为变量i从来就没背locked住
// 相反,当循环执行以后,我们在点击的时候i才获得数值
// 因为这个时候i操真正获得值
// 所以说无论点击那个连接,最终显示的都是I am link #10(如果有10个a元素的话)
var elems = document.getElementsByTagName('a');
for (var i = 0; i < elems.length; i++) {
    elems[i].addEventListener('click', function (e) {
        e.preventDefault();
        alert('I am link #' + i);
    }, 'false');
}

最终结果都显示为I am link #10(如果有10个a元素的话),这是作用链域的分配机制导致的。从表面上看,似乎每次点击某个连接的时候都应该返回自己的索引值。即点击位置为0的链接应该返回0,点击位置为1的链接应该返回1,以此类推。但实际上,点击每个链接都返回链接数的最大值,这是因为每个elems[i].addEventListener

他们都保存着全局的活动对象,所以他们引用的都是同一个变量i(按引用传递)。
解决方法:通过创建另一个匿名函数,强制使结果符合预期。

// 这个是可以用的,因为他在自执行函数表达式闭包内部
// i的值作为locked的索引存在,在循环执行结束以后,尽管最后i的值变成了a元素总数(例如10)
// 但闭包内部的lockedInIndex值是没有改变,因为他已经执行完毕了
// 所以当点击连接的时候,结果是正确的
var elems = document.getElementsByTagName('a');
for (var i = 0; i < elems.length; i++) {
    (function (lockedInIndex) {
elems[i].addEventListener('click', function (e) {
            e.preventDefault();
            alert('I am link #' + lockedInIndex);
        }, 'false');
    })(i);
}

这里匿名函数有一个参数lockedInIndex,也就是点击连接时最终要返回的结果值,在调用匿名函数时,我们传入了变量i,由于函数的变量是按值传递的,所以会将当前变量的值复制给lockedInIndex,所以最终我们能够得到预期的结果。

结语

理解Javascript的闭包是迈向高级JS程序员的必经之路,理解了其解释和运行机制才能写出更为安全和优雅的代码。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值