标签:
函数是这样一段代码,它只定义一次,但可能被执行或调用任意次。你可能从诸如子例程(subroutine)或者过程(procedure)这些名字里对函数概念有所了解。
javascript函数是参数化的:函数定义会包括一个形参(parmeter)标识符列表。这些参数在函数中像局部变量一样工作。函数会调用会给形参提供实参的值。函数使用它们实参的值计算返回值,成为该函数的调用表达式的值。
除了实参之外,么次调用还会拥有一个值——本地调用的上下文——这就是this关键字值
如果函数挂载在一个对象上,作为对象的一个属性,就称为它为对象的方法。当通过这个对象来调用函数时,该对象就是此次调用的上下文(context),也就是该函数的this值。用于初始化一个新创建对象的函数称为构造函数(constructor).本文6节i会对构造函数进一步讲解:第9章还会再谈到它。
在javascript中,函数即对象,程序可随意操作它们。比如,javascript可以把函数赋值给变量,或者作为参数传递给其他函数。因为函数就是对象,所以可以给他们设置属性,甚至调用它们的方法。
javascript的函数可以嵌套在其他函数中定义,这样他们就可以访问它们被定义时所处的作用域变量。这意味着javascript函数构成了一个闭包(closere),它给javascript带来了非常强劲的编程能力。
1.函数的定义。
函数使用function关键字来定义。它可以用在函数定义表达式(4.iii)或者函数声明语句里。在这两种形式中,函数定义都从function关键字开始,其后跟随这些部分
下面的例子中分别展示了函数语句和表达式两种方式的函数定义。注意:以表达式来定义函数只适用于它作为一个大的表达式的一部分,比如在赋值和调用的过程中定义函数。
//定义javascript函数 //输出o的每个属性的名称和值,返回undefined function printprops(o) { for (p in o) console.log(p + ":" + o[p] + "\n") } //计算两个迪卡尔坐标(x1,y1)和(x2,y2)之间的距离 function distance(x1, y1, x2, y2) { var dx = x2 - x1; var dy = y2 - y1; return Math.sqrt(dx * dx + dy * dy) } //计算递归函数(调用自身的函数) //x!的值是从x到x递减(步长为1)的值的累乘 function factorial(x) { if (x <= 1) return 1; return x * factorial(x - 1); } //这个函数表达式定义了一个函数用来求传入参数的平方 //注意我们把它赋值了给一个变量 var square = function(x) { return x * x } //函数表达式可以包含名称,这在递归时很有用 var f = function fact(x) { if (x <= 1) return 1; else return x * fact(x - 1); }; //f(7)=>5040 //函数表达式也可以作为参数传给其它函数 data.sort(function(a, b) { return a - b; }); //函数表达式有时定义后立即使用 var tensquared = (function(x) { return x * x; }(10))
注意:以表达式定义的函数,函数的名称是可选的。一条函数声明语句实际上声明了一个变量。并把一个函数对象赋值给它。相对而言,定义函数表达式时并没有声明一个变量。函数可以命名,就像上面的阶乘函数,它需要一个名称来指代自己。
如果一个函数定义表达式包含名称,函数的局部变量作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量。通常而言,以表达式方式定义函数时不需要名称,这会让定义它们的代码更紧凑。函数定义表达式特别适合用来那些只用到一次的函数,比如上面展示的最后两个例子。
在5.3.ii中,函数声明语句“被提前”到外部脚本或外部函数作用域的顶部,所以以这种方式声明的函数,可以被在它定义之前出现的代码所调用。不过,以表达式定义的函数就令当别论了。
为调用一个函数,必须要能引用它,而要使用一个表达式方式定义的函数之前,必须把它赋值给一个变量。变量的声明提前了(参见3.10.i),但给变量赋值是不会提前的。所以,以表达式定义的函数在定义之前无法调用。
请注意,上例中的大多数函数(但不是全部)包含一条return语句(5.6.iiii)。return语句导致函数停止执行。并返回它的表达式(如果有的话)的值给调用者。如果return语句没有一个与之相关的表达式,则它返回undefined值。如果一个函数不包含return语句。那它就执行函数体内的每条语句,并返回undefined值给调用者。
上面例子中的函数大多是用来计算出一个值的,他们使用return把值返回给调用者。而printprops()函数不同在于,它的任务是输出对象各属性的名称和值。没必要返回值,该函数不包含return语句,printprops()的返回值始终是undefined.(没有返回值的函数有时候被称为过程)。
嵌套函数
在javascript中,函数可以嵌套在其它函数里。例如
function hyuse(a, b) { function square(x) { return x * x } return Math.sqrt(square(a) + square(b)); }
嵌套函数的有趣之处在于它的变量作用域规则:它们可以访问嵌套它们(或者多重嵌套)的函数的参数和变量。
例如上面的代码里,内部函数square()可以读写外部函数hyuse()定义的参数a和b。这些作用域规则对内嵌函数非常重要。我们会在本文第6节在深入了解它们。
5.2.ii曾经说过,函数声明语句并非真正的语句。ECMAScript规范芝是允许它们作为顶级语句。它们可以出现在全局代码里,或者内嵌在其他函数中,但它们不能出现在循环、条件判断、或者try/cache/finally及with语句中(有些javascript并为严格遵循这条规则,比如Firefox就允许在if语句中出现条件函数声明)。注意:此限制仅适用于以语句形式定义的函数。函数定义表达式可以出现在javascript的任何地方。
2.函数调用
构成函数主题的javascript代码在定义之时并不会执行,只有调用该函数是,它们才会执行。有4种方式来调用javascript函数。
i.函数调用
使用调用表达式可以进行普通的函数调用也可以进行方法调用(4.5)。一个调用表达式由多个函数表达式组成,每个函数表达式都是由一个函数对象和左圆括号、参数列表和右圆括号组成,参数列表是由逗号分隔的逗号的零个或多个参数表达式组成。如果函数表达式是一个属性访问表达式,即该函数是一个对象的属性或数组中的一个元素。那么它就是一个方法调用表达式。下面展示了一些普通的函数调用表达式:
printprops({x: 1}); var total = distance(0,0,2,1) + distance(2,2,3,5); var probality = factorial(5)/factorial(13);
在一个调用中,每个参数表达式(圆括号之间的部分)都会计算出一个值,计算的结果作为参数传递给另外一个函数。这些值作为实参传递给声明函数时定义的行参。在函数体中存在一个形参的调用,指向当前传入的实参列表,通过它可以获得参数的值。
对于普通的函数调用,函数的返回值成为调用表达式的值。如果该函数返回是因为解释器到达结尾,返回值就是undefined。如果函数返回是因为解释器执行到一条return语句,返回的值就是return之后的表达式值,如果return语句没有值,则返回undefined。
根据ECMAScript3和非严格的ECMAScript5对函数的调用规定,调用上下文(this的值)是全局对象。然后在严格模型下,调用上下文则是undefined、
以函数的形式调用的函数通常不使用this关键字。不过 ,“this”可以用来判断当前是否为严格模式。
//定义并调用一个函数来确定当前脚本运行是否为严格模式 var strict = (function() {return !this;}())
ii.方法调用
一个方法无非是个保存在一个对象的属性里的javascript函数。如果有一个函数f和一个对象o,则可以用下面的代码给o定义一个名为m()的方法:
o.m = f;
给o定义的方法m(),调用它时就像这样:
o.m()
如果m()需要两个实参,调用起来像这样:
o.m(x,y)
上面的代码是一个调用表达式:它包括一个函数表达式o.m,以及两个实参表达式x和y,函数表达式的本身就是一个属性访问表达(4.4节),这意味着该函数被当做了一个方法,而不是作为一个普通的函数来调用。
对方法调用的参数和返回值的处理,和上面所描述的普通函数调用完全一致。但是方法调用和函数调用有一个重要的区别,即:调用上下文。属性访问表达式由两部分组成:一个对象(本例中的o)和属性名称(m)。像这样的方法在调用表达式里,对象o成为调用上下文,函数体可以使用关键字this引用该对象。如下是具体的一个例子
var calcul = { //对象直接量 oprand1: 1, oprand2: 1, add: function() { //注意this关键字的用法,this指带当前对象 return this.result = this.oprand1 + this.oprand2; } }; calcul.add(); //这个方法调用计算1+1的结果 calcul.result; //=>2
大多数方法调用使用点符号来访问属性,使用方括号(的属性访问表达式)也可以进行属性访问操作。下面两个例子都是函数的调用:
o["m"](x,y) //o.m(x,y)的另外一种写法 a[0](z)//同样是一个方法调用(这里假设a[0]是一个函数)
方法调用可能包含更复杂的函数属性访问表达式:
customer.surname.toUpperCase(); //调用customer.surname方法 f().m(); //在f()调用结束后继续调用返回值中的方法m()
方法和this关键字是面向对象编程范例的核心。任何函数只要作为方法调用实际上都会传入一个隐式的实参——这个实参是一个对象,方法调用的母体就是这个对象。通常来讲,基于那个对象的方法可以执行多种操作,方法调用的语法已经很清晰地表明了函数将基于一个对象进行操作。比较下面两行代码:
rect.setSize(windth, height);
setrectSize(rect, width, heigth);
我们假设这两行代码的功能完全一样,他们都作用域一个假定的对象rect。可以看出,第一行的方法调用语法非常清晰地表明了这个函数执行的载体是rect对象,函数中的所有操作都将基于这个对象。
方法链 当方法的返回值是一个对象,这个对象还可以再调用它的方法。这种方法调用序列中(通常称为“链”或者“级联”)每次的调用结果都是另外一个表达式组成部分。比如基于jQuery(19章会讲到),我们常这样写代码: //找到所有的header,取得他们的id的映射,转换为数组并给它们进行排序 $(":header").map(function(){return this.id}).get().sort(); 当方法并不需要返回值时,最好直接返回this。如果在设计的API中一直采用这种方式(每个方法都返回this),使用API就可以进行“链式调用”风格的编程,在这种编程风格中,只要指定一次要调用的对象即可。余下的方法都看一基于此进行调用: shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();
需要注意的是,this是一个关键字,不是变量,也不是属性名。javascript的语法不允许给this赋值。
和变量不同,关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this。如果嵌套函数作为方法调用,其this的值只想调用它的对象。如果嵌套函数作为函数调用,其this值不是全局对象(非严格模式下)就是undefined(严格模式下)。很多人误以为调用嵌套函数时this会指向调用外层函数的上下文。如果你想访问这个外部函数的this值,需要将this值保存在一个变量里,这个变量和内部函数都在一个作用域内。通常使用变量self来保存this。比如:
var o = { //对象o m: function() { //对象中的方法m() var self = this; //将this的值保存在一个变量中 console.log(this === o); //输出true,this就是这个对象o f(); //调用辅助函数f() function f() { //定义一个嵌套函数f() console.log(this === o); //"false":this的值是全局对象undefied console.log(self === o); //"true": slef指外部函数this的值 } } }; o.m();//调用对象o的方法m
在8.7.iiii的例子中,有var self = this更切合实际的用法。
iii.构造函数的调用
如果函数或者方法之前带有关键字new,它就构成构造函数调用(构造函数掉在4.6节和6.1.ii节有简单介绍,第9章会对构造函数做更详细的讨论)。构造函数调用和普通的函数调用方法以及方法调用在实参处理、调用上下文和返回值各方面都不同。
如果构造函数调用圆括号内包含一组实参列表,先计算这些实参表达式,然后传入函数内,这和函数调用和方法调用是一致的。但如果构造函数没有形参,javascript构造函数调用的语法是允许省略形参列表和圆括号的。凡是没有形参的构造函数都可以省略圆括号。如下文两个代码是等价的
var o = Object(); var o = Object;
构造函数调用创建一个新的 空对象,这个对象继承自构造函数prototype属性。构造函数试图初始化这个新创建的对象,并将这个对象用做起调用上下文,因此构造函数可以用this关键字来引用对象做起调用上下文,因此,构造函数可以使用this关键字来引用这个新创建的对象。
注意:尽管构造函数看起来像一个方法调用,它依然会使用这个新对象作为调用上下文。也就是说,在表达式new o.m()中,调用上下文并不是o。
构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它显式返回。这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。然而,如果构造函数显式的使用了return语句返回一个对象,那么调用表达式的值就是这个对象。如果构造函数使用return语句但没有指定返回值。或者返回一个原始值,那么这时将忽略返回值。同时使用这个心对象作为调用结果。
iiii.间接调用
javascript中的函数也是对象,和其它javascript对象没有什么两样,函数对象也可以包含方法。其中的两个方法call()和apply()可以用来间接的调用函数。两个方法都允许间接的调用函数。两个方法都允许显式指定调用所需的this值,也就是说,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。两个方法都可以指定调用的实参,apply()方法则要求以数组的形式传入参数。8.7.iii会有关这两种方法的详细介绍。
3.函数的实参和形参
javascript中的函数定义并未指定函数的形参类型,函数调用也未对实参做任何类型的检测。实际上javascript甚至不检查传入的形参的个数。下面几节将会讨论当调用函数时实参个数和声明的形参个数不匹配时出现的状况。同样说明了如何显式测试函数实参的类型,避免非法的实参传入函数。
i.可选形参
当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的的形参都将设置为undefined值。因此,在调用函数的时,形参是否可选以及是否可选以及是否可以省略应当保持 较好适应性。为了做到这一点,应当给省略的参数赋一个合理的默认值、来看这个例子:
var xx = {x: 1,y: 2,z: 3}; var zz = [] //将对象o中的可枚举属性名追加到数组a中,并返回这个数组a //如果省略a,则创建一个新数组并返回这个新数组 function getPropertyNames(o, /*optional*/ a) { if (a === undefined) a = []; //如果a未定义,则使用新数组 for (var property in o) a.push(property); return a; } //这个函数调用时可以使用两个实参 getPropertyNames(xx); //将o的属性存储到一个新的数组中 getPropertyNames(xx, zz); //将p的属性追加到数组a中
如果第一行代码中不使用,可以使用“||”运算符,如果第一个实参是真值的话就返回第一个实参;否则返回第二个实参。在这个场景下。如果作为第二个实参传入任意对象,那么函数就会使用这个对象。如果省略掉第二个实参(或者传递null以及其他任意假值),那么就会创建一个新的空数组赋值给a。
(需要注意的是,使用“||”运算符代替if语句的前提是a必须先声明,否则表达式会报引用错误,在这个例子中a是作为形参传入的,相当于var a,既然已经声明a,所以这样用是没有问题的)
a = a || [];
回忆"||"运算符,如果第一个实参是真值的话就返回第一个实参;否则返回第二个实参。在这个场景下,如果作为第二个实参传入任意对象。那么函数就会使用这个对象。
如果省略掉第二个实参(或者传递null或假值),那么就会创建一个空数组,赋值给a。
需要注意的是,当用这种可选实参来实现函数时,需要将可选实参放在参数列表的最后。那行调用你的函数的人是没办法省略第一个实参传入第二个实参的(它必须将undefined显式传入,【注意:函数的实参可选时往往传入一个无意义的占位符,惯用的做法是传入null作为占位符,当然也可以使用undefined】),同样要注意在函数定义中,使用注释/*optional*/来强调形参是可选的。
ii.可变长的实参列表:实参对象
当调用函数的时候,传入的实参的个数大于函数定义的形参个数时,没有办法获得未命名值的引用。参数对象解决了这个问题。在函数体内,arguments是指向实参对象的引用,实参对象是一个类数组的对象(参照7章11节),这样可以通过数字下标就能访问传入函数的实参值。而不用非要通过名字来得到实参。
假设定义函数f,它只有一个实参x。如果调用这个函数时需要传入两个实参,第一个实参可以通过参数名x来获得,也可以通过arguments[0]来得到。第二个实参只能通过arguments[1]来得到。此外和真正的数组一样,arguments也包含一个length属性,用以表示其所包含元素的个数。因此,调用函数f()时传入两个参数,arguments.length的值就是2.
实参对象在很多地方都非常有用,下面的例子展示了使用它来验证实参的个数,从而调用正确的逻辑,因为javascript本身不会这样做:
function f(x, y, z) { //首先验证传入实参的个数是否正确 if (arguments.leng != 3) { throw new Error("function f() called with" + arguments.length + "arguments,but it ecxpects 3 arguments"); } //再执行函数的其它逻辑 }
需要注意的是,通常不必这样检查实参个数。大多数情况下,javascript的默认行为可以满足需要的:省略的实参都是undefined,多出的实参会自动省略。
实参对象有一个重要的用处,就是让函数操作任意数量的实参。下面的函数就可以接受任意量的实参,并返回实参的最大值。(内置函数Max.max()的功能与之类似)
function max( /*...*/ ) { var max = Number.NEGATIVE_INFINITY; //遍历实参,查找并记住最大值 for (var i = 0; i < arguments.length; i++) if (arguments[i] > max) max = arguments[i]; //返回最大值 return max; } max(1, 10, 222, 100); //=>222
类似这样的函数可以接收任意个实参,这种函数也叫“不定参函数”(varargs function),来自古老的c语言
注意:不定实参函数的实参个数不能为零。arguments[]对象最适合的场景是在这样一类函数中,这类函数包含固定个数的命名和必须参数,以及随后个数不定的可选实参。
记住,arguments并不是真正的数组。它是一个实参对象。可以这样理解:它是一个对象,碰巧有以数组索引的属性。
数组对象包含一个非同寻常的特性。在非严格模式下,当一个函数包含若干形参,实参对象的数组元素是函数形参所对应实参别名,实参对象以数字索引,实参对象中以数字索引,并且形参名称可以可以认为是相同变量的不同命名。通过实参名字来修改实参值的话,通过arguments[]数组也可以获取到更改后的值,下面的这个例子清楚的说明了这一点。
function f(x) { console.log(x); //输出实参的初始值 arguments[0] = null; //修改实参组的元素同样会修改x的内容 console.log(x); //输“null” } f(11);
如果实参对象是一个普通的数组的话,第二条console.log(x)语句结果绝对不是null.这个例子中,arguments[]和x指代同一个值。
在ECMAScript5中移除了实参对象的这个特殊属性。在严格模型下还有一点(和非严格模式不同),在非严格模式下,函数里的arguments仅仅是一个标识符,在严格模式中,它变成了一个保留字。严格模式下 函数无法使用arguments作为形参名或局部变量名,也不能给arguments赋值。
callee和caller属性
除了数组元素,实参对象还定义了callee和caller属性。在非严格模式下(严格模式下会有一系列错误),ECMAScript标准规范规定callee属性指代当前正在执行的函数。caller属性是非标准的,但大多数浏览器都实现这个属性。它指代调运当前正在执行的函数的函数。通过方法caller属性可以访问调运栈。callee属性在某些时候非常有用,比如在匿名函数中通过callee来递归调用自身。
var factorial = function(x) { if (x <= 1) return 1; return x * arguments.callee(x - 1); }
iii.将对象属性用作实参
当一个函数包含超过3个形参时,对于程序员来说,要记住调用函数中实参的正确顺序实在让人头疼。每次调用这个函数时都不厌其烦的查阅文档,为了不让程序员每次都要梳理,最好通过名/值对的形式传入参数。这样参数的顺序就无关紧要了。为了实现这样风格的方法调用,定义函数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的名/值才是真正需要的实参数据,如下例子,这样的写法允许在函数中设置省略参数的默认值。
//将原始值数组的length元素复制至目标数组 //开始复制原始数组的from_start元素 //并且将其复制到目标数组to_start中 //要记住实现的顺序并不容易 function arrayCopy( /*array*/ from, /*index*/ from_start, /*array*/ to, /*index*/ to_start, /*integer*/ length) { //逻辑 } //这个版本的实现效率有些低,但你不必再记住实参的顺序 //并且from_start和to_start都默认为0 function easyCopy(args) { arrayCopy(args.form, args.form_start || 0, //注意,这里设置了默认值 args.to, args.to_start || 0, args.length); } //来看如何调用easyCopy var a = [1, 2, 3, 4], b = []; easyCopy({ from: a, to: b, length: 4 });
iiii.实参类型
javascript方法的形参并未声明类型,在传入时也未做任何类型检查。可以在采用语义化的单词来给函数命名,像上个例子中,给实参做补充注释,以此使代码文档化。对于可选的实参来说,可以在注释中补充下“这个实参是可选的”。当一个方法可以接收任意数量的实参时,可以使用省略号。
function max( /* number*/ ) { /*代码*/ }
3章8节提到,javascript会在必要的时候进行类型转换。因此,函数期望接收一个字符串实参,而调用函数时传入其它类型的值,所传入的值会在函数体内将其用做字符串方法转换为字符串类型。所有原始类型都可以转换为字符串,所有对象都包含toString()方法(尽管不一定有用),所以这种 场景下不会有任何错误。
然而事情不总是这样,上个例子中的arrayCopy()方法,这个方法期望他的第一个实参是一个数组,当传入一个非数组的值作为第一个实参时(通常会传入数组对象),尽管看起来没问题。但实际会出错。除非所写的函数是只用到一两次,用完即丢的那。你应当添加类似实参类型检查逻辑,因为宁愿程序在传入非法值时报错,也不愿意非法值导致程序报错。
相比而言,逻辑执行时的报错消息不甚清晰更难理解。下面的这个例子就做了这种类型检测。本节借用7章11节isArrayLike()函数
//判定o是否是一个类数组对象 //字符串和函数都length属性,但是他们可以有typeOf检测将其排除 //在客户端javascript中,DOM文本节点也有length属性,需要用额外的o.nodetype != 3将其排除 function isArrayLike(o) { if (o && //o非null、undefined等 typeof o === "object" && //o是对象 isFinite(o.length) && //o.length是有限数 o.length >= o && //o.length是非负数 o.length === Math.floor(o.length) && //o.length是整数 o.length < 4294967296) //o.length < 2^32 return true; else return fasle; //否则它不是 } //返回数组(或类数组对象)a的元素累加和 //数组a中必须为数字/ null undefined的元素都将忽略 function sum(a) { if (isArrayLike(a)) { var total = 0; for (var i = 0; i < a.length; i++) { //遍历所有元素 var element = a[i]; if (element == null) continue; //跳过null和undefiend if (isFinite(element)) total += element; else throw new Error("sum():elements must be a finte numbers"); } return total; } else throw new Error("sun():arguments mustbe array-like") }; a = [1,2,4,5,3,6,7]; sum(a)
这里的sum()方法进行了非常严格的实参检查,当传入的非法的值的时候会抛出Error到控制台。但当涉及类数组对象和真正的数组(不考虑数组元素是否是null还是undefied),这种做法带来的灵活性并不大。
javascript是一种非常灵活的弱类型语言,有时候适合编写实参类型和实参个数不确定的函数。下面的flexisum()方法就是这样(有点极端),比如它可以接收任意数量的实参,并可以递归地处理实参是数组的情况,这样的话,它就可以用做不定实参函数或者是实参是数组的函数。此外,这个方法尽可能在抛出错误在抛出错误之前将非数组转换为数字。
function flexisum(a) { var total = 0; for (var i = 0; i < arguments.length; i++) { var element = arguments[i], n; if (element == null) continue; //忽略null和undefined if (isArray(element)) //如果实参是数组 n = flexisum.apply(this, element); //递归的计算累加和 else if (typeof element === "function") //否则,如果是函数... n = Number(element()); //调用它并做类型抓换 else n = Number(element); //直接做类型抓换 if (isNaN(n)) //如果无法转换为数字,则抛出异常 throw Error("flexisum():can nont convent" + element + "to number"); total += n; //否则,将n累加到total } return total; }
(本文尚未结束,欢迎大家关注,大家可以关注前第7章:数组)
接下来将更新:
4.作为值的函数
5.作为命名空间的函数
6.闭包
7.函数属性、方法和构造函数
8.函数式编程
标签:
原文地址:http://www.cnblogs.com/ahthw/p/4282745.html