什么是闭包?
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment, 词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
闭包与作用域有关,ES5之前只有全局作用域和函数作用域,因此var也只有全局作用域和函数作用域, ES6引入let,const新增块级作用域,所以js中的作用域包括:全局作用域,函数作用域和块级作用域; 用一个例子介绍一下闭包:
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦
makeFunc()
执行完毕,你可能会认为name
变量将不能再被访问。然而,因为代码仍按预期运行,所以在 JavaScript 中情况显然与此不同。
原因在于,JavaScript 中的函数会形成了闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,
myFunc
是执行makeFunc
时创建的displayName
函数实例的引用。displayName
的实例维持了一个对它的词法环境(变量name
存在于其中)的引用。因此,当myFunc
被调用时,变量name
仍然可用,其值Mozilla
就被传递到alert
中。
作用域链:
JavaScript
利用作用域链来解析变量查找。每个执行上下文都有对其外部(封闭)上下文的引用,从而创建JavaScript
引擎用来搜索变量值的链。该链是通过词法作用域建立的,其中代码中函数和块的放置决定了作用域层次结构。
闭包有哪些作用?
闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。
数据封装和隐私:
闭包允许在模块或对象内创建私有变量和方法,类似于基于类的语言中的私有成员。该模式广泛用于模块模式、揭示模块模式以及在 JavaScript 中实现面向对象的概念。
var Counter = (function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
return privateCounter;
},
};
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
柯里化函数:
柯里化是一种函数式编程技术,其中具有多个参数的函数被转换为一系列函数,每个函数都具有单个参数。闭包对于实现柯里化函数至关重要,因为它们允许函数保留传递给它的参数,直到提供所有参数。
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
console.log(double(5)); // Outputs: 10
事件回调函数也是一种闭包
Web 中很常见一种是基于事件的定义某种行为,然后将其添加到用户触发的事件之上。我们的代码通常作为回调:为响应事件而执行的函数。
function showHelp(help) {
document.getElementById("help").textContent = help;
}
function setupHelp() {
const helpText = [
{ id: "email", help: "Your e-mail address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age (you must be over 16)" },
];
for (let i = 0; i < helpText.length; i++) {
const item = helpText[i];
document.getElementById(item.id).onfocus = () => {
showHelp(item.help);
};
}
}
setupHelp();
这里在循环中创建了三个闭包(响应事件回调函数),根据词法作用域,它们可以访问:
// 定义时各自独立的块级作用域变量:
const item = helpText[i];
// 共享的函数作用域变量:
const helpText = [
{ id: "email", help: "Your e-mail address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age (you must be over 16)" },
];
// 共享的全局作用域变量:
function showHelp(help) {
document.getElementById("help").textContent = help;
}
可以把const item = helpText[i]
中写成 var item = helpText[i]
代码会符合预期吗😊?
哪些实际场景中使用了闭包?
这里以React官网,对useState例子介绍,函数创建伴随着闭包产生,内部嵌套函数setState
逻辑比较简单,将传入变量赋值给需要更新值pair 也就是state返回内容,这里React 通过在组件之外的一层定义 componentHooks, currentHookIndex(这些状态不与其他组件共享),来维护不同useState对应各自state渲染顺序,这也是为什么React规定useHook不能出现在条件和循环中,因为这样每次渲染,hooks顺序都可能发生改变,导致渲染一致问题,具体可以看这篇博客。
let componentHooks = [];
let currentHookIndex = 0;
// useState 在 React 中是如何工作的(简化版)
function useState(initialState) {
let pair = componentHooks[currentHookIndex];
if (pair) {
// 这不是第一次渲染
// 所以 state pair 已经存在
// 将其返回并为下一次 hook 的调用做准备
currentHookIndex++;
return pair;
}
// 这是我们第一次进行渲染
// 所以新建一个 state pair 然后存储它
pair = [initialState, setState];
function setState(nextState) {
// 当用户发起 state 的变更,
// 把新的值放入 pair 中
pair[0] = nextState;
updateDOM();
}
// 存储这个 pair 用于将来的渲染
// 并且为下一次 hook 的调用做准备
componentHooks[currentHookIndex] = pair;
currentHookIndex++;
return pair;
}
function Gallery() {
// 每次调用 useState() 都会得到新的 pair
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
function handleNextClick() {
setIndex(index + 1);
}
function handleMoreClick() {
setShowMore(!showMore);
}
...
}
闭包需要性能考量
如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function () {
return this.name;
};
this.getMessage = function () {
return this.message;
};
}
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function () {
return this.name;
};
MyObject.prototype.getMessage = function () {
return this.message;
};
继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法。参见 对象模型的细节 一章可以了解更为详细的信息。