“闭包”,又称“定义在函数内部的函数”,闭包技术是javaScript中很关键的核心技术,很多框架的研发或者企业高端技术都需要使用到它。要理解闭包技术,必须先弄明白“变量的作用域”。
1.变量的作用域
javaScript沿袭的java的变量规则,但稍有改进。和java一样可分为“全局变量”和“局部变量”,在javaScript中的“局部变量”又称之为函数变量。
var x = 999;
function f1() {
var y = 888;
console.log(x);
console.log(y);
}
f1() // 999 888
console.log(y); // Uncaught ReferenceError: n is not defined
由上可知javaScript和java一样,父对象的所有变量,对子对象都是可见的,反之则不成立。而有些时候需要在外部父对象中获取子对象区域内部的变量,正常情况下是无法做到的,这时候就需要用到“闭包”技术了。
2.什么是闭包
请先看以下的函数:
function f1() {
var n = 999;
function f2() {
console.log(n); // 999
}
}
上面代码中,函数f2
就在函数f1
内部,这时f1
内部的所有局部变量,对f2
都是可见的。但是反过来就不行,f2
内部的局部变量,对f1
就是不可见的。这就是 JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。上面的代码中内部函数分f2()就是“闭包”,一个定义在函数内部的函数。
3.闭包的用途
3.1. 突破局域限制,读取函数内部的变量值。
逻辑思维分析:
上面我们已经知道了函数f2()就是闭包,那么我们如果去使用它获取函数内部的变量呢?
分析:既然f2
可以读取f1
的局部变量,那么只要把f2
作为返回值,我们不就可以在外部得到返回值,进而间接
读取它的内部变量了吗!
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
上面代码中,函数f1
的返回值就是函数f2
,由于f2
可以读取f1
的内部变量,所以就可以在外部获得f1
的内部变量了。
3.2.“记住”诞生的环境
闭包最大的特点,就是它可以“记住”诞生的环境,比如f2
记住了它诞生的环境f1
,所以从f2
可以得到f1
的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。它可以让这些变量始终保持在内存中,使得它诞生环境一直存在。
现在,假设我们有一个需求:每调用一次函数,都记录这个函数的被调用的次数。如何实现?用我们常规的思维,肯定是定义一个外部变量,然后每调用一次就++,如下所示:
var start = 0;
function test_01() {
start++;
}
test_01();
start // 1
test_01();
start // 2
test_01();
start // 3
你们会发现,上面的方式完美的实现了。但假如需求在改动一下,函数test_01()内部还有一个函数test_02(),要录test_02()函数被调用的次数,这个时候如何实现呢?我们继续按上面的套路搬砖:
var start = 0;
function test_01() {
function test_02() {
start++;
}
}
test_01();
start // 0
test_01();
start // 0
test_01();
start // 0
这时候你们会发现,无论你调用多少次函数,start都不会增长,一直是0。why?
但如果你把上面的代码改一改,将函数test_02作为返回值,并且外部定义一个变量接受它,就不一样了。
var start = 0;
function test_01() {
return function test_02() {
start++;
}
}
var temp = test_01();
temp ();
start // 1
temp ();
start // 2
temp ();
start // 3
可以看到start的值又神奇般的增长了。这究竟是为什么呢?你是否感觉到了想破脑袋也想不明白是为什么?哈哈……
其实这就是闭包技术的一种体现。用比较科学的术语来技术就是:“temp始终在内存中,而temp的存在依赖于函数test_01(),
函数test_01()
也因此始终在内存中,不会在调用结束后,被垃圾回收机制回收。所以它才能一直记录下这个‘诞生环境’ ”。
上面的这种解释可能过于“科学语言”,让人难以理解。因此我用比较通俗的语言来解释:因为我在外部声明了一个变量temp,它调用了函数test_01(),而test_01()又返回了函数test_02()。所以上面的代码可以等价与下面的代码:
var start = 0;
function test_01() {
return function test_02() {
start++;
}
}
var temp = function test_02() {
start++;
};
temp ();
start // 1
temp ();
start // 2
temp ();
start // 3
上面我举的第一个例子就已经很好的说明了,在同一个作用域操作一个变量是可以成功的。这种变换操作手法更this的作用域极其相似。javaScript中this始终指向当前对象,然而this的指向却是动态的。说到这里了我就随便提一提this的作用域吧。
this的作用域
var A = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};
var B = {
name: '李四'
};
B.describe = A.describe;
B.describe()
// "姓名:李四"
var name = '龚文学';
var tenp = function () {
return '姓名:'+ this.name;
};
temp(); // 姓名:龚文学
从上面可以看出在A对象和B对象调用同一个函数this的指向不同,所以输出了不能的结果。如果把这个函数提取出来,赋值给一个变量,this的指向就是最顶层的window对象,这个时候就输出了我的顶顶大名--“龚文学”。这与“闭包”的方式十分类似,我以此举例说明,希望能帮助大家理解。如果大家还是有不懂的地方,请在微信公众平台《Java深度编程》留言。
3.3.封装对象的私有属性和私有方法
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25
上面代码中,函数Person
的内部变量_age
,通过闭包getAge
和setAge
,变成了返回对象p1
的私有变量。因为闭包能一直记住之前的环境,所以Person
的内部变量会随之永久改变,这与java的get,set方式十分类似。
4.闭包深度理解
通过上面的论述,如果你已经认真的阅读过了,相信你已经对闭包有了一定的认识与理解,但这还不够。下面我将进一步的由浅入深的详加说明。
上面我例子中的函数是无参的,如果是一个有参的呢?假如现在有一个需求调用一个函数,传入一个固定数字,每调用一次都在这个固定参数值上加1,无论后面调用参数是否改变,都以这个固定参数为准,该怎么实现呢?
function test(num) {
return function () {
return num++;
};
}
var inc = test(5);
inc() // 5
inc() // 6
inc() // 7
从上面的代码可以看出闭包内部函数记住了第一次传的参数5,并且每次调用都是在这个5的基础上+1,所以连续三次后num的值变成了7,为什么会这样呢?按我们常规的思维,我每次调用函数传的参数都是5,那么每次都应该是5++,应该都返回6才对啊。而事实却让我们难以理解。或许我将上面的代码改造一下会让人容易理解些:
function test(num) {
var temp = num
return function () {
return temp ++;
};
}
var inc = test(5);
inc() // 5
inc() // 6
inc() // 7
其实一个函数接受一个参数后,它的作用域就当拥有了这个值,也就是说相当于我上面的var temp = 参数。我们知道“子对象会一级一级的向上寻找变量,如果找不到就报错。如果找到了就取最近的返回”。所以上面的闭包中,第一次调用的时候函数test接受了参数5,内部也就保存了这个参数5,当继续执行内部闭包函数的时候,因为这个函数内部没有声明temp这个变量,所以它会去上面找,也就找到了test函数中的temp,也就是第一次传参数的num = 5。当找到这个参数temp后,内部的闭包函数就又会保存在自己的作用域中,所以在闭包中就存在了 temp = 5这个变量,随后执行了++,temp就变成了6;当第二次调用函数test(5)的时候,test函数的内部参数的确又重新变成了5,但是当它继续执行闭包函数的时候,这时候闭包函数内部已经有这个值了,所以它不会再向父级对象继续寻找变量了,所以也就 导致了第二次传参的时候,不管你传的参数是多少都没有关系了,它已经记录了第一次传的值。
function test(num) {
return function () {
return num++;
};
}
var inc = test(5);
inc(5) // 5
inc(500) // 6
inc(1000) // 7
以上的代码就足以证明了我的推论。第二次已经调用函数,闭包中的函数已经不再接受入参了,而是取第一次的入参。
总结:
所谓的闭包,就是一个定义在函数内部的函数。每个函数都有它的作用域,并且将内部的变量“封存”起来,所以称之为"闭包"。在这内部函数(闭包)中,它能记录第一次执行的“诞生环境”,进而能使得外部的作用域能跨域读取到这个内部函数的变量值。
5.闭包的弊端
注意,因为外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,外层函数多次运行后会导致内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
亲爱的同学们,如果您对本次“闭包”技术还有不理解的,或者有更多,更深入的研究,欢迎前来微信公众平台《Java深度编程》探讨,如果您有不错的原创技术文章,欢迎前来投稿。感谢您本次的阅读。