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 12 Javascript
bgsound 背景音乐 的一些常用方法及特殊用法小结
May 11 Javascript
JavaScript中的连字符详解
Nov 28 Javascript
js 获取input点选按钮的值的方法
Apr 14 Javascript
javascript移动设备Web开发中对touch事件的封装实例
Jun 05 Javascript
解读Bootstrap v4 sass设计
May 29 Javascript
基于jQuery的AJAX和JSON实现纯html数据模板
Aug 09 Javascript
Vuex之理解Store的用法
Apr 19 Javascript
JavaScript反弹动画效果的实现代码
Jul 13 Javascript
angularjs中$http异步上传Excel文件方法
Feb 23 Javascript
生产制造追溯系统之再说条码打印
Jun 03 Javascript
微信小程序实现单个卡片左滑显示按钮并防止上下滑动干扰功能
Dec 06 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
oracle资料库函式库
2006/10/09 PHP
PHP连接SQLServer2005 的问题解决方法
2010/07/19 PHP
PHP图片处理之使用imagecopy函数添加图片水印实例
2014/11/19 PHP
PHP实现将多个文件中的内容合并为新文件的方法示例
2017/06/10 PHP
js 获取中文拼音,Select自动匹配字母获取值的代码
2009/09/23 Javascript
为jQuery增加join方法的实现代码
2010/11/28 Javascript
showModalDialog模态对话框的使用详解以及浏览器兼容
2014/01/11 Javascript
js控制TR的显示隐藏
2016/03/04 Javascript
详解Javascript中prototype属性(推荐)
2016/09/03 Javascript
canvas实现探照灯效果
2017/02/07 Javascript
jQuery图片瀑布流的简单实现代码
2017/03/15 Javascript
用JavaScript和jQuery实现瀑布流
2017/03/19 Javascript
JQuery元素快速查找与操作
2018/04/22 jQuery
使用Vue实现图片上传的三种方式
2018/07/17 Javascript
Vue Promise的axios请求封装详解
2018/08/13 Javascript
webpack4简单入门实例
2018/09/06 Javascript
JSON基本语法及与JavaScript的异同实例分析
2019/01/04 Javascript
js实现页面图片消除效果
2020/03/24 Javascript
原生js实现俄罗斯方块
2020/10/20 Javascript
JavaScript用document.write()输出换行的示例代码
2020/11/26 Javascript
wxPython 入门教程
2008/10/07 Python
深入探究Django中的Session与Cookie
2017/07/30 Python
Python文件操作函数用法实例详解
2019/12/24 Python
运行tensorflow python程序,限制对GPU和CPU的占用操作
2020/02/06 Python
python ffmpeg任意提取视频帧的方法
2020/02/21 Python
完美解决pycharm 不显示代码提示问题
2020/06/02 Python
java关于string最常出现的面试题整理
2021/01/18 Python
美国高端牛仔品牌:Silver Jeans
2019/12/12 全球购物
咨询公司各岗位职责
2013/12/02 职场文书
运动会稿件50字
2014/02/17 职场文书
幼儿园安全生产月活动总结
2014/07/05 职场文书
安全先进个人材料
2014/12/29 职场文书
匿名信格式范文
2015/05/27 职场文书
学校就业保障协议书
2019/06/24 职场文书
《传颂之物 虚伪的假面》BD发售宣传CM公开
2022/04/04 日漫
nginx之内存池的实现
2022/06/28 Servers