本文和你们聊聊闭包,闭包与变量对象和做用域链有着比较多的联系,在阅读本文前,你们须要理解执行上下文、变量对象以及做用域链等内容,这些内容对理解闭包的本质有很大的帮助,前面的两篇文章已经梳理过了,不清楚的同窗能够先阅读以前的文章。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
供外部应用使用关于闭包的应用,在这里先不作展开,由于里面也有不少本身不太清楚的东西,例如函数式编程,目前本身也不太熟悉,里面还涉及不少其余的知识,关于闭包的应用这块内容暂时不作详细的输出,避免不懂装懂,在这里先梳理闭包有哪些应用,后期对柯里化、模块化封装等内容另外作文字输出。