闭包那些事儿

闭包
一:概念
是指有权访问另一个函数作用域中的变量的函数。创建一个闭包的常见方式:就是在一个函数内部创建另一个函数。利用了javascript 函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。闭包可以简单理解成“定义在一个函数内部的函数“。
二.闭包的用途
闭包可以用在许多地方。它的最大用处有两个, 一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会在f1调用后被自动清除。
1.function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000
result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
2.
function f1(){
    var n=999;
    function f2(){
      alert(n);
    }
    return f2;
 }
  var result=f1();
  result(); // 999
函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们就可以在f1外部读取它的内部变量.
三.使用闭包的注意点
(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
四.关于立即执行函数:
1.立即执行函数的两种常见形式:( function(){…} )()和( function (){…} () ),一个是一个匿名函数包裹在一个括号运算符中,后面再跟一个小括号,另一个是一个匿名函数后面跟一个小括号,然后整个包裹在一个括号运算符中,这两种写法是等价的。要想立即执行函数能做到立即执行,要注意两点,一是函数体后面要有小括号(),二是函数体必须是函数表达式而不能是函数声明。

2.使用立即执行函数的好处
通过定义一个匿名函数,创建了一个新的函数作用域,相当于创建了一个“私有”的命名空间,该命名空间的变量和方法,不会破坏污染全局的命名空间。此时若是想访问全局对象,将全局对象以参数形式传进去即可
3.如果在即时函数中定义的变量被内部函数捕获的话,那么这个即时函数就形成了一个闭包。例如:
(function() { var a = 123; button.onclick = function() { alert(a); };})();
实际上,人们经常用即时函数来构造闭包。为什么呢?因为即时函数只能执行一次,执行完了立马消失,所以不会重复创建闭包,也不会对当前作用域造成任何污染。
所以结论就是:
  • 相同点:他们都是函数的一种特殊形态,并且可以共存。而且闭包配合即时函数“口味更佳”。
  • 不同点:即时函数是定义一个函数,并立即执行。它只能被使用一次,相当于“阅后即焚”。闭包是指一个函数与它捕获的外部变量的合体,按照MDN的说法,闭包就像一个对象---一个具有一个方法(行为)和一个或多个私有字段(状态)的对象。从这个角度看,闭包是符合面向对象的封装思想的。
四.经典面试题
1.
var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
 alert(object.getNameFunc()()); //The Window 匿名函数的作用域指向的是window

var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };
    }
  };
  alert(object.getNameFunc()()); //that.name;
2.
function count() { var arr = []; for (var i=1; i<=3; i++) { arr.push(function () { return i * i; }); } return arr;}var results = count();var f1 = results[0]; //16var f2 = results[1]; //16var f3 = results[2]; //16
原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了4,因此最终结果为16。
想要获取里面的值得话:修改代码如下
function count() { var arr = []; for (var i=1; i<=3; i++) { arr.push((function (n) { return function () { return n * n; } })(i)); } return arr;}var results = count();var f1 = results[0];var f2 = results[1];var f3 = results[2];
3. function foo(){ var a = 2; function bar(){ console.log(a); } return bar;} var baz = foo();baz(); //2
从上述说明的第5步可以看出,由于闭包bar()函数的原因,虽然foo()函数的执行环境销毁了,但其变量对象一直存在于内存中,就是为了能够使得调用bar()函数时,可以通过作用域链访问到父函数foo(),并得到其变量对象中储存的变量值。直到页面关闭,foo()函数的变量对象才会和全局的变量对象一起被销毁,从而释放内存空间
  由于闭包占用内存空间,所以要谨慎使用闭包。尽量在使用完闭包后,及时解除引用,以便更早释放内存
//通过将baz置为null,解除引用 function foo(){ var a = 2; function bar(){ console.log(a); //2 } return bar;} var baz = foo();baz(); baz = null;
五.关于this
1.this对象是在运行时基于函数的执行环境绑定的:在全局环境中,this是指window,而当函数被作为某个对象的方法调用时,this等于那个对象。
不过,匿名函数的执行环境具有全局性,因此其This对象通常指向window
2.使用场景:
有对象就指向调用对象
没调用对象就指向全局对象
用new构造就指向新对象
通过 apply 或 call 或 bind 来改变 this 的所指。
(1).被当做对象的方法被调用
如果该函数是被当做某一个对象的方法,那么该函数的this指向该对象;
(2)用new构造就指向新对象:
.所谓构造函数,就是通过这个函数生成一个新对象(object)。这时,this就指这个新对象。
代码如下:
  function test(){
    this.x = 1;
  }
  var o = new test();
  alert(o.x); // 1
(3)明确调用this,使用call和apply
apply() 方法接受两个参数第一个是函数运行的作用域,另外一个是一个参数数组(arguments)。
call() 方法第一个参数的意义与 apply() 方法相同,只是其他的参数需要一个个列举出来。
var myObject = {value: 100};
 
var foo = function(){
 console.log(this);
};
 
foo(); // 全局变量 global
foo.apply(myObject); // { value: 100 }
foo.call(myObject); // { value: 100 }

补充:
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.οnclick= 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; // 将闭包引用的外部函数中活动对象清除}
6. 总结闭包的优缺点
优点:
  • 可以让一个变量常驻内存 (如果用的多了就成了缺点
  • 避免全局变量的污染
  • 私有化变量
缺点
  • 因为闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存
  • 引起内存泄露

  • (前端小白,如有错误,欢迎指正~~)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值