1、函数闭包 1.1 概述
JavaScript采用词法作用域,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。
为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性被称为“闭包”
从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链
1.2 嵌套函数 先看下面的代码:
1 2 3 4 5 6 7 8 9 var scope = "globle scope" ;function checkscope ( ) { var scope = "local scope" ; function f ( ) { return scope; } return f (); } console .log (checkscope ());
1 2 3 4 5 6 7 8 9 10 var scope = "globle scope" ;function checkscope ( ) { var scope = "local scope" ; function f ( ) { return scope; } return f; } var ff = checkscope ();console .log (ff ());
它们可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义它们的外部函数。
1.3 闭包原理
在JavaScript中,如果一个对象不再被引用,那么这个对象就会被垃圾回收机制回收
每次调用JavaScript函数的时候,都会为之创建一个新的对象(活动对象Activation Object)用来保存局部变量,把这个对象添加至作用域链中
当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。 如果不存在嵌套的函数,也没有其他引用指向这个绑定对象,它就会被当做垃圾回收掉。 如果定义了嵌套的函数,每个嵌套的函数都各自对应一个作用域链,并且这个作用域链指向一个变量绑定对象。
如果这个函数定义了嵌套的函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。它就不会被当做垃圾回收,并且它所指向的变量绑定对象也不会被当做垃圾回收
1.3.1 实例1 为每个li标签添加点击事件,并输出下标和文本内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <body > <ul id ="test" > <li > 123</li > <li > 456</li > <li > 789</li > <li > 000</li > </ul > </body > <script > let items = document .querySelectorAll ("ul#test li" ) console .log (items); function bindclick (nodes ) { for (var i = 0 ; i < nodes.length ; i++) { nodes[i].onclick = function ( ) { console .log (i, nodes[i].innerHTML ); } } } bindclick (items) </script >
这里li标签内容显示出来后,点击li标签,报错,如:
原因: 作用域不同,所以获取不到li标签的innerHTML。
解决方案一: 将for循环里的var改成let,如下:
1 2 3 4 5 6 7 8 9 10 11 12 <script > let items = document .querySelectorAll ("ul#test li" ) console .log (items); function bindclick (nodes ) { for (let i = 0 ; i < nodes.length ; i++) { nodes[i].onclick = function ( ) { console .log (i, nodes[i].innerHTML ); } } } bindclick (items) </script >
现在就可以正常运行了,如:
解释: let声明的变量会识别块作用域,块作用域中声明的函数就形成了闭包。 每次循环都会创建一个新的函数,则这些函数每一个都会形成闭包。 因此,每次调用特定函数时所访问的i,就是其闭包中引用了外部函数作用域链上的i。所以建议在js的for循环中,使用let进行变量的声明。
解决方案二: 使用立即执行函数。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script > let items = document .querySelectorAll ("ul#test li" ) console .log (items); function bindclick (nodes ) { for (var i = 0 ; i < nodes.length ; i++) { nodes[i].onclick = (function (index ) { return function ( ) { console .log (index, nodes[index].innerHTML ); } })(i) } } bindclick (items) </script >
运行结果:
解释: 这里的立即执行函数,充当局部作用域,在console的外层形成闭包,每执行一次,都会产生一个AO活动对象,都会有一个index被放在AO对象里并被保留下来,返回的函数赋给onclick。
1.3.2 实例2 1 2 3 4 5 6 7 8 9 10 11 function fx (a, b ) { if (fx.count ) fx.count ++; else fx.count = 1 ; return a + b; } fx (1 , 2 );fx (2 , 3 );fx (3 , 4 );fx (4 , 5 );console .log (fx.count );
运行结果:4 直接运行到没有错,但这里的fx.count属性是可以被直接赋值的,会让源代码失去意义。改写后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function genfx ( ) { let count = 0 return function (a, b ) { count++ console .log (`第${count} 次调用函数` ) return a + b } } let fx = genfx ()fx (1 , 2 )fx (2 , 3 )fx (3 , 4 )fx (4 , 5 )
运行结果: 第1次调用函数 第2次调用函数 第3次调用函数 第4次调用函数
1.4 闭包缺陷 闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。
如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
2、函数式编程 2.1 高阶函数 在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数: (1)接受一个或多个函数作为输入; (2)输出一个函数;
2.2 函数链式调用 在设计函数(作为对象方法)返回值时,将处理后的当前对象(上下文)返回,以便进行紧凑的方法调用。
举个例子:
1 2 3 let str = "abc,123" let arr = str.split ("," ).reverse ().join ("-" );console .log (arr);
这里表示先将str字符串按逗号分割成数组,然后翻转数组,加入字符-后返回新的字符串。
输出结果: 123-abc
2.3 偏函数 固定一个函数的一个或者多个参数,返回一个新的函数,这个函数用于接受剩余的参数
2.3.1 实例1 1 2 3 4 5 let sum = function (x, y ) { return x + y; }; let succ = sum.bind (null , 1 );console .log (succ (2 ));
这里的succ被称为偏函数。
2.3.2 优点
通过创建一个名称易懂的独立函数,调用是无需每次传入第一个参数,因为第一个参数通过bind提供了固定值
当有一个很通用的函数,为了方便提供一个较常用的变体。例如:有函数send(from, to, text),可以创建一个针对当前用户的send函数的变体:sendTo(to, text)
2.3.3 缺陷
如果想固定一些参数,但不绑定this。 bind不允许这样做,因为必须指定第一个参数(要么指定对象,要么null),不能忽略上下文并跳转到参数。
2.3.4 实例2 实现一个自定义偏函数生成器
1 2 3 4 5 function partial (func, ...argsBound ) { return function (...args ) { return func.call (this , ...argsBound, ...args); }; }
2.3.5 实例3 自定义偏函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function partial (func, ...argsBound ) { return function (...args ) { return func.call (this , ...argsBound, ...args); }; } let jack = { firstName : "Jack" , say (time, phrase ) { console .log (`[${time} ] ${this .firstName} : ${phrase} ` ); }, }; let tom = { firstName : "Tom" , }; jack.sayNow = partial ( jack.say , new Date ().getHours () + ":" + new Date ().getMinutes () ); tom.sayNow = jack.sayNow ; jack.sayNow ("Hello" ); tom.sayNow ("World" );
输出结果: [17:32] Jack: Hello [17:32] Tom: World
2.4 函数柯里化 转换一个调用函数f(a,b,c)为f(a)(b)(c)方式调用。
2.4.1 实例1 针对两个参数的函数柯里化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function curry (func ) { return function (a ) { return function (b ) { return func (a, b) } } } function sum (a, b ) { return a + b } let currysum = curry (sum)console .log (currysum (1 )(2 ));
输出结果:3
2.4.2 实例2 实现高级柯里化函数,针对n个参数的函数
1 2 3 4 5 6 7 8 9 10 11 function curry (func ) { return function curried (...args ) { if (args.length >= func.length ) { return func.apply (this , args); } else { return function pass (...args2 ) { return curried.apply (this , args.concat (args2)); }; } }; }
注意: (1)如果传递args数与原函数已经定义的参数个数一样或更长,那么直接调用func函数。 (2)获得偏函数:否则,不调用func函数,返回另一个包装器pass,提供连接之前的参数一起做为新参数重新应用curried。然后再次执行一个新调用,返回一个新偏函数(如果参数不够)或最终结果。
2.4.3 实例3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function curry (func ) { return function (a ) { return function (b ) { return function (c ) { return func (a, b, c) } } } } function sum (a, b, c ) { return a + b + c } let curriedsum = curry (sum)console .log (curriedsum (1 )(2 )(3 ));
2.4.4 实例4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function curry (func ) { return function curried (...args ) { if (args.length >= func.length ) { return func.apply (this , args) } else { return function pass (...args2 ) { return curried.apply (this , args.concat (args2)) } } } } function log (date, importance, message ) { console .log ( `[${date.getHours()} :${date.getMinutes()} ] [${importance} ] ${message} ` ); } log = curry (log); let todayLog = log (new Date ());todayLog ("INFO" , "users has been initialized" );let todayDebug = todayLog ("DEBUG" );todayDebug ("fixed login error" );
运行结果: [17:38] [INFO] users has been initialized [17:38] [DEBUG] fixed login error