JavaScript中的闭包现象
函数A执行时,导致其内部的函数B被返回到外部并保存时,一定会产生闭包(函数A形成了闭包),闭包可以说是一种现象,它会产生原来的作用域链不释放。
MDN中对闭包的解释如下:
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。
过渡的闭包有可能会导致内存泄漏、或加载过慢。
图解闭包形成过程
我们先看下下面的代码:
js
function test1 () {
var a = 1;
function test2 () {
var b = 2;
console.log(a);
}
return test2;
}
var c = 3;
var test3 = test1();
test3();
-
当
函数test1
被定义时,系统生成[[scope]]
属性,[[scope]]
保存该函数的作用域链,该作用域链的第0
位存储当前环境下的全局执行期上下文GO,GO中存储全局下所有对象,其中包含函数test1
和全局变量c
。 -
函数test1执行的前一刻,
函数test2
被定义,它的作用域链与上级环境中的作用域链一致。 -
当函数test1执行结束时,
函数test2
被return到外部且被全局变量test3
接收。此时:test1
的AO并没有被销毁,只是连线被剪断了,而test2
的作用域链还连接test1
的AO。 -
test3执行,
test2
的作用域链上增加自己的AO,当打印变量a
时,在test2
的AO中没有找到,则会向test1
的AO继续查找。再次执行test3
时,实际操作的仍然是原来test1
的AO。 -
当test3执行结束时,
test2
的AO被销毁,但原来test1
的AO仍然存在且被test2
连着。
闭包的作用
闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或多个方法相关联。
因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。
在Web中,你想要这样做的情况特别常见。
计数器
js
var counter = (function (initialValue) {
var initialValue = initialValue || 100;
return {
add: function () {
initialValue ++;
},
reduce: function () {
initialValue --;
},
getValue: function () {
console.log(initialValue);
}
}
})(10);
counter.getValue(); // 10
counter.add();
counter.add();
counter.add();
counter.getValue(); // 13
counter.reduce();
counter.reduce();
counter.getValue(); // 11
模拟私有方法
js
var breadMgr = (function () {
var num = 3;
function changeNum (val) {
num += val;
}
return {
apply: function (val) {
changeNum(val || 1);
console.log('剩余:' + num + '个面包!');
},
sale: function () {
if (num <= 0) {
console.log('面包卖完了~');
return;
}
changeNum(-1);
console.log('剩余:' + num + '个面包!');
}
}
})();
breadMgr.sale();
breadMgr.sale();
breadMgr.sale();
breadMgr.apply(5);
breadMgr.sale();
breadMgr.sale();
利用闭包定义公共的changeNum函数
、并使其可以访问私有变量和函数。
数据缓存
js
var func = (function () {
var cache = {};
return {
calc: function () {
var key = JSON.stringify(arguments);
if (key in cache) { // 判断缓存中是否存在结果
console.log('来自缓存的结果:', cache[key]);
} else {
var args = [].slice.call(arguments),
value = args.reduce((prev, current) => {
return prev + current;
}, 0);
cache[JSON.stringify(arguments)] = value;
console.log('来自计算后的结果:', value);
}
},
getCache: function () {
console.log(cache);
}
}
})();
func.calc(1, 2); // 来自计算后的结果: 3
func.calc(2, 3); // 来自计算后的结果: 5
func.calc(4, 5); // 来自计算后的结果: 9
func.calc(0, 0); // 来自计算后的结果: 0
func.calc(1, 2); // 来自缓存的结果: 3
func.calc(2, 3); // 来自缓存的结果: 5
func.calc(4, 5); // 来自缓存的结果: 9
func.calc(0, 0); // 来自缓存的结果: 0
func.getCache(); // {{"0":1,"1":2}: 3, {"0":2,"1":3}: 5, {"0":4,"1":5}: 9, {"0":0,"1":0}: 0}
节流与防抖函数
js
// 节流
function throttle (fn, delay) {
var t = null,
begin = new Date().getTime();
return function () {
var _self = this,
args = arguments,
current = new Date().getTime();
clearTimeout(t);
if (current - begin >= delay) {
fn.apply(_self, args);
begin = cur;
} else {
t = setTimeout(function () {
fn.apply(_self, args);
}, delay);
}
}
}
// 防抖
function debounce (fn, delay, triggerNow) {
var t = null,
res;
var debounced = function () {
var _self = this,
args = arguments;
if (t) { // 存在时清除计时器
clearTimeout(t);
}
if (triggerNow) { // 如果需要立即执行
var exec = !t;
t = setTimeout(function () { // 满足第一个需求
t = null; // clearTimeout后,t还是有ID值的
}, delay);
if (exec) {
res = fn.apply(_self, args);
}
} else { // 不需要立即执行
// 直接延迟
t = setTimeout(function () {
res = fn.apply(_self, args);
}, delay);
}
}
// 设置个清除的开关
debounced.remove = function () {
clearTimeout(t);
t = null;
}
debounced.getResult = function () {
return res;
}
return debounced;
}