转载

谈js中的作用域链和闭包

什么是作用域

在编程语言中,作用域控制着变量与参数的可见性及生命周期,它能减少名称冲突,而且提供了自动内存管理(javascript 语言精粹)

静态作用域

再者,js不像其他的编程语言一样,拥有着块级作用域,就像下面一段代码。

function afunction(){  var a = 'sf';  console.log(b);  console.log(c);  var b = function(){   console.log('这是b中的内容');  }  function c(){   console.log('这是c中的内容');  }  (function d(){   console.log('这是d中的内容');  })() } 

实用var声明的变量和函数声明将会进行 声明提前afunction 函数的执行环境中,故上述代码相当于以下的代码,在一个变量声明提前的时候,其值为 undefined ,而函数声明则是将函数体作为值。

function afunction(){  var a;  var b;  function c(){   console.log('这是c中的内容');  }  a = 'sf';  console.log(b);  console.log(c);  b = function(){   console.log('这是b中的内容');  }  (function d(){   console.log('这是d中的内容');  })() } 

全局作用域与局部作用域

将上述的代码稍作改动如下

var outer = 'outer'; function afunction(){     function c(){         console.log('这是c中的内容');     }     a = 'sf';     console.log(outer); }

我们在 afunction 函数的外部定义了 outer 变量,假设这段代码运行在浏览器上,那么变量提前的过程中 outer 变量被声明在了 window 作用域上,也就是浏览器中的 全局作用域 上,而函数中的变量则在函数运行时被声明在了 afunction 作用域上,这个就是 局部作用域 ,在这个局部作用域中, outer 变量被访问到了,这种 跨作用域 的读取变量的形式就是根据作用域链来实现的。

什么是作用域链

在js中,函数也是对象,函数与其他的对象一样,拥有可以访问的属性, [[Scope]] 就是其中的一个属性,它指明了哪些东西可以被函数访问。

考虑下面的函数

function add(a,b){     var sum = a + b;     return sum; }

当函数 add 创建 时候, add 的[[Scope]]属性会指向作用域链对象,该对象的初始位置指向全局对象,如下图所示。

谈js中的作用域链和闭包

var t = add(1,2);

上述语句执行了 add 函数,对于函数的每一次执行,浏览器会创建一个 执行环境 的内部对象,一个执行环境定义了一个函数执行时的环境。函数的每次执行时对应的执行环境都说唯一的。每一个执行环境都有自己的作用域链,此对象的局部变量, thisarguments 等组成 活动对象 ,插入在作用域链对象的最前端,也就是图中所示的0号位置,当运行结束后,执行环境和活动对象都将销毁。

函数的执行过程中,每遇到一个变量,都会从作用域链的顶部,也就是0号位置查找该变量,如果查找成功则返回,查找失败则按照作用域链查找下一个位置的对象,该例子中也就是1号位置的全局对象。

谈js中的作用域链和闭包

作用域链带来的性能问题

如上面所讨论的那样,每一次遇到读取变量的时候,都意味着一次搜索作用域链的过程,如果搜索的作用域链的层次越多的话,将严重影响性能。

所以,当在函数中使用全局变量的时候,所产生的代价是最大的,因为全局对象一直处于作用域链的最末位置,读取局部变量是最快的。

所以,一个提高效率的规则是尽可能的使用局部变量。如下面的代码所示。

function demo(){     var d = document,         bd = d.body,         div = d.getElementsByTagName('div');     d.getElementById('id1').innerHTML = 'aaa';     //(许多使用document,body和div的操作) }

上面的代码首先将全局的 document 对象保存在了局部变量d中,这样当下次频繁的使用 document 对象时,仅仅需要从局部变量中即可获得。

动态作用域

js中实用的是静态作用域,作用域链一般不可改变,但是 withtry-catch 可以改变作用域链,发生在函数的执行时候

with语句

function withTest(){  var foo = 'sf';  var obj = {foo:'abc'};  with(obj){   function f(){    alert(foo);   }   (function(){    alert(foo);   })();   f();  } } withTest(); 

在函数声明的时候,作用域链没有考虑 with 的情况,当函数执行的时候,动态生成 with 的对象,推入在作用域链的首位,这就意味着函数的局部变量存在作用域链的第二个位置,访问的代价提高了,虽然访问 with 对象的代价降低了,完全可以将 with 对象保存在局部变量中,故 with 语句不推荐使用。

try-catch语句

try{     anErrorFunction(); }catch(e){     errorHandler(e); }

由于 catch 语句中只有一条语句,将error传递给 errorHandler 函数,所以运行时作用域链的改变不会影响性能。

什么是闭包

闭包是允许函数访问局部作用域之外的数据。即使外部函数已经退出,外部函数的变量仍可以被内部函数访问到。因此闭包的实现需要三个条件:

  • 内部函数实用了外部函数的变量

  • 外部函数已经退出

  • 内部函数可以访问

function a(){  var x = 0;  return function(y){   x = x + y;   return x;  } } var b = a(); b(1); 

谈js中的作用域链和闭包

上述代码在执行的时候,b得到的是闭包对象的引用,虽然a执行完毕后,但是a的活动对象由于闭包的存在并没有被销毁,在执行 b(1) 的时候,仍然访问到了x变量,并将其加1,若在此执行 b(1) ,则x是2,因为闭包的引用b并没有消除。

一个经典的闭包的实例

//ul下面有3个li,实现点击每个li,弹出li的序号 for(var i = 0,len = lis.length;i < len; i++){     lis[i].onclick = function(i){         return function(){             alert(i);         }     }(i); }

在这里,没有把闭包直接给 onclick 事件,而是先定义了一个自执行函数,该函数中包含着闭包的函数,i的值被保存在自执行的函数中,当闭包函数执行后,会从自执行函数中查找i,达到“保存”变量的目的。

注:匿名函数中的 this 指向的是window ,故在匿名闭包函数使用父函数的 this 指针时,需要将其存储下来,如 var that = this;

闭包的作用

  • 模块化代码

  • 私有成员

  • 避免全局变量的污染

  • 希望一个变量长期驻扎在内存中

使用闭包所造成的性能问题

如上面的描述,当执行闭包函数后,父函数所保留下来的活动对象并不是在闭包函数的作用域链的首位(首位存放的是闭包的活动对象),当频繁的访问跨作用域的标识符时候,每次都会造成性能的损失,我们仍然可以将常用的跨作用域变量存储在局部变量中,直接访问该局部变量

实用闭包所造成的内存泄露问题(IE9以下)

IE9及以下的版本使用的是 引用计数 的内存回收机制,当引用计数为0的时候将会回收,但有一种循环引用的情况

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

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

(javascript 高级程序设计(第三版))

解决方法:

window.onload = function(){  var el = document.getElementById("id");  var id = el.id; //解除了循环引用  el.onclick = function(){   alert(id); //并没有出现循环引用  }  el = null; // 将闭包引用的外部活动对象清除 } 
正文到此结束
Loading...