前言
函数在 JavaScript 是非常重要的知识,在上一节介绍过了 JavaScript 中一些常见的函数,本节继续聊聊相关的函数应用,其中最重要的就是函数闭包
函数作用域
在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的作用域内定义。相对应的,一个函数可以访问定义在其范围内的任何变量和函数。
比如说,在函数 multiply 内部可以访问其外部的全局变量 num1 和 num2
const num1 = 5;
const num2 = 2;
// 此函数定义在全局作用域中
function multiply() {
const sum = num1 * num2;
return sum;
}
console.log(multiply());
console.log(sum);
// 输出
> 10
> ReferenceError: sum is not defined
但是,在函数外部无法访问函数内容的变量 sum,这就是函数的作用域
嵌套函数
既然变量可以在函数的内部进行定义,而函数作为一等公民(first-class) 也可以像变量一样定义在函数的内容,即一个函数里面嵌套另外一个函数,这种函数就是嵌套函数
下面这个函数实现了计算一个数的平方,
function square(x) {
return function cal() {
return x*x
}
}
console.log(square(4)());
// 输出
> 16
square(4)
返回的是 square 函数内部的函数 cal
的一个对象,再通过()
来执行 cal 函数
转换下如下
function square(x) {
var cal = function(){
return x*x
};
return cal;
}
var testSquare = square(4);
console.log(testSquare.name)
console.log(testSquare);
console.log(testSquare());
// 输出
> cal
> function(){ return x*x }
> 16
testSquare
的 name 等于 cal, 所以被嵌套的函数就是一个函数表达式,
testSquare
也就是 cal 函数的函数表达式的一个对象,想想第一节中函数表达式怎么执行的,是不是一样的
闭包
还是上面的例子,
function square(x) {
var cal = function(){
return x*x
};
return cal;
}
var testSquare = square(4);
console.log(testSquare);
console.log(testSquare());
console.log(x);
// 输出
> function(){ return x*x }
> 16
> ReferenceError: x is not defined
发现一个问题没有,x 是 square
函数的局部变量,在函数外部直接打印提示 x is not defined
,而 testSquare
等于 function(){ return x*x }
,那为什么外部执行 testSquare()
还能使用使用这个变量 x
呢?
这里就要引出一个函数闭包的概念了
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合
简单来说就是虽然函数不在定义的词法作用域内被调用,但是仍然可以访问词法作用域中定义的变量。即有权访问另一个函数作用域中的变量
这个解释好像还是不太明白,我们知道正常情况下,函数的局部变量在其执行完毕后就会被销毁,以上面这个例子来说,变量 x
在执行完 square(4)
后还保留在内存当中,内部函数cal
可以引用外部函数的参数和局部变量,返回的内部函数testSquare
中保存了其外部函数square
的参数和变量,因此 testSquare 就是一个闭包
闭包有三步:
- 外层函数嵌套内层函数
- 内层函数使用外层函数的局部变量
- 把内层函数作为外层函数的返回值
经过这样的三步就可以形成一个闭包! 闭包就可以在全局函数里面操作另一个作用域的局部变量!闭包既能重复使用局部变量,又不污染全局
闭包的作用
我们上一节中提到了使用立即执行函数(IIFE)来解决变量提升的问题,其实这就是闭包的一种应用
封装块级作用域
for (var i = 0; i < 3; i++) {
const button = document.createElement("button");
button.innerText = `Button ${i}`;
// IIFE 写法
(function (ii){
button.onclick = function() {
console.log(ii)
}
})(i)
document.body.appendChild(button);
}
是不是很熟悉了,IIFE 函数中,每次循环的的参数 i 依然可以被内部函数 button.onclick
保留到自己的作用域中
私有化变量
这里直接使用 MDN 上的一个例子
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(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
上述示例创建了一个词法环境,为三个函数所共享:Counter.increment
,Counter.decrement
和 Counter.value
。
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter
的变量和名为 changeBy
的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
闭包的缺点
闭包引用了外部函数的作用域,所以滥用闭包会有内存问题
让函数的变量都保存在内存中,内存消耗变大。使用不当会造成内存泄漏