引言
变量的作用域:全局变量、局部变量
js
function f1() {
n = 999;
}
f1();
alert(n); // 999 直接读取全局变量
function f1() {
var n = 999;
}
alert(n); // error 函数外部无法读取函数内的局部变量
function f1() {
n = 999; // 未使用 var 命令,实际上声明了一个全局变量
}
f1();
alert(n); // 999
那如果想从外部读取局部变量,该如何实现?
在某些情况下,我们可能会需要得到函数内部的局部变量,"链式作用域"
结构(chain scope):子对象会一级一级地向上寻找所有父对象的变量(反之则不成立)。
那么,我们可以在外部函数内定义并返回一个内部函数,并在内部函数中返回该局部变量,这就是最常见的闭包。
js
function outerFun(){
let name = 'june';
function innerFun(){
return name;
}
return innerFun;
}
const getName = outerFun();
console.log(getName()); // june
闭包的定义
MDN:一个函数以及其捆绑的周边环境状态(lexical environment ,词法环境
)的引用的组合
阮一峰:闭包就是能够读取其他函数内部变量的函数(简单理解为"定义在一个函数内部的函数")
词法作用域
它是一种作用域决定机制。词法作用域根据源代码中 声明变量的位置
来确定该变量在何处可用,而非函数的调用位置。嵌套函数可访问声明于它们外部作用域的变量。
为了更好地说明词法作用域,我们来看下面例子:
js
var x = 'global';
function outer() {
var x = 'outer';
function inner() {
var x = 'inner';
console.log('1', x); // inner
}
console.log('2', x); // outer
inner();
}
console.log('3',x); // global
outer();
⚠️ 与动态作用域区分
动态作用域是基于函数的调用栈来决定变量作用域的(即变量的作用域是在运行时确定的),而不是基于代码的物理结构
闭包实例
js
const counter = function () {
let count = 1;
function acc() {
count++;
console.log(count);
};
return acc;
};
const add = counter();
add(); // 2
add(); // 3
在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦函数执行完毕,变量将不能再被访问。但这里显然不是。
词法环境包含了这个闭包创建时作用域内的任何局部变量。
在本例子中,add
是执行 counter
时创建的 acc
函数实例的引用。acc
的实例维持了一个对它的词法环境(变量 count
存在于其中)的引用。因此,当 add
被调用时,变量 count
仍然可用。
使用闭包定义一个函数工厂
js
function makeAdder(x) {
return function (y) {
return x + y;
};
}
// add5和add10共享相同的函数定义,但是保存了不同的词法环境
const add5 = makeAdder(5);
const add10 = makeAdder(10);
add5(5); // 10
add10(10); // 20
用闭包模拟私有变量
JavaScript 没有原生支持声明私有变量,可以使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式称为模块模式(module pattern)
js
const makeCounter = function () {
// 一个词法作用域
let privateCounter = 0;
function changeCount(val) {
privateCounter += val;
}
// 三个公共函数共享同一个环境的闭包
return {
increase: function () {
changeCount(1);
},
decrease: function () {
changeCount(-1);
},
value: function () {
return privateCounter;
},
};
};
const count1 = makeCounter();
const count2 = makeCounter();
count1.increase();
count2.decrease();
console.log(count1.value(), count2.value()); // 1 -1
计数器 Counter1
和 Counter2
相互独立,互不影响,各自维护自己词法作用域内的变量 privateCounter
常见的错误:在循环中创建闭包
可以将内部函数本身当做一个值类型进行传递,提供了一个入口可以更改函数内部的私有变量(在词法作用域之外执行)
js
for (var i = 1; i <= 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000 * i);
}
尽管循环的五个函数在各自迭代中分别定义但它们都被封闭在一个共享作用域中,故实际上就只有一个 i。
而循环终止的条件是 i 不再<=5,故首次满足条件的 i 为 6,延迟函数的回调会在循环结束之后才执行,故执行后的打印结果都是 6。
如果想按照预期输出结果呢?
a. IIFE
js
for (var i = 1; i <= 5; i++) {
(function () {
var j = i; // 闭包到块作用域(本质是将块作用域转换成可以被关闭的作用域); IIFE函数需要有自己的变量用来在每次迭代的时候存储i的值
setTimeout(() => {
console.log(j); // 正常输出:1 2 3 4 5
}, 1000 * j);
})();
}
b. 可以不用额外定义变量,直接将变量传递到 IIFE 函数中
js
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j); // 正常输出:1 2 3 4 5
}, 1000 * j);
})(i);
}
c. 更简洁的做法,将变量 i 的定义使用 let 取代 var
js
for (let i = 1; i <= 5; i++) {
// let声明的特殊行为:该变量在循环中不止被声明一次而是每次迭代都会声明,随后每个迭代都会使用上一个迭代结束的值来初始化这个变量
setTimeout(() => {
console.log(i); // 正常输出:1 2 3 4 5
}, 1000 * i);
}
换个例子,它的执行结果是什么?
js
var result = [];
var a = 3;
var total = 0;
function foo(a) {
for (var i = 0; i < 3; i++) {
result[i] = function () {
total += i * a;
console.log(total);
};
}
}
foo(1);
result[0]();
result[1]();
result[2]();
foo 函数传入变量 a 为 1,for 循环中 i 变量是通过 var 声明的,意味着在整个 foo 函数的作用域内,i 只有一个实例,循环后的值为 3。
第一次执行完 total 为 3,第二次 total 为 6,第三次 total 为 9,打印结果为 3 6 9。同样地,我们可以使用前面提到的 IIFE 或者 let 来获得期望的值。
再来看看这个:
js
for (var i = 0; i < 6; i++) {
(function () {
console.log(i);
})();
}
i
变量是通过var
声明的,它内部引用的i
变量是由外部for
循环作用域中的同一个i
变量。
但由于 IIFE 是"立即执行"的,它会立即打印每次循环迭代时i
的当前值,而不是等到变量i
循环完成后的最终值。
闭包的应用场景
闭包的用途主要有两个:可以读取函数内部的变量、让变量的值始终保持在内存中
- 自执行函数
- 防抖与节流
- 函数柯里化
- 订阅发布
- 迭代器...
思考题
js
var name = 'The Window';
var object = {
name: 'My Object',
getNameFunc: function () {
console.log(this); // object
return function () {
console.log(this); // window
return this.name; // 对象属性中,嵌套超过一级及以上的函数,this指向都是window(构造函数中也是这样)
};
},
};
alert(object.getNameFunc()()); // The Window
js
var name = 'The Window';
var object = {
name: 'My Object',
getNameFunc: function () {
var that = this;
return function () {
return that.name;
};
},
};
alert(object.getNameFunc()()); // My Object
当一个函数作为函数而不是方法来调用的时候,this 指向的是全局对象。
对象属性中,嵌套超过一级及以上的函数,this 指向都是 window
构造函数中的一级函数,this 指向通过构造函数; 构造函数中的二级(及以上)函数,this 指向的是 window
性能考量
需要思考是否需要使用闭包,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
滥用会导致内存泄漏:
js
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function () {
return this.name;
};
this.getMessage = function () {
return this.message;
};
}
继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法
js
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function () {
return this.name;
};
MyObject.prototype.getMessage = function () {
return this.message;
};
闭包中变量存储的位置是栈内存还是堆内存?
闭包可以访问外部函数中的变量,对于基本类型在逻辑上属于栈内存,但由于闭包的特性,这些变量实际上会被存储或复制到堆内存中,以便在外部函数执行完毕后仍然可以访问。
因此,可以认为闭包中变量的存储位置主要是在堆内存中。这也是为什么闭包可以持续访问外部函数作用域中的变量,即使那个作用域已经执行结束。
如果文章对你有用,请点赞再收藏,你的鼓励是我创作的动力~
参考文章: