JavaScript高级程序设计(第3版)学习笔记9——函数(下)
再接着看函数——具有魔幻色彩的对象。
9、作为值的函数
在一般的编程语言中,如果要将函数作为值来使用,需要使用类似函数指针或者代理的方式来实现,但是在ECMAScript中,函数是一种对象,拥有一般对象具有的所有特征,除了函数可以有自己的属性和方法外,还可以做为一个引用类型的值去使用,实际上我们前面的例子中已经有过将函数作为一个对象属性的值,又比如函数也可以作为另一个函数的参数或者返回值,异步处理中的回调函数就是一个典型的用法。
var name = 'linjisong'; var person = {name:'oulinhai'}; function getName(){ return this.name; } function sum(){ var total = 0, l = arguments.length; for(; l; l--) { total += arguments[l-1]; } return total; } // 定义调用函数的函数,使用函数作为形式参数 function callFn(fn,arguments,scope){ arguments = arguments || []; scope = scope || window; return fn.apply(scope, arguments); } // 调用callFn,使用函数作为实际参数 console.info(callFn(getName));//linjisong console.info(callFn(getName,'',person));//oulinhai console.info(callFn(sum,[1,2,3,4]));//10
再看一个使用函数作为返回值的典型例子,这个例子出自于原书第5章:
function createComparisonFunction(propertyName) { return function(object1, object2){ var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2){ return -1; } else if (value1 > value2){ return 1; } else { return 0; } }; } var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}]; data.sort(createComparisonFunction("name")); console.info(data[0].name); //Nicholas data.sort(createComparisonFunction("age")); console.info(data[0].name); //Zachary
10、闭包(Closure)
闭包是指有权访问另一个函数作用域中的变量的函数。对象是带函数的数据,而闭包是带数据的函数。
首先闭包是一个函数,然后闭包是一个带有数据的函数,那么,带有的是什么数据呢?我们往上看看函数作为返回值的例子,返回的是一个匿名函数,而随着这个匿名函数被返回,外层的createComparisonFunction()函数代码也就执行完成,按照前面的结论,外层函数的执行环境会被弹出栈并销毁,但是接下来的排序中可以看到在返回的匿名函数中依旧可以访问处于createComparisonFunction()作用域中的propertyName,这说明尽管createComparisonFunction()对应的执行环境已经被销毁,但是这个执行环境相对应的活动对象并没有被销毁,而是作为返回的匿名函数的作用域链中的一个对象了,换句话说,返回的匿名函数构成的闭包带有的数据就是:外层函数相应的活动对象。由于活动对象的属性(也就是外层函数中定义的变量、函数和形式参数)会随着外层函数的代码执行而变化,因此最终返回的匿名函数构成的闭包带有的数据是外层函数代码执行完成之后的活动对象,也就是最终状态。
希望好好理解一下上面这段话,反复理解一下。虽然我已经尽我所能描述的更易于理解一些,但是闭包的概念还是有些抽象,下面看一个例子,这个例子来自原书第7章:
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(){ return i; }; } return result; } var funcs = createFunctions(); for (var i=0,l=funcs.length; i < l; i++){ console.info(funcs[i]());//每一个函数都输出10 }
这里由于闭包带有的数据是createFunctions相应的活动对象的最终状态,而在createFunctions()代码执行完成之后,活动对象的属性i已经变成10,因此在下面的调用中每一个返回的函数都输出10了,要处理这种问题,可以采用匿名函数作用域来保存状态:
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = (function(num){ return function(){ return num; }; })(i); } return result; }
将每一个状态都使用一个立即调用的匿名函数来保存(保存在匿名函数相应的活动对象中),然后在最终返回的函数被调用时,就可以通过闭包带有的数据(相应的匿名函数活动对象中的数据)来正确访问了,输出结果变成0,1,...9。当然,这样做,就创建了10个闭包,在性能上会有较大影响,因此建议不要滥用闭包,另外,由于闭包会保存其它执行环境的活动对象作为自身作用域链中的一环,这也可能会造成内存泄露。尽管闭包存在效率和内存的隐患,但是闭包的功能是在太强大,下面就来看看闭包的应用——首先让我们回到昨天所说的函数绑定方法bind()。
(1)函数绑定与柯里化(currying)
A、再看this,先看一个例子(原书第22章):
<button id='my-btn' title='Button'>Hello</button> <script type="text/javascript"> var handler = { title:'Event', handleClick:function(event){ console.info(this.title); } }; var btn = document.getElementById('my-btn');//获取页面按钮 btn.onclick = handler.handleClick;//给页面按钮添加事件处理函数 </script>
如果你去点击“Hello”按钮,控制台打印的是什么呢?竟然是Button,而不是期望中的Event,原因就是这里在点击按钮的时候,处理函数内部属性this指向了按钮对象。可以使用闭包来解决这个问题:
btn.onclick = function(event){ handler.handleClick(event);//形成一个闭包,调用函数的就是对象handler了,函数内部属性this指向handler对象,因此会输出Event
}
B、上面的解决方案并不优雅,在ES5中新增了函数绑定方法bind(),我们使用这个方法来改写一下:
if(!Function.prototype.bind){//bind为ES5中新增,为了保证运行正常,在不支持的浏览器上添加这个方法 Function.prototype.bind = function(scope){ var that = this;//调用bind()方法的函数对象 return function(){ that.apply(scope, arguments);//使用apply方法,指定that函数对象的内部属性this }; }; } btn.onclick = handler.handleClick.bind(handler);//使用bind()方法时只需要使用一条语句即可
这里添加的bind()方法中,主要技术也是创建一个闭包,保存绑定时的参数作为函数实际调用时的内部属性this。如果你不确定是浏览器本身就支持bind()还是我们这里的bind()起了作用,你可以把特性检测的条件判断去掉,然后换个方法名称试试。
C、上面对函数使用bind()方法时,只使用了第一个参数,如果调用bind()时传入多个参数并且将第2个参数开始作为函数实际调用时的参数,那我们就可以给函数绑定默认参数了。
if(!Function.prototype.bind){ Function.prototype.bind = function(scope){ var that = this;//调用bind()方法的函数对象 var args = Array.prototype.slice.call(arguments,1);//从第2个参数开始组成的参数数组 return function(){ var innerArgs = Array.prototype.slice.apply(arguments); that.apply(scope, args.concat(innerArgs));//使用apply方法,指定that函数对象的内部属性this,并且填充绑定时传入的参数 }; }; }
D、柯里化:在上面绑定时,第一个参数都是用来设置函数调用时的内部属性this,如果把所有绑定时的参数都作为预填的参数,则称之为函数柯里化。
if(!Function.prototype.curry){ Function.prototype.curry = function(){ var that = this;//调用curry()方法的函数对象 var args = Array.prototype.slice.call(arguments);//预填参数数组 return function(){ var innerArgs = Array.prototype.slice.apply(arguments);//实际调用时参数数组 that.apply(this, args.concat(innerArgs));//使用apply方法,并且加入预填的参数 }; }; }
(2)利用闭包缓存
还记得前面使用递归实现斐波那契数列的函数吗?使用闭包缓存来改写一下:
var fibonacci = (function(){//使用闭包缓存,递归 var cache = []; function f(n){ if(1 == n || 2 == n){ return 1; }else{ cache[n] = cache[n] || (f(n-1) + f(n-2)); return cache[n]; } } return f; })(); var f2 = function(n){//不使用闭包缓存,直接递归 if(1 == n || 2 == n){ return 1; }else{ return f2(n-1) + f2(n-2); } };
下面是测试代码以及我机器上的运行结果:
var test = function(n){ var start = new Date().getTime(); console.info(fibonacci(n)); console.info(new Date().getTime() - start); start = new Date().getTime(); console.info(f2(n)); console.info(new Date().getTime() - start); }; test(10);//55,2,55,2 test(20);//6765,1,6765,7 test(30);//832040,2,832040,643
可以看到,n值越大,使用缓存计算的优势越明显。作为练习,你可以尝试自己修改一下计算阶乘的函数。
(3)模仿块级作用域
在ECMAScript中,有语句块,但是却没有相应的块级作用域,但我们可以使用闭包来模仿块级作用域,一般格式为:
(function(){ //这里是块语句 })();
上面这种模式也称为立即调用的函数表达式,这种模式已经非常流行了,特别是由于jQuery源码使用这种方式而大规模普及起来。
闭包还有很多有趣的应用,比如模仿私有变量和私有函数、模块模式等,这里先不讨论了,在深入理解对象之后再看这些内容。
关于函数,就先说这些,在网上也有很多非常棒的文章,有兴趣的可以自己搜索一下阅读。这里推荐一篇文章,《JavaScript高级程序设计(第3版)》译者的一篇译文:命名函数表达式探秘。
郴江幸自绕郴山,为谁流下潇湘去?
欲将心事付瑶琴,知音少,弦断有谁听?
倩何人,唤取红巾翠袖,揾英雄泪!
零落成泥碾作尘,只有香如故!