从JS的运行机制的角度谈谈闭包

JS中的做用域、闭包、this机制和原型每每是最难理解的概念之一。笔者将经过几篇文章和你们谈谈本身的理解,但愿对你们的学习有一些帮助。javascript

上一篇中咱们说了做用域,这一篇咱们谈一谈闭包。笔者避免了使用JS中一些复杂的概念,仅仅阐述一些必要的概念和原理,避免由于复杂的概念使得闭包让你们望而生畏。java

闭包是什么?看似一个晦涩难懂的名词。MDN上给对闭包的解释是:面试

closure is the combination of a function and the lexical environment within which that function was declared.编程

这个说法实在是太不明确了,组合?只要组合了就是闭包么?若是不是的话?那怎么组合才能造成闭包呢?数组

咱们再来看看百度百科上对于闭包的定义。闭包

闭包就是可以读取其余函数内部变量的函数。框架

仿佛各不相同的说法,那么,咱们究竟应该如何理解闭包呢?编程语言

美丽的意外

首先抛出一个概念,自由变量,就是下文代码中的foo1()函数内部的变量a,为何叫它自由变量呢, 由于它既不是它所在函数内的参数,也不是在所在函数的内部建立的。 做为人咱们可能很好理解a表明着什么,可是JS引擎怎么理解这个a呢,确定有一套相应的规则帮助JS引擎理解这个a。JS中的词法做用域,就是帮助JS引擎理解这个a的一套规则。ide

function foo() {
  var a = 1;
  function foo1() {
    console.log(a);
  }
  foo1();
}

foo();	// 1
复制代码

咱们经过咱们掌握的词法做用域的知识分析一下上面的代码,全局做用域中定义了foo,foo的做用域中定义了a和foo1,foo1做用域中什么都没定义,可是有一个输出语句,当foo1执行时console.log(a);语句时,会按照做用域从里到外查找,找到它上层的a的值并打印出来。理论上来讲,这个时候,其实已经建立了一个闭包(实际上,在JS中,任何一个拥有了自由变量的函数都是闭包)。因此咱们如今再来看看MDN和百度百科上的定义,其实两个说的都对,MDN的说法相对抽象一些,百度百科说的也对,由于在JavaScript中,因为词法做用域的规则存在,能访问函数内部的局部变量的只有定义在该函数内部的子函数,因此一些人理解JS中的闭包必定是存在嵌套的函数的,这样的理解也没有什么错误。函数

一个值得注意的地方是,闭包是在代码建立时候产生的,而不是在代码运行时产生的, 不少人会在这个地方产生误解。不过毫无疑问的是,因为闭包的存在致使了JS代码在运行时能够产生一些独特的表现。

JS中的垃圾回收机制

用过Vue和React等框架的同窗,对于组件生命周期确定不陌生,其实在代码的执行过程当中,一段段的代码也和组件同样,存在的属于本身的生命周期。JS代码的生命周期大体分为三个阶段,内存分配阶段、内存使用阶段、内存回收阶段。其中,明白内存回收机制(垃圾回收机制)对于透彻的理解和使用闭包具备重要的做用。

垃圾回收机制是编程语言中必备的一个机制,代码运行在内存中,定义的变量毫无疑问的会占用必定的内存。学习JS的同窗应该能够直观的感觉到,JS相较于C/JAVA系列的语言而言,自由了太多(例如在C/JAVA中,若是定义数组的话,初始化的时候须要指定为这个数组分配的内存大小,它们对于内存的控制相对严格的多)。

如此宽松的内存分配,若是尚未垃圾回收机制的话,占用的内存会随着代码的增多而越多越多,最后耗尽系统的内存,形成系统的崩溃。JS引擎对于再也不须要的变量占用的内存资源进行回收,能够尽量的减小代码运行时候的内存占用。

不一样语言中实现垃圾回收机制的作法各不相同,大致上的实现策略有两种,一种是标记清除,一种是引用计数。可是对于理解闭包而言,理解两种垃圾清除策略十分的重要。

标记清除的策略是这样的,对于进入执行环境的变量,标记为进入执行环境的状态,当离开当前执行环境的时候,标记为离开的状态,下次垃圾回收器来的时候对离开的状态的变量释放内存,即垃圾回收。

引用计数的策略则不一样,MDN中对于引用计数的描述:

The main notion garbage collection algorithms rely on is the notion of reference. Within the context of memory management, an object is said to reference another object if the former has an access to the latter (either implicitly or explicitly). For instance, a JavaScript object has a reference to its prototype (implicit reference) and to its properties values (explicit reference). In this context, the notion of "object" is extended to something broader than regular JavaScript objects and also contains function scopes (or the global lexical scope).

例如一段代码中定义了var a={c:1};,而后定义var b=a;,最后令b=1;a=1;。这个时候{c:1}的引用为0,能够吧{c:1}进行回收了。事实上,只要一个对象不管是a仍是b ,只要任何一个有访问另外一个对象{c:1}的权限,就始终不能回收{c:1}对象。这里的对象不只仅是javascript对象,也包括了做用域。 明白这一点相当重要。

JS中的运行时机制

JS中代码的执行通常分红两个环境,一个是建立时环境,即词法做用域;还有一个就是运行时环境,咱们一般叫它执行环境(执行上下文和执行上下文栈)。JS是单线程的,每进行一个函数调用时就会建立一个执行上下文对象, 将这个对象压入执行上下文栈中,函数调用结束时则将该执行上下文对象从执行上下文栈中弹出。 这样确保了单线程的JS在同一时间只在同一个执行上下文中。

首先JS代码进入全局,建立全局执行上下文对象并压入栈中。
每当发生一个函数调用,就建立一个执行上下文对象并将这个对象压栈。
当函数调用完成时,将该函数调用时建立的执行上下文对象出栈。
最后整个代码执行完毕,弹出全局上下文执行对象。

执行上下文是一个相对抽象的概念,用于标记函数调用的过程。

特殊的闭包

明白了JS中的垃圾回收机制和执行上下文机制,咱们再来分析一下第一段代码:

function foo() {
  var a = 1;
  function foo1() {
    console.log(a);
  }
  foo1();
}

foo();	// 1
复制代码

词法做用域:
全局(global)=> foo => foo1
执行上下文栈的操做顺序:
Global Execution Context(push) => foo Execution Context(push) => foo1 Exection Context(push)
=> foo1 Execution Context(pop) => foo Execution Context(pop) => Global Execution Context
内存管理:
给foo分配内存 => foo 函数调用 => 给foo1,a分配内存 => foo1 函数调用 => foo1 调用结束 => 释放foo1的内存 => foo调用结束   => 释放a的内存 => 全局调用结束 => 释放foo内存

怎么样,没什么特别的吧,咱们再来看看第二段代码:

function foo() {
    var a = 1;
    function foo1() {
        console.log(a);
    }
    return foo1;
}

var b = foo();
b();
复制代码

第二段代码的词法做用域和执行上下文栈的操做顺序都是同样的,可是在内存管理上略有不一样。

内存管理
给foo,b分配内存 => foo 函数调用 => 给foo1,a分配内存 => 将foo1的引用返回给b => foo 函数调用结束 => b(foo1) 函数调用 => foo1调用结束 => 释放foo1, a的内存 => 全局调用结束 => 释放foo, b的内存

这里说明一下,JS中的函数使用是引用方式传递的,return foo1; 是将指向foo1函数的指针返回给b,因此b()其实是foo1();

看出来区别了么,由于foo 中的return将foo1返回到全局变量b中,因此b始终经过foo1的做用域保持着对局部变量a引用(参考上面垃圾回收引用计数策略的定义),在b执行结束前都不会释放a的内存。

除了return以外,还有一种常见的写法会致使局部变量的内存没法释放,那就是将函数做为参数,这种写法经常在回调函数中见到。

function foo () {
    var a = 1;
    setTimeout(function foo1 () {
        console.log(a);
    },1000);
    setTimeout(function () {	
        console.log(a);
    },1000);
}

foo();
复制代码

说明一下,具名函数和匿名函数的差异仅仅在是否能够调用自身上已是否方便定位错误,除此以外并没什么区别。这里foo1内部存在自由变量a,因此是一个闭包。setTimeout是一个全局的方法,因此实际上,foo1被做为参数传到了全局方法中,在setTimeout方法执行完成前,始终保持着对foo内部做用域的引用,a的内存也不会被释放。

第二段和第三段写法的闭包都具共同的特征,就是局部函数的变量和参数不会被垃圾回收,是常驻内存的!咱们暂且称呼它们为特殊的闭包

如何正确使用特殊的闭包

普通的闭包的存在看似毫无用处,特殊的闭包看似有百害无一益,实际上,只要运用获得,特殊的闭包也能够很强大。特殊的闭包可让局部变量常驻内存,同时避免局部变量污染了全局变量,使得设计私有的方法和变量成为可能!
特殊闭包的主要用法一:函数工厂

function foo(value1) {
    return function v2(value2) {
        console.log(value1 * value2);
    }
}

var a = foo(2);
var b = foo(3);

a(2); // 4
b(2); // 6
复制代码

特殊闭包的主要用法二:设计私有变量和方法

function foo() {
    var value = 0;
    function addOne() {
        value += 1; 
    }
    return {
        addOne: addOne,
        getValue: function getValue() {
            console.log(value);
        }
    }
}

var b = foo();
var c = foo();
b.addOne();
b.getValue();	// 1
c.getValue(); // 0
复制代码

特殊闭包的主要用法三:函数柯里化
函数柯里化和闭包的结合并不简单,涉及到求值策略和编程思想的转换,笔者将在后续的文章中单独介绍这一部分的用法。

特殊的闭包还有不少其余的用法,这里就不一一列举了,有兴趣的读者能够自行百度。

坑外话

闭包与当即执行函数(IIFE)

在ES6出现以前,闭包的做用就是模拟块级做用域,说到模拟块级做用域,则和当即执行函数分不开。
什么是当即执行函数?

(function foo() {
	...
})()
  
!function foo() {
	...
}()
  
+function foo() {
	...
}
复制代码

首先把函数声明或者匿名函数加上一些特殊运算符,如加上()、+、!等,将其变成函数表达式,再在后面加上()表示当即执行,就建立了一个当即执行函数。当即执行函数内部定义的方法和变量不会污染全局变量。

for(var i=0;i<10;i++) {
    setTimeout(function(){
      console.log(i);
    },1000);
}
复制代码

上面是很常见的一道面试题,延时函数中的方法在循环结束以后才会执行,因为没有块级做用域,全部的setTimeout中的回调函数在闭包的做用下共享一个全局变量i,这个i的值是10。因此打印出来的结果是10个10。
这个时候就须要用一个东西捕获每一个循环中i的值,放在本身的做用域中。当即执行函数恰好派上了用场。

for(var i=0;i<10;i++) {
    (function (i) {
      setTimeout(function() {
        console.log(i)
      },1000);
    })(i);
}
复制代码

上面代码中回调函数打印的i其实是每一次循环中当即执行函数捕获的i的副本。

有同窗会说,我把setTimeout的延时改为0是否是就能够了?

for(var i=0;i<10;i++) {
    setTimeout(function(){
      console.log(i);
    },0);
}
复制代码

事实上并不行,仍然打印出来的仍是10个10,为何呢?由于JS是单线程的,只能为setTimeout维护了一个单独的队列,当前任务处理完了才会处理setTimeout队列中的内容,因此setTimeout的时间参数并非相对于调用该函数的时间差,仍是相对于开始执行setTimeout队列时的时间差。

码字不易,若是喜欢就点个赞吧~

相关文章
相关标签/搜索