javascript中闭包closure的深入讲解


Posted in Javascript onMarch 03, 2021

简介

闭包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)); // 7
console.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 scope
var 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中非常强大和有用的概念,希望大家能够喜欢。

到此这篇关于javascript中闭包closure的文章就介绍到这了,更多相关javascript闭包closure内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
javascript控制Div层透明属性由浅变深由深变浅逐渐显示
Nov 12 Javascript
使用jQuery在对象中缓存选择器的简单方法
Jun 30 Javascript
javascript实现dom元素可拖动
Mar 21 Javascript
js 自带的sort() 方法全面了解
Aug 16 Javascript
Node.js 日志处理模块log4js
Aug 28 Javascript
Javascript计算二维数组重复值示例代码
Dec 18 Javascript
JS鼠标滚动分页效果示例
Jul 05 Javascript
Angular实现点击按钮后在上方显示输入内容的方法
Dec 27 Javascript
iview Upload组件多个文件上传的示例代码
Sep 30 Javascript
JavaScript继承的特性与实践应用深入详解
Dec 30 Javascript
VUE.CLI4.0配置多页面入口的实现
Nov 25 Javascript
解决vue自定义组件@click点击失效问题
Apr 30 Vue.js
详解Vue.js 可拖放文本框组件的使用
Mar 03 #Vue.js
详解vue3中组件的非兼容变更
Mar 03 #Vue.js
three.js如何实现3D动态文字效果
Mar 03 #Javascript
Webpack3+React16代码分割的实现
Mar 03 #Javascript
微信小程序input抖动问题的修复方法
Mar 03 #Javascript
微信小程序组件生命周期的踩坑记录
Mar 03 #Javascript
vite2.0+vue3移动端项目实战详解
Mar 03 #Vue.js
You might like
PHP在线生成二维码(google api)的实现代码详解
2013/06/04 PHP
phpQuery让php处理html代码像jQuery一样方便
2015/01/06 PHP
php eval函数一句话木马代码
2015/05/21 PHP
php实现上传图片文件代码
2015/07/19 PHP
thinkPHP3.2简单实现文件上传的方法
2016/05/16 PHP
php+js实现的无刷新下载文件功能示例
2019/08/23 PHP
js单例模式详解实例
2013/11/21 Javascript
JS常用函数使用指南
2014/11/23 Javascript
jQuery实现的网页竖向菜单效果代码
2015/08/26 Javascript
window.onload使用指南
2015/09/13 Javascript
使用Angular.js开发的注意事项
2016/10/19 Javascript
使用canvas进行图像编辑的实例
2017/08/29 Javascript
解决vue动态为数据添加新属性遇到的问题
2018/09/18 Javascript
Vue 组件参数校验与非props特性的方法
2019/02/12 Javascript
vue单页面在微信下只能分享落地页的解决方案
2019/04/15 Javascript
微信小程序可滑动周日历组件使用详解
2019/10/21 Javascript
uni-app 支持多端第三方地图定位的方法
2020/01/03 Javascript
Vue前端项目部署IIS的实现
2020/01/06 Javascript
部署vue+Springboot前后端分离项目的步骤实现
2020/05/31 Javascript
Node.js fs模块原理及常见用途
2020/10/22 Javascript
Vue 数据绑定的原理分析
2020/11/16 Javascript
python3访问sina首页中文的处理方法
2014/02/24 Python
分享Python文本生成二维码实例
2016/01/06 Python
使用python的pexpect模块,实现远程免密登录的示例
2019/02/14 Python
Python re正则表达式元字符分组()用法分享
2020/02/10 Python
Python脚本导出为exe程序的方法
2020/03/25 Python
Python实现简单的猜单词小游戏
2020/10/28 Python
在浏览器端如何得到服务器端响应的XML数据
2012/11/24 面试题
房地产还款计划书
2014/01/10 职场文书
幼儿园的门卫岗位职责
2014/04/10 职场文书
青春飞扬演讲稿
2014/09/11 职场文书
2014光棍节大学生联谊活动方案
2014/10/10 职场文书
学生抄作业检讨书(2篇)
2014/10/17 职场文书
2015年音乐教师个人工作总结
2015/05/20 职场文书
大学生学生会工作总结2015
2015/05/26 职场文书
微信小程序结合ThinkPHP5授权登陆后获取手机号
2021/11/23 PHP