一起聊聊JavaScript闭包(总结分享)

本篇文章给大家带来了关于JavaScript中闭包的相关知识,其中包括从堆栈的角度看待闭包、闭包的共享变量问题等相关问题,希望对大家有帮助。

一起聊聊JavaScript闭包(总结分享)

1.闭包自结

闭包概念:

函数执⾏后返回结果是⼀个内部函数,并被外部变量所引⽤,如果内部函数持有被执⾏函数作⽤域的变量,即形成了闭包。可以在内部函数访问到外部函数作⽤域。

使⽤闭包,⼀可以读取函数中的变量,⼆可以将函数中的变量存储在内存 中,保护变量不被污染。⽽正因闭包会把函数中的变量值存储在内存中,会对内存有消耗,所以不能滥⽤闭包,否则会影响⽹⻚性能,造成内存泄漏。当不需要使⽤闭包时,要及时释放内存,可将内层函数对象的变量赋值为null。

闭包特点:一个外函数生成的多个闭包内存空间彼此独立。

闭包应用场景:

  1. 在内存中维持变量:如果缓存数据、柯里化  
  2. 保护函数内的变量安全:如迭代器、生成器。

缺点:闭包会导致原有的作用域链不释放,造成内存的泄漏。

  1. 内存消耗有负⾯影响。因内部函数保存了对外部变量的引⽤,导致⽆法被垃圾回收,增⼤内存使⽤量,所以使⽤ 不当会导致内存泄漏
  2. 对处理速度具有负⾯影响。闭包的层级决定了引⽤的外部变量在查找时经过的作⽤域链⻓度
  3. 可能获取到意外的值(captured value)

优点:

  1. 可以从内部函数访问外部函数的作⽤域中的变量,且访问到的变量⻓期驻扎在内存中,可供之后使⽤
  2. 避免变量污染全局
  3. 把变量存到独⽴的作⽤域,作为私有成员存在

2.闭包概念

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

词法作用域

请看下面的代码:

function init() {
    var name = "Mozilla"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
        alert(name); // 使用了父函数中声明的变量
    }
    displayName();
}
init();

init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。displayName() 是定义在 init() 里的内部函数,并且仅在 init() 函数体内可用。请注意,displayName() 没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name 。

使用这个 JSFiddle 链接运行该代码后发现, displayName() 函数内的 alert() 语句成功显示出了变量 name 的值(该变量在其父函数中声明)。这个词法作用域的例子描述了分析器如何在函数嵌套的情况下解析变量名。词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。


3.从堆栈的角度看待闭包

基本数据类型的变量的值一般存在栈内存中,基本的数据类型: Number 、Boolean、Undefined、String、Null。;而对象类型的变量的值存储在堆内存中,栈内存存储对应空间地址。

var a = 1 
//a是一个基本数据类型 
var b = {m: 20 } 
//b是一个对象

对应内存存储:

当我们执行 b={m:30}时,堆内存就有新的对象{m:30},栈内存的b指向新的空间地址( 指向{m:30} ),而堆内存中原来的{m:20}就会被程序引擎垃圾回收掉,节约内存空间。我们知道js函数也是对象,它也是在堆与栈内存中存储的,我们来看一下转化:

var a = 1;
function fn(){
    var b = 2
    function fn1(){
        console.log(b)
    }
    fn1()
}
fn()

栈是一种先进后出的数据结构:

  1. 在执行fn前,此时我们在全局执行环境(浏览器就是window作用域),全局作用域里有个变量a;
  2. 进入fn,此时栈内存就会push一个fn的执行环境,这个环境里有变量b和函数对象fn1,这里可以访问自身执行环境和全局执行环境所定义的变量
  3. 进入fn1,此时栈内存就会push 一个fn1的执行环境,这里面没有定义其他变量,但是我们可以访问到fn和全局执行环境里面的变量,因为程序在访问变量时,是向底层栈一个个找(这就是Javascript语言特有的"链式作用域"结构(chain scope),如果找到全局执行环境里都没有对应变量,则程序抛出underfined的错误。
  4. 随着fn1()执行完毕,fn1的执行环境被杯销毁,接着执行完fn(),fn的执行环境也会被销毁,只剩全局的执行环境下,现在没有b变量,和fn1函数对象了,只有a 和 fn(函数声明作用域是window下)

在函数内访问某个变量是根据函数作用域链来判断变量是否存在的,而函数作用域链是程序根据函数所在的执行环境栈来初始化的,所以上面的例子,我们在fn1里面打印变量b,根据fn1的作用域链的找到对应fn执行环境下的变量b。所以当程序在调用某个函数时,做了一下的工作:准备执行环境,初始函数作用域链和arguments参数对象

我们现在看下闭包例子

function outer() {
     var  a = '变量1'
     var  inner = function () {
            console.info(a)
     }
    return inner    // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域
}
var  inner = outer()   // 获得inner闭包函数
inner()   //"变量1"

当程序执行完var inner = outer(),其实outer的执行环境并没有被销毁,因为他里面的变量a仍然被被inner的函数作用域链所引用,当程序执行完inner(), 这时候,inner和outer的执行环境才会被销毁调;《JavaScript高级编程》书中建议:由于闭包会携带包含它的函数的作用域,因为会比其他函数占用更多内容,过度使用闭包,会导致内存占用过多。

4.闭包的共享变量问题

下面通过outer外函数和inner内函数来讲解闭包的共享变量问题。

同一个外函数生成的多个闭包是独立空间还是共享空间如何判断?请先看实例

//第一种情况 调用时给外函数传入变量值
function outer(name){
    return function(){
        console.log(name)
    }
}

f1 = outer('yang')
f2 = outer('fang')

console.log(f1.toString())
f1() //yang 
f2() //fang
f1() //yang 

//第二种情况:外函数局部变量值为变化
function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push(function () {
            return i * i;
        });
    }
    return arr;
}

var results = count();
var f1 = results[0];  //16
var f2 = results[1];  //16
var f3 = results[2];  //16
console.log(f1 )

//第三种情况:外函数的局部变量值变化。
function test(){
    var i = 0;
    return function(){
       console.log(i++)
    }
}; 
var a = test();
var b = test();
//依次执行a,a,b,控制台会输出什么呢?0 1 0  
//b为什么不是2
a();a();b();

同一个外函数生成的多个闭包是独立空间还是共享空间如何判断?

  1. 第一种情况说明多次调用外函数生成的不同闭包函数没有共享name变量
  2. 第二种情况说明外函数内部循环生成的多个内函数共享 i 局部变量
  3. 第三种情况说明a 、b为两个 不同闭包 函数,同一闭包函数 a 多次调用 共享 i 变量,a b之间不共享。

可以总结出记住三个闭包共享变量的原则

  1. 调用外函数,就会生成内函数和外函数的局部变量组成的闭包。每调用一次生成一个闭包函数。不同闭包函数之间内存空间彼此独立。
  2. 调用同一个闭包函数多次,共享内存空间,即外函数的局部变量值。
  3. 第二种情况for循环。没有调用外函数,只是将内函数存到了数组中,故并没有生成3个独立的闭包函数。而是3个内函数共享一个外函数局部变量,即3个内函数和外函数局部变量组成了一个整体的闭包环境。

简记:调用一次外函数,生成一个独立的闭包环境;外函数内部生成多个内函数,那么多个内函数共用一个闭包环境。


5.闭包应用场景

应用场景主要就两个

  • 在内存中维持变量:如果缓存数据、柯里化  
  • 保护函数内的变量安全:如迭代器、生成器。

场景一:保存局部变量在内存中

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。

在 Web 中,你想要这样做的情况特别常见。大部分我们所写的 JavaScript 代码都是基于事件的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。

假如,我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定 body 元素的 font-size,然后通过相对的 em 单位设置页面中其它元素(例如header)的字号:

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}
h1 {
  font-size: 1.5em;
}
h2 {
  font-size: 1.2em;
}

我们的文本尺寸调整按钮可以修改 body 元素的 font-size 属性,由于我们使用相对单位,页面中的其它元素也会相应地调整。

以下是 JavaScript

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + &#39;px&#39;;
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

size12,size14 和 size16 三个函数将分别把 body 文本调整为 12,14,16 像素。我们可以将它们分别添加到按钮的点击事件上。如下所示:

document.getElementById(&#39;size-12&#39;).onclick = size12;
document.getElementById(&#39;size-14&#39;).onclick = size14;
document.getElementById(&#39;size-16&#39;).onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>

场景二:用闭包模拟私有方法,保护局部变量

编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。

JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern):

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

可以将上面的代码拆分成两部分:(function(){}) () 。第1个() 是一个表达式,而这个表达式本身是一个匿名函数,所以在这个表达式后面加 () 就表示执行这个匿名函数。

在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.increment,Counter.decrement 和 Counter.value。

该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数 返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

你应该注意到我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量Counter。我们可以把这个函数储存在另外一个变量makeCounter中,并用他来创建多个计数器。
var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

请注意两个计数器 Counter1 和 Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。


6.循环中创建闭包的一个常见错误

在 ECMAScript 2015 引入 let 关键字 之前,在循环中有一个常见的闭包创建错误。参考下面的示例:

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById(&#39;help&#39;).innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {&#39;id&#39;: &#39;email&#39;, &#39;help&#39;: &#39;Your e-mail address&#39;},
      {&#39;id&#39;: &#39;name&#39;, &#39;help&#39;: &#39;Your full name&#39;},
      {&#39;id&#39;: &#39;age&#39;, &#39;help&#39;: &#39;Your age (you must be over 16)&#39;}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();
//一、将function直接返回,会发生闭包
 //二、将函数赋值给一个变量,此变量函数外部使用,此时也是闭包。比如,数组、多个变量等。 举例下面也是闭包情况。
 var arr = []
 for (var i = 0; i < 10; i++) {
    arr[i] = function(){console.log(i)}
  }
  arr[6]()此时也是闭包,将十个匿名函数+i组成了一个闭包返回。

数组 helpText 中定义了三个有用的提示信息,每一个都关联于对应的文档中的input 的 ID。通过循环这三项定义,依次为相应input添加了一个 onfocus 事件处理函数,以便显示帮助信息。

运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input上,显示的都是关于年龄的信息。

原因是赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。这是因为变量item使用var进行声明,由于变量提升,所以具有函数作用域。当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。

解决这个问题的一种方案是使用更多的闭包:特别是使用前面所述的函数工厂:

function showHelp(help) {
  document.getElementById(&#39;help&#39;).innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {&#39;id&#39;: &#39;email&#39;, &#39;help&#39;: &#39;Your e-mail address&#39;},
      {&#39;id&#39;: &#39;name&#39;, &#39;help&#39;: &#39;Your full name&#39;},
      {&#39;id&#39;: &#39;age&#39;, &#39;help&#39;: &#39;Your age (you must be over 16)&#39;}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, makeHelpCallback 函数为每一个回调创建一个新的词法环境。在这些环境中,help 指向 helpText 数组中对应的字符串。

另一种方法使用了匿名闭包:

function showHelp(help) {
  document.getElementById(&#39;help&#39;).innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {&#39;id&#39;: &#39;email&#39;, &#39;help&#39;: &#39;Your e-mail address&#39;},
      {&#39;id&#39;: &#39;name&#39;, &#39;help&#39;: &#39;Your full name&#39;},
      {&#39;id&#39;: &#39;age&#39;, &#39;help&#39;: &#39;Your age (you must be over 16)&#39;}
    ];

  for (var i = 0; i < helpText.length; i++) {
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // 马上把当前循环项的item与事件回调相关联起来
  }
}

setupHelp();

如果不想使用过多的闭包,你可以用ES2015引入的let关键词:

function showHelp(help) {
  document.getElementById(&#39;help&#39;).innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {&#39;id&#39;: &#39;email&#39;, &#39;help&#39;: &#39;Your e-mail address&#39;},
      {&#39;id&#39;: &#39;name&#39;, &#39;help&#39;: &#39;Your full name&#39;},
      {&#39;id&#39;: &#39;age&#39;, &#39;help&#39;: &#39;Your age (you must be over 16)&#39;}
    ];

  for (var i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

这个例子使用let而不是var,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。

另一个可选方案是使用 forEach()来遍历helpText数组,如下所示:

function showHelp(help) {
  document.getElementById(&#39;help&#39;).innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {&#39;id&#39;: &#39;email&#39;, &#39;help&#39;: &#39;Your e-mail address&#39;},
      {&#39;id&#39;: &#39;name&#39;, &#39;help&#39;: &#39;Your full name&#39;},
      {&#39;id&#39;: &#39;age&#39;, &#39;help&#39;: &#39;Your age (you must be over 16)&#39;}
    ];

  helpText.forEach(function(text) {
    document.getElementById(text.id).onfocus = function() {
      showHelp(text.help);
    }
  });
}

setupHelp();

7.性能考量

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

但是如果某个函数需要不停新建,那么使用闭包保存到内存中对性能有好处。

释放闭包只需要将引用闭包的函数置为null即可。


8.闭包注意事项

第一:多个内函数引用同一局部变量

function outer() {
      var result = [];
      for (var i = 0; i<10; i++){
        result.[i] = function () {
            console.info(i)
        }
     }
     return result
}

看样子result每个闭包函数对打印对应数字,1,2,3,4,...,10, 实际不是,因为每个闭包函数访问变量i是outer执行环境下的变量i,随着循环的结束,i已经变成10了,所以执行每个闭包函数,结果打印10, 10, ..., 10
怎么解决这个问题呢?

function outer() {
      var result = [];
      for (var i = 0; i<10; i++){
        result.[i] = function (num) {
             return function() {
                   console.info(num);    // 此时访问的num,是上层函数执行环境的num,数组有10个函数对象,每个对象的执行环境下的number都不一样
             }
        }(i)
     }
     return result
}

第二: this指向问题

var object = {
     name: &#39;&#39;object",
     getName: function() {
        return function() {
             console.info(this.name)
        }
    }
}
object.getName()()    // underfined
// 因为里面的闭包函数是在window作用域下执行的,也就是说,this指向windows

第三:内存泄露问题

function  showId() {
    var el = document.getElementById("app")
    el.onclick = function(){
      aler(el.id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
}

// 改成下面
function  showId() {
    var el = document.getElementById("app")
    var id  = el.id
    el.onclick = function(){
      aler(id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
    el = null    // 主动释放el
}

相关推荐:javascript学习教程

以上就是一起聊聊JavaScript闭包(总结分享)的详细内容,更多请关注其它相关文章!