你不知道的JavaScript---闭包

目录

闭包的定义

闭包的理解

闭包的运用

回调函数中的闭包

for循环与闭包

重返块作用域

 模块

  未来的模块机制

es6模块机制

闭包的缺点


闭包的定义

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

闭包是什么?闭包是一个函数对自己所在的词法作用域的引用
闭包的作用? 闭包可以使得函数可以继续访问自己被定义时的词法作用域

  • 1. 通过return返回内部函数,然后在作用域外引用
  • 2. 将内部函数当作实参传递,然后在作用域外引用
  • 3. 将内部函数赋值到另外一个变量当中,然后在作用域外引用

闭包的理解

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

bar()依然持有对该作用域的引用,而这个引用就叫做闭包。 

上述代码中,函数 bar() 的词法作用域能够访问 foo() 的内部作用域,因为函数bar引用了函数foo的变量a。然后我们将 bar() 函数本身当作 一个值类型进行传递,当做函数foo()的返回值。

在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),执行函数baz()实际上是调用了foo()内部的函数 bar(),这就是定义中的“函数是在当前词法作用域之外执行”,即函数bar()在foo()内部定义,在foo()外部调用。

一般情况下,当一个函数执行完之后,函数内部的作用域会被销毁,js引擎的垃圾回收器会释放函数内的不再使用的内存空间。然而上述代码中,当函数foo()执行完之后,其内部的作用域未被销毁,因为函数bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。闭包使得函数可以继续访问定义时的词法作用域。


闭包的运用

回调函数中的闭包

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

上述代码中,将一个内部函数(名为 timer)传递给 setTimeout(…)。此时timer 具有涵盖 wait(…) 作用域的闭包,因此还保有对wait()的变量 message 的引用。在wait(…) 执行 1000 毫秒后,它的内部作用域并不会被销毁,因为timer 函数依然保有 wait(…) 作用域的闭包。

在定时器、事件监听器、 Ajax 请求等任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

for循环与闭包

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

正常情况下,我们对上述代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。
首先解释 6 是从哪里来的。这个循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是 6。因此,输出显示的是循环结束时 i 的最终值。

我们明明执行了五次函数,并且每次都是打印i,按理说i每次都是不同的,但最终结果是每次打印的i都是6,这是为什么呢?一个合理的解释是虽然这五个函数是在各个迭代中分别定义的, 但是它们其实都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。

这段可以用事件循环去解释,i的赋值是同步事件,在执行栈中不断执行直到i=6,执行栈空并开始传递i=6给消息队列中的5个异步函数setTimeout(),每次i赋值都会在消息队列中添加一个setTimeout()

重返块作用域

每次迭代我们都需要一个块作用域。let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

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

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

--------------------------

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

 模块

如果要更简单的描述,模块模式需要具备两个必要条件。

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

命名将作为公共api的返回对象,通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。        

 var foo = (function CoolModule(id) {
        function change() {
            // 修改公共API
            publicAPI.identify = identify2;
        }

        function identify1() {
            console.log(id);
        }

        function identify2() {
            console.log(id.toUpperCase());
        }

        var publicAPI = {
            change: change,
            identify: identify1
        };

        return publicAPI;
    })("foo module");

    foo.identify(); // foo module
    foo.change();
    foo.identify(); // FOO MODULE

  未来的模块机制

这里所说的模块概念,我觉得核心是其对数据的封装特性,而要达到这种效果就只能利用闭包原理来实现数据私有化的效果,所以,要实现一个模块需要分2步,一是创建一个函数并保证执行一次,二是返回一个具有函数特性的对象。本质上就是创建闭包的过程

 // 母模块
    var MyModules = (function Manager() {
        var modules = {};
        // 可以用户定义自己的模块
        // (name:要定义的模块名字,deps:模块所要的依赖,impl:接口实现过程)
        function define(name, deps, impl) {
            // deps他告诉MyModules所要依赖的子模块
            // 遍历deps并且把模块转为字符串【bar】---【MyModules.bar】
            for (var i = 0; i < deps.length; i++) {
                deps[i] = modules[deps[i]];
            }
            //【MyModules.bar】注入到第三个参数impl:接口实现 function(bar){}
            // this仍然指向自身,并把该函数赋值给modules
            modules[name] = impl.apply(impl, deps);
        }
        // 获取已有模块
        function get(name) {
            return modules[name];
        }
        return {
            define: define,
            get: get
        };
    })();

    MyModules.define("bar", [], function () {
        function hello(who) {
            return "Let me introduce: " + who;
        }

        return {
            hello: hello
        };
    });
    // 我们需要一个参数告诉MyModules帮我们把bar注入到foo
    MyModules.define("foo", ["bar"], function (bar) {
        var hungry = "hippo";
        // 需要调用bar方法的hello,需要把模块bar传入
        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

MyModules
|_ _  bar
 |         |_ _ hello(who)
 | _ _  foo
          |_ _awesome()


以上代码运行后如结构图所示:MyModules是个母模块,下面有两个子模块,分别实现了自己的方法。

  • 1. 然后解释一下母模块,MyModules提供了两个方法,define可以给用户定义自己的模块,get获取已有的模块。重点解释一下define。define有三个参数:name :要定义模块的名字,deps:模块所要依赖的,impl「接口」:实现的过程;
  • 2. 然后直接看foo子模块的定义: MyModules.define("foo", ["bar"], function(bar) { … },这里比较模糊的是第二个参数 ["bar”] 有什么作用。在foo 的实现中,需要调用bar的方法hello,因此需要把模块bar传入,但是,bar是在哪里实现的?在MyModules实现的,我们需要提供一个参数来告诉MyModules帮我们把bar注入到foo中,这就是define函数第二个参数的用法。
  • 3. 回到define的实现代码中:for (var i=0; i&lt;deps.length; i++) { deps[i] = modules[deps[i]]; }, 还记得第二个参数的作用吗?['bar'],它是告知MyModules我所要依赖的子模块,但这个参数是个数组,并且里面的元素'bar'是字符串,所以这段代码的功能是:遍历deps,并把字符串换成模块,也就是['bar'] 变成了[MyModules.bar]
  • 4. 回到最重要的一段代码:modules[name] = impl.apply(impl,deps),这个的用法是把[MyModules.bar] 注入到 第三个参数impl : function(bar) { … } 的实现中,然后this仍然指向自身,并把该函数赋值给modues。

总结:这种实现既保证了各个子模块的封闭性,又不缺乏可扩展性,很值得学习

es6模块机制

ES6中为模块增加了一级语法支持。在通过模块系统进行加载时,ES6会将文件当作独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员

bar.js

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

 export hello;

foo.js

// 仅从"bar"模块导入hello()
import hello from "bar";

var hungry = "hippo";

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

 export awesome;

 baz.js

   // 导入完整的"foo"和"bar"模块

    module foo from "foo";
    module bar from "bar";

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

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

import和export编译成es5,其实就是一个个的自执行函数生成的单例模块 

闭包的缺点

使用闭包可以帮我们解决很多问题,但闭包的缺点是导致内存泄露,因为闭包会一直保持着对某些本该释放的作用域的引用,因而作用域内的变量所占用的内存不会被释放。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值