在C/C++一类语言里好像没有看到闭包和装饰器的概念,在学习Python、JavaScript、TypeScript时,闭包和装饰器好像是一条迈不过去的坎,有必要深入理解一下这两个语言概念。
JavaScript是面向过程的语言,不是面向对象的语言,其核心语法是函数。其虽然没有面向对象的语法,但其将函数function玩得溜溜的,在function函数内还套用函数,这种做法已十分接近面向对象的语法了,只差把function改为class了。
JavaScript函数内的函数叫闭包,C++内里的函数叫方法,对比来理解就很好懂。——我是学C++的,转过来学JavaScript,也就这么对比来理解了。
查看他人的文章,了解JavaScript引入闭包,是为了解决变量作用域的问题。为了后续表述方便,及表述完整,后面把闭包叫做闭包函数,并将其上层函数叫做闭包父函数。——闭包和装饰器都是函数,完整地叫闭包函数和装饰器函数,有助于理解概念。
看如下的代码,可以完整理解闭包函数的概念:
<script>
function fatherFunc() {
console.log("闭包父函数执行......")
// 闭包父函数中定义了一个局部变量
var localVar = 1;
function closureFunc() {
// 在闭包函数内可以读闭包父函数中的局部变量
console.log("闭包函数开始时的输出结果:" + localVar)
// 在闭包函数内还可以改变闭包父函数中的局部变量
localVar = localVar + 1;
console.log("闭包函数结束时的输出结果:" + localVar);
}
return closureFunc;
}
// 1. 调用闭包父函数
var obj = fatherFunc();
// 2. 第一次调用闭包函数
obj();
// 3. 第二次调用闭包函数
obj();
</script>
// 执行结果:
闭包父函数执行......
闭包函数开始时的输出结果:1
闭包函数结束时的输出结果:2
闭包函数开始时的输出结果:2
闭包函数结束时的输出结果:3
现在来慢慢解读这段代码,有5点关键内容需要完全掌握,缺一不可:
- 定义了一个闭包父函数fatherFunc()。
- 在闭包父函数内部定义了一个局部变量localVar,并赋值为1。
- 在闭包父函数内还定义了一个闭包函数closureFunc()。
- 在闭包函数内使用了闭包父函数的局部变量localVar,做了加1操作。
- 闭包父函数返回了闭包函数(指针或地址)。——JavaScript语言中没有指针和地址的概念,这里引用C++的说法,比较容易理解。实质就是函数的内存首地址。
现在来看函数调用:
- 调用闭包父函数:从执行结果看,闭包父函数内的闭包函数没有执行。
- 闭包父函数执行后的结果赋值给了变量obj。从代码看,这个obj是闭包函数的内存首地址。
- 第一次调用闭包函数:闭包父函数的局部变量localVar由1变为2。
- 第二次调用闭包函数:闭包父函数的局部变量localVar由2变为3。
可以预测,持续调用闭包函数,闭包父函数的局部变量会持续增加,实际也是这样。
这就是这段代码的奇特之处,一个函数内的局部变量,闭包函数可以反复访问,其值会持续保持上次操作的结果。这和我们通常理解的局部变量有很大不同,局部变量在函数退出后(闭包父函数执行结束后)即消失,也就是作用域消失。现在的实际情况是,这个局部变量好像被一个什么东东包起来了、保护起来了,没有消失。这个“包包”就是闭包!闭包包什么?包的就是局部变量,使其作用域持续保持。
如果你是C++/Java程序员,把闭包父函数理解为类Class,把闭包父函数的局部变量理解为类的属性,把闭包函数理解为类的方法,一切都顺理成章了。
下面来分析装饰器这个概念。装饰器是一个函数,下面对其的完整称呼为装饰器函数。
基于闭包函数,理解装饰器函数就比较容易了。这里使用三个函数,分别是原函数、闭包函数/打包函数和装饰器函数/闭包父函数。装饰器函数就是闭包父函数。
<script>
function decoratorFunc(srcFunc){
var localVar = srcFunc;
console.log("我是装饰器函数,对原函数进行装饰。其实我啥也不干,干活的是我的小弟闭包函数。");
function closureFunc(){
console.log("我就是装饰器函数的闭包函数,对原函数的装饰累活、脏话都由我来干,哎!");
console.log("调用原函数前做一些事情......");
srcFunc();
console.log("......调用原函数后再做一些事情");
}
return closureFunc;
}
function srcFunc(){
console.log("我是原函数");
}
var obj = decoratorFunc(srcFunc);
obj()
</script>
//执行结果:
我是装饰器函数,对原函数进行装饰。其实我啥也不干,干活的是我的小弟闭包函数。
我就是装饰器函数的闭包函数,对原函数的装饰累活、脏话都由我来干,哎!
调用原函数前做一些事情......
我是原函数
......调用原函数后再做一些事情
代码解读:
- 定义一个装饰器函数decoratorFunc()。
- 装饰器函数的输入参数为原函数srcFunc的内存首地址。——要理解为:原函数的内存首地址是装饰器函数的局部变量。
- 装饰器函数内定义了一个闭包函数closureFunc()。
- 闭包函数内部使用了装饰器函数的局部变量srcFunc。
- 装饰器函数返回了闭包函数的内存首地址。
- 定义了一个原函数srcFunc()。
比较 | 装饰器函数 – 打包函数 – 原函数 | 闭包父函数 – 闭包函数 – 闭包父函数局部变量 |
---|---|---|
1 | 定义一个装饰器函数decoratorFunc()。 | 定义了一个闭包父函数fatherFunc()。 |
2 | 装饰器函数的输入参数为原函数srcFunc的内存首地址。——要理解为:原函数的内存首地址是装饰器函数的局部变量。 | 在闭包父函数内部定义了一个局部变量localVar,并赋值为1。 |
3 | 装饰器函数内定义了一个闭包函数closureFunc()。 | 在闭包父函数内还定义了一个闭包函数closureFunc()。 |
4 | 闭包函数内部使用了装饰器函数的局部变量srcFunc。 | 在闭包函数内使用了闭包父函数的局部变量localVar,做了加1操作。 |
5 | 装饰器函数返回了闭包函数的内存首地址。 | 闭包父函数返回了闭包函数(指针或地址)。——JavaScript语言中没有指针和地址的概念,这里引用C++的说法,比较容易理解。实质就是函数的内存首地址。 |
6 | 定义了一个原函数srcFunc()。 |
把装饰器函数和闭包函数放在一起来看,两者既形似、也神似,只有两点稍有区别:
- 闭包父函数的局部变量变成了装饰器函数的输入参数。
- 输入参数是外部的一个原函数的内存首地址。
闭包函数的作用是解决局部变量的作用域问题,装饰器函数的作用是什么呢?与闭包函数类似,其作用是对原函数进行装饰。要这样来理解装饰:——与装饰房间有所不同。
- 保持原函数的纯正,不要在原函数内增加代码。——原函数会被很多地方调用,不能增加无用的华丽装饰。装饰器的真正目的不是为了装饰,而是保持原函数的纯正。真让人意外!这就是辩证法。
- 在某些场合,在原函数的基础上增加点操作,增加点“装饰”。