欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

每日一题——JS闭包

程序员文章站 2024-02-21 14:15:27
...

闭包可以说是前端js内容的一块很重要的部分,也是面试时很容易被问到的一部分。而对于闭包红宝书上给的定义:闭包是指有权访问另外一个函数作用域中的变量的函数可是单单这样一句话,很难理解它内部的执行过程,以下是自己总结的一些关于闭包相关的东西

首先为什么要用闭包,闭包的作用是什么

先来看看菜鸟教程中给的案例,同样一个计数器,不同的写法,不同的效果

var counter = 0;
 function add() {
   return counter += 1;
}
 add();   //1 
add();  //2
add();  //3

这种写法中因为counter是一个全局变量,所以在任何地方都可以访问到,每调用一次add(),counter也就自加一次。这样的写法简单,也很方便,但是因为counter它是一个全局变量,任何地方都可以调用,那么它便也很容易被修改,一旦被修改,那么这个计数器的值也会受到影响。
为了避免这样的事情发生,紧接着,菜鸟教程上给出这样一段代码

function add() {
    var counter = 0;
    return counter += 1;
}
 
add();  //1
add();  //1
add();  //1

这样的写法将counter变成了局部变量,只能在函数内部调用,不会被外部轻易修改,可是因为每重新调用一次add(),都会讲counter重新初始化为0,这样执行结束后永远都是1
于是出现了闭包

var add = (function () {
    var counter = 0;
    return function () {return counter += 1;}
})();
 
add();  //1
add();  //2
add();  //3 

闭包指的就是function () {return counter += 1;}这个函数。首先解释一下这段代码,在变量add被赋值之前,第一个function执行了一次(执行且仅会执行一次),因为这是一个函数表达式声明方式并且声明后加上了(),所以会自动执行一次。执行后add被赋值(匿名函数)了,add= function () {return counter += 1;} 。然后每次调用add()函数时,返回的都是这个函数,因为这个函数在第一个函数的内部,所以即使第一个函数执行完了,第二个函数依然能访问counter(JS设计的作用域链,当前作用域能访问上级的作用域.
闭包的作用
1、读取函数内部的变量
2、让这些变量的值始终保持在内存中。不会再因为函数调用后被自动清除。
3、方便调用上下文的局部变量。利于代码封装。

其次,再来想一个问题

想之前先说一下执行上下文、作用域的问题

执行上下文的执行过程:首先,会创建一个全局执行上下文,并且将其push到栈中,当遇到函数时,会创建函数执行上下文并将其push到栈顶,当函数执行完毕之后,函数执行上下文又会被pop出去,继续执行全局上下文

也因为这样全局作用域和函数作用域的变量生命周期不同

全局作用域 函数作用域
全局作用域中变量的生命周期是永久的,除非你主动销毁; 而函数作用域中变量的生命周期则是在函数调用结束后就消失

那么既然函数中变量的生命周期会在函数执行结束后结束,那么为什么闭包还可以调用到函数中的变量呢?
先来看一个简单的例子:

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

var foo = checkscope(); // foo指向函数f
foo();					// 调用函数f()

函数f 执行的时候,checkscope 函数上下文已经被销毁了,那函数f是如何获取到scope变量的呢?
因为函数f 执行上下文维护了一个作用域链,会指向checkscope作用域,作用域链是一个数组

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

所以指向关系是当前作用域 --> checkscope作用域–> 全局作用域,即使 checkscope执行上下文被销毁了,但是 JavaScript 依然会让 这个函数的AO(活动对象) 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,这就是上面说过的与闭包有关的作用。

然后再来说几个例题

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

循环结束后,全局执行上下文的VO是

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

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

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

由于其自身没有i变量,就会向上查找,所有从全局上下文查找到i为3,data[1] 和 data[2] 是一样的。

那么要想避免这样的事情发生,有以下几种方法
一:将其改成闭包,方法就是data[i]返回一个函数,并访问变量i

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

循环结束后的全局执行上下文没有变化。

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

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

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

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

因为闭包执行上下文中贮存了变量i,所以根据作用域链会在globalContext.VO中查找到变量i,并输出0。
二:使用es6let

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; 
    data[0] = function() {
    	console.log(i);
	};
}

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

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

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

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

for (var i = 0; i < 3; i++) {
    (function(num) {
        setTimeout(function() {
            console.log(num);
        }, 1000);
    })(i);
}
// 0
// 1
// 2

最后的一些注意事项

1:闭包只能取得包含函数中任何变量的最后一个值。因为别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量
2:闭包和内存泄露有关系的地方是,使用闭包的同时比较容易造成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候就有可能造成内存泄漏
内存泄露举例:
每日一题——JS闭包
解决:把循环引用中的变量设为null即可,即为切断变量和它此前引用的值之间的连接,当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存
每日一题——JS闭包
参考链接:https://muyiy.cn/blog/2/2.3.html#%E4%BD%9C%E7%94%A8%E5%9F%9F

相关标签: 每日一题