不得不知的闭包

闭包的概念

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

换言之,闭包是由函数以及声明该函数的词法环境组合而成的。词法环境包含了这个闭包创建时作用域内的任何局部变量

上下文与作用域

变量或函数的上下文决定了它们可以访问哪些数据(变量),以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。每个函数调用都有自己的上下文。上下文中的代码在执行的时候会创建变量对象(variable object)的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。

每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中搜索。换言之,内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文的任何东西。位于最顶端或最外层的上下文称为全局上下文(global context),全局上下文取决于执行环境,如 Node 中的global和 Browser 中的window

注意:函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则。

var color = "blue"; 
function changeColor() { 
   let anotherColor = "red"; 
   function swapColors() { 
     let tempColor = anotherColor; 
     anotherColor = color; 
     color = tempColor; 
     // 这里可以访问 color、anotherColor 和 tempColor 
   } 
   // 这里可以访问 color 和 anotherColor,但访问不到 tempColor 
   swapColors(); 
} 
// 这里只能访问 color 
changeColor();

以上代码涉及 3 个上下文:全局上下文、 changeColor() 的局部上下文和 swapColors() 的局部
上下文。全局上下文中有一个变量 color 和一个函数 changeColor()changeColor() 的局部上下文中有一个变量 anotherColor 和一个函数 swapColors() ,但在这里可以访问全局上下文中的变量 color
swapColors() 的局部上下文中有一个变量 tempColor ,只能在这个上下文中访问到。全局上下文和 changeColor() 的局部上下文都无法访问到 tempColor 。而在 swapColors() 中则可以访问另外两个上下文中的变量,因为它们都是父上下文。

在这里插入图片描述

再举个栗子

var a = 1
function out(){
    var a = 2
    inner()
}
function inner(){
    console.log(a)
}
out()  //====>  1

闭包的原因

当函数能够记住并访问所在的词法作用域时,就产生了闭包。

function func() {
    let name = "Closure";
    function alertName() {
        alert(name);
    }
    return alertName;
}
const myFunc = func();
myFunc();

在上面的例子中,myFunc 是执行 func 时创建的 alertName 函数实例的引用。 alertName 的实例维持了一个对它的词法环境(变量 name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 Closure 就被 alert

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(2));  // 7
console.log(add10(2)); // 12

在上面的示例中, makeAdder(x) 接受一个参数 x ,并返回一个新的函数。返回的函数接受一个参数 y,并返回 x+y 。创建两个新函数:一个将其参数和 5 求和,另一个和 10 求和。

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

闭包的作用

  • 保护函数的私有变量不受外部的干扰,把一些函数内的值保存下来。
  • 使用闭包来实现方法和属性的私有化。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
const Counter = (function() {
  let 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 */

在上面的示例中,每个闭包都有它自己的词法环境;而这次只创建了一个词法环境,为三个函数所共享 Counter.increment Counter.decrement Counter.value
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

常见错误:循环中使用闭包

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

上面的这段代码,预期是每隔一秒,分别输出 0, 1, 2, 3, 4, 但实际上依次输出的都是 5setTimeout 是个闭包,这五个闭包在循环中被创建,这里的 i 使用 var 进行声明,由于变量提升,所以具有全局作用域,共享同一个词法作用域,在这个作用域中存在一个变量i ,当函数执行完毕 i = 5 ,导致输出的结果都是 5。

解决办法一:新增匿名闭包

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

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

解决办法二:使用 let 关键字

for(let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, i * 1000);
}

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

解决办法三:使用 forEach

[0, 1, 2, 3, 4].forEach((i) => {
    setTimeout(() => {
        console.log(i)
    }, i * 1000)
})

本质上与解法一是一样的,分别产生五个闭包函数,入参分别在对应的上下文中。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值