写点什么

javascript 中的闭包 closure 详解

发布于: 2021 年 03 月 02 日

简介

闭包 closure 是 javascript 中一个非常强大的功能。所谓闭包就是函数中的函数,内部函数可以访问外部函数的作用域范围,从而可以使用闭包来做一些比较强大的工作。


今天将会给大家详细介绍一下闭包。


函数中的函数

我们提到了函数中的函数可以访问父函数作用域范围的变量,我们看一个例子:


function parentFunction() {  var address = 'flydean.com';   function alertAddress() {     alert(address);   }  alertAddress();}parentFunction();
复制代码

上面的例子中,我们在 parentFunction 中定义了一个变量 address,在 parentFunction 内部定义了一个 alertAddress 方法,在该方法内部访问外部函数中定义的 address 变量。


上面代码运行是没问题的,可以正确的访问到数据。


Closure 闭包

函数中的函数有了,那么什么是闭包呢?


我们看下面的例子:


function parentFunction() {  var address = 'flydean.com';   function alertAddress() {     alert(address);   }  return alertAddress;}var myFunc = parentFunction();myFunc();
复制代码

这个例子和第一个例子很类似,不同之处就是我们将内部函数返回了,并且赋值给了 myFunc。


接下来我们直接调用了 myFunc。


myFunc 中访问了 parentFunction 中的 address 变量,虽然 parentFunction 已经执行完毕返回。


但是我们在调用 myFunc 的时候,任然可以访问到 address 变量。这就是闭包。


闭包的这个特性非常拥有,我们可以使用闭包来生成 function factory,如下所示:


function makeAdder(x) {  return function(y) {    return x + y;  };}
var add5 = makeAdder(5);var add10 = makeAdder(10);
console.log(add5(2)); // 7console.log(add10(2)); // 12
复制代码

其中 add5 和 add10 都是闭包,他们是由 makeAdder 这个 function factory 创建出来的。通过传递不同的 x 参数,我们得到了不同的基数的 add 方法。


最终生成了两个不同的 add 方法。


使用 function factory 的概念,我们可以考虑一个闭包的实际应用,比如我们在页面上有三个 button,通过点击这些 button 可实现修改字体的功能。


我们可以先通过 function factory 来生成三个方法:


function makeSizer(size) {  return function() {    document.body.style.fontSize = size + 'px';  };}
var size12 = makeSizer(12);var size14 = makeSizer(14);var size16 = makeSizer(16);
复制代码

有了这三个方法,我们把 DOM 元素和 callback 方法绑定起来:


document.getElementById('size-12').onclick = size12;document.getElementById('size-14').onclick = size14;document.getElementById('size-16').onclick = size16;
复制代码

使用闭包实现 private 方法

对比 java 来说,java 中有 private 访问描述符,通过 private,我们可以指定方法只在 class 内部访问。


当然,在 JS 中并没有这个东西,但是我们可以使用闭包来达到同样的效果。


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()); // 0.
counter.increment();counter.increment();console.log(counter.value()); // 2.
counter.decrement();console.log(counter.value()); // 1.
复制代码

我们在父 function 中定义了 privateCounter 属性和 changeBy 方法,但是这些方法只能够在内部 function 中访问。


我们通过闭包的概念,将这些属性和方法封装起来,暴露给外部使用,最终达到了私有变量和方法封装的效果。


闭包的 Scope Chain

对于每个闭包来说,都有一个作用域范围,包括函数本身的作用域,父函数的作用域和全局的作用域。


如果我们在函数内部嵌入了新的函数,那么就会形成一个作用域链,我们叫做 scope chain。


看下面的一个例子:


// global scopevar e = 10;function sum(a){  return function(b){    return function(c){      // outer functions scope      return function(d){        // local scope        return a + b + c + d + e;      }    }  }}
console.log(sum(1)(2)(3)(4)); // log 20
复制代码

闭包常见的问题

第一个常见的问题就是在循环遍历中使用闭包,我们看一个例子:


function showHelp(help) {  document.getElementById('help').innerHTML = help;}
function setupHelp() { var 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 (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } }}
setupHelp();
复制代码

上面的例子中,我们创建了一个 setupHelp 函数,setupHelp 中,onfocus 方法被赋予了一个闭包,所以闭包中的 item 可以访问到外部 function 中定义的 item 变量。


因为在循环里面赋值,所以我们实际上创建了 3 个闭包,但是这 3 个闭包共享的是同一个外部函数的作用域范围。


我们的本意是,不同的 id 触发不同的 help 消息。但是如果我们真正执行就会发现,不管是哪一个 id,最终的消息都是最后一个。


因为 onfocus 是在闭包创建完毕之后才会触发,这个时候 item 的值实际上是变化的,在循环结束之后,item 的值已经指向了最后一个元素,所以全部显示的是最后一条数据的 help 消息。


怎么解决这个问题呢?


最简单的办法使用 ES6 中引入的 let 描述符,从而将 item 定义为 block 的作用域范围,每次循环都会创建一个新的 item,从而保持闭包中的 item 的值不变。


  for (let i = 0; i < helpText.length; i++) {    let item = helpText[i];    document.getElementById(item.id).onfocus = function() {      showHelp(item.help);    }  }
复制代码

还有一种方法,就是再创建一个闭包:


function makeHelpCallback(help) {  return function() {    showHelp(help);  };}
for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); }
复制代码

这里用到了之前我们提到的 function factory 的概念,我们为不同的闭包创建了不同的作用域环境。


还有一种方法就是将 item 包含在一个新的 function 作用域范围之内,从而每次创建都是新的 item,这个和 let 的原理是相似的:


  for (var i = 0; i < helpText.length; i++) {    (function() {       var item = helpText[i];       document.getElementById(item.id).onfocus = function() {         showHelp(item.help);       }    })();   }
复制代码

第二个常见的问题就是内存泄露。


 function parentFunction(paramA) { var a = paramA; function childFunction() { return a + 2; } return childFunction(); }
复制代码

上面的例子中,childFunction 引用了 parentFunction 的变量 a。只要 childFunction 还在被使用,a 就无法被释放,从而导致 parentFunction 无法被垃圾回收。


闭包性能的问题

我们定义了一个对象,并且通过闭包来访问其私有属性:


function MyObject(name, message) {  this.name = name.toString();  this.message = message.toString();  this.getName = function() {    return this.name;  };
this.getMessage = function() { return this.message; };}
复制代码

上面的对象会有什么问题呢?


上面对象的问题就在于,对于每一个 new 出来的对象,getName 和 getMessage 方法都会被复制一份,一方面是内容的冗余,另一方面是性能的影响。


通常来说,我们将对象的方法定义在 prototype 上面:


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;};
复制代码

注意,我们不要直接重写整个 prototype,这样会导致未知的错误,我们只需要根据需要重写特定的方法即可。


总结

闭包是 JS 中非常强大和有用的概念,希望大家能够喜欢。


本文作者:flydean 程序那些事

本文链接:http://www.flydean.com/js-closure/

本文来源:flydean 的博客

欢迎关注我的公众号:「程序那些事」最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!


发布于: 2021 年 03 月 02 日阅读数: 28
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
javascript中的闭包closure详解