每日JavaScript -20 (闭包Closure)

最hardcore的键盘
hardcore描述
闭包是学习JavaScript的一大难题。
所以觉得系列第20篇就来讨论闭包。
参考文章:
https://github.com/mqyqingfeng/Blog/issues/9
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
https://zhuanlan.zhihu.com/p/22486908
https://www.jianshu.com/p/21a16d44f150

首先是对闭包的定义:
在《JavaScript高级程序设计 第三版》中对闭包的定义是指有权访问另一个函数作用域中的变量的函数。
但是在MDN中,简单明了的定义:闭包是函数和声明该函数的词法环境的组合。
个人倾向于MDN的解释。

ECMAScript中,闭包指的是:

从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量

总的来说,广义上所有函数都是闭包。

闭包的作用,模拟私有变量和私有方法。

有点说不下去,看看代码吧

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() {
        console.log(a);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}

function bar() {
    fn(); // 此处的保留的innerFoo的引用
}

foo();
bar(); // 2

在上面的例子中,foo()执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值。
闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量
所以,闭包会造成内存泄露。

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

从本质上讲,makeAdder 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。

add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。

模拟私有方法

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 */

但我们不想直接暴露私有变量(return这个变量),可以用闭包提供接口调用私有变量和私有函数。
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问
再如:

var foo = (function() {
  var secret = 'secret';
  return {
    get_secret : function () {
      // 通过定义的接口来访问secret
      return secret;
    },
    new_secret: function (new_secret) {
      // 通过定义的接口来修改secret
      secret = new_secret;
    }
  };
})();

闭包用于模块化

 function add(num1, num2) {
        var num1 = !!num1 ? num1 : a;
        var num2 = !!num2 ? num2 : b;

        return num1 + num2;
    }
   window.add = add;
})();

add(10, 20);

典型题目

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

解答

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

或者使用let

典型题目:

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

解答:

for (var i = 0; i < 5; i++) {
   // 用立即执行函数包裹定时器只为了形成单独的作用域 
  (function(e) {
    setTimeout(() => console.log(e), i * 1000);
  })(i);
}

for (var i = 0; i < 5; i++) {
    // setTimeout里面的匿名函数和里面函数形成闭包,只为了形成单独的作用域
    // 只所以return函数,只是为了setTimeout第一个参数必须为函数
  setTimeout(function(e) {
     return function() {
       console.log(e);
    }
  }(i), i*1000)
}

for (var i = 0; i < 5; i++) {
    // 注意,setTimeout第一个函数要用到后面的参数,所以必须要把这个参数传进来
  setTimeout((i) => console.log(i), i * 1000, i);
}

或者使用let

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值