《你不知道的JS(05)》---作用域闭包

1.闭包的基本概念

1.1 什么是闭包(闭包的形成)

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。(也可以说闭包是:函数可以访问不属于自身作用域范围内的变量 的现象)

1.2 闭包的应用场景

定时器,事件监听,ajax,跨窗口通信,Web Workers
【tips】在上述应用中,只要使用了回调函数,就是在使用闭包

回调函数: 被作为实参传入另一函数,并在该外部函数中被调用,用以来完成某些任务的函数

2.闭包与循环

2.1 循环中的顺序流
// 依次输出1-5
for(var i = 1;i <= 5;i++) {
    console.log(i)          
}

以上代码依次输出1-5,这是完全符合我们认知的,但将console语句写在延时函数的回调里,作为回调函数来处理时,情况就完全不一样了:

2.2 循环中的异步程序
// 每隔1s输出一次6
for(var i = 1;i <=5;i++) {
    setTimeout(function () {
        console.log(i)
    },i*1000)
}

关于延时函数等异步程序: 程序在运行过程中,遇到异步处理函数(特点:使用了回调函数;一般是系统默认比较耗时的函数),就会把这些函数放入任务栈,等待其余所有需要顺序的程序执行完毕之后,任务栈才会向系统发送请求,开始执行栈中的任务

上述循环执行五次,每次执行都会将当前的setTimeout延时函数放入任务栈,待“循环”这一相对“不太耗时”的顺序执行程序执行完毕之后,开始执行五个延时函数。但此时执行时,当前词法作用域下的i的值已经变成了6,所以执行延时函数时进行的RHS查询查询到的i值为6。
也就是说,我们以为的每个迭代在运行时都会给自己“捕获”一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的作用域中,(上述代码for循环所在的词法作用域),因此实际上只有一个i,其值为6

2.3 如何做到每隔一秒依次打印1-5

尝试一: 失败
用前文提及的IIFE立即执行函数将执行代码包裹起来,构建每一次循环中各自的作用域

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

思路无比正确,但是呢?不行!!程序执行完毕依旧每隔1s依次打印一次6。
原因:
上述代码使用IIFE创建了作用域,但是,作用域内部是空的! ,即它们各自作用域内部是没有自己的属性(也就是变量i)的。当执行延时函数,引擎执行RHS查找时,在延时函数内部没有找到其私有的变量i,只能向上(也就是又回到了for循环的词法作用域)查找,这时找到的变量i的值同上一步一样,也是6。
尝试二: 成功

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

在IIFE内部声明一个私有属性(即变量j)来接住每次循环时传递进来的变量i的值,当前延时函数执行时就可以访问到它自身此法作用域(上述IIFE花括号内部范围)下的变量了。
但是上述代码还不够简洁,可以再简化如下:
尝试三: 成功–>更简化

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

3.重返块作用域

仔细想想上文中我们对“如何做到每隔一秒依次打印1-5”这一问题的解决过程,其本质是在每一次循环时创建一个新的作用域,换句话说,每次迭代我们都需要一个块作用域。由此联想到第三章中讲到的let:

let关键字可以将变量绑定在任意作用域中,也就是说,let可以为声明的变量隐式地劫持当前所在的作用域。被let声明的变量不会存在变量的提升

故上述解决方案还可简化为:

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

=======> 意思就是:

for(var i = 1;i <= 5;i++) {
    let j = i       // 变量j只在此花括号内部存在并提供访问
    setTimeout(function () {
        console.log(j)
    },j*1000)
}

4.闭包的强大应用—模块

4.1 什么是模块模式
function CoolModule() {
    var something = "cool",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!

解读:
上述代码中的CoolModule从本质上来看只是一个普通函数,只是它的返回值是一个对象,对象内含有对内部函数(doSomething&doAnother)的引用。而闭包真正形成之处,在代码的最后三行——在外部调用该函数。
关于函数的返回值: 返回值是一个对象字面量形式的对象,我们通常保持内部数据的私有,而仅仅抛出内部函数。可以将这个对象类型的返回值看做本质上是模块的公共API

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

从函数调用返回的,只有数据属性而没有闭包函数的对象并不是模块!

4.3 单例模式的实现

上述4.1中模块的构建方式,在每一次调用时都会创建一个新的实例,但有的时候我们并不需要复用这一模块,此时,这样的构建方式就不太合适了。那么,如何改进成单例模式呢?
简单的来说,就是将模块的构建放入IIFE中并将其赋值给一个单例的模块实例标识符:

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

foo.doSomething()   // cool
foo.doAnother()     // 1!2!3!

模块本质上就是一个普通函数,所以它也可以接受并传递参数

4.4 命名将要作为公共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
4.5 现代的模块机制

大多数模块依赖加载器/管理器本质上都是将上述模块定义封装进一个友好的API,以下是其本质的核心代码实现:
定义模块的包装函数:

var MyModules = (function() {
    var modules = {}
    function define(name,deps,impl) {    // 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
    }
})()

这段代码的核心是modules[name] = impl.apply(impl,deps),为模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,也就是模块的API储存在一个根据名字来管理的模块列表中(上述modules对象)。

modules[name] = impl.apply(impl,deps)可以看做是将deps作为impl的参数传递到impl内部,即可以在impl内部调用到不属于它本身的方法(deps内值在modules对应对象处的引用)—闭包

使用包装函数来定义模块:

MyModules.define("bar",[],function () {
    function hello(who) {
        return "Let me introduce:" + who
    }
    return {
        hello: hello
    }
})
MyModules.define("foo",["bar"],function () {
    var hungry = "hippo"
    function awesome() {
        console.log(bar.hello(hungry).toUpperCase())
    }
    return {
        awesome: awesome
    }
})

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

console.log(
    bar.hello("hippo")  // Let me introduce:hippo
)
foo.awesome()           // LET ME INTRODUCE:HIPPO
4.6 “未来”的模块机制

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

上文中提及的基于函数的模块并不是一个能被静态识别的模式(编译器无法识别),他们的API语义只有在运行时才会被考虑进来,因此可以在运行时修改一个模块的API(参考前文4.4中关于publicAPI的讨论)
相比之下,ES6模块API是静态的(API不会在运行时改变),由于编译器知道这一点,因此可以在编译期检查对导入模块的API成员的引用是否真实存在。如果API引用不存在,编译器会在编译时就抛出“早期”错误,而不会等到运行时再动态解析(并且报错)。

此处尚未补充完整,待复习一下之前的笔记再补上

5.小结

  • 闭包的产生: 当函数可以记住并访问所在的作用域,即使函数是在当前词法作用域之外执行,这是就产生了闭包
  • 模块的两个主要特征:
    • 为创建内部作用域而调用了一个包装函数
    • 包装函数的返回值必须至少包含一个对内部函数的引用(这样就会创建涵盖整个包装函数内部作用域的闭包)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值