JavaScript:闭包

  本文和你们聊聊闭包,闭包与变量对象和做用域链有着比较多的联系,在阅读本文前,你们须要理解执行上下文、变量对象以及做用域链等内容,这些内容对理解闭包的本质有很大的帮助,前面的两篇文章已经梳理过了,不清楚的同窗能够先阅读以前的文章。java

自由变量

  上篇文章没有提到自由变量这个概念,如今须要理解这个概念。编程

  在一个做用域中使用了一个变量,可是这个变量没有在这个做用域中声明(在其余做用域中声明),对于该做用域而言,这个变量就是一个自由变量。闭包

let a = 10;
function foo() {
  let b = 20;
  console.log(a + b); // 10 在foo函数做用域中,a就是一个自由变量
}
foo();
复制代码

  从上面的实例来看,调用foo函数时,a的取值是来自全局做用域,因此变量a相对foo函数做用域而言变量a是一个自由变量,而b的取值是来自foo做用域,因此变量b对于foo做用域变量b不是自由变量。模块化

定义

  闭包是函数和声明该函数的词法环境的组合。函数式编程

  其实闭包的概念很差解释,彷佛解释不清楚,目前业界对闭包的概念解释有两种,可是不论是哪一种解释,思想是一致的,只是包含的范围不一样而已,咱们看下面的实例,再来讲说闭包这个东西。函数

function foo() {
  let a = 10;
  function bar() {
    console.log(a); // 10
  }
  return bar;
}
let baz = foo();
baz();
复制代码

  上面是一个很简单的实例,这就产生了闭包,为啥产生了闭包???工具

  函数foo中建立了函数bar,并返回了函数bar,并在函数foo做用域外执行了函数bar,当函数bar执行时,访问了foo做用域中的变量a,这就产生了闭包。性能

  也就是说当一个函数有权访问另外一个函数做用域中的变量,而且该函数在另外一个函数的词法做用域外执行就会产生闭包。ui

  从上面的实例来看,也就有人会理解函数foo是闭包,也有人理解函数bar是闭包,Chrome开发者工具中会以函数foo代指闭包。其实不用管闭包是指哪一个,咱们须要理解什么状况下会产生闭包,闭包产生是在一个什么样的场景。下面从底层原理上分析闭包产生的缘由。this

原理

  咱们先看一个实例:

function foo() {
  let a = 10;
  function bar() {
    console.log(a); // 10
  }
  return bar;
}
let baz = foo();
baz();
复制代码

  这个实例和上面的举例是同一个,产生了闭包,咱们分析下这个实例在代码执行过程当中,执行上下文栈的状况:

// 建立执行上下文栈
ECStack = [];

// 最早进入全局环境,全局执行上下文被建立被压入栈
ECStack.push(globalContext);

// foo() 建立该函数执行上下文并压入栈中
ECStack.push(<foo> functionContext);

// foo()执行完毕弹出
ECStack.pop();

// baz被调用,建立baz执行上下文并压入栈中
ECStack.push(<baz> functionContext);

// baz执行完毕弹出
ECStack.pop();

// 代码全局执行完毕,全局执行上下文弹出
ECStack.pop();
复制代码

  在来看看bar函数执行上下文的内容:

bar.[[scope]] = [fooContext.VO, globalContext.VO];

barContext = {
  VO: {xxx}, // 变量对象
  this: xxx,
  scopeChain: [barContext.VO].concat(bar.[[scope]]) // [barContext.VO, fooContext.VO, globalContext.VO]
}
复制代码

  从上面的执行上下文栈的执行状况来看,baz函数执行的时候,foo函数的执行上下文已经出栈了,按照JavaScript垃圾回收机制,foo函数执行上下文的变量对象失去引用后会被垃圾回收机制回收。

  可是上面的实例特殊,bar函数在foo函数中建立,foo函数最终是返回了bar函数,并经过变量baz,在foo函数做用域外执行了,以及访问了foo函数做用域中的a变量。

  函数bar执行上下文中的做用域链包含了函数foo执行上下文中的变量对象fooContext.VO,因此函数foo执行上下文的变量对象不会被垃圾回收机制回收,函数bar访问了函数foo中的变量,阻止了函数foo执行上下文的变量对象被垃圾回收机制回收,正所以函数bar在函数foo的词法做用域外执行,同时也能够访问foo做用域中的变量a,这也就是产生闭包的缘由。

  咱们来概括下闭包本质是什么:

  闭包是一个函数,上面的实例来看,不论是foo函数仍是bar函数,归根结底仍是一个函数,可是和普通函数不同,其拥有特殊能力。

  归纳的讲,咱们能够把闭包看做是一个场景,若是一个函数B在函数A中建立,当函数A的执行上下文已经出栈了,可是函数B在函数A的词法做用域外执行并仍然能访问函数A中的变量对象,咱们就能够说这产生了闭包。咱们能够不用在乎函数A是闭包仍是函数B是闭包,但咱们要清楚什么场景下会产生闭包。

  概括下闭包的特色:

  • 函数A的执行上下文已经出栈
  • 函数B能访问函数A执行上下文的变量对象
  • 函数B在函数A的词法做用域外执行

  最后总结性的说,函数A调用完成后,函数A的执行上下文已经出栈,其变量对象会失去引用等待被垃圾回收机制回收,然而闭包,阻止这一过程,由于函数B的做用域链包含了函数A的执行上下文的变量对象。

  下面咱们看一个实例,熟悉下闭包,加强对闭包的理解。

function foo() {
  let a = 'Hello world';
  function bar() {
    a += ' 6';
    console.log(a);
  }
  return bar;
}
let baz = foo();
baz(); // Hello world 6
baz(); // Hello world 6 6
复制代码

  函数foo调用完成后,此时函数foo执行上下文的变量对象内容以下:

fooContext.VO = {
  bar:  <reference to FunctionDeclaration 'bar'>,
  a: 'Hello world'
}
复制代码

  当函数foo调用完成后,其执行上下文出栈后,它的变量对象没有被垃圾回收机制回收,由于baz函数调用,函数bar的做用域链保存了函数foo执行上下文的变量对象,其变量对象一直在内存中,没有被销毁。

  在函数baz第一次调用后,访问了函数foo做用域中的变量a,并对变量a作相关的操做,使得变量a的值发生了变化,值为Hello world 6,此时函数foo执行上下文的变量对象内容以下:

fooContext.VO = {
  bar:  <reference to FunctionDeclaration 'bar'>,
  a: 'Hello world 6'
}
复制代码

  第一次调用baz后,函数foo中的变量a值为Hello world 6,没有被销毁,因此第二次调用baz时,函数foo中的变量a值为Hello world 6 6

  也正由于闭包会阻止垃圾回收机制对变量进行回收,变量会永久存在内存中,至关于全局变量同样会占用着内存,内存消耗很大,因此不能滥用闭包,不然会形成网页的性能问题,在IE中可能致使内存泄露。解决方法是,在退出函数以前,将不使用的局部变量所有删除。如上面实例,咱们能够将变量设置为null

function foo() {
  let a = 'Hello world';
  function bar() {
    a += ' 6';
    console.log(a);
  }
  return bar;
}
let baz = foo();
baz(); // Hello world 6
baz(); // Hello world 6 6
baz = null; //若是baz再也不使用,将其指向的对象释放
复制代码

闭包应用

  在JavaScript中,由于闭包独有的特性,其应用场景不少。

  • 用于保存私有属性,将不须要对外暴露的属性、函数保存在闭包函数的父函数里,避免外部操做对值的干扰
  • 避免局部属性污染全局变量空间致使的命名空间混乱
  • 模块化封装,将对立的功能模块经过闭包进去封装,只暴露较少的API供外部应用使用
  • 柯里化,在函数式编程中,利用闭包可以实现不少炫酷的功能,柯里化即是其中很重要的一种

  关于闭包的应用,在这里先不作展开,由于里面也有不少本身不太清楚的东西,例如函数式编程,目前本身也不太熟悉,里面还涉及不少其余的知识,关于闭包的应用这块内容暂时不作详细的输出,避免不懂装懂,在这里先梳理闭包有哪些应用,后期对柯里化、模块化封装等内容另外作文字输出。

相关文章
相关标签/搜索