JS进阶篇 - 闭包

315 阅读5分钟

前言

上一篇 JS进阶篇-作用域 中我们对javascript 的作用域进行了介绍,并且提到了 函数作用域 / 词法作用域/ 作用域链 / 执行上下文栈, 这里将对JavaScript 闭包做深入的介绍。

正文

提到闭包,必然不能脱离了作用域(脱离了作用域谈闭包都是在耍流氓),这里先来看下闭包是怎么定义的。


[维基百科] 闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。


看了这个官方定义之后,可以发现其实闭包随时都存在我们写的每一段代码之中,只是我们很少去主动察觉到它的存在,就如我们在全局定义了一个变量,然后在函数内部去使用它(RHS 或者 LHS),其实这也是闭包的一种表现形式。

下面主要列出几种在 javascript 中闭包的常见表现/使用形式,来一一进行介绍。


1. 私有变量与闭包

function Test() {
  var _inner = "xxx";
  this.setVal = function(val) {
      _inner = val;
  }
  this.getVal = function() {
    return _inner;
  }
}
var test = new Test();

从运行结果可以看出,test 中并没有 _inner 属性,但是呢,却可以对其进行访问和赋值。

接下来对 test 的 getVal 属性进行展开,可以发现它拥有一个私有属性[[scopes]] -- 也就是其自身作用域,其 [[scopes]] 属性中有个元素是闭包(Closure) 其为 {_innner: "zzz" }

原来 _inner 被藏到了 [[scopes]] 里面,我们在进行 getVal 、 setVal 的时候

正是操作的闭包里的 _inner 。 我们通过 test._inner 是无法访问的,因此这种写法就 借助闭包来实现类的私有变量或属性。

是不是很魔法 ~


2. 回调函数与闭包

function operate(id) {
  var dom = document.querySelector("#"+id);
  var count = 0;
  var timer = null;
  timer = setInterval(() => {
    if(count === 100)
       clearInterval(timer);
    dom.style.left = count++;    
  }, 30);
}

operate("id1");
operate("id2");

正如上面代码所表达的,我们创建了一个dom 的动画 放到定时器的回调中, 在两次调用 operate 时候,都会分别创建独立的 id / dom / count / timer

变量,不会造成相互污染,只要有一个通过闭包访问这些变量的函数存在,这个词法环境就一直存在,直到不再使用被清理回收,这可以很好的简化我们的代码,提高代码的易读性/维护性。


3. for 循环与闭包

function test() {
  for(var i=0; i<10; i++) {
    setTimeout(() => {
      console.log(i);
    })
  }
}
test();

其执行结果如上图所示,定时器回调中 i = 10 打印 10 遍, 这是为什么?

其实仔细思考一下,其作用域范围,这个似乎存在缺陷的代码结果也是在情理之中,因为在10次循环中,定时器回调访问的 i 变量,似乎是一个全局的 i , 并不是每一个循环都会拥有一个循环内所在作用域的 i ,当 i 执行到 i = 10 时循环终止,也就是 i 这个变量最终的值,被每个定时器回调中所引用的值,

其实上面的写法好比全局的 i++ , 然后写了 10 遍定时器,如下所示。

var i;
for(i=0; i<10; i++);
setTimeout(() => {
  console.log(i);
})
setTimeout(() => {
  console.log(i);
})
// ...

那么怎么才能让它输出为 1 , 2, 3... 9 呢?我们可以为每个定时器回调创建一个独立的作用域,形成 10 个独立的闭包变量,这里我们借助 iife 自执行函数来 实现其效果。

function test() {
  for(var i=0; i<10; i++) {
    (function() {
      var j = i;
      setTimeout(() => {
        console.log(j);
      })
    })()
  }
}
test();

这样就定时器回调函数可以拥有属于自己作用域的变量 j , 并且每次循环都会创建 一个独立的 j,运行结果如下图所示。

这正是我们想要的运行结果。这段似乎有缺陷的代码 借助闭包作用域的知识 有效得到了解决 ! 是不是很有趣~

结语

经过上面的介绍,想必已经对闭包已经有了一定了解,闭包可以记住并且访问所在的词法作用域,函数在当前作用域之外执行时就产生了闭包。

闭包是 javscript 中一种魔法的存在,也是 javascript 作用域的副作用,既然了解了闭包,那么就去使用它做一些用趣的事情吧。


问题:下面代码执行时,哪个变量是通过闭包访问的?

function Test() {
  var _static = "xxx";
  this.getStic() = function () {
    return _static;
  }
  this.str = _static + "zzz";
  this.getStr = function() {
    rteurn this.str;
  }
}
var test = new Test();
test.getStr() //
test.getStic() //


ps:如有不足 欢迎指正


一只前端小菜鸟 | 求知若渴 | 梦想与爱皆不可辜负