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 模拟气泡屏保效果代码
Jul 10 Javascript
Jquery实现点击切换图片并隐藏显示内容(2种方法实现)
Apr 11 Javascript
jquery实现文字由下到上循环滚动的实例代码
Aug 09 Javascript
jquery判断小数点两位和自动删除小数两位后的数字
Mar 19 Javascript
基于jQuery实现下拉框
Nov 24 Javascript
使用Javascript简单实现图片无缝滚动
Dec 05 Javascript
JavaScript动态创建div等元素实例讲解
Jan 06 Javascript
AngularJS之页面跳转Route实例代码
Mar 10 Javascript
JS实现中文汉字按拼音排序的方法
Oct 09 Javascript
JavaScript数组特性与实践应用深入详解
Dec 30 Javascript
一文了解Vue中的nextTick
May 06 Javascript
浅谈Vuex注入Vue生命周期的过程
May 20 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
ThinkPHP中redirect用法分析
2014/12/05 PHP
php 实现Hash表功能实例详解
2016/11/29 PHP
理清PHP在Linxu下执行时的文件权限方法
2017/06/07 PHP
学习ExtJS border布局
2009/10/08 Javascript
基于jquery的无缝循环新闻列表插件
2011/03/07 Javascript
jquery focus(fn),blur(fn)方法实例代码
2011/12/16 Javascript
jQuery提示效果代码分享
2014/11/20 Javascript
node.js中的fs.writeFile方法使用说明
2014/12/14 Javascript
jQuery实现的漂亮表单效果代码
2015/08/18 Javascript
javascript实现tab响应式切换特效
2016/01/29 Javascript
Jquery实现遮罩层的简单实例(就是弹出DIV周围都灰色不能操作)
2016/07/14 Javascript
ES6新特性之数组、Math和扩展操作符用法示例
2017/04/01 Javascript
Node.js环境下Koa2添加travis ci持续集成工具的方法
2017/06/19 Javascript
JavaScript中in和hasOwnProperty区别详解
2017/08/04 Javascript
Bootstrap框架建立树形菜单(Tree)的实例代码
2017/10/30 Javascript
vue2.0 资源文件assets和static的区别详解
2018/04/08 Javascript
JS实现快递单打印功能【推荐】
2018/06/21 Javascript
vue-router的HTML5 History 模式设置
2018/09/08 Javascript
JavaScript中的this/call/apply/bind的使用及区别
2020/03/06 Javascript
详解vue中使用transition和animation的实例代码
2020/12/12 Vue.js
Vue中inheritAttrs的使用实例详解
2020/12/31 Vue.js
[01:50]2014DOTA2西雅图邀请赛 专访欢乐周宝龙
2014/07/08 DOTA
Python编写百度贴吧的简单爬虫
2015/04/02 Python
python如何实现excel数据添加到mongodb
2015/07/30 Python
Django原生sql也能使用Paginator分页的示例代码
2017/11/15 Python
详解python中Numpy的属性与创建矩阵
2018/09/10 Python
PyCharm2018 安装及破解方法实现步骤
2019/09/09 Python
python输入错误后删除的方法
2019/10/12 Python
利用CSS的Sass预处理器(框架)来制作居中效果
2016/03/10 HTML / CSS
Dyson加拿大官方网站:购买戴森吸尘器,风扇,冷热器及配件
2016/10/26 全球购物
人事部主管岗位职责
2013/12/26 职场文书
项目施工员岗位职责
2014/03/09 职场文书
新年晚会主持词
2014/03/24 职场文书
机电一体化专业毕业生自荐信
2014/06/19 职场文书
人事局接收函
2015/01/30 职场文书
Python的这些库,你知道多少?
2021/06/09 Python