闭包

理解作用域链是理解闭包的基础,而闭包在 JavaScript 中几乎无处不在,同时作用域和作用域链还是所有编程语言的基础。

function bar() {
  console.log(myName)
}
function foo() {
  var myName = '13pro'
  bar()
}
var myName = '13press'
foo()
function bar() {
  // 加餐:this 指向?
  console.log(this.myName)
}
function foo() {
  var myName = '13pro'
  bar()
}
var myName = '13press'
foo()

很遗憾,打印的都是全局作用域中的变量13press。也就是说函数执行时的变量查找并非是按照调用栈顺序,实际上是通过另一种机制——作用域链 👇。

作用域链

每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。当一段代码使用了一个变量时,若是在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。我们把这个查找的链条就称为作用域链

作用域链查找

其中 outer 的指向是由词法作用域决定的。

词法作用域

我们常说的作用域就是指词法作用域。

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。

也就是说词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。

Closure

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

通俗的讲就是内部函数引用外部函数的变量,就形成了闭包。

闭包的关键

function foo() {
  var myName = 'zhang13pro'
  let test1 = 1
  const test2 = 2
  var innerBar = {
    setName: function (newName) {
      myName = newName
    },
    getName: function () {
      console.log(test1)
      return myName
    },
  }
  return innerBar
}
var bar = foo()
bar.setName('Lex')
bar.getName()
console.log(bar.getName())















 



当代码执行到 16 行时,闭包就产生了。堆内存空间保存着“closure(foo)”对象,被内部的 getName 和 setName 方法引用。虽然 foo 执行上下文销毁了,foo 函数中的对 closure(foo) 的引用也断开了,但是 setName 和 getName 里面又重新建立起了对 closure(foo) 的引用。查看 V8 的堆内存快照,setName 对象下面包含了 raw_outer_scope_info_or_feedback_metadata,对闭包的引用数据就在这里面。

简言之,产生闭包的核心有两步:

  1. 第一步是需要预扫描内部函数;
  2. 第二步是把内部函数引用的外部变量保存到堆内存中。

闭包的作用

  • 在函数外部访问函数内部变量,实现变量私有化
  • 函数执行完后仍保存变量对象,不被回收。被引用的变量不会被GC.

闭包的应用

从应用中感受闭包:私有化数据,在此基础上保存数据。

  • 防抖,延迟指定时间后执行。类比电梯门关闭。

Hello Debounce

function debounce(func, delay = 100) {
  let timer
  return function () {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => void func.apply(this, arguments), delay)
  }
}

ele.addEventListener(
  'input',
  debounce(function (e) {
    console.log(e + '防抖')
  }, 1000)
)
  • 节流,固定时间内执行一次。类比 LOL 技能 CD。

Hello Throttle

function throttle(func, wait = 100) {
  let flag = true
  return function () {
    if (!flag) return
    flag = false
    setTimeout(() => {
      func.apply(this, arguments)
      flag = true
    }, wait)
  }
}

ele.addEventListener(
  'input',
  throttle(function (e) {
    console.log(e + '节流')
  }, 1000)
)

如果不使用闭包,再次触发事件时将取不到上次的 timer 或 flag。

小结

var bar = {
  myName: 'https://13press.vercel.app/',
  printName: function () {
    console.log(myName)
  },
}
function foo() {
  let myName = '13pro'
  return bar.printName
}
let myName = '13press'
let _printName = foo()
_printName()
bar.printName()

代码执行输出什么?产生闭包了吗?

answer

13press

13press

没有产生闭包

你可能会好奇为什么调用 bar 对象的 printName 方法获取的不是 bar 的内部属性myName?特别是对于有其他编程语言经验的 Coder 来说,这无法理解。在对象内部的方法中使用对象内部的属性是一个非常普遍的需求,但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套机制——this。