理解 JavaScript 闭包
栏目: JavaScript · 发布时间: 7年前
内容简介:这是本系列的第 4 篇文章。作为 JS 初学者,第一次接触闭包的概念是因为写出了类似下面的代码:给列表项循环添加事件处理程序。当你点击列表项时不会有任何反应。如何在初学就理解闭包?你需要接着读下去。
这是本系列的第 4 篇文章。
作为 JS 初学者,第一次接触闭包的概念是因为写出了类似下面的代码:
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).click = function() {
showHelp(item.help);
}
}
给列表项循环添加事件处理程序。当你点击列表项时不会有任何反应。如何在初学就理解闭包?你需要接着读下去。
§ 什么是闭包
说闭包前,你还记得词法作用域吗?
var num = 0;
function foo() {
var num = 1;
function bar() {
console.log(num);
}
bar();
}
foo(); // 1
执行上面的代码打印出 1。
bar 函数是 foo 函数的内部函数,JS 的词法作用域允许内部函数访问外部函数的变量。那我们可不可以在外部访问内部函数的变量呢?理论上不允许。
但是我们可以通过某种方式实现,即将内部函数返回。
function increase() {
let count = 0;
function add () {
count += 1;
return count;
}
return add;
}
const addOne = increase();
addOne(); // 1
addOne(); // 2
addOne(); // 3
内部函数允许访问其父函数的内部变量,那么将内部函数返回到出来,它依旧引用着其父函数的内部变量。
这里就产生了闭包。
简单来说, 可以把闭包理解为函数返回函数 。
上面的代码中,当 increase 函数执行,压入执行栈,执行完毕返回一个 add 函数的引用,所以 increase 函数内部的变量对象依旧保存在内存中,不会被销毁。
调用 addOne 函数,相当于执行内部函数 add,它可以访问其父函数的内部变量,从而修改变量 count。而调用 addOne 函数所在的环境为全局作用域,不是定义 add 函数时的函数作用域。
所以,我理解的闭包是一个函数,它在执行时与其定义时所处的词法作用域不一致,并且具有能够访问定义时词法作用域的能力。MDN 这样定义: 闭包是函数和声明该函数的词法环境的组合 。
§ 闭包的利与弊
◆ 利
第一,闭包可以在函数外部读取函数内部的变量。
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
Counter.value(); // 0
Counter.increment();
Counter.increment();
Counter.value(); // 2
Counter.decrement();
Counter.value(); / 1
上面这种模式称为模块模式。我们使用立即执行函数 IIFE 将代码私有化但是提供了可访问的接口,通过公共接口来访问函数私有的函数和变量。
第二,闭包将内部变量始终保存在内存中。
function type(tag) {
return function (data) {
return Object.prototype.toString.call(data).toLowerCase() === '[object ' + tag + ']';
}
}
var isNum = type('number');
var isString = type('string');
isNum(1); // true
isString('abc'); // true
利用闭包将内部变量(参数)tag 保存在内存中,来封装自己的类型判断函数。
◆ 弊
第一,既然闭包会将内部变量一直保存在内存中,如果在程序中大量使用闭包,势必造成内存的泄漏。
$(document).ready(function() {
var button = document.getElementById('button-1');
button.onclick = function() {
console.log('hello');
return false;
};
});
在这个例子中,click 事件处理程序就是一个闭包(在这里是个匿名函数),它将引用着 button 变量;而 button 在这里本身依旧引用着这个匿名函数。从而产生循环引用,造成网页的性能问题,在 IE 中可能会内存泄漏。
解决办法就是手动解除引用。
$(document).ready(function() {
var button = document.getElementById('button-1');
button.onclick = function() {
console.log('hello');
return false;
};
button = null; // 添加这一行代码来手动解除引用
});
第二,如果你将函数作为对象使用,将闭包作为它的方法,应该特别注意不要随意改动函数的私有属性。
§ 闭包的经典问题
◆ 循环
现在我们来解决一下文章开头出现的问题。
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).click = makeHelpCallback(item.help);
}
额外声明一个 makeHelpCallBack 的函数,将循环每次的上下文环境通过闭包保存起来。
◆ setTimeout
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
};
结果为 1 秒后,打印 5 个 5。
我们可以利用闭包保留词法作用域的特点,来修改代码达到目的。
for (var i = 0; i < 5; i++) {
setTimeout((function(i) {
return function () {
console.log(i);
}
}(i)), 1000);
};
结果为 1 秒后,依次打印 0 1 2 3 4。
§ 小结
闭包在 JS 中随处可见。
闭包是 JS 中的精华部分,理解它需要具备一定的作用域、执行栈的知识。理解它你将收获巨大,你会在 JS 学习的道路上走得更远,比如会在后面的文章来讨论高阶函数和柯里化的问题。
◆ 文章参考
闭包 | MDN
学习 JavaScript 闭包 | 阮一峰
Understanding JavaScript Closures: A practical Approach | Paul Upendo
闭包造成问题泄漏的解决办法 | CSDN
§ JavaScript 系列文章
欢迎关注我的公众号 cameraee
前端技术 | 个人成长
以上所述就是小编给大家介绍的《理解 JavaScript 闭包》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 理解JavaScript的闭包
- 理解 JavaScript 中的闭包
- 简单理解 Scala 的闭包
- [译] 理解 Rust 中的闭包
- [译]理解JS中的闭包
- [译] 我从没理解过 JavaScript 闭包
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
500 Lines or Less
Amy Brown、Michael DiBernardo / 2016-6-28 / USD 35.00
This book provides you with the chance to study how 26 experienced programmers think when they are building something new. The programs you will read about in this book were all written from scratch t......一起来看看 《500 Lines or Less》 这本书的介绍吧!