以下大部分为学习《JavaScript 高级程序设计》(第 3 版) 所做笔记。
目录:
1、了解闭包
2、闭包与变量
4、闭包用途
5、闭包使用场景
由于闭包会携带包含它的函数的作用域,因此会比其它函数占用更多的内存。过度使用闭包可能会导致内存占用过多。
闭包不等同于匿名函数。闭包是指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见方式是在一个函数内部创建另一个函数。
要理解闭包需要理解作用域链的概念,这里有作用域链相关笔记:https://www.cnblogs.com/xiaoxuStudy/p/12535960.html
一般来讲,当函数执行完毕后,局部对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况有所不同。在另一个函数内部定义的函数会被包含函数(即外部函数)的活动对象添加到它的作用域链中。
1 <script> 2 function createComparisonFn( propertyName ){ 3 return function( obj1, obj2 ){ 4 var value1 = obj1[propertyName]; 5 var value2 = obj2[propertyName]; 6 if( value1 < value2 ){ 7 return -1; 8 }else if( value1 > value2 ){ 9 return 1; 10 }else{ 11 return 0; 12 } 13 }; 14 } 15 var compareNames = createComparisonFn("name"); //创建函数 16 var result = compareNames( { name:"mo" }, { name:"na" } ); //调用函数 17 console.log(result); //输出:-1 18 </script>
在 createComparisonFn() 函数内部定义的匿名函数的作用域链中,实际将会包含外部函数 createComparisonFn() 的活动对象。在匿名函数从 createComparisonFn() 中被返回之后,它的作用域链被初始化为包含 createComparisonFn() 函数的活动对象和全局变量对象。这样匿名函数就可以访问在 createComparisonFn() 中定义的所有变量。更为重要的是,createComparisonFn() 函数在执行完毕之后,其活动对象也不会销毁,因为匿名函数的作用域链仍然在引用这个活动对象,换句话说,当 createComparisonFn() 函数返回之后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中。直到匿名函数被销毁后,createComparisonFn() 的活动对象才会被销毁。
调用 compareNames() 过程中产生的作用域链之间的关系:
怎么销毁匿名函数呢?
创建函数保存在变量中,通过将变量设置为 null 解除函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全地销毁了。
1 //创建函数 2 var compareNames = createComparisonFn("name"); 3 //调用函数 4 var result = compareNames( { name:"mo" }, { name:"na" } ); 5 //解除对匿名函数地引用(以便释放内存) 6 compareNames = null;
作用域链的配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。
1 <script> 2 function fn(){ 3 var result = new Array(); 4 //给数组的前10项各自创建一个闭包 5 for(var i=0; i<10; i++){ 6 result[i] = function(){ 7 return i; 8 }; 9 } 10 //循环完毕,此时 i=10 11 return result; 12 } 13 var fn1 = fn(); 14 for( var j=0; j<fn1.length; j++ ){ 15 console.log( fn1[j]() ); //输出了10个10 16 } 17 </script>
表面上看,数组 result 的值应该是[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] ,实际上是[ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]。
fn() 函数要返回时(即执行 return result;语句时),搜索过程从作用域链的前端开始,变量 result 是 fn() 中的局部变量,搜索到了定义 result 的地方,搜索停止,变量 result 保存的是一个数组,而在 fn() 返回之前已经用 for 语句将闭包赋值给数组的每一项且循环结束后 i = 10,每个函数都引用着变量 i, 变量 i 是 fn() 的局部变量,所以 fn() 返回后数组的每项函数内部 i 的值都是10。
变量 i 是 fn() 函数的活动对象 。当 fn() 函数返回后,变量 i 的值是 10,此时每个函数都引用着保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是10。
那么怎么让闭包的行为符合预期呢?
可以创建另一个匿名函数强制让闭包的行为符合预期。不直接把闭包赋值给数组,而是定义一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。这里匿名函数有一个参数 num ,也就是最终函数要返回的值,在调用每个匿名函数时传入变量 i , 因为函数参数是按值传递的,所以就会将变量 i 的当前值复制给参数 num。而在这个匿名函数内部,又创建了一个访问 num 的闭包。这样一来,result 数组中的每个函数都有自己 num 变量的一个副本。
(立即执行函数相关的笔记:https://www.cnblogs.com/xiaoxuStudy/p/12354095.html#IIFE 函数参数按值传递相关的笔记:https://www.cnblogs.com/xiaoxuStudy/p/12509729.html#three)
1 <script> 2 function fn(){ 3 var result = new Array(); 4 for(var i=0; i<10; i++){ 5 result[i] = function( num ){ 6 return function(){ 7 return num; 8 } 9 }( i ); 10 } 11 return result; 12 } 13 var fn1 = fn(); 14 for( var j=0; j<fn1.length; j++ ){ //依次输出数组 result 的元素 15 console.log( fn1[j]() ); 16 } 17 /*输出: 18 0 19 1 20 2 21 3 22 4 23 5 24 6 25 7 26 8 27 9 28 */ 29 </script>
this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window, 而当函数被作为某个对象的方法调用时, this 等于那个对象。
不过,匿名函数的执行具有全局性,因此其 this 对象通常指向 window。但有时候由于编写闭包的方式不同这一点可能不会那么明显。
1 <script> 2 var name = "the window"; 3 var obj = { 4 name : "the object", 5 //闭包 6 //函数作为某个对象的方法,返回一个匿名函数 7 sayName : function(){ 8 return function(){ 9 return this.name; 10 } 11 }, 12 //函数作为某个对象的方法 13 sayName2 : function(){ 14 return this.name; 15 } 16 } 17 //闭包 18 console.log( obj.sayName()() ); //输出:the window 19 //函数作为某个对象的方法被调用 20 console.log( obj.sayName2() ); //输出:the object 21 </script>
为什么匿名函数没有取得其包含作用域(或外部作用域)的 this 对象呢?
每个函数在被调用时都会自动取得 2 个特殊变量:this 和 arguments 。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
那么怎么能让闭包访问其包含作用域(或外部作用域)的 this 对象呢?
把外部作用域中的 this 对象保存在一个闭包能够访问的变量中,就可以让闭包访问该对象了。
1 <script> 2 var name = "the window"; 3 var obj = { 4 name : "the object", 5 sayName : function(){ 6 //在定义匿名函数之前把 this 对象赋值给一个名叫 that 的变量 7 var that = this; 8 return function(){ 9 return that.name; 10 } 11 } 12 } 13 console.log( obj.sayName()() ); //输出:the object 14 </script>
即使是语法的细微改变,都有可能意外改变 this 的值。
1 <script> 2 var name = "the window"; 3 var obj = { 4 name : "the object", 5 sayName : function(){ 6 console.log( this.name ); 7 } 8 } 9 obj.sayName(); //输出:the object 10 (obj.sayName()); //输出: the object 11 (obj.sayName = obj.sayName )(); //输出:the window 12 </script>
调用 obj.sayName(),返回的是:“the object”,因为 this.name 就是 object.name。
后 2 种调用方式我看不懂,先记录下来:调用方法前加括号 ( obj.sayName )() ,虽然加上括号之后,就好像只是在引用一个函数,但 this 的值得到了维持,因为 obj.sayName 和 (obj.sayName) 的定义是相同的。(obj.sayName = obj.sayName )() 先执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是函数本身,所以 this 的值不能得到维持,结果就返回了 "the window"。
截图自网络资料:
setTimeout 的传递的第一个函数不能带参数,使用闭包可以实现传参效果。
不使用闭包,第一个函数带参数会报错:
function func(param){ console.log(param); } var f1 = func('小许'); setTimeout(f1, 1000);
//报错:Callback must be a function. Received undefined
使用闭包可以实现传参效果:
function func(param){ return function(){ console.log(param); } } var f1 = func('小许'); setTimeout(f1, 1000); //输出:小许