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 相关文章推荐
读jQuery之三(构建选择器)
Jun 11 Javascript
jquery的map与get方法详解
Nov 04 Javascript
JavaScrip实现PHP print_r的数功能(三种方法)
Nov 12 Javascript
用于deeplink的js方法(判断手机是否安装app)
Apr 02 Javascript
js禁止页面刷新与后退的方法
Jun 08 Javascript
JavaScript toUpperCase()方法使用详解
Aug 26 Javascript
Js动态设置rem来实现移动端字体的自适应代码
Oct 14 Javascript
分享ES6的7个实用技巧
Jan 18 Javascript
微信小程序实现获取小程序码和二维码java接口开发
Mar 29 Javascript
layui点击弹框页面 表单请求的方法
Sep 21 Javascript
Vue实现一种简单的无限循环滚动动画的示例
Jan 10 Vue.js
vue完美实现el-table列宽自适应
May 08 Vue.js
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 网页游戏开发入门教程一(webgame+design)
2009/10/26 PHP
PHP array_multisort() 函数的深入解析
2013/06/20 PHP
使用URL传输SESSION信息
2015/07/14 PHP
微信公众平台开发教程⑤ 微信扫码支付模式介绍
2019/04/10 PHP
javascript 中对象的继承〔转贴〕
2007/01/22 Javascript
将CKfinder整合进CKEditor3.0的新方法
2010/01/10 Javascript
jquery 页面全选框实践代码
2010/04/02 Javascript
jquery 如何动态添加、删除class样式方法介绍
2012/11/07 Javascript
类似php的js数组的in_array函数自定义方法
2013/12/27 Javascript
Javascript学习笔记之 函数篇(三) : 闭包和引用
2014/11/23 Javascript
JavaScript中number转换成string介绍
2014/12/31 Javascript
jquery通过closest选择器修改上级元素的方法
2015/03/17 Javascript
详解Bootstrap四种图片样式
2016/01/04 Javascript
Angular Module声明和获取重载实例代码
2016/09/14 Javascript
Jquery Easyui日历组件Calender使用详解(23)
2016/12/18 Javascript
jquery图片放大镜效果
2017/06/23 jQuery
JS实现unicode和UTF-8之间的互相转换互转
2017/07/05 Javascript
解决angular2 获取到的数据无法实时更新的问题
2018/08/31 Javascript
vue+webpack中配置ESLint
2018/11/07 Javascript
vue结合element-ui使用示例
2019/01/24 Javascript
ant-design-vue按需加载的坑的解决
2020/05/14 Javascript
基于ajax及jQuery实现局部刷新过程解析
2020/09/12 jQuery
解决vue字符串换行问题(绝对管用)
2020/08/06 Javascript
[01:51]开启你的城市传奇 完美世界城市挑战赛开始报名
2018/10/09 DOTA
Python实现在线音乐播放器
2017/03/03 Python
Windows平台Python连接sqlite3数据库的方法分析
2017/07/12 Python
Flask模拟实现CSRF攻击的方法
2018/07/24 Python
python 设置xlabel,ylabel 坐标轴字体大小,字体类型
2019/07/23 Python
Python range、enumerate和zip函数用法详解
2019/09/11 Python
python argparse传入布尔参数false不生效的解决
2020/04/20 Python
澳大利亚男士西服品牌:M.J.Bale
2018/02/06 全球购物
会计电算化专业毕业生自荐信
2013/12/20 职场文书
手工社团活动方案
2014/02/17 职场文书
先进个人材料怎么写
2014/12/30 职场文书
Java 超详细讲解数据结构中的堆的应用
2022/04/02 Java/Android
超越Nginx的Web服务器caddy优雅用法
2022/06/21 Servers