作用域
当我们在理解闭包的时候,首先要明确的是JavaScript中作用域及作用域链的概念。
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的前端是当前环境的变量对象,下一个变量对象来自于包含的外部环境,逐级往上到全局执行环境。在读取或者写入一个变量标识的时候,会从作用域链前端开始,逐层向后查找,直到找到为止,找不到则会抛出错误。
// 全局环境,只能访问color
var color = 'blue'
function changeColor() {
// changeColor环境,这里可以访问color 和anotherColor,但不能访问tempColor
var anotherColor = 'red'
function swapColors() {
// swapColors环境,这里可以访问color、anotherColor 和tempColor
var tempColor = anotherColor
anotherColor = color
color = tempColor
}
swapColors()
}
changeColor()
上述代码涉及到三个逐级嵌套的作用域:swapColors()的局部环境、changeColor()的局部环境、全局环境。在swapColors环境中访问color变量时,首先从自身环境开始检查,没有找到时继续到changeColor环境中查找,一直逐级向上在全局环境中找到了该变量。
这里需要区分的一点是,向上查找变量是从创建这个函数的那个作用域中取值——是“创建”,而不是“调用”。
var x = 100
function fn() {
console.log(x)
}
function show(f) {
var x = 10
f()
}
show(fn) // 100
通常,当函数执行完成后,其执行环境会被销毁,而在闭包中,其外部环境的作用域链会被保留下来。
闭包的概念
简单来说,闭包指的是能够访问另一个函数作用域中变量的函数。在闭包内部可以访问外部环境的变量对象。
创建闭包
通常,在一个函数内部创建另一个函数就是一个创建闭包的过程。
function add(base) {
return function(num) {
return num + base
}
}
var foo = add(5)
foo(5) // 10
闭包的作用域链包含着自己的作用域,以及包含它的函数和全局的作用域。理论上,在add函数运行完毕后,整个函数内部的作用域会被销毁。然而内部的匿名函数持有add函数的内部作用域,使得add函数的作用域一直被引用,以供该匿名函数在其余的地方别调用时可以访问。
闭包的特性
保留外部环境变量对象
闭包即使是在其他地方被调用了,仍然能够访问外层函数的变量,这是因为它的作用域链中包含了外层函数的作用域。
var outerParam = 'outer'
var temp
function foo() {
var innerParam = 'inner'
return function (param) {
console.log(outerParam, innerParam, param, temp)
}
}
var inner = foo()
inner('insert')
var temp = 'hello'
inner('insert')
// outer inner insert undefined
// outer inner insert hello
从上述代码中可以看出,我们在全局环境中调用了闭包后,在其内部仍然保存了定义时的词法作用域。而对于未声明的变量,不能够提前进行引用。
保存外部变量的最后一个值
由于闭包保存的是整个变量对象,而不是某个特定的值,因此闭包只能够取到函数中任何变量的最后一个值。典型的例子是在for循环中使用闭包。
function test(){
var arr = [];
for(var i = 0; i < 3; i++){
arr[i] = function(){
return i;
};
}
for(var a = 0; a < 3; a++){
console.log(arr[a]());
}
}
test(); // 2 2 2
上述代码打印出来的是3个2 ,由于for循环没有块级作用域,而闭包取得的是其变量的最后一个 i 的值,即为2。那么如果我们想要让函数输出从0~2的数字,可以使用let或是立即执行函数。
// 使用let
function test(){
var arr = [];
for(let i = 0; i < 3; i++) { // 可看作是形成了块级作用域
arr[i] = function(){
return i;
};
}
for(var a = 0; a < 3; a++){
console.log(arr[a]());
}
}
test(); // 0 1 2
// 使用立即执行函数
function test(){
var arr = [];
for(var i = 0; i < 3; i++){
arr[i] = (function(num){
return num;
})(i);
}
for(var a = 0; a < 3; a++){
console.log(arr[a]);
}
}
test(); // 0 1 2
this的指向
var name = "The Window";
var obj = {
name: "My Object",
getName: function(){
return function(){
return this.name;
};
}
};
console.log(obj.getName()()); // The Window
匿名函数的执行环境具有全局性,通常我们会使用匿名函数来创建闭包,因此此时的this对象通常会指向window。我们可以通过缓存obj内部的this来达到访问obj内部name属性的目的。
var name = "The Window";
var obj = {
name: "My Object",
getName: function(){
var that = this
return function(){
return that.name;
};
}
};
console.log(obj.getName()()); // My Object
内存空间的释放
函数的作用域及其所有的变量会在函数执行结束之后销毁。但是在创建了闭包之后,这个函数的作用域会一个保存到闭包不存在为止。
function handler() {
let ele = document.getElementById('app')
ele.onclick = function() {
console.log(ele.id)
}
}
上述代码中onClick事件创建了一个闭包,保存了对于外部ele的引用,使得其引用数至少为1,因此无法被回收。我们可以通过改造代码来将其释放。
function handler() {
const ele = document.getElementById('app')
const id = ele.id
ele.onclick = function() {
console.log(id)
}
ele = null
}
上述代码中,即使闭包不直接引用ele,包含函数的活动对象中也仍然会保留一个引用,因此我们有必要手动将ele变量设置为null。这样就能够解除对DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。
由于闭包的这种特性会使得其携带包含它函数的作用域,因此会比其他函数占用更多的内存,在过度使用闭包的情况下可能会导致内存占用过多。
应用
私有作用域
function outputNumbers(count){
for (var i=0; i < count; i++){
alert(i);
}
alert(i); // 计数
}
变量 i 在for循环结束后没有被销毁
使用闭包可以在JavaScript中模仿块级作用域,通常可以使用匿名函数来模仿块级作用域。
function outputNumbers(count) {
;(function() {
for (var i = 0; i < count; i++) {
alert(i)
}
})()
alert(i) // error
}
上述代码中,for循环外部加入了私有作用域。匿名函数的任何变量都会在执行结束的时候销毁。同时这个匿名函数是一个闭包,它能够访问外部作用域中的变量count。
这种做法不会在内存中留下对于该函数的引用,同时也可以避免往全局作用域中添加过多的变量与函数,不必担心搞乱全局作用域。
创建私有变量
闭包的主要应用场景是设计私有方法与变量。
在任何函数内部定义的变量,都可以认为是私有变量,因为不能够在函数外部访问这些变量。这里的私有变量包括函数的参数、局部变量与函数内定义的函数。
而如果在函数内创建闭包,那么闭包可以通过自己的作用域链来访问到这些变量。利用这一点,我们可以创建用于访问私有变量的公有方法,我这类方法我们称为特权方法。
var calc = (function() {
var goodsList = [] // 私有变量
goodsList.push(new Good()) // 初始化操作
return {
add: function(val) {
goodsList.push(val)
},
getTotal: function() {
return goodsList.length
}
}
})()
上面的这种方式也叫做模块模式。所谓的模块模式,指的是能为单例模式添加私有变量与方法并减少全局变量的使用。
上述代码中,模块模式使用了一个返回对象的匿名函数,在这个函数内部,首先定义了私有变量goodsList。返回的对象字面量中只可包含公开的属性与方法。这种模式在需要对单例进行初始化,同时又需要维护其私有变量的时候非常有用。
总结
什么是闭包
当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量。闭包的作用域链包含着自己的作用域、包含(创建该函数)函数的作用域与全局作用域。
通常函数的作用域及其变量会在函数执行结束后销毁,但是当函数反悔了一个闭包时,这个函数的作用域会一直保存在内存中直到闭包被释放。
闭包的应用
- 模仿块级作用域
- 创建私有变量