Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

javaScript-闭包 #7

Open
dark9wesley opened this issue Mar 11, 2021 · 0 comments
Open

javaScript-闭包 #7

dark9wesley opened this issue Mar 11, 2021 · 0 comments

Comments

@dark9wesley
Copy link
Owner

什么是闭包?

闭包指的是,即使外部上下文已经被销毁,但依旧能访问到已销毁上下文中的变量的函数。

为了理解闭包,我们首先要回忆一下,执行上下文和执行上下文栈的整体流程

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

var foo = checkscope();
foo();

这段代码的上下文变化如下:

  1. 进入全局代码,创建全局执行上下文,将全局执行上下文压入栈中。

  2. 初始化执行全局上下文。创建全局对象,确定作用域链,以及this指向。

  3. 初始化全局上下文的同时,函数checkscope被创建,将父级作用域链保存至内部属性[[scope]]

  4. checkscope被执行,创建checkscope函数执行上下文,压入栈中。

  5. 初始化checkscope函数执行上下文。

    1. 复制[[scope]]属性,创建作用域链。
    2. 用arguments创建活动对象。
    3. 初始化活动对象,即加入形参,var声明的变量,函数声明。
    4. 将活动对象压入作用域顶端,确定完整的作用域链。
    5. 确定this指向。
  6. 初始化checkscope函数的同时,f函数被创建,保存父级作用域链至内部属性[[scope]]。

  7. checkscope函数代码执行完毕,返回f函数,出栈。

  8. foo函数执行(引用同f函数一致)。创建foo函数执行上下文,压入栈中。

  9. foo函数执行上下文初始化。

    1. 复制[[scope]]属性,创建作用域链。
    2. 用arguments创建活动对象。
    3. 初始化活动对象,即加入形参,var声明的变量,函数声明。
    4. 将活动对象压入作用域顶端,确定完整的作用域链。
    5. 确定this指向。

10.foo函数代码执行完毕,返回scope属性,出栈。

分析

这段代码最后输出的结果为local scope。

知道了结果,我们就要思考一个问题,为什么checkscope函数已经被弹出栈,foo函数仍然能访问到它的内部变量scope呢?

关键原因就在于第6步,f函数将父级作用域链保存在了内部属性[[scope]]上。

因此, 即使checkscope函数已经被销毁,f函数仍然能拿到它内部的变量。

这就是所谓闭包的奥秘。

使用闭包的例子

私有变量

由于闭包访问的变量所处的上下文已经被销毁,所以除了闭包本身,其它方式无法访问这个变量。

我们可以利用这一点,创建一个私有变量,只允许通过规定的方式获取或改变这个变量。

1.自增器

创建一个自增器,每调用一次,自增1

const add = ( function addCount(){
 let i = 0;
 return function(){
   console.log( i++);
 }
})();

add(); //0
add(); //1
add(); //2

2.在实际的业务需求中,有一次遇到需要记录用户浏览时长,并在用户离开页面时上报埋点。这种时候,就可以用上闭包。

创建一个计时器,在用户进入页面时计时,离开页面时获取浏览的时间

let {start, get} = (function(){
  let time = 0;
  return {
    start: () => {
      setInterval(() => time++, 1000)
    },
    get: () => {
      return time
    }
  }
})()

start() //进入页面时调用,开始计时
get() //离开页面时调用,获取浏览的时间

缓存

闭包的引用如果不被销毁,那么就会一直留在内存里,我们可以利用这一点,在页面生命周期里实现简单的数据存取。

let { set, get } = (function(){
  let storage = {};
  return {
    set: (key, value) => {
      storage[key] = value;
    },
    get: key => {
      return storage[key]
    }
  }
})()

set('name', 'pengyw97'); // 存储
get('name');  //取值

利用闭包解决全局变量的问题

看下面这道题

var data = [];

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

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

可以预知的是,由于for循环中var字声明的是全局变量,所以在最后打印出来的时候,都会是同一个全局变量,都为3;

这时候闭包就可以派上用场了。

var data = [];

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

每个data[i]都是一个闭包,都引用了已经被销毁的作用域里的变量i,不再引用相同的全局变量的值。

当然这个问题在ES6的let,const出现后已经很好解决,但通过这个问题我们还是能看出闭包的巧妙之处。

闭包的缺点

闭包的缺点显而易见,本该被回收的变量或对象被闭包紧紧抓着,占用了本该被释放的空间。

如果闭包已经不再有用,请马上告诉内存回收器,可以将这个闭包回收了。

 function large() {
    var arr = new Array[100000];   //这个数组占用了很大的内存空间
    function f() {
      console.log(arr.length)
    }
    return f
  }

  var f = large()
  f()

  f = null //告诉内存回收器,可以将这个闭包回收了。

参考链接

JavaScript深入之执行上下文
JavaScript深入之闭包

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant