JavaScript高级程序设计(第3版)学习笔记9 js函数(下)


Posted in Javascript onOctober 11, 2012

再接着看函数——具有魔幻色彩的对象。

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版)》译者的一篇译文:命名函数表达式探秘。

Javascript 相关文章推荐
HTML中不支持静态Expando的元素的问题
Mar 08 Javascript
Domino中运用jQuery读取视图内容的方法
Oct 21 Javascript
js 上传图片预览问题
Dec 06 Javascript
浅析JavaScript中的事件机制
Jun 04 Javascript
javascript中if和switch,==和===详解
Jul 30 Javascript
JS+CSS实现类似QQ好友及黑名单效果的树型菜单
Sep 22 Javascript
window.open不被拦截的简单实现代码(推荐)
Aug 04 Javascript
JS封装通过className获取元素的函数示例
Dec 20 Javascript
jQuery复合事件用法示例
Jun 10 jQuery
Textarea输入字数限制实例(兼容iOS&amp;安卓)
Jul 06 Javascript
Vue精简版风格指南(推荐)
Jan 30 Javascript
Node.js path模块,获取文件后缀名操作
Nov 07 Javascript
JavaScript高级程序设计(第3版)学习笔记8 js函数(中)
Oct 11 #Javascript
JavaScript高级程序设计(第3版)学习笔记7 js函数(上)
Oct 11 #Javascript
JavaScript高级程序设计(第3版)学习笔记6 初识js对象
Oct 11 #Javascript
JavaScript高级程序设计(第3版)学习笔记5 js语句
Oct 11 #Javascript
JavaScript高级程序设计(第3版)学习笔记4 js运算符和操作符
Oct 11 #Javascript
JavaScript高级程序设计(第3版)学习笔记3 js简单数据类型
Oct 11 #Javascript
JavaScript高级程序设计(第3版)学习笔记2 js基础语法
Oct 11 #Javascript
You might like
php+mysql开源XNA 聚合程序发布 下载
2007/07/13 PHP
如何用C语言编写PHP扩展的详解
2013/06/13 PHP
浅谈php7的重大新特性
2015/10/23 PHP
Javascript 构造函数,公有,私有特权和静态成员定义方法
2009/11/30 Javascript
jquery(live)中File input的change方法只起一次作用的解决办法
2011/10/21 Javascript
通过JS获取用户本地图片路径并显示的代码
2012/02/16 Javascript
可自己添加html的伪弹出框实现代码
2013/09/08 Javascript
微信JS接口汇总及使用详解
2015/01/09 Javascript
JavaScript中style.left与offsetLeft的使用及区别详解
2016/06/08 Javascript
Bootstrap实现带动画过渡的弹出框
2016/08/09 Javascript
BootStrap实现手机端轮播图左右滑动事件
2016/10/13 Javascript
微信小程序 实战程序简易新闻的制作
2017/01/09 Javascript
nodejs服务搭建教程 nodejs访问本地站点文件
2017/04/07 NodeJs
JavaScript使用链式方法封装jQuery中CSS()方法示例
2017/04/07 jQuery
vue实现全选和反选功能
2017/08/31 Javascript
Js判断H5上下滑动方向及滑动到顶部和底部判断的示例代码
2017/11/15 Javascript
利用Vue2.x开发实现JSON树的方法
2018/01/04 Javascript
Node如何后台数据库使用增删改查功能
2019/11/21 Javascript
Python Pandas 获取列匹配特定值的行的索引问题
2019/07/01 Python
python程序 线程队列queue使用方法解析
2019/09/23 Python
keras 特征图可视化实例(中间层)
2020/01/24 Python
Python字符串格式化常用手段及注意事项
2020/06/17 Python
浅谈Python描述数据结构之KMP篇
2020/09/06 Python
Pytorch生成随机数Tensor的方法汇总
2020/09/09 Python
html5视频播放_动力节点Java学院整理
2017/07/13 HTML / CSS
美国和加拿大房车出售在线分类广告:RVT.com
2018/04/23 全球购物
英语系本科生求职信范文
2013/12/18 职场文书
影视动画专业个人的自我评价
2013/12/31 职场文书
《陈毅探母》教学反思
2014/05/01 职场文书
小学安全汇报材料
2014/08/14 职场文书
企业委托书范本
2014/09/13 职场文书
员工2014年度工作总结
2014/12/09 职场文书
员工辞职信范文
2015/03/02 职场文书
银行实习推荐信
2015/03/27 职场文书
浅谈Redis的keys命令到底有多慢
2021/10/05 Redis
Go语言安装并操作redis的go-redis库
2022/04/14 Golang