闭包不是什么魔法
本篇文章介绍了闭包, 方便程序员们能够进一步理解 javascript 代码, 本文适合有一定编程经验的程序员, 比如可以看懂如下代码: 大神请绕道
- #####Example 1
- function sayHello(name) {
- var text = 'Hello' + name;
- var say = function() { console.log(text); }
- say();
- }
- sayHello('Joe'); //Hello Joe
一旦深刻理解了核心概念, 闭包就并不难分析和运用了
一个关于闭包的案例
两句话总结:
第一级函数支持闭包闭包它是一个表达式, 可以在闭包的范围内引用变量(当它被首次声明), 被赋值给变量, 作为参数传递给函数, 或作为函数结果返回(译者注: 在 JavaScript 世界中函数是一等公民, 它不仅拥有一切传统函数的使用方式(声明和调用), 还可以做到像原始值一样赋值传参返回, 这样的函数也称之为第一级函数(First-class Function))
闭包是在函数开始执行时分配的堆栈帧, 并且在函数返回后不会释放(就像堆栈帧分配在堆上而不是栈上!)
- #####Example 2 以下代码返回一个函数的引用:
- function sayHello2(name) {
- var text = 'Hello' + name; // Local variable
- var say = function() { console.log(text); }
- return say;
- }
- var say2 = sayHello2('Bob');
- say2(); // logs "Hello Bob"
大多数 JavaScript 程序员都了解如何将一个函数的引用赋给上述代码中的变量 say2 如果你不了解, 那么在学习闭包之前你需要先了解一下一个使用 C 的程序员会将其看作是返回指向某函数的指针, 会认为变量 say 和 say2 都是指向函数的指针 C 语言指向函数的指针和 JavaScript 的函数引用之间存在着很关键的区别在 JavaScript 中, 您可以将函数引用变量看作既包含指向函数的指针, 也包含指向闭包的隐藏指针
上述代码中存在一个闭包, 因为匿名函数
function() { console.log(text); }
在另一个函数 sayHello2()中声明在这个例子中, 如果你在另一个函数体里使用 function 关键字, 那么你正在创建闭包
在 C 语言和其他大多数类似语言中, 在函数返回后, 所有局部变量不可再被访问, 因为堆栈帧已经被销毁了
而在 Javascript 语言中, 如果你在一个函数体内再声明一个函数, 这个函数被返回到了全局, 局部变量依然可以被访问如上面所示, 我们在函数 sayHello2()返回后调用了函数 say2(), 请注意, 变量 text 是函数 sayHello2()的局部变量
function() { console.log(text); } // Output of say2.toString();
注意 say2.toString()的输出, 我们可以看到这段代码引用了变量 text, 由于 sayHello2()的局部变量被保存到闭包内, 所以这个匿名函数可以引用存储 "Hello Bob" 的变量 text
在 Javascript 中函数引用包含指向它所创建的闭包的隐藏指针就类似于 js 中的事件委托(一个事情本需要自己做, 但自己委托给别人做了)
- ## 更多案例 出于某种原因, 闭包似乎很难理解, 但是当你多看一些案例之后, 它的工作原理变得逐渐清晰 (我花了很长时间才搞清楚) 我建议你仔细研究这些案例, 直至弄明白闭包是如何工作的如果你在没弄明白之前就使用闭包, 就一定会碰到一些非常奇怪的错误 #####Example 3 这个案例表明, 局部变量没有被复制, 而是它们的引用被保存就好像当外部函数退出后在内存保留一个堆栈帧
- function say667() {
- // Local variable that ends up within closure
- var num = 42;
- var say = function() { console.log(num); }
- num++;
- return say;
- }
- var sayNumber = say667();
- sayNumber(); // logs 43
- #####Example 4 所有这三个全局函数都有一个对同一个闭包的共同引用, 因为它们都是在同一个 setupSomeGlobals()函数中声明的
- var gLogNumber, gIncreaseNumber, gSetNumber;
- function setupSomeGlobals() {
- // Local variable that ends up within closure
- var num = 42;
- // Store some references to functions as global variables
- gLogNumber = function() { console.log(num); }
- gIncreaseNumber = function() { num++; }
- gSetNumber = function(x) { num = x; }
- }
- setupSomeGlobals();
- gIncreaseNumber();
- gLogNumber(); // 43
- gSetNumber(5);
- gLogNumber(); // 5
- var oldLog = gLogNumber;
- setupSomeGlobals();
- gLogNumber(); // 42
- oldLog() // 5
这三个函数共用一个闭包三个函数被定义时, 函数 setupSomeGlobals()的局部变量 请注意, 在上述案例中, 如果再次调用 setupSomeGlobals(), 则会创建一个新的闭包 (堆栈帧) 旧的 gLogNumber, gIncreaseNumber, gSetNumber 变量被具有新闭包的新函数覆盖(在 Javascript 中, 无论何时在另一个函数内声明一个函数, 每次调用外部函数时都会重新创建内部函数)
- #####Example 5 这个案例对于许多人来说是一个大难题, 你需要仔细理解一下如果你要在一个循环体中定义一个函数, 要非常小心, 闭包中的局部变量可不会想你想当然那样工作
- function buildList(list) {
- var result = [];
- for (var i = 0; i < list.length; i++) {
- var item = 'item' + i;
- result.push( function() {console.log(item + ' ' + list[i])} );
- }
- return result;
- }
- function testList() {
- var fnlist = buildList([1,2,3]);
- // Using j only to help prevent confusion -- could use i.
- for (var j = 0; j < fnlist.length; j++) {
- fnlist[j]();
- }
- }
- testList() //logs "item2 undefined" 3 times
这行代码
result.push( function() {console.log(item + ' ' + list[i])} );
所示, 将一个匿名函数的引用添加到 result 数组中三次如果你对匿名函数不熟悉, 也可当成如下:
- pointer = function() {console.log(item + ' ' + list[i])};
- result.push(pointer);
请注意, 当案例执行时,"item2 undefined" 会输出三次! 这是因为跟之前案例一样, buildList 的局部变量只有一个闭包当在执行 fnList[j]()调用匿名函数时, 三个匿名函数都共用一个闭包, 并且它们使用的是循环结束后的当前值作为该闭包中的 i 和 item(循环已完成, i 的值为 3,item 值为 "item2")请注意, 该循环从 0 开始索引, 到循环结束前 item 值为 "item2", 而 i ++ 会将 i 值增加到 3
- #####Example 6 此案例显示: 在外部函数退出前, 外部函数内声明的所有全局变量都包含在闭包内请注意, 变量 alice 实际上是在匿名函数之后声明的, 匿名函数是最先声明的, 当该函数被调用时, 它仍然可以访问 alice 变量, 因为该变量处于相同作用域内 (Javascript 声明提升) 另外, sayAlice()()只是直接调用从 sayAlice()返回的函数引用
- function sayAlice() {
- var say = function() { console.log(alice); }
- // Local variable that ends up within closure
- var alice = 'Hello Alice';
- return say;
- }
- sayAlice()();// logs "Hello Alice"
需要注意的是: say 变量也在闭包中, 可以通过 sayAlice()中任何可能声明的其他函数访问, 或者可以在内部函数内递归访问
- #####Example 7 最后这个案例表明, 每次调用外部函数都会为局部变量创建一个单独的闭包不是每个函数声明都有单独闭包, 而是每次函数调用都会创建一个闭包
- function newClosure(someNum, someRef) {
- // Local variables that end up within closure
- var num = someNum;
- var anArray = [1, 2, 3];
- var ref = someRef;
- return function(x) {
- num += x;
- anArray.push(num);
- console.log('num:' + num + '; anArray:' + anArray.toString() + '; ref.someVar:' + ref.someVar + ';');
- }
- }
- obj = {
- someVar: 4
- };
- fn1 = newClosure(4, obj);
- fn2 = newClosure(5, obj);
- fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
- fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
- obj.someVar++;
- fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
- fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
总结
如果对闭包并不完全明白, 那么最好的办法就是回过头研究研究这些案例我对闭包和堆栈帧等概念的解释可能在专业上并不完全正规, 这都是为了帮助大家更好地理解一旦这些基础知识得到掌握, 你可以在以后的日子里去抠那些更专业的细节
最后几点
任何时候, 你在一个函数体内用了另外一个函数, 闭包就产生了
任何时候, 你在一个函数体内用了 eval(), 闭包就产生了在 eval 中的内容可以引用函数里的局部变量, 你甚至可以在 eval 内声明新的局部变量, 比如:
eval('var foo = ...')
当你在一个函数体内使用构造函数(new Function(...)), 不会产生闭包(这个构造函数不能引用外部函数的局部变量)
Javascript 中的闭包就像外部函数返回后, 用来保存所有局部变量的存储副本一样
最好可以这样认为: 闭包只是一个函数的入口, 函数的局部变量被添加到这个闭包中
每次调用一个带有闭包的函数时, 都会保存一组新的局部变量(假定该函数内包含一个函数声明, 并且返回到外部, 或者以某种方式为其保留外部引用)
两个函数可能看起来代码相同, 但是由于隐藏的闭包, 它们有着完全不同的行为我并不认为通过 Javascript 代码可以很容易看出一个函数引用是否拥有闭包
如果你想进行动态修改代码(比如:
myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));
), 如果 myFunction 是闭包, 将行不通(当然, 你应该永远不会想要这样进行源代码字符串替换, 但是)
很可能出现这种情况: 在函数体内的函数声明中还有函数声明, 那么你会发现有不止一个层级上的闭包出现
我怀疑 Javascript 中的闭包和那些函数式语言的闭包不同
闭包的应用(译者注)
实现封装, 私有化属性 / 变量
模块化开发, 防止全局污染
用作缓存
用作公有变量
等等
闭包的危害(译者注)
闭包会导致原有作用域链不释放, 造成内存泄露
来源: https://juejin.im/post/5abb5b5d5188255c2721fc02