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.小结
- 闭包的产生: 当函数可以记住并访问所在的作用域,即使函数是在当前词法作用域之外执行,这是就产生了闭包
- 模块的两个主要特征:
- 为创建内部作用域而调用了一个包装函数
- 包装函数的返回值必须至少包含一个对内部函数的引用(这样就会创建涵盖整个包装函数内部作用域的闭包)