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

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

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

(0)

相关推荐

  • JavaScript高级程序设计(第3版)学习笔记11 内建js对象

    内建对象就好比是JDK中的类库,开发者可以直接拿来使用,这极大的方便了常见的编程任务.这篇文章就来浏览一下主要的内建对象,当然,我们并不是第一次接触内建对象,前面已经接触到的就有Object.Function.Boolean.Number.String,对于已经介绍过的,这里再总结复习一下,没有介绍过的,根据相关性来对比的总结,RegExp对象及正则表达式在下一篇中再单独介绍. 1.内建全局单例对象 (1)内建全局单例对象:在整个执行环境中只有一个对象实例,这些对象没有内部属性[[Constru

  • JavaScript高级程序设计(第3版)学习笔记12 js正则表达式

    需要指出的是,这里只是总结了正则表达式的常用的且比较简单的语法,而不是全部语法,在我看来,掌握了这些常用语法,已经足够应对日常应用了.正则表达式不只是应用在ECMAScript中,在JAVA..Net.Unix等也有相应应用,这篇文章则是以ECMAScript中的正则表达式为基础总结的. 一.正则表达式基础 1.普通字符:字母.数字.下划线.汉字以及所有没有特殊意义的字符,如ABC123.在匹配时,匹配与之相同的字符. 2.特殊字符:(需要时,使用反斜杠"\"进行转义) 字符 含义 字

  • JavaScript高级程序设计(第3版)学习笔记3 js简单数据类型

    ECMAScript是一种动态类型的语言,构建于5种简单数据类型(Undefined.Null.Boolean.Number.String)和一种复杂数据类型(Object)的基础之上.这篇文章就来复习一下简单数据类型,我会尽量从编程实践的角度来描述,下面代码运行环境为FireFox 14.0.1. 简单数据类型 简单数据类型 取值 Undefined undefined(只有一个值) Null null(只有一个值) Boolean true|false(只有两个值) Number 数值 St

  • JavaScript高级程序设计(第3版)学习笔记2 js基础语法

    这一篇复习一下ECMAScript规范中的基础语法,英文好的朋友可以直接阅读官方文档.JavaScript本质上也是一种类C语言,熟悉C语言的朋友,可以非常轻松的阅读这篇文章,甚至都可以跳过,不过建议你最好还是看一看,在介绍的同时,我可能会引用一些自认为不易理解且比较流行的用法. 基础语法 1.标识符:所谓标识符,实际上就是指一个满足一定规范,能够被引擎识别的名字,可以用来表示常量.变量.函数名.函数参数.对象.对象属性等所有可命名对象的名称. (1)区分大小写. (2)以字母.下划线(_)或美

  • JavaScript高级程序设计(第3版)学习笔记10 再访js对象

    1.对象再认识 (1)对象属性和特性 什么是属性(Property),什么是特性(Attribute),这有什么区别?我不想也不会从语义学上去区分,对于这系列文章来说,属性就是组成对象的一个部分,广义上也包括对象的方法,而特性则是指被描述主体所具有的特征,换句话说,属性是我们可以通过编码来访问的具体存在,而特性则主要是为了便于理解概念的抽象存在,当然,特性也可以通过相应的属性来具体外化.这一小节所讲的对象属性的特性就是对对象属性特征的一个描述,主要来自于ECMA-262规范的第5版,该规范使用两

  • JavaScript高级程序设计(第3版)学习笔记 概述

    在JavaScript面世之初,没有人会想到它会被应用的如此广泛,也远比一般人想象中的要复杂强大的多,在我自己学习的过程中,曾经有过多次震撼,只是常常没有过多久,很多美轮美奂的用法就又模糊起来,希望通过对JavaScript高级程序设计(第3版)的专题学习笔记,能够较为系统的将基础知识梳理一次,也能够将自己平常学习与工作过程中遇到的一些美妙用法记录下来,便于自己再次学习,当然,也希望可以给有需要的朋友们一些力所能及的帮助. 相关术语 先简要说一下和JavaScript相关的一些背景术语,就不详细

  • JavaScript高级程序设计(第3版)学习笔记4 js运算符和操作符

    在ECMAScript中,有非常丰富的运算符和操作符,在这篇文章中将按通常的分类来稍微整理一下,不过在整理之前,先说明一下: 1.虽然标题是运算符和操作符,然而在我看来并没有多少严格区分的必要,在英文中,貌似也是用一个Operator来表示,所以在下文中我可能会混用.甚至,一些不属于运算符和操作符范畴的,我也整理在这里,只要我觉得必要. 2.对于运算符的优先级,你无需一一牢记--我相信你知道最简单的"先乘除,后加减",至于其它的,如果你不确定,加上括号好了.在ECMAScript中,优

  • JavaScript高级程序设计(第3版)学习笔记7 js函数(上)

    变量类型 在说函数之前,先来说说变量类型. 1.变量:变量在本质上就是命名的内存空间. 2.变量的数据类型:就是指变量可以存储的值的数据类型,比如Number类型.Boolean类型.Object类型等,在ECMAScript中,变量的数据类型是动态的,可以在运行时改变变量的数据类型. 3.变量类型:是指变量本身的类型,在ECMAScript中,变量类型就只有两种:值类型和引用类型.当变量的数据类型是简单数据类型时,变量类型就是值类型,当变量的数据类型是对象类型时,变量类型就是引用类型.在不引起

  • JavaScript高级程序设计(第3版)学习笔记8 js函数(中)

    6.执行环境和作用域 (1)执行环境(execution context):所有的JavaScript代码都运行在一个执行环境中,当控制权转移至JavaScript的可执行代码时,就进入了一个执行环境.活动的执行环境从逻辑上形成了一个栈,全局执行环境永远是这个栈的栈底元素,栈顶元素就是当前正在运行的执行环境.每一个函数都有自己的执行环境,当执行流进入一个函数时,会将这个函数的执行环境压入栈顶,函数执行完之后再将这个执行环境弹出,控制权返回给之前的执行环境. (2)变量对象(variable ob

  • JavaScript高级程序设计(第3版)学习笔记13 ECMAScript5新特性

    接下来应该是BOM和HTML5了,但是鉴于ECMAScript5相对于ECMAScript3的新变化比较多,而且这些变化也非常的有意思,因此在这篇文章中再将我认为的有意思的变化(并非全部变化)集中整理一下,但这里只是列举,不具体展开. 一.语法变化 1.关键字和保留字 在ES3中,使用关键字做标识符会导致"Identifier Expected "错误,而使用保留字做标识符可能会也可能不会导致相同的错误,具体取决于特定的引擎.在ES5中,关键字和保留字虽然不能作为标识符 使用,但可以作

  • JavaScript高级程序设计(第3版)学习笔记6 初识js对象

    在房子里面可以放你想放的任意事物--如果你有足够的美学造诣,你甚至可以弄一个房中房试试--当然,为了方便管理,我们会给房子里存放的所有事物都会取上一个不重复的名字,比如医药房间里的各种药品名称.在ECMAScript中,你可以在对象中存放任意你想放的数据,同样,我们需要给存放的数据取一个名字--也就是对象的属性名,再存放各种数据.再看看ECMA-262中对象的定义:无序属性的集合,其属性可以包含简单数据类型值.对象或者函数. 进入对象,我开始有些激动了,说实话,让我想起做这系列学习笔记的最初原因

  • JavaScript高级程序设计(第3版)学习笔记5 js语句

    砖瓦和水泥都有了,接下来该是砌墙了,在ECMAScript中,语句就是我们需要砌的墙了.语句也和操作符一样,对于有C背景的人来说再自然不过了,下面采用类似的形式整理一下语句的相关知识,重点突出一些ECMAScript中比较特别和个人认为比较有意思的地方,同样,没有强调的但比较基础的语句并非不重要,而是我认为你已经熟悉. 语句一览 语句 语法 简要描述 简单语句 ; 语句以分号(;)结束,在不引起歧义的情况下也可以省略分号. 语句块 {} 使用大括号({})将一组语句放一起组成一个语句块,在ECM

随机推荐