nicoleJT914 / blog

一只游行的火烈鸟/用issues记博客
0 stars 0 forks source link

发誓这一次一定要彻底搞懂“闭包” #5

Open nicoleJT914 opened 7 years ago

nicoleJT914 commented 7 years ago

这篇基本上是U don't know JS等对闭包的一些总结

定义

先抛出闭包(closure)的定义: 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

我们用一个列子来解释对闭包的定义:

function foo() { 
    var a = 2; 
    function bar() { 
        console.log( a );
    }
    return bar; 
}
var baz = foo();
baz(); // 2 

可以看出,bar()的作用域为foo(),通过baz=foo()将函数bar()传递到了全局作用域,最终bar()在全局作用域下执行,并成功访问到了其所在作用域foo()中的局部变量a。

这是由于在 foo() 执行后,bar() 依然持有对该作用域的引用,这个引用就叫做闭包。而闭包使得函数可以继续访问定义时的词法作用域。

因此像上述一样使用类似手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

被举烂的有关闭包的列子

1.回调函数 本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

function wait(message) {
         setTimeout( function timer() {
             console.log( message );
}, 1000 ); }
wait( "Hello, closure!" );

2.IIFE

var a = 2;
(function IIFE() { 
    console.log( a );
})();

以上IIFE严格来讲它并不是闭包。 因为函数并不是在它本身的词法作用域以外执行的。它在定义时所在的作用域中执行(而外部作用域,也就是全局作用域也持有a)。a 是通过普通的词法作用域查找而非闭包被发现的。

3.循环

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

众所周知,以上代码会以每秒一次的频率输出五次 6。

现在来加上闭包:

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

在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

再进行一些优化:

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

或者使setTimeout()指向一个新的闭包:

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

使用ES6的语法let也可以在迭代过程中创建新的作用域:

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

当然for循环和let组合才是精髓(for 循环头部的let声明还会有一 个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。):

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

闭包的实际应用:模块

function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() { 
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( " ! " ) );
    }
    return {
        doSomething: doSomething, 
        doAnother: doAnother
    }; 
}
var foo = CoolModule(); 
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

doSomething() 和 doAnother() 函数具有涵盖模块实例内部作用域的闭包,并通过调用 CoolModule() 实现。

模块模式需要具备两个必要条件:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
nicoleJT914 commented 7 years ago

当函数可以记住并访问所在的作用域时,即使函数是在当前作用域之外执行,就产生了闭包。

举例:

function foo() {
  var a = 2
  function bar() {
    console.log(a)
  }
  return bar
}
var baz = foo()
baz()

因为函数是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。

在 foo() 执行后,bar() 依然持有对该作用域的引用,这个引用就叫做闭包。

如果将函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。

闭包的作用:

1.访问私有变量

闭包通常用来创建内部变量,些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。

function counter() {
  var count = 3
  return {
    get: function() {
      return count
    }
  }
}
var foo = counter()
console.log(count)
console.log(foo.get())

count在counter作用域中,无法在其外部作用域中访问到,唯一途径就是通过闭包使get函数保持对counter作用域的引用

2.柯里化

将代码封装成闭包形式,在内存中维持变量,等待时机成熟的时候再使用,比如实现柯里化和反柯里化

function sum(x) {
  return function(y) {
    return x+y
  }
}
var foo = sum(1)
console.log(foo(2))
nicoleJT914 commented 7 years ago

所以出现闭包基本就是这两种情况:

nicoleJT914 commented 7 years ago

实现函数 makeClosures,调用之后满足如下条件: 1、返回一个函数数组 result,长度与 arr 相同 2、运行 result 中第 i 个函数,即 result[i](),结果与 fn(arr[i]) 相同