闭包没有想象的那么简单前端
闭包的概念在JavaScript中占据了十分重要的地位,有很多开发者分不清匿名函数和闭包的概念,把它们混为一谈,我但愿借这篇文章可以让你们对闭包有一个清晰的认识。数组
你们都知道变量的做用域有两种:全局变量和局部变量。在JavaScript中函数内部能够访问外部全局变量,而函数外部没法访问函数的内部局部变量。安全
上边这一小段话,看似简单,其实它是咱们理解闭包最基础的东西。在下边的内容中,咱们会对这一现象作出解释。咱们先来看一个很简单的例子:闭包
const a = 100; function f1() { console.log(a); // => 100 } f1();
上边的代码中的函数f1
打印出了全局变量a
的值,这说明函数内部能够访问外部全局变量。出于某种目的,或者是为了安全,或者是想使用私有变量,咱们如今须要访问函数内部的一个局部变量。咱们先看看下边的代码:函数
function f1() { const a = 100; console.log(a); // => 100 } console.log(a);
上边的代码会产生一个错误,说明咱们没法在函数外部访问函数内部的局部变量。为了解决这个问题,咱们就引出了闭包,看一个使用闭包解决上述问题的例子:this
function f1() { const a = 100; return function () { console.log(a); } } f1()();
上边的代码是一个很简答的例子,使用闭包后咱们打印出的结果是100.这正好验证了上边说的,使用闭包的目的就是解决函数外部没法访问函数内部局部变量这一问题。设计
要完全搞清楚其中的细节,必须从理解函数第一次被调用的时候都会发生什么入手。3d
当某个函数第一次被调用时,会建立一个执行环境(execution context)和相应的做用域链,并把做用域链赋值给一个特殊的内部属性(Scope),而后使用this,arguments和其余命名参数的值来初始化函数的活动对象(activation object)。但在做用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,直到做为做用域链重点的全局执行环境。指针
上边的这一段话很是重要,它解释了函数执行的基本原理,闭包也是函数。你们有可能对上边的话不太理解。咱们经过一个例子来解释一下:code
function compare(value1, value2) { if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } } var result = compare(5, 10);
上边的代码中,咱们首先定义了一个compare()函数,而后又在全局做用域中调用了它。当第一次调用它的时候,一共建立了如下几个对象:
咱们看一个图:
后台的每一个执行环境都有一个表示变量的对象---变量对象,全局环境的变量对象始终存在,而想compare()函数这样的局部环境的变量对象,则只在函数执行的过程当中存在。
变量对象本质上是一个对象,他存储了某些变量值。
其实,在compare()函数建立的时候,就已经建立了一个预先包含全局变量对象的做用域链,这个做用域链被保存在内部的Scope属性中。
这句话说明函数在建立后,其内部就有了一个属性保存着当前的做用域链,上边说compare()函数的做用域链指向全局变量对象,这说明做用域链中的每一项指向的都是一个变量对象。
当调用compare()函数时,会为函数建立一个执行环境,而后经过复制函数的Scope属性中的对象构建起执行环境的做用域链。
这句话说明函数在执行环境中,会新建立一个做用域链,这个新建的做用域链会把函数建立时的做用域链复制过来。
此后,又有一个活动对象(在此做为变量对象使用)被建立并被推入执行环境做用域链的前段。
这句话说明,函数执行后,会他this,arguments,函数的参数,函数内部的局部变量这四个做为属性,保存到一个对象中,而后把该对象放到做用域链的前段,所以咱们就可以经过做用域链访问到咱们须要的数据。
你们能够再次回到上边看看那个图,因为compare()函数是在全局环境中建立的,所以在执行的时候,它的做用域链只有两个对象,最前端的0指向了执行时的活动对象,1指向了全局的变量对象。
显然,做用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
这句话很是重要,这使咱们理解函数调用过程最基本的原理。不管何时在函数中访问一个变量时,就会从做用域链中搜索具备相应名字的变量。通常来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局做用域(也就是全局执行环境的变量对象)。可是闭包的状况又有所不一样。
咱们再看一个带有闭包的例子:
function createCompareFunction(propertyName) { return function (object1, object2) { const value1 = object1[propertyName]; const value2 = object2[propertyName]; if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } } } const compare = createCompareFunction("name"); const result = compare({name: "James"}, {name: "Bond"});
上边的代码实现了按照对象的属性排序的功能。当咱们在一个函数的内部定义了另外一个函数,那么在该函数执行时,就会把该函数的活动对象添加到它内部的函数的做用域链之中。这也就是为何compare()函数为何能访问createCompareFunction内部参数的缘由。
更为重要的是,createCompareFunction()函数在执行完毕后,器活动对象也不会被销毁,由于匿名函数的做用域链仍然在引用这个活动对象。换句话说,当createCompareFunction()函数返回后,其执行环境的做用域链会被销毁,但它的活动对象人让亲会留在内存中,直到匿名函数被销毁以后,createCompareFunction()函数的活动对象才会被销毁。
const compare = createCompareFunction("name"); const result = compare({name: "James"}, {name: "Bond"}); compare = null;
这其中关于闭包最终要的问题就是,他内部的做用域链中会有一个外部函数的活动对象的引用。
咱们看看上边代码执行过程当中发生了什么:
因为闭包会携带包含它的函数的做用域,所以会比其余函数占用更多的内存。过分使用闭包可能会致使内存占用过多,建议你们只在绝对必要时再考虑使用闭包。
可是闭包在使用不当的状况下会产生必定的反作用,上文中,咱们反复提到,闭包只能取得包含函数中任何变量的最后一个值,由于闭包保存的是整个变量对象,而是否是某个特殊的变量。
function createFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function () { return i; } } return result; } const funcs = createFunctions(); console.log(funcs[2]());
上边的代码中,看似createFunctions()函数应该返回一个函数数组,数组中的每一个函数都应该返回本身的索引值,但实际上,每一个函数都返回10。createFunctions()函数返回的闭包中保存的是createFunctions()函数的活动对象,这个活动对象中的其中一个属性就是i。createFunctions()函数执行完毕后,i变成了10,所以当咱们调用闭包函数的时候,他实际上是去访问了活动对象中的i。基于这个原理,咱们可使用这种方式:
function createFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function (num) { return function () { return num; }; }(i); } return result; } const funcs = createFunctions(); console.log(funcs[2]());
杀精编的例子中,用了两层闭包,最内层的闭包访问num,外层的闭包访问i而且当即执行。
在闭包中使用this对象也可能会致使一些问题。咱们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被做为某个对象的方法调用时,this等于那个对象。
匿名函数的执行环境具备全局性,在没有指定调用对象的前提下,this对象一般指向window》
const name = "The window"; const object = { name: "My object", getNameFunction: function () { return function () { return this.name; } } }; console.log(object.getNameFunction()());
上边的代码在调用了object.getNameFunction()返回了一个函数,而后在调用这个返回的函数,就返回了“The window”。
这里惟一的问题是,为何匿名函数没有取得其包含做用域(或外部做用域)的this对象呢?
前面咱们曾经提到过,每一个函数在被调用时,其活动对象都会自动获取两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,所以永远不可能直接访问外部函数中的这两个变量。
这说明访问内部函数的参数时,获得的就是内部函数的参数,而不是其余值,不然就乱套了。
把外部做用域中的this对象保存在一个闭包可以访问到的变量中,就可让闭包访问该对象了:
const name = "The window"; const object = { name: "My object", getNameFunction: function () { const that = this; return function () { return that.name; } } }; console.log(object.getNameFunction()());
记住,this和arguments这两个比较特殊,只能访问自身的活动对象。
在几种特殊的状况下,this的值可能会意外的改变:
const name = "The window"; const object = { name: "My object", getNameFunction: function () { return this.name; } }; console.log(object.getNameFunction()); // => "My object" console.log((object.getNameFunction)()); // => "My object" console.log((object.getNameFunction = object.getNameFunction)()); // => "The window"
最后一行代码比较有意思,赋值表达式的结果就是函数,而后调用函数以后就打印出了"The window"。
本篇大部份内容来源于<<JavaScript高级程序设计>>