JavaScript中的闭包现象

前端开发
2020年02月15日
604

函数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();
  1. 函数test1被定义时,系统生成[[scope]]属性,[[scope]]保存该函数的作用域链,该作用域链的第0位存储当前环境下的全局执行期上下文GO,GO中存储全局下所有对象,其中包含函数test1和全局变量c

    闭包1

  2. 函数test1执行的前一刻函数test2被定义,它的作用域链与上级环境中的作用域链一致。

    闭包2

  3. 当函数test1执行结束时函数test2被return到外部且被全局变量test3接收。此时:test1的AO并没有被销毁,只是连线被剪断了,test2的作用域链还连接test1的AO

    闭包3

  4. test3执行test2的作用域链上增加自己的AO,当打印变量a时,在test2的AO中没有找到,则会向test1的AO继续查找。再次执行test3时,实际操作的仍然是原来test1的AO。

    闭包4

  5. 当test3执行结束时test2的AO被销毁,但原来test1的AO仍然存在且被test2连着。

    闭包5

闭包的作用

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或多个方法相关联。

因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。

在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; }