您将如何向了解其闭包概念(例如函数,变量等)的人解释JavaScript闭包,但却不了解闭包本身?
闭包是以下内容的配对:
词法环境是每个执行上下文(堆栈框架)的一部分,并且是标识符(即局部变量名称)和值之间的映射。
JavaScript中的每个函数都对其外部词汇环境保持引用。此引用用于配置调用函数时创建的执行上下文。此引用使函数内部的代码可以“查看”在函数外部声明的变量,而不管调用函数的时间和位置。
如果一个函数由一个函数调用,而另一个函数又调用了另一个函数,则将创建对外部词汇环境的引用链。该链称为作用域链。
在下面的代码中,inner与foo调用时创建的执行上下文的词法环境形成一个闭包, 对 变量进行 闭包secret:
inner
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中的函数可以像变量一样传递(一流的函数),这意味着功能和状态对可以在程序中传递:类似于在C ++中传递类的实例的方式。
如果JavaScript没有闭包,则必须在函数之间 显式 传递更多状态,从而使参数列表更长,代码更嘈杂。
因此,如果您希望函数始终有权访问私有状态,则可以使用闭包。
......频频我们 也 希望与功能关联状态。例如,在Java或C ++中,当您将私有实例变量和方法添加到类时,您正在将状态与功能相关联。
在C语言和大多数其他常见语言中,函数返回后,所有本地变量将不再可访问,因为堆栈框架被破坏了。在JavaScript中,如果您在另一个函数中声明一个函数,则外部函数的本地变量在返回后仍可访问。这样一来,在上面的代码,secret仍然可用的函数对象inner, 之后 它已经从返回foo。
每当需要与函数关联的私有状态时,闭包都是有用的。这是一种非常常见的情况- 请记住:JavaScript直到2015年才使用类语法,并且仍然没有私有字段语法。封闭件可满足此需求。
在以下代码中,函数toString关闭了汽车的详细信息。
toString
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())
在以下代码中,函数同时inner关闭fn和args。
fn
args
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
在以下代码中,函数onClick在variable之上关闭BACKGROUND_COLOR。
onClick
BACKGROUND_COLOR
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它们完成工作所需的私有状态和功能关闭。封闭使我们能够模块化和封装我们的代码。
tick
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())
此示例显示局部变量未在闭包中复制:闭包维护对原始变量 本身 的引用。好像在外部函数退出后,堆栈框架仍在内存中保持活动状态。
function foo() { let x = 42 let inner = function() { console.log(x) } x = x+1 return inner } var f = foo() f() // logs 43
在下面的代码,三种方法log,increment和update所有密切在同一词法环境。
log
increment
update
每次createObject调用时,都会创建一个新的执行上下文(堆栈框架),并创建一个全新的变量x,并创建一组新的函数(log等),这些函数将覆盖此新变量。
createObject
x
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被提升。这是非常现代的JavaScript的问题较少,由于引进let和const。
var
let
const
在以下代码中,每次循环时,inner都会创建一个新函数,该函数关闭i。但是由于var i悬挂在循环外部,所有这些内部函数都在同一变量上闭合,这意味着将i(3)的最终值打印了三遍。
i
var i
function foo() { var result = [] for (var i = 0; i < 3; i++) { result.push(function inner() { console.log(i) } ) } return result } const result = foo() // The following will print `3`, three times... for (var i = 0; i < 3; i++) { result[i]() }
function
eval()
eval
eval('var foo = …')
new Function(…)