“闭包”,又称“定义在函数内部的函数”,闭包技术是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.闭包的弊端 注意,因为外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,外层函数多次运行后会导致内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。