闭包


定义:

当一个函数被定义在其他函数中,一个内部函数处理可以访问自己的参数和变量,也可以访问把它嵌套在其中的父函数的参数和变量,通过函数字面量创建的函数对象包含一个链接到外部上下文的连接,这被称为闭包.

JavaScript语言精粹修订版


  • A function
  • 对该函数的外部作用域(词法环境)的引用

词法环境是每个执行上下文(堆栈框架)的一部分,是标识符之间的映射(即。局部变量名)和值。

JavaScript 中的每个函数都维护对其外部词法环境的引用。此引用用于配置在调用函数时创建的执行上下文。此引用使函数内部的代码能够“查看”函数外部声明的变量,而不管函数在何时何地被调用。

如果一个函数被一个函数调用,而这个函数又被另一个函数调用,那么就会创建一个对外部词法环境的引用链。

这个链称为作用域链。


作用域链和原型链

变量取值会到创建这个变量的函数的作用域中取值,如果找不到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链

区别
  • 作用域最顶层是windown,原型链最顶层是Object
  • 作用域链是相对变量而言,原型是相对属性而言

在下面的代码中,当 foo 被调用时创建的执行上下文的词法环境形成一个闭包,闭包变量 secret:

function foo() {
  const secret = Math.trunc(Math.random()*100)
  return function inner() {
    console.log(`The secret number is ${secret}.`)
  }
}
const f = foo() // `secret` is not directly accessible from outside `foo`
f() // The only way to retrieve `secret`, is to invoke `f`

换句话说:在JavaScript中,函数携带对私有“状态框”的引用,只有它们(以及在同一词汇环境中声明的任何其他函数)都有访问。该框的状态对函数的来电者是看不见的,为数据隐藏和封装提供出色的机制。

请记住:JavaScript中的函数可以像变量(firs class function)一样传递,这意味着这些功能和状态可以通过您的程序传递:类似于您如何通过C ++中的类的实例。

如果JavaScript没有封闭,那么在函数中必须明确地传递更多状态,使参数列出更长,代码嘈杂。

因此,如果您想要始终访问私有状态的功能,则可以使用closure闭包

…并且经常我们确实希望将状态与功能相关联。例如,在Java或C ++中,当您将私有实例变量和方法添加到类时,将状态与功能相关联。

在C和大多数其他常用语言中,在函数返回之后,所有本地变量都不再可访问,因为堆栈帧被销毁。在JavaScript中,如果在另一个函数中声明函数,则从IT返回后,外部功能的局部变量可以保持可访问。通过这种方式,在上面的代码中,秘密仍然可用于函数对象内部,之后从foo返回之后。

当您需要与函数关联的私有状态时,闭包非常有用。这是一个非常常见的场景——记住:JavaScript直到2015年才有class syntax,而且它仍然没有私有字段语法。闭包满足了这一需求。


私有实例变量

在下面的代码中,toString函数在Car函数内部形成闭包。

function Car(manufacturer, model, year, color) {
  return {
    toString() {
      return `${manufacturer} ${model} (${year}, ${color})`
    }
  }
}
const car = new Car('Aston Martin','V8 Vantage','2012','Quantum Silver')
console.log(car.toString())  //Aston Martin V8 Vantage (2012, Quantum Silver)

函数式编程:

function curry(fn) {
  const args = []
  return function inner(arg) {
    if(args.length === fn.length) return fn(...args)
    args.push(arg)
    return inner
  }
}

function add(a, b) {
  return a + b
}

const curriedAdd = curry(add)
console.log(curriedAdd(2)(3)()) // 5

事件导向编程

const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = 'rgba(200,200,242,1)'

function onClick() {
  $('body').style.background = BACKGROUND_COLOR
}

$('button').addEventListener('click', onClick)
<button>Set background color</button> //点击事件触发改变了背景色

模块化

在下面的例子中,所有的实现细节都隐藏在一个立即执行的函数表达式中。函数tick和toString闭包私有状态和它们需要完成工作的函数。闭包使我们能够模块化和封装我们的代码。

let namespace = {};

(function foo(n) {
  let numbers = []
  function format(n) {
    return Math.trunc(n)
  }
  function tick() {
    numbers.push(Math.random() * 100)
  }
  function toString() {
    return numbers.map(format)
  }
  n.counter = {
    tick,
    toString
  }
}(namespace))

const counter = namespace.counter
counter.tick()
counter.tick()
console.log(counter.toString()) 
//[98,9]

这个例子说明了局部变量不会被复制到闭包中:闭包维护了对原始变量本身的引用。这就好像堆栈帧即使在外部函数退出后也仍然活在内存中一样。

function foo() {
  let x = 42
  let inner  = function() { console.log(x) }
  x = x+1
  return inner
}
var f = foo()
f() // logs 43

在下面的代码中,三个方法分别在同一个词法环境中记录、增量和更新。

每次调用 createObject 时,都会创建一个新的执行上下文(堆栈框架) ,并创建一个全新的变量 x,以及一组新的函数(log 等) ,这些函数将封闭这个新变量。

function createObject() {
  let x = 42;
  return {
    log() { console.log(x) },
    increment() { x++ },
    update(value) { x = value }
  }
}

const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42

如果您使用的是用var声明的变量,请注意您要封闭的是哪个变量。使用var声明的变量被提升。由于引入了let和const,在现代JavaScript中这个问题要小得多。

在下面的代码中,每次循环的时候,都会创建一个新的内部函数,它在i上关闭。但是因为var i被提升到循环的外部,所有这些内部函数都在同一个变量上闭包,这意味着i(3)的最终值被打印了三次。

function foo() {
  var result = []
  for (var i = 0; i < 6; i++) {
    result.push(function inner() { console.log(i) } )
  }
  return result
}

const result = foo()
// The following will print `6`, three times...
for (var i = 0; i < 6; i++) {
  result[i]() 
}

闭包的产生:

当函数可以记住并访问所在的词法作用域,即使函数是在当前的词法作用域之外执行,这时就产生了闭包.

本质上无论何时何地,如果将函数(访问他们各自的词法作用域)当作第一级的值类型并导出传递,就能看到闭包在这些函数的应用.定时器、事件监听器、Ajax请求,跨窗口通信,web work或其他任何的异步或者同步任务中,凡是使用了回调函数,实际上就是在使用闭包

你不知知道的JavaScript上卷

IIFE(立即调用函数表达式)并不是观察闭包最好的例子,但是的确创建了闭包,并且也是最常用来创建可以被封闭的闭包工具.

从技术上来讲,闭包是发生在定义时.

一个经典面试题

for ( var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}

能否解释下会打印什么?为什么?我们后面给出

同样的这个demo

for (var i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
//10个10
for (var i = 0; i < 10; i++) {
  (() => {
    var i2 = i
    setTimeout(() => {
      console.log(i2)
    }, 1000)
  })()
}
//0-9
for (var i = 0; i < 10; i++) {
  setTimeout((i2 => {
    return () => {
      console.log(i2)
    }
  })(i), 1000)
}
//0-9

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出五个 6。

解决办法两种,第一种使用闭包

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}
//1~5

第二种就是使用 setTimeout 的第三个参数

for ( var i=1; i<=5; i++) {
	setTimeout( function timer(j) {
		console.log( j );
	}, i*1000, i);
}
//1~5

第三种就是使用 let 定义 i

for ( let i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}
//1~5

最后的要点:

  • 当在JavaScript闭包中声明一个函数时。

  • 从另一个函数内部返回一个函数是闭包的典型例子,因为外部函数内部的状态对返回的内部函数是隐式可用的,即使外部函数已经完成执行。

  • 每当在函数中使用eval()时,都会使用闭包。你eval的文本可以引用函数的局部变量,在非严格模式下,你甚至可以通过使用eval(‘var foo =…’)创建新的局部变量。

  • 当你在函数中使用new Function(…)(Function构造函数)时,它不会在它的词法环境中关闭:而是在全局上下文中关闭。新函数不能引用外部函数的局部变量。

  • JavaScript中的闭包就像在函数声明处保留对作用域的引用(而不是副本),而函数声明又保留对其外部作用域的引用,以此类推,一直到作用域链的顶部的全局对象。

  • 闭包是在函数声明时创建的;此闭包用于在调用函数时配置执行上下文。

  • 每次调用函数时,都会创建一组新的局部变量。

在一个函数使用了外部函数中的局部变量,使用变量的地方发生了闭包现象,变量定义所在的函数称为闭包函数

优点:防止变量污染作用域

缺点:不释放则会导致内存泄漏


Author: xt_xiong
转载要求: 如有转载请注明出处 :根据 CC BY 4.0 告知来自 xt_xiong !
评论
  标题