作用域,作用域链,闭包

822 阅读5分钟

作用域,作用域链,闭包

作用域

作用域:代码执行环境,变量起作用的区域,即变量可以被访问到的区域

全局环境就是全局作用域,是最外围的执行环境。函数的执行环境就是私有作用域,他们都是栈内存。

作用域就是代码执行开辟栈内存。

常见变量作用域:词法作用域,变量作用域。

词法作用域:(静态作用域)函数的作用域在函数定义的时候就决定了。

动态作用域:函数的作用域是在函数调用的时候才决定的。

JavaScript采用的是词法作用域

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链(作用域形成的链条)

注意:

  1. 全局变量对象始终都是作用域链上的最后一个对象。
  2. 内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境的任何变量和函数。
  3. 作用域链的前端,始终都是当前执行的代码所在环境的变量对象。

下面来分析几个例子:

第一个:

let a = 1;
function foo(){
  let a = 2;
  function baz(){            
    console.log(a);     
  }
  bar(baz);
}
function bar(fn){
  let a = 3;    
  fn();
}
foo();

(1)假设采用静态作用域分析:

首先,执行foo函数,foo函数中包含baz函数,先从baz函数内部查找是否有局部变量,没有,根据书写位置,查找上一层代码,也就是a=2。所以打印结果为2.

(2)假设采用动态作用域分析:

首先,执行foo函数,foo函数中包含baz函数,也是从baz函数内部查找是否有局部变量a。如果没有,就从调用函数的作用域,也就是bar函数内部查找 a变量,在bar函数中a=3,所以打印的结果为3。

但由于JS中采用的是静态作用域,所以这个例子的打印结果为2.

接下来是第二个例子:

function foo() {
    let a = 1;
    function bar() {
        let a = 2;
        function baz() {
            let a = 3;
            console.log(a);
        }
        baz();
    }
    bar();
}
foo();

静态作用域分析:

首先,执行foo函数,foo函数中包含bar函数,bar函数中又包含baz函数,baz函数中有局部变量a=3,所以打印结果为a=3.

闭包

什么是闭包?

闭包就是能够读取其他函数内部变量的函数。

在javascript中,只有函数内 部的子函数才能读取局部变量,所以闭包可以理解成"定义在一个函数内部的函数"。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。

闭包是怎样的形成的?

产生一个闭包 创建闭包最常见方式,就是在一个函数内部创建另一个函数。下面例子中的 bar就是一个闭包:

function fn1(){
  var a = 1,b = 2;
  function fn2(){
    return a+b;
  }
  return fn2;
}
var fn3 = fn1();
fn3();

闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。

分析:

fn2的词法作用域能访问到fn1的作用域。

将fn2作为一个返回值返回。

fn1执行后,将fn2的引用赋值给fn3

执行fn3,返回a+b的值

当fn1函数执行后,其作用域应该被销毁并释放其内存空间。有了闭包之后fn1的作用域就没有被销毁。,fn2依然有该作用域的引用,这个引用就是闭包。在这个过程中,因为局部变量fn2的声明周期延长,使得Javascript的垃圾回收机制不会收回函数fn1所占用的资源,因为函数fn1的局部变量fn2的执行需要依赖函数fn1中的变量。

闭包特点:

1、函数嵌套函数

2、内部函数可以访问外部函数的变量

3、参数和变量不会被回收。

闭包的优缺点:

闭包优点:延长外部函数局部变量生命周期

闭包缺点:长时间占用容易内存泄露

闭包的作用

  1. 可以读取函数内部的变量
  2. 使变量的值始终保持在内存中

闭包的应用

点击列表中的每一项

<ul>
	<li>0</li>
	<li>1</li>
	<li>2</li>
</ul>

var a = document.getElementsByTagName('li');
for (var i = 0; i < a.length; i++) {
    a[i].onclick = function () 
    {
    	console.log(i);
	}
}

运行结果:无论点击哪一个li,输出的结果都是3。

原因:在点击li时,for循环已经遍历完毕。

解决方法:

  1. 记录索引值

     for (var i = 0; i < a.length; i++) {
     a[i].index = i;
     a[i].onclick = function () {
         var index = this.index;
         console.log(index);
     }
    

    }

2.用let

for (let i = 0; i < oList.length; i++) {
   oList[i].onclick = function () {
        console.log(i);
    }
}

3.闭包

function foo(i){
    return function(){
        console.log(i);
    }
}
for(var i=0;i<a.length;i++){
    a[i].onclick = foo(i);
}

闭包的销毁

  1. js最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,使变量a = null。将变量设置为 null, 意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
  2. 在局部作用域中,当函数执行完毕的时候,局部变量就没有用了,垃圾收集器就会将他回收。
  3. 全局变量什么时候需要自动释放很难判断,所以尽量避免使用全局变量。

eg:

function foo () {
  var a = document.getElementById("la");
  a.onclick = function () {
    alert(a.id);
  }
}

可以通过将变量设置为 null

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