介绍
闭包在js中还是挺重要的,而且在前端面试中很有可能会被问到。网上其实有很多的文章有对闭包进行介绍,不过感觉写的有点乱而且不是很全面。而官方的写的又太官方了,写的不是那么通俗易懂。当然啦官方的肯定是最完整的,例如MDN中的介绍。
在查找资料的过程中发现对于闭包理解的争论还是挺多的。所以在这里简单介绍一下我对于闭包的理解。希望能够帮助到有需要的童鞋
闭包要从作用域说起,在js中子作用域可以访问所有父作用域的数据,他会一直向上找直到到达全局作用域。而父作用域无法访问子作用域中的数据。由于每个函数都有自己的函数作用域,所以我们在全局作用域中没法直接访问函数中定义的数据,而函数中却可以直接访问全局作用域中定义的数据,同理嵌套函数的关系也是这样的,子函数可以访问父函数中定义的数据。
如果我们想在全局作用域中访问某个函数中的数据,我们可以让函数暴露一个子函数(可以在函数中return这个子函数、可以将子函数设置在全局作用域的某个对象中,也可以将子函数作为当前函数的一个方法(函数也是对象嘛)),全局作用域中可以直接访问这个子函数,然后由于子函数可以访问父函数中的数据,这样全局作用域中就可以通过子函数访问对应父函数作用域中的数据了。从而就实现了父作用域访问子作用域的数据,并且由于子函数中包含了父函数中数据的引用,所以父函数的数据不会在函数执行完之后被销毁。所以在全局作用域可以通过子函数操作其父函数作用域中的数据了。这个其实就是闭包。
那么闭包有什么用呢,简单的说闭包不就是通过一个子函数访问其父函数中的数据嘛,子函数和父函数作用域中的数据产生关联后就形成了闭包。这不就是跟对象一样嘛。对象也是提供一个方法然后可以通过方法操作内部的数据。我直接用对象不香嘛。所以这里就要重点说一下闭包和对象的区别了同时也是闭包的作用。
闭包和对象的区别是:闭包可以隐藏函数中的属性,在闭包中你会发现我们无法直接操作父函数中的属性只能通过提供的子函数进行操作,所以闭包中属性是私有的,相当于java中private修饰的属性。而在对象中我们是可以直接通过对象操作对象中的属性的,所以对象中定义的属性是公共的,相当于java中的public修饰的属性。并且由于在子函数中引用了父函数作用域的属性所以我们还可以实现让父函数作用域中的局部变量一直存在(当然我们外部得一直有引用指向子函数才行,如果子函数被回收了那么局部变量没有引用指向他自然也就被回收了),这样就类似于实现了全局变量的功能并且使用这种方式还不会对全局变量造成污染。下面来看一下例子会更容易理解
闭包实现
闭包有很多种写法,当然他们实现的功能都是一致的
第一种写法:
// 普通函数
function a(){
var num = 0;
console.log(++num);
}
// 未使用闭包时函数执行后函数中的数据都会被销毁,所以每次执行都相当于重新执行,因此也就无法让变量一直存在
a(); //1
a(); //1
// 闭包
function a(){
var aa = 0;
// 将子函数暴露出去,当然不用return也是可以的,只要在全局作用域能拿到这个子函数就行,例如将这个函数赋值给全局作用域中的某个变量,这样在全局作用域中也能拿到这个函数
function b(){
aa ++;
console.log(aa);
}
return b;
}
// 执行父函数拿到其子函数,由于子函数中引用了父函数的数据所以父函数中的数据并不会在执行后被销毁,所以aa变量依然存在
var ab = a();
// 通过子函数操作其父函数中的数据
ab(); //1
ab(); //2
第二种写法:
// 定义父函数的时候直接执行他从而拿到子函数
// 这里如果需要在定义后立即执行需要通过()将函数包裹起来。如果不包裹起来,js会把它当作函数声明来处理,如果包裹起来就是表达式。而最后的()则表示函数调用
var bi = (function(){
var a = 0;
function b(){
a ++;
console.log(a);
}
return b;
})();
// 直接执行子函数操作其父函数中的数据
bi(); //1
bi(); //2
第三种写法:
// 实际中最常用的写法,用于实现对象中私有属性的定义
function Person(name,age,publicval){
// 定义私有属性
var names = name;
var ages = age;
// 对象公有属性
this.publicval = publicval;
// 在对象中定义方法,通过方法返回私有属性
this.getName = function (){
// 子作用域是可以访问父作用域的数据的
return name;
}
this.getAge = function(){
return age;
}
}
var person = new Person('张三',23,'共有属性');
// 这里如果要访问私有属性,必须通过方法进行访问,无法直接操作
console.log(person.getName()); //张三
console.log(person.getAge()); //23
// 对象的属性也就是共有属性则可以直接访问并操作
console.log(person.publicval);
person.publicval = 'updateval';
// 这里并不会修改对象中的names变量,而是会在Person对象中新增一个names属性
person.names = "updatename";
并且闭包还可以解决js在ES5之前没有块级作用域的问题,例如
var fnBox = [];
function foo() {
for(var i = 0; i < 2; i++) {
fnBox[i] = function() {
return i;
}
}
}
foo();
var fn0 = fnBox[0];
var fn1 = fnBox[1];
// 由于var不存在块级作用域,所以for循环中的i的作用域为foo函数。并且所有fnBox函数中的i指向的都是foo函数中的i,他们指向的都是同一个变量。当变量被修改了,之后再去获取那么获取到的都是修改后的值
console.log(fn0()); // 2
console.log(fn1()); // 2
// 通过闭包解决
var fnBox = [];
function foo() {
for(var i = 0; i < 2; i++) {
// 通过闭包的方式将i的值放在父函数函数的作用域内,这样子函数的num值就是父函数函数作用域内的值,而不再是foo函数中i的值了
fnBox[i] = (function(num) {
return function() {
return num;
}
})(i);
}
}
foo();
var fn0 = fnBox[0];
var fn1 = fnBox[1];
console.log(fn0()); // 0
console.log(fn1()); // 1
不过在ES6之后提供了let和const带代替var。使用let和const声明的变量是存在块级作用域的。所以在ES6中我们只需要把i的声明改为let就可以了,当然也更推荐这种做法
var fnBox = [];
function foo() {
for(let i = 0; i < 2; i++) {
fnBox[i] = function() {
return i;
}
}
}
foo();
var fn0 = fnBox[0];
var fn1 = fnBox[1];
console.log(fn0()); // 0
console.log(fn1()); // 1