Javascript中的闭包Closure


对JS中的闭包还没有了解过的可看 MDN 闭包, 在面试的时候闭包也是经常被提及的Topic

一、闭包的概念

简单的说,闭包就是有权访问其他函数作用域内部变量的函数

闭包 = 函数 + 函数能够访问的自由变量

闭包的三个特点:

  • 函数嵌套函数
  • 内部的函数可以引用外部函数的参数或变量
  • 参数和变量不会被垃圾回收机制回收,因为内部还在引用

引用其他博主对于闭包的解释,很精辟:

通俗地讲就是别人家有某个东西,你想拿到但是因为权限不够(不打死你才怪),但是你可以跟家里的孩子套近乎,通过他拿到!
这个家就是局部作用域,外部无法访问内部变量,孩子是返回对象,对家里的东西有访问权限,借助返回对象间接访问内部变量
子函数在外被调用,子函数所在的父函数的作用域不会被释放

闭包的作用域链包含着它自己的作用域,以及父函数的作用域和全局作用域。

function func(){
  var a = 1, b = 2
  function sum(){
    return a + b
  }
  return sum
}

二、闭包的优势

我们为什么要使用闭包呢?因为闭包有如下好处:

  • 变量可以长期驻扎在内存之中,实现结果的缓存
    (闭包不会释放外部的引用,所以内部的值得以保留,比如可以利用这个实现累加器)
  • 避免全局变量的污染,有私有成员变量
    (全局变量:可重用,但是易被污染;私有变量:不可重用,仅函数内使用,不会被污染,而使用闭包既可以实现重用变量,还不会造成其被污染)
  • 匿名执行函数
    (有些函数只需要执行一次,内部变量无需维护,可以使用闭包)

三、闭包的特性

1. 闭包的应用:

  • 模块化的代码定义,模仿块级作用域
  • 在循环中找到索引
  • 设计私有的方法和变量,实现封装
  • 保存外部函数的变量

2. 使用闭包的步骤:

  1. 用外层函数包裹住要保护的变量和内层函数
  2. 外层函数将内层函数返回到外部
  3. 掉用外层函数,获取内层函数的对象,保存在外部的变量中——形成了闭包
function module(){
  var arr = []
  function add(val){
    if(typeof(val) === 'Number'){
      arr.push(val)
    }
  }
  function get(index){
    if(index < arr.length){
      return arr[index]
    }else{
      return null
    }
  }
  return{
    add: add,
    get: get
  }
}
var m = module()
m.add(1)
m.add(2)
m.add('xxx')
console.log(m.get(2))

简单来说,创建闭包就是在一个函数内部创建另一个函数

3. 闭包的变量特点

在javascript中,如果一个对象不再被引用,那么这个对象就会被垃圾回收机制回收;
如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。

闭包只会取得包含函数中任何变量的最后一个值,这是因为闭包所保存的是整个变量对象,而不是某个特殊的变量

function test(){
  var arr = []
  for(var i = 0; i < 10; i++){
    arr[i] = function(){
      return i
    }
  }
  for(var j = 0; j < 10; j++){
    console.log(arr[j]()) // 输出的是变量最后一个值
  }
}
test() // 连续打印10个10

若是想输出0,1,2 … 9,可将上面的 var i 改为 let i

4. 闭包中的this对象

匿名函数的执行环境具有全局性,因此其this对象通常指向window

var name = "The window"
var obj = {
  name: "My Object"
  
  getName: function(){
    return function(){
      return this.name
    }
  }
}
console.log(obj.getName()) // The window

Tips: 函数与函数的功能(函数值)是分割开的,并不是函数在哪里,内部的this就指向哪里

5. call & apply
callapply都可以改变this的指向,但是调用方式有所不同,call传递参数,apply以数组的形式传递与arguments是一对好基友,建议使用

四、闭包的缺点

闭包在带来优势的同时,也毫不例外的会有一些问题

1. 常驻内存,更多的内存使用量
Solution: 在闭包不使用时,要及时释放,将引用内层函数对象的变量赋值为null

// 用外层函数包裹要保护的变量和内存函数
function outer() {
  var i = 1
  // 外层函数返回内层函数到外部
  return function(){
    console.log(i++)
  }
  // 调用外层函数获得内层函数对象
  var getNum = outer()
  getNum() // 1
  getNum() // 2
  i = 1
  getNum() // 3
  getNum() // 4
}
//

通常,函数的作用域及其所有的变量都会在函数执行结束之后被销毁。但是,在创建了一个闭包只有,这个函数的作用域就会一直保存到闭包不存在为止。

释放闭包的引用:

function makeAdder(x){
  return function(y){
    return x+y
  }
}
var addFive = makeAdder(5)
var addTen = makeAdder(10)

console.log(addFive(2)) // 7
console.log(addTen(2)) // 12

//释放对闭包的引用
addFive = null
addTen = null
//addFive 和 addTen 都是闭包。它们共享相同的函数定义,但是保存了不同的环境。

2. 在IE下使用不当会引发内存泄露
当一个变量为DOM节点获取或宿主对象,他的一个属性引用了一个内部函数,而内部函数又在引用外部变量,会造成互相引用,从而引起内存泄漏。

闭包会引用包含函数的整个变量对象,如果闭包的作用域链中保存着一个HTML元素,那么就以为着该元素无法被销毁,所以我们有必要在对这个元素操作完之后进行主动销毁

function assignHandler(){
  var element = document.getElementById('someElement')
  var id = element.id
  element.onclick = function(){
    alert(id)
  }
  element = null
}

总之,如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

附:关于闭包的常见的面试题 参考

function func(n, o){
  console.log(o)
  return {
    func: function(m){
      return func(m,n)
    }
  }
}
var a = func(0) // ?
a.func(1) // ?
a.func(2) // ?
a.func(3) // ?

var b = func(0).func(1).func(2).func(3) // ?

var c = func(0).func(1) // ?
c.func(2) // ?
c.func(3) // ?

结果:

undefined
0
0
0
undefined,0,1,2
undefined,0
1
1

Author: Casey Lu
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Casey Lu !
评论
 Previous
探究Vuex的使用和原理 探究Vuex的使用和原理
在使用Vue框架的时候,经常会面临着组件间传值的问题,这时候就需要使用Vuex作为其状态管理器。 简单回顾一下,传统的Vue组件常用的通信的方式有: 父子通信: 父→子传值(props),子→父传值(events($emit));父调用子
2020-03-11
Next 
JavaScript笔试刷题小结(二) JavaScript笔试刷题小结(二)
继续整理最近的刷题记录 1. 剑指Offer 39. 数组中出现次数过半的数字 书中提到了两种复杂度均为O(n)的解法: 1)基于Partition函数: 如果这个数字存在,那它必然是数组排序后的中位数,基于随机快排中的Partition的
2020-03-10
  TOC