JS闭包、作用域链、垃圾回收、内存泄露相关知识小结


Posted in Javascript onMay 16, 2016

补充:

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

闭包的特性

闭包有三个特性:

1.函数嵌套函数

2.函数内部可以引用外部的参数和变量

3.参数和变量不会被垃圾回收机制回收

闭包的定义及其优缺点

闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量

闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。

闭包是javascript语言的一大特点,主要应用闭包场合主要是为了:设计私有的方法和变量。

一般函数执行完毕后,局部活动对象就被销毁,内存中仅仅保存全局作用域。但闭包的情况不同!

围着主题展开话题

1. 闭包的定义?

来看一些关于闭包的定义:

1.闭包是指有权访问另一个函数作用域中变量的函数

2.函数对象可以通过作用域链相关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性称为 ‘闭包' 。

3.内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments)。

大家想系统的学习js闭包的概念可以参考三水点靠木网站的js电子书栏目学习吧。

来个定义总结

1.可以访问外部函数作用域中变量的函数

2.被内部函数访问的外部函数的变量可以保存在外部函数作用域内而不被回收---这是核心,后面我们遇到闭包都要想到,我们要重点关注被闭包引用的这个变量。

来创建个简单的闭包

var sayName = function(){
var name = 'jozo';
return function(){
alert(name);
}
};
var say = sayName(); 
say();

来解读后面两个语句:

•var say = sayName() :返回了一个匿名的内部函数保存在变量say中,并且引用了外部函数的变量name,由于垃圾回收机制,sayName函数执行完毕后,变量name并没有被销毁。

•say() :执行返回的内部函数,依然能访问变量name,输出 'jozo' .

2. 闭包中的作用域链

理解作用域链对理解闭包也很有帮助。

变量在作用域中的查找方式应该都很熟悉了,其实这就是顺着作用域链往上查找的。

当函数被调用时:

1.先创建一个执行环境(execution context),及相应的作用域链;

2.将arguments和其他命名参数的值添加到函数的活动对象(activation object)

作用域链:当前函数的活动对象优先级最高,外部函数的活动对象次之,外部函数的外部函数的活动对象依次递减,直至作用域链的末端--全局作用域。优先级就是变量查找的先后顺序;

先来看个普通的作用域链:

function sayName(name){
return name;
}
var say = sayName('jozo');

这段代码包含两个作用域:a.全局作用域;b.sayName函数的作用域,也就是只有两个变量对象,当执行到对应的执行环境时,该变量对象会成为活动对象,并被推入到执行环境作用域链的前端,也就是成为优先级最高的那个。 看图说话:

这图在JS高级程序设计书上也有,我重新绘了遍。

在创建sayName()函数时,会创建一个预先包含变量对象的作用域链,也就是图中索引为1的作用域链,并且被保存到内部的[[Scope]]属性中,当调用sayName()函数的时候,会创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起作用域链,此后,又有一个活动对象(图中索引为0)被创建,并被推入执行环境作用域链的前端。

一般来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是,闭包的情况又有所不同 :

再来看看看闭包的作用域链:

function sayName(name){
return function(){
return name;
}
}
var say = sayName('jozo');

这个闭包实例比上一个例子多了一个匿名函数的作用域:

在匿名函数从sayName()函数中被返回后,它的作用域链被初始化为包含sayName()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在sayName()中定义的所有变量和参数,更为重要的是,sayName()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链依然在引用这个活动对象,换句话说,sayName()函数执行完后,其执行环境的作用域链会被销毁,但他的活动对象会留在内存中,知道匿名函数会销毁。这个也是后面要讲到的内存泄露的问题。

作用域链问题不写那么多了,写书上的东西也很累 o(?□?)o

3. 闭包的实例

实例1:实现累加

// 方式1
var a = 0;
var add = function(){
a++;
console.log(a)
}
add();
add();
//方式2 :闭包
var add = (function(){
var a = 0;
return function(){
a++;
console.log(a);
}
})();
console.log(a); //undefined
add();
add();

相比之下方式2更加优雅,也减少全局变量,将变量私有化

实例2 :给每个li添加点击事件

var oli = document.getElementsByTagName('li');
var i;
for(i = 0;i < 5;i++){
oli[i].onclick = function(){
alert(i);
}
}
console.log(i); // 5
//执行匿名函数
(function(){
alert(i); //5
}());

上面是一个经典的例子,我们都知道执行结果是都弹出5,也知道可以用闭包解决这个问题,但是我刚开始始终不能明白为什么每次弹出都是5,为什么闭包可以解决这问题。后来捋一捋还是把它弄清晰了:

a. 先来分析没用闭包前的情况:for循环中,我们给每个li点击事件绑定了一个匿名函数,匿名函数中返回了变量i的值,当循环结束后,变量i的值变为5,此时我们再去点击每个li,也就是执行相应的匿名函数(看上面的代码),这是变量i已经是5了,所以每个点击弹出5. 因为这里返回的每个匿名函数都是引用了同一个变量i,如果我们新建一个变量保存循环执行时当前的i的值,然后再让匿名函数应用这个变量,最后再返回这个匿名函数,这样就可以达到我们的目的了,这就是运用闭包来实现的!

b. 再来分析下运用闭包时的情况:

var oli = document.getElementsByTagName('li');
var i;
for(i = 0;i < 5;i++){
oli[i].onclick = (function(num){
var a = num; // 为了说明问题
return function(){
alert(a);
}
})(i)
}
console.log(i); // 5

这里for循环执行时,给点击事件绑定的匿名函数传递i后立即执行返回一个内部的匿名函数,因为参数是按值传递的,所以此时形参num保存的就是当前i的值,然后赋值给局部变量 a,然后这个内部的匿名函数一直保存着a的引用,也就是一直保存着当前i的值。 所以循环执行完毕后点击每个li,返回的匿名函数执行弹出各自保存的 a 的引用的值。

4. 闭包的运用

我们来看看闭包的用途。事实上,通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。

1. 匿名自执行函数

我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,比如UI的初始化,那么我们可以使用闭包:

//将全部li字体变为红色
(function(){ 
var els = document.getElementsByTagName('li');
for(var i = 0,lng = els.length;i < lng;i++){
els[i].style.color = 'red';
} 
})();

我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此els,i,lng这些局部变量在执行完后很快就会被释放,节省内存!

关键是这种机制不会污染全局对象。

2. 实现封装/模块化代码

var person= function(){ 
//变量作用域为函数内部,外部无法访问 
var name = "default"; 
return { 
getName : function(){ 
return name; 
}, 
setName : function(newName){ 
name = newName; 
} 
} 
}();
console.log(person.name);//直接访问,结果为undefined 
console.log(person.getName()); //default 
person.setName("jozo"); 
console.log(person.getName()); //jozo

3. 实现面向对象中的对象

这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,我们可以模拟出这样的机制。还是以上边的例子来讲:

function Person(){ 
var name = "default"; 
return { 
getName : function(){ 
return name; 
}, 
setName : function(newName){ 
name = newName; 
} 
} 
}; 
var person1= Person(); 
print(person1.getName()); 
john.setName("person1"); 
print(person1.getName()); // person1 
var person2= Person(); 
print(person2.getName()); 
jack.setName("erson2"); 
print(erson2.getName()); //person2

Person的两个实例person1 和 person2 互不干扰!因为这两个实例对name这个成员的访问是独立的 。

5. 内存泄露及解决方案

垃圾回收机制

说到内存管理,自然离不开JS中的垃圾回收机制,有两种策略来实现垃圾回收:标记清除 和 引用计数

标记清除:垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,如果变量再被标记则表示此变量准备被删除。 2008年为止,IE,Firefox,opera,chrome,Safari的javascript都用使用了该方式;

引用计数:跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个值的引用次数就是1,如果这个值再被赋值给另一个变量,则引用次数加1。相反,如果一个变量脱离了该值的引用,则该值引用次数减1,当次数为0时,就会等待垃圾收集器的回收。

这个方式存在一个比较大的问题就是循环引用,就是说A对象包含一个指向B的指针,对象B也包含一个指向A的引用。 这就可能造成大量内存得不到回收(内存泄露),因为它们的引用次数永远不可能是 0 。早期的IE版本里(ie4-ie6)采用是计数的垃圾回收机制,闭包导致内存泄露的一个原因就是这个算法的一个缺陷。

我们知道,IE中有一部分对象并不是原生额javascript对象,例如,BOM和DOM中的对象就是以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数。因此,虽然IE的javascript引擎采用的是标记清除策略,但是访问COM对象依然是基于引用计数的,因此只要在IE中设计COM对象就会存在循环引用的问题!

举个栗子:

window.onload = function(){
var el = document.getElementById("id");
el.onclick = function(){
alert(el.id);
}
}

这段代码为什么会造成内存泄露?

el.onclick= function () {
alert(el.id);
};

执行这段代码的时候,将匿名函数对象赋值给el的onclick属性;然后匿名函数内部又引用了el对象,存在循环引用,所以不能被回收;

解决方法:

window.onload = function(){
var el = document.getElementById("id");
var id = el.id; //解除循环引用
el.onclick = function(){
alert(id); 
}
el = null; // 将闭包引用的外部函数中活动对象清除
}

以上所述是小编给大家介绍的JS闭包作用域链垃圾回收内存泄露相关知识小结,希望对大家有所帮助!

Javascript 相关文章推荐
js 解决“options为空或不是对象”
Dec 22 Javascript
js 三级关联菜单效果实例
Aug 13 Javascript
jquery及原生js获取select下拉框选中的值示例
Oct 25 Javascript
js和jq使用submit方法无法提交表单的快速解决方法
May 17 Javascript
JS获取字符串实际长度(包含汉字)的简单方法
Aug 11 Javascript
解析JavaScript实现DDoS攻击原理与保护措施
Dec 26 Javascript
React Native中NavigatorIOS组件的简单使用详解
Jan 27 Javascript
使用vue-cli打包过程中的步骤以及问题的解决
May 08 Javascript
深入理解JS的事件绑定、事件流模型
May 13 Javascript
setTimeout与setInterval的区别浅析
Mar 23 Javascript
如何为你的JavaScript代码日志着色详解
Apr 08 Javascript
微信小程序网络请求实现过程解析
Nov 06 Javascript
js流动式效果显示当前系统时间
May 16 #Javascript
JavaScript禁止复制与粘贴的实现代码
May 16 #Javascript
JavaScript代码性能优化总结(推荐)
May 16 #Javascript
AngularJS学习笔记之依赖注入详解
May 16 #Javascript
javascript表单事件处理方法详解
May 15 #Javascript
基于jquery实现ajax无刷新评论
Aug 19 #Javascript
JavaScript代码性能优化总结篇
May 15 #Javascript
You might like
PHP常用的文件操作函数经典收藏
2013/04/02 PHP
解析php利用正则表达式解决采集内容排版的问题
2013/06/20 PHP
ThinkPHP提示错误Fatal error: Allowed memory size的解决方法
2015/02/12 PHP
php一个解析字符串排列数组的方法
2015/05/12 PHP
php版微信自动获取收货地址api用法示例
2016/09/22 PHP
PHP生成二维码与识别二维码的方法详解【附源码下载】
2019/03/07 PHP
PHP实现微信提现功能(微信商城)
2019/11/21 PHP
js 获取(接收)地址栏参数值的方法
2013/04/01 Javascript
浅谈javascript六种数据类型以及特殊注意点
2013/12/20 Javascript
javascript中实现兼容JAVA的hashCode算法代码分享
2020/08/11 Javascript
高性能JavaScript循环语句和条件语句
2016/01/20 Javascript
js实现select二级联动下拉菜单
2020/04/17 Javascript
vue.js将unix时间戳转换为自定义时间格式
2017/01/03 Javascript
使用jquery的jsonp如何发起跨域请求及其原理详解
2017/08/17 jQuery
vue+VeeValidate 校验范围实例详解(部分校验,全部校验)
2018/10/19 Javascript
ztree加载完成后显示勾选节点的实现代码
2018/10/22 Javascript
使用jquery模拟a标签的click事件无法实现跳转的解决
2018/12/04 jQuery
详解小程序退出页面时清除定时器
2019/04/28 Javascript
vue实现pdf文档在线预览功能
2019/11/26 Javascript
[06:37]2014DOTA2国际邀请赛 昔日王者渴望重回巅峰
2014/07/12 DOTA
[02:44]完美大师赛主赛事淘汰赛第二日观众采访
2017/11/24 DOTA
python计算两个数的百分比方法
2018/06/29 Python
python高阶爬虫实战分析
2018/07/29 Python
python做反被爬保护的方法
2019/07/01 Python
Python监控服务器实用工具psutil使用解析
2019/12/19 Python
jupyter notebook的安装与使用详解
2020/05/18 Python
浅谈TensorFlow中读取图像数据的三种方式
2020/06/30 Python
Sephora丝芙兰菲律宾官方网站:购买化妆品和护肤品
2017/04/05 全球购物
洛杉矶健身中心女性专用运动服饰品牌:Marika
2018/05/09 全球购物
英智兴达软件测试笔试题
2016/10/12 面试题
20年同学聚会感言
2014/02/03 职场文书
工商局领导班子存在的问题整改措施思想汇报
2014/10/05 职场文书
个人整改方案范文
2014/10/25 职场文书
反邪教警示教育活动总结
2015/05/09 职场文书
阿里云服务器搭建Php+Apache运行环境的详细过程
2021/05/15 PHP
端午节将至,用Python爬取粽子数据并可视化,看看网友喜欢哪种粽子吧!
2021/06/11 Python