不得不知的闭包

·  阅读 332
不得不知的闭包

闭包的概念

一个函数和对其周围状态**(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() 中则可以访问另外两个上下文中的变量,因为它们都是父上下文。

QQ截图20210401135825.png

再举个栗子

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)
})
复制代码

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

分类:
前端
标签: