html5

笔记:JavaScript闭包

2018-05-23  本文已影响63人  chenhong_f1e2

本文摘录及参考自:
1. 学习Javascript闭包(Closure)
2. 闭包的秘密
3. JavaScript 闭包
4. JavaScript深入之闭包
5. JavaScript深入之执行上下文
5. 闭包-JavasScript
6. 闭包
7. 闭包,懂不懂由你,反正我是懂了
8. 什么是闭包,我的理解

闭包的由来

1.JS变量的作用域

要理解闭包,首先要理解JavaScript的变量作用域。与大多数语言相同,JavaScript变量的作用域分为全局变量和局部变量。函数内部可以访问全局变量,但是函数外部无法访问函数内部的局部变量

例:

function f1(){
  let  n =100;
  var m=99;
  console.log(n); 
  console.log(m);  
}
f1();   //输出:100 , 99
console.log(n); //输出:undefined
console.log(m);  //输出:undefined

注意,在函数内部声明变量的时候一定要用let或者var。否则,实际上声明了一个全局变量

2. 函数外部如何读取局部变量

要在函数外部读取局部变量,可以通过在函数内部再定义一个函数的方法来实现

function f1(){
  let  n =100;
  var m=99;
  function f2(){
    console.log(n);
    console.log(m);
  }
  return f2;
}

let result =f1();
result();  //输出100,99

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的(子对象会一级一级向上寻找父对象的变量)。 所以,父对象的所有变量,对于子对象都是可见的,反之则不成立。因此,只要返回f2,我们就可以在f1的外部读取到局部变量了(通过f2访问)。

3. 闭包的概念

上一节代码中的f2函数,就是闭包。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部链接起来的一座桥梁。

4. 闭包的用途

闭包有两个非常重要的用途:

例:

function f1(){
    var n = 999;
    nAdd = function(){n=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也始终在内存中,不会在调用结束后,被垃圾回收机制回收。此外,这段代码中,nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。

例:

var funArr = []
for(var i=0;i<6;i++){
    funArr[i] = function(){
        console.log(i);
    }
}
funArr.forEach( (f) => f() )   //输出:6,6,6,6,6,6

查看控制台输出结果为 6,6,6,6,6,6。原因是f()函数保存的变量i,在其执行时,已经被赋值为6。可以使用闭包保存变量

var funArr = []
for(var i=0;i<6;i++){
    funArr[i] = (function(j){
            return function(){
                console.log(j);
            }
        }
    )(i)
}

funArr.forEach( (f) => f() )   // 0,1,2,3,4,5

注意,funArr[i]实际上获得的赋值是一个函数 function(){ console.log(j) } ,所以之后调用funArr[i]实际上就是调用console.log(j)。 闭包会持有父方法的局部变量并且不会随父方法销毁而销毁,因此j被一直保留在内存中。

避免使用过多的闭包,可以用let关键词:

var funArr = []
for(var i=0;i<6;i++){
    let temp = i;
    funArr[i] = function(){
        console.log(temp);
    }
}
funArr.forEach( (f) => f() )   // 0,1,2,3,4,5

这个例子使用let而不是var,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。

5. 闭包原理解析

要理解JavaScript闭包的原理,首先要先理解它的执行上下文,以下面这段代码为例进行分析
例:

let scope = "global scope";
function checkscope(){
    let scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

let foo = checkscope();
console.log(foo()) ; //local scope

简要分析上述代码的执行过程:

  1. 进行全局代码,创建全局执行上下文,全局执行上文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 执行checkscope函数,创建checkscope函数执行上下文,checkscope执行上下文被压入执行上下文栈
  4. checkscope执行上下文初始化,创建变量对象、作用域链、this等
  5. checkscope函数执行完毕,checkscop执行上下文从执行上下文栈中弹出
  6. 执行f函数,创建f函数执行上下文,f执行上下文被压入执行上下文栈
  7. f执行上下文初始化,创建变量对象、作用域链、this等
  8. f函数执行完毕,f函数上下文从执行上下文栈中弹出

问题1: 当f函数执行的时候,checkscope函数上下文已经被销毁,f函数为什么还可以读取到checkscope作用域下的scope值?

在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 checkscope() 执行完毕,我们会认为 scope 变量将不能被访问。

然后JavaScript却是可以的
当我们了解了具体的执行过程后,我们知道f执行上下文维护了一个作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。总结一下函数的作用域链的规则,函数会把自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以及类推直至全局对象为止。

6. 自检

  1. 一道非常有意思的考题,如果能清楚的知道为什么输出99和100,对于闭包算是理解清楚了
let nAdd;
let t = () => {
    let n = 99;
    nAdd = () => {
        n++;
    };
    let t2 = () => {
        console.log(n);
    };
    return t2;
};

let a1 = t();
let a2 = t();

nAdd();
a1();    //99
a2();    //100

当执行let a1= t() 的时候,变量nAdd被赋值为一个函数,该函数为fn1。接着执行let a2=t()的时候,变量nAdd又被重写了,这个函数跟以前的函数长得一摸一样,是fn2。所以当执行nAdd函数的时候,我们执行的其实是fn2,而不是fn1,我们更改的是a2形成的闭包里的n的值,并没有更改a1形成的闭包里的n的值。所以a1()的结果是99, a2()的结果是100.

7. 闭包可能引起的问题

内存泄漏

当闭包造成JavaScript对象之间的循环引用,导致对象无法被垃圾回收机制回收时,就导致了内存泄漏。

例:

function example(){  
    var element =document.getElementByID("div"); //①  
    element.onclick = function() {  
        alert("This is a leak!"); //②  
    }  
}  

由于JavaScript对象element引用了一个DOM对象,该DOM对象的onclick属性引用了匿名函数闭包,而闭包可以引用外部函数example()的整个活动对象,包括element,因此形成了dom对象引用js对象,js对象又引用dom对象的闭环。

上一篇 下一篇

猜你喜欢

热点阅读