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

补充:

闭包(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闭包作用域链垃圾回收内存泄露相关知识小结,希望对大家有所帮助!

(0)

相关推荐

  • 深入理解Javascript中的作用域链和闭包

    首先我们回顾下之前一篇关于介绍数组遍历的文章: 请先看上一篇中提到的for循环代码: var array = []; array.length = 10000000;//(一千万) for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi'; } var t1 = +new Date(); for(var i=0,length=array.length;i<length;i++){ } var t2 = +new Date();

  • 深入Javascript函数、递归与闭包(执行环境、变量对象与作用域链)使用详解

    函数表达式 1.JavaScript中定义函数有2钟方法: 1-1.函数声明: 复制代码 代码如下: function funcName(arg1,arg2,arg3){  //函数体} ①name属性:可读取函数名.非标准,浏览器支持:FF.Chrome.safari.Opera. ②函数声明提升:指执行代码之前会先读取函数声明.即函数调用可置于函数声明之前. 1-2.函数表达式: 复制代码 代码如下: var funcName = function(arg1,arg2,arg3){  //函

  • javascript从作用域链谈闭包

    神马是闭包 关于闭包的概念,是婆说婆有理. 闭包是指有权访问另外一个函数作用域中的变量的函数 这概念有点绕,拆分一下.从概念上说,闭包有两个特点: 1.函数 2.能访问另外一个函数作用域中的变量 在ES 6之前,Javascript只有函数作用域的概念,没有块级作用域(但catch捕获的异常 只能在catch块中访问)的概念(IIFE可以创建局部作用域).每个函数作用域都是封闭的,即外部是访问不到函数作用域中的变量. function getName() { var name = "美女的名字&

  • JavaScript中的作用域链和闭包

    作用域 全局作用域 局部作用域 作用域链 执行上下文 活动对象 闭包 闭包优化 JavaScript中出现了一个以前没学过的概念--闭包.何为闭包?从表面理解即封闭的包,与作用域有关.所以,说闭包以前先说说作用域. 作用域(scope) 通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域,作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突. 全局作用域(Global Scope) 在代码中任何地方都能访问到的对象拥有全局作用域,以下几种情形拥有全局作

  • 图解Javascript——作用域、作用域链、闭包

    什么是作用域? 作用域是一种规则,在代码编译阶段就确定了,规定了变量与函数的可被访问的范围.全局变量拥有全局作用域,局部变量则拥有局部作用域. js是一种没有块级作用域的语言(包括if.for等语句的花括号代码块或者单独的花括号代码块都不能形成一个局部作用域),所以js的局部作用域的形成有且只有函数的花括号内定义的代码块形成的,既函数作用域. 什么是作用域链? 作用域链是作用域规则的实现,通过作用域链的实现,变量在它的作用域内可被访问,函数在它的作用域内可被调用. 作用域链是一个只能单向访问的链

  • 浅析JavaScript作用域链、执行上下文与闭包

    闭包和作用域链是JavaScript中比较重要的概念,这两天翻阅了一些资料,把相关知识点给大家总结了以下. JavaScript 采用词法作用域(lexical scoping),函数执行依赖的变量作用域是由函数定义的时候决定,而不是函数执行的时候决定.以下面的代码片段举例说明,通常来说(基于栈的实现,如 C 语言) foo 被调用之后函数内的本地变量 scope 会被释放,但是从词法上看 foo 的内嵌匿名函数中 scope 应该指的是 foo 的本地变量 scope ,并且实际上代码的运行结

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

    补充: 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 闭包的特性 闭包有三个特性: 1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会被垃圾回收机制回收 闭包的定义及其优缺点 闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量 闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露. 闭包是javascript

  • C指针原理教程之垃圾回收-内存泄露

    一.内存泄露 1.正常的链表操作 下面程序建立一个10元素的链表,输出它们的节点,每个节点是一个员工的工号和年龄.最后删除每个节点,释放列表. dp@dp:~/memorytest % cat 1.c #include <stdlib.h> #include <stdio.h> //code:myhaspl@myhaspl.com //author:myhaspl //date:2014-01-10 typedef struct listnode mynode; struct li

  • 浅谈JavaScript 执行环境、作用域及垃圾回收

    执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为.每个执行环境都有一个与之关联的变量对象. 全局执行环境是最外围的一个执行环境.根据JavaScript实现所在的宿主环境不同,表示执行环境的对象也不一样.在Web浏览器中,全局执行环境被认为是window对象.因此,所有的全局变量和函数都是作为window对象的属性和方法创建的. 变量对象:环境中定义的所有变量和函数都保存在这个对象中. 作用域链:当代码在一个环境中执行时,会创建变量对象的一个作用域链.作用域链的用途是保证对执行环

  • JAVA内存空间相关知识汇总

    Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天我们再次深入Java核心,详细介绍一下Java在内存分配方面的知识.一般Java在内存分配时会涉及到以下区域: ◆寄存器:我们在程序中无法控制 ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中 ◆堆:存放用new产生的数据 ◆静态域:存放在对象中用static定义的静态成员 ◆常量池:存放常量 ◆非RAM存储:硬盘等永久存储空间 Java内存

  • 基于java中stack与heap的区别,java中的垃圾回收机制的相关介绍

    #. 在java中有两类内存.分别称为stack(堆栈)和heap(堆). stack是程序内存空间,因此所有的基本类型和对象的引用是存在stack中. heap是java虚拟机储存对象的,它是一个巨大的内存,当你创造一个对象,java虚拟机把对象放入heap中,把创造的对象的地址放入stack中. 因此,基本类型.对象的引用储存在stack中:对象储存在heap中. #. java中的垃圾回收机制 当你new一个新的对象,java分配必需的内存.当你用完一个对象时,java的垃圾回收器为你把内

  • JVM内存结构相关知识解析

    最近在看< JAVA并发编程实践 >这本书,里面涉及到了 Java 内存模型,通过 Java 内存模型顺理成章的来到的 JVM 内存结构,关于 JVM 内存结构的认知还停留在上大学那会的课堂上,一直没有系统的学习这一块的知识,所以这一次我把< 深入理解Java虚拟机JVM高级特性与最佳实践 >.< Java虚拟机规范 Java SE 8版 >这两本书中关于 JVM 内存结构的部分都看了一遍,算是对 JVM 内存结构有了新的认识.JVM 内存结构是指:Java 虚拟机定义

  • JS面向对象的程序设计相关知识小结

    面向对象的语言有一个标志,即拥有类的概念,抽象实例对象的公共属性与方法,基于类可以创建任意多个实例对象,一般具有封装.继承.多态的特性!但JS中对象与纯面向对象语言中的对象是不同的,ECMA标准定义JS中对象:无序属性的集合,其属性可以包含基本值.对象或者函数.可以简单理解为JS的对象是一组无序的值,其中的属性或方法都有一个名字,根据这个名字可以访问相映射的值(值可以是基本值/对象/方法). 一.理解对象: 第一种:基于Object对象 var person = new Object(); pe

  • Java内存模型相关知识总结

    [1]CPU和缓存的一致性 我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道.而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存啦. ​ 刚开始,还相安无事的,但是随着CPU技术的发展,CPU的执行速度越来越快.而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间. ​ 所以,人们想出来了一个好的办法,就是在CPU和内存之间增

  • 总结JavaScript在IE9之前版本中内存泄露问题

    IE9之前的版本对JScript对象和COM对象使用不同的垃圾回收例程(COM对象采用"引用计数"收集策略),因此闭包在IE的这些版本中会导致一些特殊问题.具体来说,如果闭包的作用域中保存着一个HTML元素,那么就意味着该元素将无法被销毁. 来看下面的例子: function assignHandler() { var elem = document.getElementById('elem_id'); elem.onclick = function(evt) { alert(elem

  • 跟我学习javascript的垃圾回收机制与内存管理

    一.垃圾回收机制-GC Javascript具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存. 原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存. JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行. 不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生

随机推荐