看不懂的看这个视频讲解
《彻底搞定闭包是什么》
动动你的小手,欢迎大家的关注-点赞-收藏。下一篇更新JavaScript的this指向
闭包
闭包已经成为近乎神话的概念,它非常重要又难以掌握,而且还难以定义。它是javascript语言的一个难点,也是它的特色,很多高级应用都是依靠闭包实现。
如何站好队
理解闭包,首先必须理解变量作用域。上章提到,javascript有两种作用域:全局作用域和函数作用域。
var a = 123; function fn1(){ console.log(a); } fn1();// 123
上面代码中,函数fn1
可以读取全局变量a
。
但是,函数外部无法读取函数内部声明的变量
function fn1(){ var a = 123; } console.log(a); //Uncaught ReferenceError: a is not defined
上面代码中,函数fn1
内部声明的变量a
,函数外是无法读取的
如果处于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,但是通过变通方法才能实现。在函数内部,再定义一个函数
function fn1(){ var a = 123; function fn2(){ console.log(a); //123 } }
上面代码中,函数fn2
就在函数fn1
内部,这时fn1
内部的所有局部变量,对fn2
都是可见的。但是反过来就不行,fn2
内部的局部变量,对fn1
就是不可见的。这就是javascript语言特有的链式作用域
结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见,反之则不成立。
既然fn2
可以读取fn1
的局部变量,那么只要把fn2
作为返回值,我们不就可以fn1
外部读取它的内部变量了吗!
function fn1(){ var a = 123; function fn2(){ console.log(a); //123 } return fn2; } var result = fn1(); console.log(result()); //123
上面代码中,函数fn1
的返回值就是函数fn2
,由于fn2
可以读取fn1
的内部变量,所以就可以在外部获得fn1
的内部变量
闭包就是函数fn2
,既能够读取其它函数内部变量的函数。由于在javascript语言中,只有函数内部的子函数才能读取父函数的内部变量,因为可以把闭包简单理解为:定义在一个函数内部的函数。
闭包最大的特点:就是它可以记住
诞生的环境,比如fn2
记住了它诞生的环境fn1
,所以在fn2
可以得到fn1
的内部变量。
本质上,闭包就是函数内部和函数外部链接的一座桥梁
闭包的用途
【1】读取函数内部的变量,让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
例子:闭包使得内部变量记住上一次调用时的运算结果
function a() { var start = 5; function b() { return start++; }; return b; } var inc = a(); inc();// 5 inc();// 6 inc();// 7 //释放内存 inc = null;
上面代码中,start
是函数a
的内部变量。通过闭包(函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包),start
的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看出,闭包inc
使得函数a
的内部环境一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
为什么会这样呢?原因就在于inc
始终在内存中,而inc
的存在依赖与a
,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的inc引用,这就是为什么函数a执行后不会被回收的原因。
【2】封装对象的私有属性和私有方法
function Person(name){ var _age; function setAge(n){ _age = n; } function getAge(){ return _age; } return { name:name, getAge:getAge, setAge:setAge } } var p1 = Person('mjj'); p1.setAge(18); p1.getAge();//18
上面代码中,函数Person
的内部变量_age
,通过闭包getAge
和setAge
,变成了返回对象p1
的私有变量。
注意,外层函数每次运行完,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
使用闭包的注意点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
- 《彻底搞定闭包的坑》
总结
闭包需要满足三个条件:
【1】访问所在作用域【2】函数嵌套 【3】在所在作用域外被调用
立即执行函数
实现
在 Javascript 中,圆括号()
是一种运算符,跟在函数名之后,表示调用该函数。比如,fn()
就表示调用fn
函数。
但有时需要定义函数之后,立即调用该函数。这种函数就叫做立即执行函数,全称为立即调用的函数表达式IIFE(Imdiately Invoked Function Expression)
注意:javascript引擎规定,如果function关键字出现在行首,一律解释成函数声明语句
函数声明语句需要一个函数名,由于没有函数名,所以报错
function (){}(); //Uncaught SyntaxError: Unexpected token (
解决方法就是不要让function
出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面
常用的两种写法
(function(){/* code */}()); //或者 (function(){/* code */})();
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。
注意
注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。
// 报错 (function(){ /* code */ }()) (function(){ /* code */ }())
上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。
其它写法
推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。
var i = function(){return 10}(); true && function (){/* code */}(); 0,function(){/* code */}(); !function(){ /* code */ }(); ~function(){ /* code */ }(); -function(){ /* code */ }(); +function(){ /* code */ }(); new function(){ /* code */ }; new function(){ /* code */ }();
用途
IIFE一般用于构造私有变量,避免全局污染。
接下来用一个需求实现来更直观地说明IIFE的用途。假设有一个需求,每次调用函数,都返回加1的一个数字(数字初始值为0)
【1】全局变量
一般情况下,我们会使用全局变量来保存该数字状态
var a = 0; function add(){ return ++a; } console.log(a);//1 console.log(b);//2
变量a实际上只和add函数相关,却声明为全局变量,不太合适。
【2】自定义属性
将变量a更改为函数的自定义属性更为恰当
function add(){ return ++add.count; } add.count = 0; console.log(add());//1 console.log(add());//2
有些代码可能会无意中将add.count重置
【3】IIFE
使用IIFE把计数器变量保存为私有变量更安全,同时也可以减少对全局空间的污染
var add = (function (){ var counter = 0; return function (){ return ++counter; } })();
注意事项
执行如下代码会报错,提示此时的a是undefined
var a = function(){ return 1; } (function(){ console.log(a());//报错 })();
这是因为没有加分号,浏览器将上面代码解释成如下所示
var a = function(){ return 1; }(function(){ console.log(a());//报错 })();
如果加上分号,就不会出错了
var a = function(){ return 1; }; (function(){ console.log(a());//1 })()
对循环和闭包的错误理解
容易犯错的一件事
function foo(){ var arr = []; for(var i = 0; i < 2; i++){ arr[i] = function (){ return i; } } return arr; } var bar = foo(); console.log(bar[0]());//2
犯错原因是在循环的过程中,并没有把函数的返回值赋值给数组元素,而仅仅是把函数赋值给了数组元素。这就使得在调用匿名函数时,通过作用域找到的执行环境中储存的变量的值已经不是循环时的瞬时索引值,而是循环执行完毕之后的索引值
IIFE解决容易犯错的问题
可以利用IIF传参和闭包来创建多个执行环境来保存循环时各个状态的索引值。因为函数传参是按值传递的,不同的参数的函数被调用时,会创建不同的执行环境
function foo() { var arr = []; for (var i = 0; i < 2; i++) { arr[i] = (function(j) { return function (){ return j; }; })(i); } return arr; } var bar = foo(); console.log(bar[1]()); //1
或者
function foo() { var arr = []; for (var i = 0; i < 2; i++) { (function(i) { arr[i] = function() { return i; } })(i) } return arr; } var bar = foo(); console.log(bar[1]());
块作用域
使用IIFE还是较为复杂,使用块作用域则更为方便
由于块作用域可以将索引值i重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值,相当于为每一次索引值都创建一个执行环境
function foo(){ var arr = []; for(let i = 0; i < 2; i++){ arr[i] = function(){ return i; } } return arr; } var bar = foo(); console.log(bar[1]());//0
名言
在编程中,如果实际和预期结果不符,就按照代码顺序一步一步地把执行环境图示画出来,会发现很多时候就是在想当然
闭包的的10种形式
根据闭包的定义,我们知道,无论何种手段,只要将内部函数传递到所在的作用域以外,它都会持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包,接下来,将详细介绍闭包的10中形式
返回值
最常用的一种形式是函数作为返回值被返回
var fn = function(){ var a = 'mjj'; var b = function(){ return a; } return b; } console.log(fn()());
函数赋值
一种变形的形式是将内部函数赋值给一个外部变量
var fn2; var fn = function(){ var a = 'mjj'; var b = function(){ return a; } fn2 = b; } fn(); console.log(fn2());
函数参数
闭包可以通过函数参数传递函数形式来实现
var fn2 = function(fn){ console.log(fn()); } var fn = function(){ var a = 'mjj'; var b = function(){ return a; } fn2(b); } fn();
IIFE
由前面的示例代码可知,函数fn()都是声明后立即被调用的,因此可以使用IIFE来替代。但是,要注意的是,这里的fn2()只能使用函数声明语句的形式,而不能使用函数表达式。
function fn2(fn){ console.log(fn()); } (function(){ var a = 'mjj'; var b = function(){ return a; } fn2(b); })();
循环赋值
在闭包问题上,最常见的一个错误就是循环赋值的错误
function foo(){ var arr = []; for(var i = 0; i < 5; i++){ arr[i] = function(){ return i; } } return arr; } var bar = foo(); console.log(bar[0]());//6
正确的写法如下
function foo(){ var arr = []; for(var i = 0; i< 5; i++){ arr[i] = (function(j){ return function test(){ return j; } })(i) } return arr; } var bar = foo(); console.log(bar[0]());//0
getter和setter
我们通过提供getter()和setter()函数来将要操作的变量保存在函数内部,防止其暴露在外部
var getValue,setValue; (function(){ var secret = 0; getValue = function(){ return secret; } setValue = function(v){ if(typeof v === 'number'){ secret = v; } } })(); console.log(getValue());//0 setValue(1); console.log(getValue());//1
迭代器
我们经常使用闭包来实现一个累加器
var add = (function(){ var counter = 0; return function (){ return ++counter; } })(); console.log(add());//1 console.log(add());//2
类似地,使用闭包可以很方便的实现一个迭代器
function setup(x){ var i = 0; return function (){ return x[i++]; } } var next = setup(['a','b','c']); console.log(next());//'a' console.log(next());//'b' console.log(next());//'c'
区分首次
var firstLoad = (function(){ var _list = []; return function(id){ if(_list.indexOf(id) >= 0){ return false; }else{ _list.push(id); return true; } } })(); firstLoad(10);//true firstLoad(10);//false firstLoad(20);//true firstLoad(20);//false
缓存机制
通过闭包加入缓存机制,使得相同的参数不用重复计算,来提高函数的性能
未加入缓存机制前的代码如下
var mult = function (){ var a = 1; for(var i = 0; i < arguments.length; i++){ a = a * arguments[i]; } return a; } console.log(mult(1,1,1,2,3,3));//18
加入缓存机制后,代码如下
var mult = function(){ var cache = {}; var calculate = function(){ var a = 1; for(var i = 0,len = arguments.length; i<len; i++){ a = a * arguments[i]; } return a; }; return function(){ var args = Array.prototype.join.call(arguments,','); if(args in cache){ return cache[args]; } return cache[args] = calculate.apply(null,arguments); } }() console.log(mult(1,1,1,2,3,3));//18
img对象
img对象经常用于数据上报
var report = function (src){ var img = new Image(); img.src = src; } report('http://xx.com/getUserInfo');
但是,在一些低版本的浏览器中,使用report函数进行数据上报会丢失30%左右的数据,也就是说,report函数并不是每一次都成功地发起了HTTP请求
原因是img是report函数中的局部作用域,当report函数的调用结束后,img局部变量随即被销毁,而此时或许还没来得及发出HTTP请求,所以此次请求就会丢失掉
现在把img变量用闭包封存起来,就能解决请求丢失的问题
var report = (function(){ var imgs = []; return function(src){ var img = new Image(); imgs.push(img); img.src = src; } })() report('http://xx.com/getUserInfo');