JavaScript基础–闭包

JavaScript基础–闭包

理解闭包的概念对于学习JavaScript至关重要,很多新手(包括我)开始学习闭包时,都会感觉似懂非懂,之前看了一些资料,整理了闭包的一篇博客,若有疏忽与错误,希望大家多多给意见。

概述

理解闭包的概念前,建议大家先回想一下JS作用域的相关知识,如果有疑问的同学,可以参考:JavaScript基础–作用域。闭包的定义如下:

Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.

意译出来就是:当函数在其词法作用域外执行时,依然可以访问其词法作用域里的变量。这里的“词法作用域”,就是我们通常理解的作用域。

我们先来看个例子 
eg1

function foo() {
    var a = 2;

    function bar() {
        console.log( a ); 
    }

    bar();
}

foo(); //--> 2

上述例子中,在调用函数bar()时,变量a的值是取自函数foo的作用域,也就是函数bar()的上层作用域,从闭包的概念来说,这个例子基本属于一个闭包。为什么说是“基本”,因为实际上a也是属于函数bar()的作用域链上的变量,我们更多称之为嵌套作用域。我们再看一个例子: 
eg2

function foo() {
    var a = 2;

    function bar() {
        console.log( a );
    }

    return bar;
}

var test = foo();

test(); // --> 2 

 

eg2的例子也许更能体现闭包的概念:我们在定义函数foo()时,返回的是一个函数;var test = foo();将函数的引用赋值给test,然后在执行test();语句时,我们发现a的值依然能够取到,我们称bar()为一个闭包。

闭包的原理:编译器在执行var test = foo();时,会标识其为一个闭包,垃圾回收器在回收内存时,就会保留闭包的作用域链。所以运行test()时,就可以访问到闭包所定义的词法作用域了。

函数setTimeout()

其实我们自己在写JS代码时,经常用到闭包,只是我们没有意识到,比如setTimeout(): 
eg3

function wait(message) {
    setTimeout( function timer(){
        console.log( message );
    }, 1000 );
}

wait( "Hello, closure!" );

相信同学们或多或少的用到过setTimeout(),在eg3中我们细心注意,就可以发现我们定义在wait()中的匿名函数是延迟运行的,但它依然可以访问到变量message。对照闭包的概念,是不是就明白了。同样我们在定义很多异步的函数时,都用到了闭包。是不是发现闭包其实我们时时刻刻都在用。

循环中的闭包

大家还是先来看一个例子 
eg4

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

大家觉得eg4中会输入什么?是1,2,3,4,5吗?如果你把代码赋值到浏览器console面板中,也许会让你失望,代码输出结果为6,6,6,6,6;很多同学觉得每一个i不是单独运行的吗?输出怎么都是6。

分析这个例子前,我们脑子中要有一个概念:JS应用的是函数作用域,而不是块级作用域。反映到eg4中,就是循环中利用的i是公有的。所以在执行timer()时,i已经变为了6。

如果应用JS的IIFE(立即执行函数),输出结果还是5个6吗?比如: 
eg5

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

我们可以测试一下,结果还是6,6,6,6,6。或者有人认为如果把延迟时间缩小的足够短,结果是不是就可以正常了?实际结果也许会让你失望。我们就算把延迟时间设置为0,结果还是一样的,这是因为for执行效率天生就比setTimeout()高,setTimeout()再怎么缩短延迟时间,也赶不上for

为了达到我们的期望的结果,解决的办法就是将每次循环的i引入到time()的作用域中,如: 
eg6

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

或者是: 
eg7

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

扩展:在ES6中引入了let关键字,而引入的目的就是为了在JS中实现块级作用域,所以eg5中代码还可以修改为: 
eg8

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

或者 
eg9

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

闭包的应用–模块(module)

模块是应用闭包的典型例子,我们先来看一个例子: 
eg10

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

在eg10中CoolModule是一个函数,返回值为一个对象;那么foo在调用doSomethingdoAnother时,就产生了闭包。这是module中最简单的利用闭包的例子,接下来我们来看一个怎么解决module依赖的例子。 
eg11 
定义依赖模块的实现

var MyModules = (function Manager() {
    var modules = {};

    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply( impl, deps );
    }

    function get(name) {
        return modules[name];
    }

    return {
        define: define,
        get: get
    };
})();
 

我们分析一下以上的代码。首先定义了一个空对象module,其次定义了函数define,其中三个参数:name,定义模块的名称;deps,定义模块的依赖项;impl,定义模块的实现方法。

 
MyModules.define( "foo", ["bar"], function(bar){
    var hungry = "hippo";

    function awesome() {
        console.log( bar.hello( hungry ).toUpperCase() );
    }

    return {
        awesome: awesome
    };
} );

var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );

console.log(
    bar.hello( "hippo" )
); // Let me introduce: hippo

foo.awesome(); // LET ME INTRODUCE: HIPPO

当然ES6中也引入了module,使得调用更加方便,直接看例子吧 
eg12 
bar.js

function hello(who) {
    return "Let me introduce: " + who;
}

export hello;

foo.js

// import only `hello()` from the "bar" module
import hello from "bar";

var hungry = "hippo";

function awesome() {
    console.log(
        hello( hungry ).toUpperCase()
    );
}

export awesome;

  

// import the entire "foo" and "bar" modules
module foo from "foo";
module bar from "bar";

console.log(
    bar.hello( "rhino" )
); // Let me introduce: rhino

foo.awesome(); // LET ME INTRODUCE: HIPPO

注意在eg12中调用module有两种方式,分别是importmodule,前者调用的是接口,而后者调用的是模块,用法也有些许不同,前者是直接接口本身hello(hungry),而后者则是调用模块中的方法bar.hello("rhino")

参考文献
原文地址:https://www.cnblogs.com/fengzheqi/p/5132134.html