• 理解函数作用域与闭包


    前言

    但凡读书,或者学一门技术,都要问自己以下几个问题。

    • 它是什么?
    • 它有什么用?/发明它是为了解决什么问题?
    • 它有什么弊端?

    我下面就试着从这几个方向来阐述闭包这个概念。

    概念

    在了解闭包之前,我们需要了解几个概念。本文在这里只做简单介绍,如需要进一步了解,请参考文章末尾的链接。

    作用域

    变量和函数的可作用范围,分为局部作用域和全局作用域。Javascript不具有块级作用域,而具有函数作用域。

    执行环境(execuation context)

    变量和函数有权访问的其他数据。

    执行环境栈(execuation context stack)

    每个函数在执行的时候,会把它的执行环境推入一个栈中,在函数执行完毕后执行环境出栈并被销毁。保存在其中的所有函数和比变量定义随之销毁,控制权返回到之前的执行环境中。全局的执行环境在应用程序退出(浏览器关闭)才会被销毁。

    作用域链(scope chain)

    作用域链用于保证对执行环境有权访问的变量和函数的有序访问。

    什么是闭包?

    闭包这个概念,在函数式编程里很常见,简单的说,就是使内部函数可以访问定义在外部函数中的变量。严格一点的定义是

    在函数内声明另一个函数,并且返回这个函数。这个返回的函数和它的执行环境整体叫做闭包。

    让我们来看一个例子:

    function f1(){
      var val = 10;
    }
    console.log(val);    //Uncaught ReferenceError: val is not defined(…)

    由于从函数外部无法访问函数内部的变量,所以报出了错误。那么如何能够访问到局部作用域的变量呢?

    function f1(){
      var val = 10;
      function f2(){
        console.log(val);
      }
      return f2;
    }
    
    var f2 = f1();
    f2();           // 10

    在这段代码中,f2 函数和其执行环境构成了一整个闭包。对于常规的 f1() 方法, 在其内部的变量 val 应该在 f1() 方法执行完毕以后就被垃圾回收。但是 f1() 返回了一个新的方法 f2()。由于 f2() 访问了其外部函数的变量 val,val就构成了f2函数的执行环境。val 存在于f2的作用域链中,只要f2()方法没有被销毁,其作用域链中的变量和函数就不会被销毁, val 也就会一直存在。

    闭包有什么用?

    for循环变量无法保持的问题

    for (var i = 0; i < 5; i++) {
      setTimeout(function () {
        console.log(i);
      }, 5);
    }

    上面这个代码块会打印五个 5 出来,而我们预想的结果是打印 1 2 3 4 5。

    之所以会这样,是因为 setTimeout 中的 i 是对外层 i 的引用。当 setTimeout 的代码被解析的时候,运行时只是记录了 i 的引用,而不是值。而当 setTimeout 被触发时,五个 setTimeout 中的 i 同时被取值,由于它们都指向了外层的同一个 i,而那个 i 的值在迭代完成时为 5,所以打印了五次 5

    为了得到我们预想的结果,我们可以把 i 赋值成一个局部的变量,从而摆脱外层迭代的影响。

    for (var i = 0; i < 5; i++) {
      (function (idx) {
        setTimeout(function () {
          console.log(idx);
        }, 5);
      })(i);
    }

    制造函数构造器

    假如我们要实现一系列的函数:add10,add20。我们为此构造了一个名为 adder 的构造器,如下:

    var adder = function (x) {
      var base = x;
      return function (n) {
        return n + base;
      };
    };
    
    var add10 = adder(10);
    console.log(add10(5));
    
    var add20 = adder(20);
    console.log(add20(5));

    每次调用 adder 时,adder 都会返回一个函数给我们。我们传给 adder 的值,会保存在一个名为 base 的变量中。由于返回的函数在其中引用了 base 的值,于是 base 的引用计数被 +1。当返回函数不被垃圾回收时,则 base 也会一直存在。

    闭包有什么弊端?

    由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

    面试题

    面试题一

    请定义这样一个函数

    function repeat (func, times, wait) {
    }
    //  这个函数能返回一个新函数,比如这样用
    //  var repeatedFun = repeat(alert, 10, 5000)
    //  调用这个 repeatedFun ("helloworld")
    //  会alert十次 helloworld, 每次间隔5秒

    代码参见:JS bin 闭包面试题一

    面试题二

    写一个函数stringconcat, 要求能

    var result1 = stringconcat("a", "b")  result1 = "a+b"
    var stringconcatWithPrefix = stringconcat.prefix("helloworld");
    var result2 = stringconcatWithPrefix("a", "b")  result2 = "helloworld+a+b"

    代码参见:JS bin 闭包面试题二

    参考:
    学习Javascript闭包(Closure)

    node-lessons/lesson11 at master · alsotang/node-lessons · GitHub

    JavaScript作用域链
  • 相关阅读:
    [转] 献给所有正在找路的人
    在同一表单内,多个提交按钮的处理方式
    javascript高级选择器querySelector和querySelectorAll
    一位年轻女董事长的37条忠告很受启发吧?
    函数的延迟加载
    WCF的CommunicationObjectFaultedException异常问题
    WCF Test Client对象数组输入问题
    [转载]C#开发Winform记录用户登录状态的方法
    using(C#)
    使用 SCTP 优化网络
  • 原文地址:https://www.cnblogs.com/timl525/p/5106221.html
Copyright © 2020-2023  润新知