lznbuild / my-blog

自己的博客
9 stars 1 forks source link

JavaScript解读闭包 #4

Open lznbuild opened 4 years ago

lznbuild commented 4 years ago

红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数 关键在于下面两点:

1.是一个函数
2.能访问另外一个函数作用域中的变量

对于闭包有下面三个特性:

1、闭包可以访问当前函数以外的变量

function getOuter(){
  var date = '815';
  function getDate(str){
    console.log(str + date);  //访问外部的date
  }
  return getDate('今天是:'); //"今天是:815"
}
getOuter();

2、即使外部函数已经返回,闭包仍能访问外部函数定义的变量

function getOuter(){
  var date = '815';
  function getDate(str){
    console.log(str + date);  //访问外部的date
  }
  return getDate;     //外部函数返回
}
var today = getOuter();
today('今天是:');   //"今天是:815"
today('明天不是:');   //"明天不是:815"

3、闭包可以更新外部变量的值

function updateCount(){
  var count = 0;
  function getCount(val){
    count = val;
    console.log(count);
  }
  return getCount;     //外部函数返回
}
var count = updateCount();
count(815); //815
count(816); //816

为什么闭包可以这样访问不属于自身作用域中的变量??
让我们先写个例子,例子依然是来自《JavaScript权威指南》,稍微做点改动

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

var foo = checkscope();
foo();

首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况,之前的文章有相关介绍,这里只给简单描述。

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

了解到这个过程,我们应该思考一个问题,那就是:
当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

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

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

对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。(这里看完我上一篇执行上下文,会很好理解)

必刷题

接下来,看这道刷题必刷,面试必考的闭包题:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

答案是都是 3,让我们分析一下原因:
当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。

data[1] 和 data[2] 是一样的道理。

所以让我们改成闭包看看:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

跟没改之前一模一样。

当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

匿名函数执行上下文的AO为:

匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。

data[1] 和 data[2] 是一样的道理。

再来一个问题。 把for循环中的var i = 0,改成let i = 0。结果是什么,为什么?

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

解释下原理:

var data = [];// 创建一个数组data;

// 进入第一次循环
{ 
    let i = 0; // 注意:因为使用let使得for循环为块级作用域
               // 此次 let i = 0 在这个块级作用域中,而不是在全局环境中
    data[0] = function() {
        console.log(i);
    };
}

循环时,let声明i,所以整个块是块级作用域,那么data[0]这个函数就成了一个闭包。这里用{}表达并不符合语法,只是希望通过它来说明let存在时,这个for循环块是块级作用域,而不是全局作用域。

上面的块级作用域,就像函数作用域一样,函数执行完毕,其中的变量会被销毁,但是因为这个代码块中存在一个闭包,闭包的作用域链中引用着块级作用域,所以在闭包被调用之前,这个块级作用域内部的变量不会被销毁。

// 进入第二次循环
{ 
    let i = 1; // 因为 let i = 1 和上面的 let i = 0     
               // 在不同的作用域中,所以不会相互影响
    data[1] = function(){
         console.log(i);
    }; 
}

当执行data[1]()时,进入下面的执行环境。

{ 
     let i = 1; 
     data[1] = function(){
          console.log(i);
     }; 
}

在上面这个执行环境中,它会首先寻找该执行环境中是否存在i,没有找到,就沿着作用域链继续向上到了其所在的块作用域执行环境,找到了i = 1,于是输出了1。

思考题

代码1:

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

checkscope()();  

代码2:

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

var foo = checkscope(); 
foo();     

上面的两个代码中,checkscope()执行完成后,闭包f所引用的自由变量scope会被垃圾回收吗?为什么?

js采用词法作用域,就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的,为了实现这种词法作用域,js函数对象的内部状态不仅包含函数的代码逻辑还必须引用当前的作用域链,函数对象可以通过作用域链相互关联起来。函数体内部的变量都可以保存在函数作用域内。如果你理解了词法作用域的规则,就能很容易的理解闭包,函数定义时的作用域链到函数执行时依然有效。有些人不理解,认为在外部函数中定义的局部变量在函数返回后就不存在了,那么嵌套的函数如何能调用不存在的作用域链?每次调用js函数的时候,都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域链中,当函数返回的时候,就从作用域链中将这个绑定变量的对象删除,所以我们在函数调用后,获取不到函数执行后的局部变量。如果不存在嵌套函数(闭包),也没有其他引用指向这个绑定对象,他就被当作垃圾回收掉.

函数中arguments.length表示实参的数量,arguments.callee.length表示形参的数量。

call,apply的第一个实参是要调用函数的母对象,他就是调用上下文,在函数体内通过this来获取对他的引用。

fn.call(window) 功能类似 在window上绑定一个属性,保留对fn函数的引用,调用这个属性,将临时方法再去掉。 思路有了,就可以实现call,apply了

闭包,就是在内存中保留了一个值,有人会说,我直接在全局环境var 一个变量,一样在内存中,区别就是,我只能用闭包函数去修改那个变量,其他方式根本就访问不到那个变量,单向数据流???redux???没错,后面会有redux源码的解析. 如果不去释放那个变量,闭包用的多了,会内存溢出,如何去释放那个变量??