二十一、js的闭包了解吗?闭包的常见用法说一下

91 阅读9分钟

一、闭包的定义

闭包是指有权访问另一个函数作用域中的变量的函数 --《JavaScript高级程序设计》

函数对象可以通过作用域关联起来,函数体内的变量都可以保存在函数作用域内,这在计算机科学文献中称为“闭包”,所有的javascirpt函数都是闭包 --《Javascript权威指南》

相关概念学习:

  • 作用域

  • 执行上下文

  • 执行上下文堆栈

  • 变量对象

  • 活动对象

  • 作用域链

1、作用域 Scope

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)

作用域共有两种主要的工作模型:

  • 词法作用域:作用域是在编写代码的时候确定的

  • 动态作用域:作用域是在代码运行的时候确定的

javascript使用的是词法作用域

2、执行上下文 Execution Contexts

Javascript中代码的执行上下文分为以下三种:

  • 全局级别的代码 – 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。

  • 函数级别的代码 – 当执行一个函数时,运行函数体中的代码。

  • Eval的代码 – 在Eval函数内运行的代码。

一个执行的上下文可以抽象的理解为一个对象。每一个执行的上下文都有一系列的属性(变量对象(variable object),this指针(this value),作用域链(scope chain) )


Execution Contexts = {

variable object:变量对象;

this value: this指针;

scope chain:作用域链;

}

3、执行上下文堆栈 Execution Contexts Stack

活动的执行上下文在逻辑上组成一个堆栈。堆栈底部永远都是全局上下文(globalContext),而顶部就是当前(活动的)执行上下文。


<script>

function add(num){

var sum = 5;

return sum + num;

}

var sum = add(4);

</script>

当add函数被调用时,add函数执行上下文被压入执行上下文堆栈的顶端,此时执行上下文堆栈可表示为:


EC Stack = [

<add> functionContext

globalContext

];

add函数执行完毕后,其执行上下文将会从执行上下文堆栈顶端弹出并被销毁。全局执行上下文只有在浏览器关闭时才会从执行上下文堆栈中销毁

4、变量对象 Variable Object

如果变量与执行上下文相关,那变量自己应该知道它的数据存储在哪里,并且知道如何访问。这种机制称为变量对象(variable object)。

可以说变量对象是与执行上下文相关的数据作用域(scope of data) 。它是与执行上下文关联的特殊对象,用于存储被定义在执行上下文中的变量(variables)、函数声明(function declarations) 。

当进入全局上下文时,全局上下文的变量对象可表示为:


VO = {

add: <reference to function>,

sum: undefined,

Math: <...>,

String: <...>

...

window: global //引用自身

}

5、活动对象 Activation Object

当函数被调用者激活时,这个特殊的活动对象(activation object) 就被创建了。它包含普通参数(formal parameters) 与特殊参数(arguments)对象(具有索引属性的参数映射表)。活动对象在函数上下文中作为变量对象使用。

当add函数被调用时,add函数执行上下文被压入执行上下文堆栈的顶端,add函数执行上下文中活动对象可表示为


AO = {

num: 4,

sum :5,

arguments:{0:4}

}

6、作用域链 Scope Chain

函数上下文的作用域链在函数调用时创建的,包含活动对象AO和这个函数内部的[[scope]]属性。


var x = 10;

function foo() {

var y = 20;

function bar() {

var z = 30;

alert(x + y + z);

}

bar();

}

foo();

在这段代码中我们看到变量"y"在函数"foo"中定义(意味着它在foo上下文的AO中)"z"在函数"bar"中定义,但是变量"x"并未在"bar"上下文中定义,相应地,它也不会添加到"bar"的AO中。乍一看,变量"x"相对于函数"bar"根本就不存在;

函数"bar"如何访问到变量"x"?理论上函数应该能访问一个更高一层上下文的变量对象。实际上它正是这样,这种机制是通过函数内部的[[scope]]属性来实现的。

[[scope]]是所有父级变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。

注意: [[scope]]在函数创建时被存储是静态的(不变的),直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。

在这里我们逐步分析下

全局上下文的变量对象是:


globalContext.VO === Global = {

x: 10

foo: <reference to function>

};

在"foo"创建时,"foo"的[[scope]]属性是:


foo.[[Scope]] = [

globalContext.VO

];

在"foo"激活时(进入上下文),"foo"上下文的活动对象是:


fooContext.AO = {

y: 20,

bar: <reference to function>

};

"foo"上下文的作用域链为:


fooContext.Scope = [

fooContext.AO,

globalContext.VO

];

内部函数"bar"创建时,其[[scope]]为:


bar.[[Scope]] = [

fooContext.AO,

globalContext.VO

];

在"bar"激活时,"bar"上下文的活动对象为:


barContext.AO = {

z: 30

};

"bar"上下文的作用域链为:


bar.Scope= [

barContext.AO,

fooContext.AO,

globalContext.VO

];

二、闭包的原理

我们通过一个闭包的例子来分析一下闭包的形成原理


function add(){

var sum =5;

var func = function () {

console.log(sum);

}

return func;

}

var addFunc = add();

addFunc(); //5

js执行流进入全局执行上下文环境时,全局执行上下文可表示为:


globalContext = {

VO: {

add: <reference to function>,

addFunc: undefined

},

this: window,

scope chain: window

}

当add函数被调用时,add函数执行上下文可表示为:


addContext = {

AO: {

sum: undefined //代码进入执行阶段时此处被赋值为5

func: undefined //代码进入执行阶段时此处被赋值为function (){console.log(sum);}

},

this: window,

scope chain: addContext.AO + globalContext.VO

}

add函数执行完毕后,js执行流回到全局上下文环境中,将add函数的返回值赋值给addFunc。

由于addFunc仍保存着func函数的引用,所以add函数执行上下文从执行上下文堆栈顶端弹出后并未被销毁而是保存在内存中。

当addFunc()执行时,func函数被调用,此时func函数执行上下文可表示为:


funcContext = {

this: window,

scope chain: addContext.AO + globalContext.VO

}

当要访问变量sum时,func的活动对象中未能找到,则会沿着作用域链查找,由于js遵循词法作用域,作用域在函数创建阶段就被确定,在add函数的活动对象中找到sum = 5;

形成闭包的原因:

Javascript允许使用内部函数---即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

三、闭包的用途

闭包可以用在许多地方。它的最大用处有两个:

  • 读取函数内部的变量

  • 让这些变量的值始终保持在内存中

1、保护变量的安全实现JS私有属性和私有方法

利用闭包可以读取函数内部的变量,变量在函数外部不能直接读取到,从而达到保护变量安全的作用。因为私有方法在函数内部都能被访问到,从而实现了私有属性和方法的共享。

常见的模块模式就是利用闭包的这种特性建立的


var Counter = (function() {

//私有属性

var privateCounter = 0;

//私有方法

function changeBy(val) {

privateCounter += val;

}

return {

increment: function() {

changeBy(1);

},

decrement: function() {

changeBy(-1);

},

value: function() {

return privateCounter;

}

}

})();

console.log(privateCounter); //privateCounter is not defined

console.log(Counter.value()); // 0

Counter.increment();

Counter.increment();

console.log(Counter.value()); // 2

Counter.decrement();

console.log(Counter.value()); // 1

在jQuery框架的私有方法和变量也是这么设计的


var $ = jQuery = function(){

return jQuery.fn.init();

}

jQuery.fn = jQuery.prototype = {

init:function(){

return this; //this指向jQuery.prototype

},

length: 1,

size: function(){

return this.length;

}

}

console.log($.size()); // 1

2、将处理结果缓存


var mult = (function(){

var cache = {};

var calculate = function(){

var a = 1;

for(vari=0,l=arguments.length;i<l;i++){

a = a*arguments[i];

}

return a;

};

return function(){

var args = Array.prototype.join.call(arguments,',');

if(args in cache){

return cache[args];

}

return cache[args] = calculate.apply(null,arguments);

}

})();

这样我们在第二次调用的时候,就会从缓存中读取到该对象。

3、模块化代码


var mod = (function(){ //mod是一个模块,私有变量是a,私有函数是func1和func2

var a = 1;

function func1(){

a++;

alert(a);

}

function func2(){

a++;

alert(a);

}

return {

b:func1,

c:func2

}

})() //函数表达式自执行,结果是return后面的对象

mod.b(); //2

mod.c(); //3

4、在循环中找到索引


<body>

<ul>

<li>111111</li>

<li>111111</li>

<li>111111</li>

</ul>

<script>

window.onload = function(){

var aLi = document.getElementsByTagName('li');

for(var i=0;i<aLi.length;i++){

aLi[i].onclick = function(){

alert(i); //顺序点击三个li,分别弹出3 3 3。因为点击的时候for循环已经执行完毕

}

}

}

</script>

</body>

可使用闭包改为:


<body>

<ul>

<li>111111</li>

<li>111111</li>

<li>111111</li>

</ul>

<script>

window.onload = function(){

var aLi = document.getElementsByTagName('li');

for(var i=0;i<aLi.length;i++){

(function(i){

aLi[i].onclick = function(){

alert(i); //0 1 2

}

})(i) //将i作为参数传递给内部函数,i 在for循环执行完毕不会被释放

}

}

</script>

</body>

或者另一种写法:


<body>

<ul>

<li>111111</li>

<li>111111</li>

<li>111111</li>

</ul>

<script>

window.onload = function(){

var aLi = document.getElementsByTagName('li');

for(var i=0;i<aLi.length;i++){

aLi[i].onclick = (function(i){

return function(){

alert(i);

}

})(i) //循环的时候,这个函数表达式自执行,i也是驻扎在内存当中。

}

}

</script>

</body>

理解了闭包的原理我们发现闭包的这些用途都是利用了闭包保存了当前函数的活动对象的特点,这样闭包函数在作用域之外被调用时依然能够访问其创建时的作用域

四、闭包的缺点

  • 闭包将函数的活动对象维持在内存中,过度使用闭包会导致内存占用过多,所以在使用完后需要将保存在内存中的活动对象解除引用;

  • 闭包只能取得外部函数中任何变量的最后一个值,在使用循环且返回的函数中带有循环变量时会得到错误结果;

  • 当返回的函数为匿名函数时,注意匿名函数中的this指的是window对象。

五、闭包面试题解

1、用闭包实现计数器


function addCount() {

var count = 0;

return function() {

count = count + 1;

console.log(count);

};

}

addCount() 执行的时候, 返回一个函数, 函数是可以创建自己的作用域的, 但是此时返回的这个函数内部需要引用 addCount() 作用域下的变量 count, 因此这个 count 是不能被销毁的.接下来需要几个计数器我们就定义几个变量就可以,并且他们都不会互相影响,每个函数作用域中还会保存 count 变量不被销毁,进行不断的累加。


var fun1 = addCount();

fun1(); //1

fun1(); //2

var fun2 = addCount();

fun2(); //1

fun2(); //2

2、for 循环中打印


for (var i = 0; i < 4; i++) {

setTimeout(function() {

console.log(i);

}, 300);

}

上边打印出来的都是 4, 可能部分人会认为打印的是 0,1,2,3

原因:js 执行的时候首先会先执行主线程,异步相关的会存到异步队列里,当主线程执行完毕开始执行异步队列, 主线程执行完毕后,此时 i 的值为 4,所以在执行异步队列的时候,打印出来的都是 4(这里需要大家对 event loop 有所了解(js 的事件循环机制))

如何修改使其正常打印:采用闭包使其正常打印


//方法一:

for (var i = 0; i < 4; i++) {

setTimeout(

(function(i) {

return function() {

console.log(i);

};

})(i),

300

);

}

// 或者

for (var i = 0; i < 4; i++) {

setTimeout(

(function() {

var temp = i;

return function() {

console.log(temp);

};

})(),

300

);

}

// 这个是通过自执行函数返回一个函数,然后在调用返回的函数去获取自执行函数内部的变量,此为闭包

//方法二:

for (var i = 0; i < 4; i++) {

(function(i) {

setTimeout(function() {

console.log(i);

}, 300);

})(i);

}

// 方法二是通过创建一个自执行函数,使变量存在这个自执行函数的作用域里

3、真实的获取多个元素并添加点击事件


var op = document.querySelectorAll("p");

for (var j = 0; j < op.length; j++) {

op[j].onclick = function() {

alert(j);

};

}

//alert出来的值是一样的

// 解决办法一:

for (var j = 0; j < op.length; j++) {

(function(j) {

op[j].onclick = function() {

alert(j);

};

})(j);

}

// 解决办法二:

for (var j = 0; j < op.length; j++) {

op[j].onclick = (function(j) {

return function() {

alert(j);

};

})(j);

}

//解决方法三

for (var j = 0; j < op.length; j++) {

op[j].onclick = (function(j) {

var temp = j;

return function() {

alert(j);

};

})(j);

}