资讯专栏INFORMATION COLUMN

大话javascript 3期:闭包

Freelander / 1703人阅读

摘要:由此可知闭包是函数的执行环境以及执行环境中的函数组合而构成的。此时产生了闭包。二闭包的作用闭包的特点是读取函数内部局部变量,并将局部变量保存在内存,延长其生命周期。三闭包的问题使用闭包会将局部变量保持在内存中,所以会占用大量内存,影响性能。

一、什么是闭包 1.闭包的定义

闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境(包含自由变量)。环境由闭包创建时在作用域中的任何局部变量组成。

闭包是指有权访问另外一个函数作用域中的变量函数

闭包是函数以及函数声明所在的词法环境的组合。

由此,我们可以看出闭包共有两部分组成:闭包 = 函数 + 函数能够访问的自由变量

举个例子:

var a = 1;
function foo() {
  console.log(a);
}
foo();

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛

2.闭包的概念
function fa(){
    var va = "this is fa";
    function fb(){
        console.log(va);
    }    
    return fb;
}

var fc = fa();
fc();
//"this is fa"

其实,简单点说,就是在 A 函数内部,存在 B 函数, B函数 在 A 函数 执行完毕后再执行。B执行时,访问了已经执行完毕的 A函数内部的变量和函数。

由此可知:闭包函数A的执行环境以及执行环境中的函数B组合而构成的。

变量都储存在其所在执行环境的活动对象中,所以说是函数A的执行环境。

当函数A执行完毕后,函数B再执行,B的作用域中就保留着函数A的活动对象,因此B中可以访问A中的变量,函数,arguments对象。此时产生了闭包。大部分书中,都把函数B称为闭包,而在谷歌浏览器中,把A函数称为闭包。

3.闭包的本质

之前说过,当函数执行完毕后,局部活动对象就会被销毁。其中保存的变量,函数都会被销毁。内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况就不同了。

以上面的例子来说,函数fb和其所在的环境函数fa,就组成了闭包。函数fa执行完毕后,按道理说, 函数fa执行环境中的 活动对象就应该被销毁了。但是,因为函数fa执行时,其中的函数fb被返回,被变量fc引用着。导致,函数fa的活动对象没有被销毁。而在其后fc()执行,就是函数fb执行时,构建的作用域中保存着函数fa的活动对象,因此,函数fb中可以通过作用域链访问函数fa中的变量。

其实,简单的说:就是fa函数执行完毕了,其内部的fb函数没有执行,并返回fb的引用,当fb再次执行时,fb的作用域中保留着fa函数的活动对象。

二、闭包的作用
闭包的特点是读取函数内部局部变量,并将局部变量保存在内存,延长其生命周期。

作用

通过闭包,在外部环境访问内部环境的变量。

使得这些变量一直保存在内存中,不会被垃圾回收。

以使用闭包实现以下功能:

1.解决类似循环绑定事件的问题

在实际开发中,经常会遇到需要循环绑定事件的需求,比如在id为container的元素中添加5个按钮,每个按钮的文案是相应序号,点击打印输出对应序号。
其中第一个方法很容易错误写成:

var container = document.getElementById("container");
for(var i = 1; i <= 5; i++) {
 var btn = document.createElement("button"),
     text = document.createTextNode(i);
 btn.appendChild(text);
 btn.addEventListener("click", function(){
   console.log(i);
 })
 container.appendChild(btn);
}

虽然给不同的按钮分别绑定了事件函数,但是5个函数其实共享了一个变量 i。由于点击事件在 js 代码执行完成之后发生,此时的变量 i 值为6,所以每个按钮点击打印输出都是6。
为了解决这个问题,我们可以修改代码,给各个点击事件函数建立独立的闭包,保持不同状态的i。

var container = document.getElementById("container");
for(var i = 1; i <= 5; i++) {
 (function(_i) {
   var btn = document.createElement("button"),
       text = document.createTextNode(_i);
   btn.appendChild(text);
   btn.addEventListener("click", function(){
     console.log(_i);
   })
   container.appendChild(btn);
 })(i);
}
注:解决这个问题更好的方法是使用 ES6 的 let,声明块级的局部变量。
2.封装私有变量 (1) 经典的计数器例子:
function makeCounter() {
  var value = 0;
  return {
    getValue: function() {
      return value;
    },
    increment: function() {
      value++;
    },
    decrement: function() {
      value--;
    }
  }
}

var a = makeCounter();
var b = makeCounter();
b.increment();
b.increment();
b.decrement();
b.getValue(); // 1
a.getValue(); // 0
a.value; // undefined

每次调用makeCounter函数,环境是不相同的,所以对b进行的increment/decrement操作不会影响a的value属性。同时,对value属性,只能通过getValue方法进行访问,而不能直接通过value属性进行访问。

(2) 经典的循环闭包面试题
for (var i=1;i<=5;i++){
    setTimeout(function timer(){
        console.log(i);
    },i*1000);
}

正常预想下,上面这段代码我们以为是分别输出数字1-5,每秒一个。
但实际上,运行时输出的却是每秒输出一个6,一共五次。

Why?

for循环有一个特点,就是“i判断失败一次才停止”。所以,i在不断的自加1的时候,直到i等于5,i才失败,这时候循环体不再执行,会跳出,所以i等于5没错。那么为什么5次循环的i都等于5?原因就是setTimeout()的回调,也就是console.log(i);被压到任务队列的最后for循环是同步任务,所以先执行,等于是空跑了5次循环。于是,i都等于5之后,console.log(i);刚开始第一次执行,当然输出全是5。

根据setTimeout定义的操作在函数调用栈清空之后才会执行的特点,for循环里定义了5个setTimeout操作。而当这些操作开始执行时,for循环的i值,已经先一步变成了6。因此输出结果总为6。而我们想要让输出结果依次执行,我们就必须借助闭包的特性,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操作执行时,则访问对应闭包保存的i值即可。

简单来说,原因是,延迟函数的回调会在循环结束时才执行
根据作用域的工作原理,循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,实际上只有一个i

解决办法

方法1:用立即执行函数模拟块级作用域

利用立即执行函数和函数作用域来解决,用自执行函数传参,这样自执行函数内部形成了局部作用域,不受外部变量变化的影响。

我们可以通过立即执行函数创建作用域。(立即执行函数会通过声明并立即执行一个函数来创建作用域)。

  for (var i=1; i<=5; i++) { 
    (function(i) {
        setTimeout( function timer() {
            console.log(i);
        }, i*1000 );
    })(i)
  }
// 1
// 2
// 3
// 4
// 5
方法2:利用闭包
function makeClosures(i){     //这里就和 内部的匿名函数构成闭包了
    var i = i;               //这步是不需要的,为了让看客们看的轻松点
    return function(){
        console.log(i);     //匿名没有执行,它可以访问i的值,保存着这个i的值。
    }
}

for (var i=1; i<=5; i++) {
    setTimeout(makeClosures(i),i*1000);  
    
    //这里简单说下,这里makeClosures(i), 是函数执行,并不是传参,不是一个概念
    //每次循环时,都执行了makeClosures函数,都返回了一个没有被执行的匿名函数
    //(这里就是返回了5个匿名函数),每个匿名函数都是一个局部作用域,保存着每次传进来的i值
    //因此,每个匿名函数执行时,读取`i`值,都是自己作用域内保存的值,是不一样的。所以,就得到了想要的结果
}

//1
//2
//3
//4
//5
方法3:利用块级作用域

ES6引入的let在循环中不止会被声明一次,在每次迭代都会声明:

for (let i=1;i<=5;i++){
    setTimeout(function timer(){
        console.log(i);
    },i*1000);
}

因为使用let,导致每次循环都会创建一个新的块级作用域,这样,虽然setTimeout 中的匿名函数内没有 i 值,但它向上作用域读取i 值,就读到了块级作用域内 i 的值。

三、闭包的问题

使用闭包会将局部变量保持在内存中,所以会占用大量内存,影响性能。所以在不再需要使用这些局部变量的时候,应该手动将这些变量设置为null, 使变量能被回收。

当闭包的作用域中保存一些DOM节点时,较容易出现循环引用,可能会造成内存泄漏。原因是在IE9以下的浏览器中,由于BOM 和DOM中的对象是使用C++以COM 对象的方式实现的,而COM对象的垃圾收集机制采用的是引用计数策略,当出现循环引用时,会导致对象无法被回收。当然,同样可以通过设置变量为null解决。

举例如下:

function func() {
  var element = document.getElementById("test");
  element.onClick = function() {
      console.log(element.id);
  };
}

func 函数为 element 添加了闭包点击事件,匿名函数中又对element进行了引用,使得 element 的引用始终不为0。解决办法是使用变量保存所需内容,并在退出函数时将 element 置为 null。

function func() {
  var element = document.getElementById("test"),
      id = element.id;
  element.onClick = function() {
      console.log(id);
  };
  element = null;
}
四、应用场景:模块与柯里化

模块也是利用了闭包的一个强大的代码模式。

function CoolModule(){
    var something="cool";
    var anothor=[1,2,3];
    
    function doSomething(){
        console.log(something);
    }
    
    function doAnthor(){
        console.log(anothor.join("!"));
    }
    
    return{
        doSomethig:doSomething,
        doAnothor:doAnother
    };
}

var foo=CoolMOdule();
foo.doSomething();//cool
foo.doAnother();//1!2!3

模块有2个主要特征:

为创建内部作用域而调用了一个包装函数;

包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

关于模块的引入

import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上;

module会将整个模块的API导入并绑定到一个变量上;

export会将当前模块的一个标识符导出为公共API。

如果你觉得这篇文章对你有所帮助,那就顺便点个赞吧,点点关注不迷路~

黑芝麻哇,白芝麻发,黑芝麻白芝麻哇发哈!

前端哇发哈

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103002.html

相关文章

  • 大话javascript 2:执行上下文与执行上下文栈

    摘要:在中,通过栈的存取方式来管理执行上下文,我们可称其为执行栈,或函数调用栈。因为执行中最先进入全局环境,所以处于栈底的永远是全局环境的执行上下文。 一、什么是执行上下文? 执行上下文(Execution Context): 函数执行前进行的准备工作(也称执行上下文环境) JavaScript在执行一个代码段之前,即解析(预处理)阶段,会先进行一些准备工作,例如扫描JS中var定义的变量、...

    denson 评论0 收藏0
  • 大话javascript 4:事件循环(1)

    摘要:脚本执行,事件处理等。引擎线程,也称为内核,负责处理脚本程序,例如引擎。事件触发线程,用来控制事件循环可以理解为,引擎线程自己都忙不过来,需要浏览器另开线程协助。异步请求线程,也就是发出请求后,接收响应检测状态变更等都是这个线程管理的。 一、进程与线程 现代操作系统比如Mac OS X,UNIX,Linux,Windows等,都是支持多任务的操作系统。 什么叫多任务呢?简单地说,就是操...

    codergarden 评论0 收藏0
  • 大话javascript 4:事件循环(3

    摘要:令人困惑的是,文档中称,指定的回调函数,总是排在前面。另外,由于指定的回调函数是在本次事件循环触发,而指定的是在下次事件循环触发,所以很显然,前者总是比后者发生得早,而且执行效率也高因为不用检查任务队列。 一、定时器 除了放置异步任务的事件,任务队列还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做定时器(timer)功能,也就是定时执行的代码。 定时器功能主要由setTim...

    liujs 评论0 收藏0
  • 大话javascript 1:作用域和作用域链

    摘要:全局作用域局部作用域局部作用域全局作用域局部作用域块语句没有块级作用域块级声明包括和,以及和循环,和函数不同,它们不会创建新的作用域。局部作用域只在该函数调用执行期间存在。 一、什么是作用域? 作用域是你的代码在运行时,各个变量、函数和对象的可访问性。(可产生作用的区域) 二、JavaScript中的作用域 在 JavaScript 中有两种作用域 全局作用域 局部作用域 当变量定...

    NicolasHe 评论0 收藏0
  • 大话javascript 8:正则表达式

    摘要:许多程序设计语言都支持利用正则表达式进行字符串操作。为字符串定义规则,为输入内容定义规则正则表达式用于字符串处理表单验证等场合,实用高效。匹配检查字符串是否符合正则表达式中的规则,有一次不匹配,则返回。 一、正则表达式的定义 正则表达式(Regular Expression,在代码中常简写为regex、regexp或RE)是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系...

    LeviDing 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<