【JavaScript】闭包

361 阅读5分钟

前置必备知识点

1. 调用栈

  • JS引擎追踪函数的一个机制,管理一份代码的执行关系

  • 调用栈不能设计太大,否则JS引擎在查找上下文时会花费大量时间,非常影响性能

  • 调用栈寻找变量先从词法环境寻找,再前往变量环境查找

相关文章点这里

2. 作用域链

  • 作用域链是一个变量查找机制,在当前作用域中查找变量,如果没有找到则向上一级作用域查找,直到全局作用域

  • 作用域链由多个词法作用域组成,每个词法作用域都有指向上一级词法作用域的指针outer

  • outer指向上一级词法作用域,形成一个链表结构,这就叫作用域链

  • outer的具体指向取决于该函数的定义位置,而不是调用位置。这一过程在编译过程就完成

作用域链详解


闭包

  1. 根据作用域查找规则,内部函数一定有权力访问外部函数的变量

  2. 另外,一个函数执行完毕后,对应执行环境会被销毁

为了满足以上两个规则:

  • 如果内部函数引用了外部函数的变量,而内部函数在外部函数外部被调用,那么外部函数的执行环境中被引用的资源就不会被销毁而会形成一个集合留在内存空间中,这种情况就叫做闭包(Closure)

优点

  1. 实现私有变量
  2. 防抖函数

缺点

  1. 长期持有外部变量引用组织GC回收会导致内存泄漏
  2. 太长的作用域链会导致调试难度加大

代码示例

function foo() {
  var myname = 'FWB'
  var age = 18
  function bar() {
    console.log(myname)
  }
  bar() // 'FWB'
}
foo() // 调用函数foo

正常输出,验证内部函数可以访问外部函数的变量

function foo() {
  var myname = 'FWB'
  var age = 18
}
foo() // 调用函数foo
console.log(myname) // ReferenceError: myname is not defined

出现报错,输出结果时找不到myname,代表foo函数执行上下文此时被销毁

function foo() {
  var myname = 'FWB'
  var age = 18

  return function bar() {
    console.log(myname) // 使用了外层函数的变量
  }

}
var baz = foo() // 获得返回函数bar
baz() // 调用返回函数
console.log(age) // ReferenceError: age is not defined

可以看到foo函数中返回了一个bar函数,并且bar函数使用了外部函数foo的变量myname。但此时我们运行发现此时仍能正常显示我们想要的结果,并且输出age时也按规则报错了。

为什么呢?按道理来说foo的执行上下文已经被销毁了,怎么还能读取到里面的myname值呢?

这个时候其实就已经形成闭包了,咱们用图理解一下:

全局以及foo函数编译运行过程

  • 此时foo已经给出返回值,对应函数上下文即将被销毁
  • 全局此时调用baz函数即bar函数

baz函数调用

  • 由于bar函数使用了自由变量myname
  • 根据规则,先从对应上下文词法环境再往变量环境寻找,找不到就去oute指向的上一级作用域寻找
  • 此时看到foo函数上下文已经被销毁。但由于闭包的存在,outer直接指向闭包,正常读取myname

为什么age就没有被正常输出呢?

因为age并没有被内层函数所使用,所以会随着执行上下文一起被销,不会放在闭包中


小结

  • 其实闭包就可以简单概括为内层函数加上使用的外层变量
  • outer在外层作用域销毁后指向对应闭包,达到访问需求变量的目的

检验自己 --经典面试题

根据学过的知识点,var变量提升导致以下代码的输出都为6,你有办法能使输出为1 2 3 4 5吗?

var arr = [] 
for (var i = 0; i <= 5; i++) { 
  arr.push(function () { 
    console.log(i) // 数组存放函数题
  }) 
} 

for (var j = 0; j < arr.length; j++) { 
  arr[j]() //调用函数
}

看题解以前一定要自己先思考哦!

解法一:varlet

  • 仅需将变量声明使用let关键字就可解决问题
    • 因为let不会存在变量提升,与for形成块级作用域,每个i取值互相独互不影响
var arr = [] 
for (let i = 0; i <= 5; i++) { 
  arr.push(function () { 
    console.log(i) // 数组存放函数题
  }) 
} 

for (var j = 0; j < arr.length; j++) { 
  arr[j]() //调用函数
}

解法二:闭包大法好

  • 前面提到,闭包可以使变量私有化,这正是我们想要的结果,所以可以使用闭包
    • 每次外层函数执行完毕后,都会留下一个闭包中存放需要的i值,达到取值互不相同的效果
var arr = [] // function(){}   function(){}  ...
for (var i = 1; i <= 5; i++) {
  // function foo(j) {
  //   arr.push(function() {  // 被拿到 foo 外面
  //     console.log(j);
  //   })
  // }
  // foo(i)

  (function(j) {
    arr.push(function() {  // 被拿到 foo 外面
      console.log(j);
    })
  })(i)
}


// run
for (var j = 0; j < arr.length; j++) {
  arr[j]()
}
  1. 第一段代码为正常闭包写法
  2. 第二段为立即执行函数写法,该方法可以不手动调用函数,效果相同代码相对简洁

拓展知识:如何在不使用函数return的情况下形成闭包

  1. 将内部函数赋值给数组 -- 详情见检验自己

  2. 将内部函数赋值给对象属性

const counter = {};

function createCounter() {
  let num = 0;
  counter.increment = () => ++num;
  counter.get = () => num;
}

createCounter();
counter.increment(); // 1
console.log(counter.get()); // 1
  1. 将内部函数挂载到windowglobal进行全局化