闭包
闭包是指有权访问另一个函数作用域中的变量的函数。通常是在一个函数的内部创建另一个函数,那个函数则被称为闭包。这个概念很难弄懂,我到现在也是一知半解,但是有几点是肯定的:想要理解什么是闭包我们就要知道创建函数开始到函数被执行,这一过程中到底发生了什么?这之中又涉及到词法作用域。
词法作用域
词法作用域是一个比较好理解的概念。它是由编译器在编译阶段就可以确定的事情。全局有个全局作用域,每定义一个函数,编译过程中就创建一个函数作用域。 而我们熟知的作用域链就是一层套一层的作用域构成的。这里我们需要知道一点:作用域(链)的作用是在运行阶段为引擎找到存在的变量(等,以上是我自己的理解)
function bar (name) {
var age = 23
return function () {
alert(name, age)
}
}
bar('Nicholas') // Nicholas 23
复制代码
以上代码,bar 函数中有一个 匿名函数 这个匿名函数可以访问到 bar 函数中的变量。其实更明显的做法是下面这样:
function bar ...
var fn = bar('Nicholas')
fn() // Nicholas 23
复制代码
上面这段代码,我们取得 bar 函数中匿名函数的引用后,垃圾处理机制并未销毁 bar 函数的活动对象,而让匿名函数获取到了 bar 函数的作用域,这就是闭包的用处
值得注意的是,闭包无处不在,同时闭包与词法作用域息息相关,而次发作用域是在为引擎执行阶段 获取变量引用,要理解这个意思需要理解下面这段经典的一批的代码:
function createFn () {
var fns = []
for (var i = 0; i < 10; i++) {
fns.push(function () {
console.log(i)
})
}
return fns
}
var fns = createFn()
fns.forEach(function (fn) {
fn()
}) // 10 个 10 啦
复制代码
答案也没啥稀奇,但是这中间到底发生了什么?涉及到作用域的概念:作用域其实包含了执行环境的变量对象,而函数的作用域的变量对象就是函数的活动对象,而活动对象就是包含了函数的参数、定义的变量等。 而我们又知道 js是不存在块级作用域 的,因此在 createFn 函数中 for 循环中定义的 i 变量,其实保存在 createFn 的函数作用域的变量对象中,因此当定义完了10个函数之后,i 也变成了 10,再调用 函数的时候,会访问 i,而匿名函数中没有 i,就向上搜索到 createFn 的作用域,这里有 i,输出 i 就输出了 10。
作用域链,就是一堆作用域连起来啦,没啥好稀奇的。在下一节之前我要强调的是:词法作用域是词法作用域,this 是 this,不要试图在理解 this 的时候,带入词法作用域的知识
以上代码如何输出 0 ~ 9?我们再创建一个闭包,缓存 i 的值然后让闭包访问到就可以了。理解了这个,闭包就差不多理解了。
function createFn () {
var fns = []
for (var i = 0; i < 10; i++) {
fns.push(function (i) {
return function () {
console.log(i)
}
}(i))
}
return fns
}
var fns = createFn()
fns.forEach(function (fn) {
fn()
})
复制代码
闭包有什么用?
闭包可以用来做什么?其实可以做很多事情。从事情的最开始来讲吧。
- 如果所有的变量都在全局定义,会怎么样?会没办法定义同名变量,会让代码变得杂乱无章,会不知道代码在做什么。于是有了函数,我们使用函数包装起一段代码,这样定义在这段代码里的变量,根据词法作用域的规则,全局是没办法访问到函数里的变量的。
- 既然定义了一个函数,为什么会有闭包出现?其实闭包完全可以定义成另一个函数扔在全局中,但是很有可能一个函数会改变另一个函数之中值(通过调用函数的方式,而非直接改变,因为词法作用域的关系,一个函数如果不是闭包绝对没有办法访问到另一个函数的作用域的), 这就很危险了,我们不愿意让一些我们不知道的方式引用了一个函数,而改变另一个函数,于是我们把一个函数从全局定义扔到了另一个函数中去,这样既减少了全局作用域中的变量个数,又更加安全。这就是闭包的作用。
- 闭包的好处:可以访问另一个函数的作用域!这看来好像没什么特别的,但是这恰恰就是闭包最厉害的地方:如果一个函数需要依赖一些其他函数怎么办?我们完全可以将所有的依赖函数统一管理。于是我们可以通过闭包写一个自己的模块依赖机制!
function ModuleManager () {
// 存放定义的模块
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
}
}
var myModules = ModuleManager()
myModules.define('bar', [], function () {
return {
hello: function (who) {
return 'Hello, I am ' + who
}
}
})
myModules.define('foo', ['bar'], function (bar) {
var name = 'Nicholas'
return {
awesome: function () {
console.log(bar.hello(name).toUpperCase())
}
}
})
var bar = myModules.get('bar')
var foo = myModules.get('foo')
console.log(bar.hello('Bob'))
foo.awesome()
复制代码
为什么以上代码没有使用原型对象,类的方式去写,就是为了展示闭包的魔力:modules 能被 define 函数和 get 函数时刻访问到,于是我们就得到了一个模块管理的入口,这就是闭包的一个作用,我们可以自己写出很多关于闭包的东西,我们要拥抱闭包,而不是惧怕它原理它。
持续跟新在github