定义
函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。
通俗的说就是:定义在一个函数内部的函数,并且该被函数访问外部函数作用域
只有调用外部函数时才会生成新闭包,不然就是同一个闭包
作用域链
对变量进行访问时,最先在自己的作用域中查找,如果找到则停止查找,否则向向一级作用域中查找,直到全局作用域中。如果还是没有找到就会报错。
例子:
// var myName = 'global'
function fn(){
// var myName = 'fn'
function fn1(){
// var myName = 'fn1'
console.log(myName)
}
fn1()
}
fn()
在上面的代码里依次从下面向上注释,输出结果依次是fn1
,fn
,global
,Uncaught ReferenceError: myName is not defined
,最后在全局作用域里都没找到就会报错;
词法作用域
词法作用域:指函数在定义(或声明)它们的作用域里运行,而不是在执行(或调用)它们的作用里运行;看个例子
var name = 'fn1_name'
function fn1(){
console.log(name)
}
function fn2(){
var name = 'fn2_name'
fn1()
}
fn2()// fn1_name
fn1是在全局中声明的,因此fn1是在全局作用域中运行的,然后根据作用域链查找name,fn1函数中没有向向一级作用域查找。最终在全局中找到它为fn1_name
闭包
总所周知,一个函数运行完毕以后,该函数作用域里的所有变量都会被垃圾机制回收;而闭包就是延长了作用域链;
闭包例子:
- 函数作为返回值返回
function fn(){
var myName = 'fn'
return function bar(){
console.log(myName)
}
}
var f = fn() //bar函数的引用赋值给变量f
f() //调用b函数 输出fn
上面的函数就是闭包的一种,var f = fn()
产生了闭包,延长myName
的作用域链,导致fn函数运行完毕后,myName得不到释放;
我们可以打个断点就清楚,闭包产生Closure
,记着只有引用了外部函数的变量才会产生闭包。还有上面的Call Stack代表了调用栈,现在是bar,是函数名,如果该函数为匿名函数则为anonymous,如果点到下面的anonymous,就是匿名的全局,断点就会跑到37行;
例子2:
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5); //返回匿名函数,x对外部函数有引用
var add10 = makeAdder(10);
console.log(add5(2)); // 7 相当于 function(2){return 5+2}
console.log(add10(2)); // 12
例子3:
for(var i=0;i<3;i++){
btn[i].onclick = function(){
console.log(i) //输出都是3
}
}
上面的代码运行了3次也就注册了3个按钮的点击事件,但是只有通过点击按钮才会执行函数,所以那时已经变为3了。
利用闭包:
for(var i=0;i<3;i++){
(function(j){
btn[i].onclick = function(){
console.log(j)
}
})(i)
}
这样操作的话,产生了3个闭包也保存了它们的值。当然我们开发时一般都是使用属性保存索引,ES6中的let
块作用域也可以解决该问题。
用闭包模拟私有方法:
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
上面的代码,对象的privateCounter只能通过暴露的方法修改,从而有了私有属性或者方法;模块化应该的思想应该就是这样,像JQuery,向外暴露一些方法。
试题
var name = 'The window'
var obj = {
name:'my obj',
sayName:function(){
return function(){
return this.name
}
}
}
alert(obj.sayName()()) //The window
alert(obj.sayName().call(obj)) //my obj
上述代码输出The window,因为obj.sayName()()的调用对象是window ,所以this指向The window;第二个是修改this的指向为obj;
var name = 'The window'
var obj = {
name:'my obj',
sayName:function(){
var that = this
return function(){
return that.name
}
}
}
alert(obj.sayName()()) //my obj
这个是把this的指向的引用赋值给that,然后是闭包,内部函数引用了外部函数的变量;
function fun(n,o){
console.log(o)
return{
fun:function(m){
return fun(m,n)
}
}
}
var a = fun(0);//返回一个对象{fun:function(){}} 输出undefined,产生一个闭包
a.fun(1);//向function(m)传1,n引用为0,输出0
a.fun(2);//向function(m)传2,n引用为0,输出0
a.fun(3)//向function(m)传3,n引用为0,输出0
var b = fun(0).fun(1).fun(2).fun(3)
//返回一个对象{fun:function(){}} 输出undefined,产生闭包
//第二次再次调用了外部函数又产生一个闭包,输出0
//第三次再次调用了外部函数又产生一个闭包,输出1
//第四次再次调用了外部函数又产生一个闭包,输出2
var c = fun(0).fun(1);
//在第二个闭包的基础上开始,不再生成新闭包,使用先输出undefined,0
c.fun(2);//输出1
c.fun(3)//输出1
上述代码,注意有没有生成新的闭包;
总结
闭包,我们可能都用过,但是也没有刻意的去使用它,都是潜意识的。但是闭包可能会造成内存泄漏,所以要谨慎使用。