作为八股文中最常见的一个问题,闭包一开始对小白来说是比较难理解的一个部分,虽然平时可能使用闭包的地方比较少,但是确实面试中热门的问题。本文参考地址
☀️ 什么是闭包
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来----MDN
简单来说:闭包存在俩个特点
- 在一个函数中返回另外一个函数
- 内函数使用了外函数的参数或者是变量
最常见的写法是长这样子的
function outSide() {
var name = "catch me";
return function inSide() {
console.log(name);
}
}
const inSides = outSide()
inSides();
outSide函数中有一个局部变量name和一个inSide函数,inSide属于outSide的内部函数,但是inSide中没有自己的变量,但他可以访问到外部函数outSide的变量。当outSide函数执行完之后,一般来说一个函数中的局部变量执行完就无法访问了。然而在inSide仍然能拿到变量的值,所以闭包情况与此不同,执行上下文被销毁,但创建时所在词法环境依然存在,变量仍然能够保存下来。
☀️ 闭包的使用场景
一个简单的应用场景,该方法使用了一个闭包返回设置fontsize 的不同大小,我们可以通过设置不同的参数在同一个闭包上,返回同一函数不同参数的几种方法,这样就可以批量的产出我们需要用到的函数
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
1.柯里化函数
柯里化主要是对相同参数的函数进行复用,比如下图可以实现一个计算面积大小的函数。
function getArea(width, height) {
return width * height
}
// 假设需要计算几个宽度不变高度发生变化的面积,这样重复的写会比较麻烦
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)
// 使用闭包柯里化固定一个宽度参数去计算面积
function getArea(width) {
return height => {
return width * height
}
}
const getTenWidthArea = getArea(10)
// 生成一个计算宽度为10高度任意变化的函数
const area1 = getTenWidthArea(20)
const area2 = getTenWidthArea(30)
// 生成一个计算宽度为20高度任意变化的函数
const getTwentyWidthArea = getArea(20)
2.模拟私有方法
在
JavaScript
中,没有支持声明私有变量,闭包可以用来模拟私有方法
在下面这段代码中,我们需要生成多个计数器,多个计数器之间互不干扰,各自执行自己的计数加减,通过闭包的方式可以将计算的加减方法变成每个计数器的公有函数法,并在其中访问计数器的私有方法和变量,这样各个计数器之间不会影响到。
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
在方法执行的过程中,改变这个私有变量的值,闭包的词法环境被改变,但不会影响另一个闭包中的变量,因为两个闭包之间都是私有的。
性能优化
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
上面的闭包写法在日常中可能并不太经常存在,由于闭包对性能方面会有一定的影响,所以一般是比较少用到闭包的,对于上面的写法可以通过关联对象原型的方法进行使用,比如下面这段例子,变量已经通过this的方式放在对象中,所以我们需要用到的方法可以直接在原型上进行绑定。
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;
};
作用域的认识
开始的时候我们说到闭包是在一个函数中返回另一个函数,内部函数可以访问到外部函数的作用域,那么这个作用域是是指什么呢,作用域其实就是一段段的代码块,比如声明变量和方法或者执行方法的代码,处在不同位置会成为不同的作用域,一般分为三块作用域
- 全局作用域
- 函数作用域
- 块级作用域
全局作用域
像以下这种变量、函数、函数执行都在全局作用下,不在函数中,或者一个大括号中声明的,则视为处在全局作用域中
// 全局变量
var greeting = 'Hello World!';
function greet() {
console.log(greeting);
}
// 打印 'Hello World!'
greet();
函数作用域
这个比较简单,在函数中的代码都处在函数作用域中,外部访问不到,进行访问的时候会报错,通常也叫做局部作用域
function greet() {
var greeting = 'Hello World!';
console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);
块级作用域
块级作用域主要保存着let和const声明的变量,主要表现为一个大括号,大括号中let和const声明的变量视为存在于块级作用域中,在大括号外无法访问,但是var声明的为全局变量,会造成变量提升,在大括号外仍然是可以访问的到的。
{
// 块级作用域中的变量
let greeting = 'Hello World!';
var lang = 'English';
console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);