zoukankan      html  css  js  c++  java
  • 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")

    参考文献
  • 相关阅读:
    Chrome开发者工具中Elements(元素)断点的用途
    最简单的SAP云平台开发教程
    Java实现 LeetCode 495 提莫攻击
    Java实现 LeetCode 494 目标和
    Java实现 LeetCode 494 目标和
    Java实现 LeetCode 494 目标和
    Java实现 LeetCode 493 翻转对
    Java实现 LeetCode 493 翻转对
    Java实现 LeetCode 493 翻转对
    Java实现 LeetCode 492 构造矩形
  • 原文地址:https://www.cnblogs.com/fengzheqi/p/5132134.html
Copyright © 2011-2022 走看看