JavaScript 闭包

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。通常是在一个函数的内部创建另一个函数,那个函数则被称为闭包。这个概念很难弄懂,我到现在也是一知半解,但是有几点是肯定的:想要理解什么是闭包我们就要知道创建函数开始到函数被执行,这一过程中到底发生了什么?这之中又涉及到词法作用域。

词法作用域

词法作用域是一个比较好理解的概念。它是由编译器在编译阶段就可以确定的事情。全局有个全局作用域,每定义一个函数,编译过程中就创建一个函数作用域。 而我们熟知的作用域链就是一层套一层的作用域构成的。这里我们需要知道一点:作用域(链)的作用是在运行阶段为引擎找到存在的变量(等,以上是我自己的理解)

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()
})
复制代码

闭包有什么用?

闭包可以用来做什么?其实可以做很多事情。从事情的最开始来讲吧。

  1. 如果所有的变量都在全局定义,会怎么样?会没办法定义同名变量,会让代码变得杂乱无章,会不知道代码在做什么。于是有了函数,我们使用函数包装起一段代码,这样定义在这段代码里的变量,根据词法作用域的规则,全局是没办法访问到函数里的变量的。
  2. 既然定义了一个函数,为什么会有闭包出现?其实闭包完全可以定义成另一个函数扔在全局中,但是很有可能一个函数会改变另一个函数之中值(通过调用函数的方式,而非直接改变,因为词法作用域的关系,一个函数如果不是闭包绝对没有办法访问到另一个函数的作用域的), 这就很危险了,我们不愿意让一些我们不知道的方式引用了一个函数,而改变另一个函数,于是我们把一个函数从全局定义扔到了另一个函数中去,这样既减少了全局作用域中的变量个数,又更加安全。这就是闭包的作用。
  3. 闭包的好处:可以访问另一个函数的作用域!这看来好像没什么特别的,但是这恰恰就是闭包最厉害的地方:如果一个函数需要依赖一些其他函数怎么办?我们完全可以将所有的依赖函数统一管理。于是我们可以通过闭包写一个自己的模块依赖机制!
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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值