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 相关文章推荐
JavaScript 判断浏览器类型及版本
Feb 21 Javascript
jquery固定底网站底部菜单效果
Aug 13 Javascript
javascript中apply和call方法的作用及区别说明
Feb 14 Javascript
使用JavaScript 编写简单计算器
Nov 24 Javascript
在JavaScript中操作时间之getUTCDate()方法的使用
Jun 10 Javascript
jQuery实现带动画效果的多级下拉菜单代码
Sep 08 Javascript
jQuery插件HighCharts绘制2D带Label的折线图效果示例【附demo源码下载】
Mar 08 Javascript
Bootstrap如何激活导航状态
Mar 22 Javascript
webpack配置的最佳实践分享
Apr 21 Javascript
快速搭建vue2.0+boostrap项目的方法
Apr 09 Javascript
Angular中innerHTML标签的样式不起作用的原因解析
Jun 18 Javascript
vue中监听返回键问题
Aug 28 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 基本语法格式
2009/12/15 PHP
PHP编码规范之注释和文件结构说明
2010/07/09 PHP
php使用glob函数快速查询指定目录文件的方法
2014/11/15 PHP
PHP实现微信图片上传到服务器的方法示例
2017/06/29 PHP
thinkphp5实现无限级分类
2019/02/18 PHP
js innerHTML 的一些问题的解决方法
2008/06/22 Javascript
js文本框输入点回车触发确定兼容IE、FF等
2013/11/19 Javascript
js获取select标签选中值的两种方式
2014/01/09 Javascript
jquery文档操作wrap()方法实例简述
2015/01/10 Javascript
简介AngularJS中使用factory和service的方法
2015/06/17 Javascript
javascript 中设置window.location.href跳转无效问题解决办法
2017/02/09 Javascript
vue2.0页面前进刷新回退不刷新的实现方法
2018/07/31 Javascript
JS判断用户用的哪个浏览器实例详解
2018/10/09 Javascript
vue  elementUI 表单嵌套验证的实例代码
2019/11/06 Javascript
vue请求数据的三种方式
2020/03/04 Javascript
JavaScript变量Dom对象的所有属性
2020/04/30 Javascript
python采用django框架实现支付宝即时到帐接口
2016/05/17 Python
flask中使用蓝图将路由分开写在不同文件实例解析
2018/01/19 Python
python删除不需要的python文件方法
2018/04/24 Python
Python使用get_text()方法从大段html中提取文本的实例
2019/08/27 Python
在Python中使用MongoEngine操作数据库教程实例
2019/12/03 Python
jupyter notebook 多环境conda kernel配置方式
2020/04/10 Python
高中毕业自我鉴定范文
2013/10/02 职场文书
求职简历自荐信范文
2013/10/21 职场文书
物流专业大学生的自我鉴定
2013/11/13 职场文书
经典优秀个人求职信分享
2013/12/12 职场文书
岗位竞聘演讲稿
2014/01/10 职场文书
《小猫刮胡子》教学反思
2014/02/21 职场文书
离职证明范本(5篇)
2014/09/19 职场文书
城管年度个人总结
2015/02/28 职场文书
财务部岗位职责范本
2015/04/14 职场文书
新员工试用期工作总结2015
2015/05/28 职场文书
学会感恩主题班会
2015/08/12 职场文书
廉洁自律心得体会2016
2016/01/13 职场文书
2016五一手机促销广告语
2016/01/28 职场文书
Python连续赋值需要注意的一些问题
2021/06/03 Python