Javascript函数

在学习任何语言时候,熟练掌握函数是非常重要的技术,这点同样适合javascript.这是因为javascript函数有很多用,并且很多语言 的灵活性和价值也来自于函数。其他大多数语言都有特殊的语法来表达面向对象的特性,而javascript却使用函数。下面是本章所涵盖的内容:
如何定义和使用函数 向函数传递参数 可以”免费使用的”预定义函数 Javascript变量的作用域 函数的上下文是数据,虽然是一种特殊的数据 理解这些内容是你继续本章第二部分的一个基本的基础,那里会列举一些有意思的函数的应用。
使用匿名函数 回调函数 自调用函数 内部函数(在函数内部定义的函数) 返回函数的函数 重新定义自己的函数 闭包 函数是什么?
函数允许你组织一些代码,给这些代码一个名字,可以便于以后重复使用,靠名字来定位。让我们看看这个例子吧:
function sum(a, b) {
var c = a + b;
return c;
}
组成函数的各部分是什么?
函数的声名 函数名称,这个例子种间是sum. 接受的参数,这个例子中是a和b.函数可以接受零个到多个参数,由逗号分开。 代码块,也叫函数的主体部分。 返回语句。函数一般情况下都返回一个值。如果没有明确指定返回值,隐含返回undefined值。 注意函数只能返回一个值。如果需要返回多个值,可以使用数组来包含所有的值,然后返回包含所有元素的数组。
调用函数
要使用函数,需要调用它。你可以简单的使用函数名字加上一些参数来调用函数。”To invoke”是另外一种调用函数的方式。
我们调用sum()函数,传入两个参数并将函数返回的值赋值给result变量:
>>> var result = sum(1, 2);
>>> result;
3
参数
在定义函数的时候,你可以指定函数在调用时候接受什么参数。函数不一定需要参数,但是如果你不传入给它,javascript会自动将那个值赋为undefined。在下一个例子中,函数将返回NaN,因为试图将1和undefined相加:
>>> sum(1)
NaN
JavaScript不会使用所有传入的参数。如果你传入的参数比函数能接受的参数数目多,多余的参数将被自动忽略掉:
>>> sum(1, 2, 3, 4, 5)
3
甚至你还可以创建接受任意数目参数的函数。这要多亏arguments数组,它是函数自动在内部创建的。这里有一个简单函数,返回它所传入的参数。
>>> function args() { return arguments; }
>>> args();
[]
>>> args( 1, 2, 3, 4, true, ‘ninja’);
[1, 2, 3, 4, true, "ninja"]
使用arguments数组你可以改善sum()函数,让它能接受任意数目的参数来把它们相加起来。
function sumOnSteroids() {
var i, res = 0;
var number_of_params = arguments.length;
for (i = 0; i < number_of_params; i++) {
res += arguments;
}
return res;
}
如果用这个函数测试,调用不同数量的参数(甚至不调用参数),你都可以得到如期结果:
>>> sumOnSteroids(1, 1, 1);
3
>>> sumOnSteroids(1, 2, 3, 4);
10
>>> sumOnSteroids(1, 2, 3, 4, 4, 3, 2, 1);
20
>>> sumOnSteroids(5);
5
>>> sumOnSteroids();
0
表达式arguments.length返回调用函数传入参数的数目。如果这里的语法不是很熟悉,不要担心,我们下一章将详细介绍。我们也会看到arguments在技术上不是数组,而是像数组的对象。
预定义函数
Javascript引擎内部建立了一些函数可供你直接使用。让我们看看它们吧。这样做的同时,你可以有机会体验函数,以及它们的参数和返回值,并且能习惯使用它们工作。内置函数列表如下:
parseInt() parseFloat() isNaN() isFinite() encodeURI() decodeURI() encodeURIComponent() decodeURIComponent() eval() 黑盒子函数
通常,调用函数的时候,你的程序不要知道这些函数在内部是如何工作的。你可以把函数想象成一个黑色的盒子:你可以给它一些值(作为输入参数)并且然后可以 输出它返回的结果。这对于每个函数都成立–不管是javascript引擎内建函数,或者是你自己写的或者合作伙伴写的等等。
parseInt()
parseInt() 接受任意类型的输入(常用的是字符串),试图将传入的参数转换成整数输出。如果失败,返回NaN。
>>> parseInt(’123′)
123
>>> parseInt(’abc123′)
NaN
>>> parseInt(’1abc23′)
1
>>> parseInt(’123abc’)
123
该函数还有一个可选参数,基数,告诉函数期望什么类型的数据,比如十进制,十六进制,二进制等等。例如输出字符串FF为十进制的,结果为NaN,而以十六进制输出,结果是255。
>>> parseInt(’FF’, 10)
NaN
>>> parseInt(’FF’, 16)
255
另外一个例子解析一个字符串基于10和8进制。
>>> parseInt(’0377′, 10)
377
>>> parseInt(’0377′, 8 )
255
如果调用parseInt()省略第二个参数,函数默认为10进制,除了下面的例外:
如果传入以0x开头的字符串作为第一个参数,那么第二个参数会默认为16(假定为16进制数) 如果传入以0开头的字符串作为第一个参数,那么第二个参数会默认为8 >>> parseInt(’377′)
377
>>> parseInt(’0377′)
255
>>> parseInt(’0×377′)
887
最安全的办法是始终带上第二个参数基数。如果忽略基数,程序可能在99%的情况下工作(因为大多数情况下需要解析为10进制),但是每次可能会导致 你头痛的测试以致抓耳挠腮。例如,假如你有一个表单,可以接受日历日期然后用户输入08;如果忽略掉基数,可能就会发生不可预料的结果。
parseFloat()
parseFloat() 和 parseInt() 一样,但是它会从你给的参数返回小数。这个函数只有一个参数。
>>> parseFloat(’123′)
123
>>> parseFloat(’1.23′)
1.23
>>> parseFloat(’1.23abc.00′)
1.23
>>> parseFloat(’a.bc1.23′)
NaN
parseInt(), parseFloat()在遇到第一个字符为不认识的字符时就会放弃,即使后面真正的是数字,也不会读出来。
>>> parseFloat(’a123.34′)
NaN
>>> parseFloat(’12a3.34′)
12
parseFloat()能认识输入的指数(不像parseInt())。
>>> parseFloat(’123e-2′)
1.23
>>> parseFloat(’123e2′)
12300
>>> parseInt(’1e10′)
1
isNaN()
使用isNaN()可以检查输入值是否是有效的数字,这对算数运算是比较安全的。这个函数也可方便检查parseInt() or parseFloat()是否执行成功。
>>> isNaN(NaN)
true
>>> isNaN(123)
false
>>> isNaN(1.23)
false
>>> isNaN(parseInt(’abc123′))
true
isNaN函数也尝试将输入转换为数字
>>> isNaN(’1.23′)
false
>>> isNaN(’a1.23′)
true
isNaN()函数很有用,因为NaN和它自己本身是不相等的。所以一点也不奇怪NaN===NaN是false.
isFinite()
isFinite()检查输入是否为有限的或者是NaN。
>>> isFinite(Infinity)
false
>>> isFinite(-Infinity)
false
>>> isFinite(12)
true
>>> isFinite(1e308)
true
>>> isFinite(1e309)
false
如果你对最后一个函数调用感到奇怪,记着前一章有讲过javascript最大的数字是1.7976931348623157e+308.
Encode/Decode URIs
在URL (Uniform Resource Locator)或者URI (Uniform Resource Identifier)中, 一些字符是有特殊意思的。如果要避开这些字符,你可以使用encodeURI() 或者encodeURIComponent(). 前者返回可用的URL,而后者假设你仅仅传入URL的一部分,比如query字符串,并编码所有合适的字符。
>>> var url = ‘http://www.packtpub.com/scr ipt.php?q=this and that’;
>>> encodeURI(url);
“http://www.packtpub.com/scr%20ipt.php?q=this%20and%20that”
>>> encodeURIComponent(url);
“http%3A%2F%2Fwww.packtpub.com%2Fscr%20ipt.php%3Fq%3Dthis%
20and%20that”
encodeURI()和encodeURIComponent()相反的函数分别是decodeURI()和 decodeURIComponent().有时候,在比较老的代码里,你可能会看到类似的函数escape()和unescape(),但是这些函数已 经废弃掉了,不该再使用了。
eval()
eval()接受一个字符串并且当作javascript代码来执行它:
>>> eval(’var ii = 2;’)
>>> ii
2
因此 eval(’var ii = 2;’)和var ii = 2是相同的;
eval()在某些情况下是非常有用的,但是如果有选择的话,尽量避免使用。大多数情况下,都会有替代的做法,那些替代的方法更加简洁,并且容易写作和管理。老练的javascript编程者经常说”Eval is evil”.使用eval()的缺点有:
性能–它是比直接在脚本中的代码评估实时代码是比较慢的。 安全性–Javascript是强大的,也就是意味着能产生强大的破坏力。如果不信任输入的资源,最好不要用它。 A Bonus—the alert() Function
变量的作用域
非常值得注意的是,特别是你从其他语言转入javascript的,要注意javascript中那些不是在区块代码中定义,而是在函数内部定义的 变量。这就意味着在函数内部定义的变量在函数体外边是不可见的。然而,变量在if或者for区块中定义的,那是可以被外边的代码看到的。术语“全局变量” 描述的是在函数外部定义的变量,相反“局部变量”是在函数内部定义的。函数内部代码有访问所有全局变量的权限,就像访问它自己的局部变量一样。
在下面的一个例子中;
函数f()具有访问全局变量的能力 在函数f()外边,local是不存在的 var global = 1;
function f() {
var local = 2;
global++;
return global;
}
>>> f();
2
>>> f();
3
>>> local
local is not defined
如果你不使用var来声明一个变量,这个变量会自动赋予全局范围。让我们看这个例子:
function f(){local=2;}
local
>>>2
这里究竟发生了什么事?函数f()包含变量local.在调用函数之前,这个变量是不存在的,当你调用这个函数第一次后,变量local被创建为一个全局变量。然后如果你在函数外边访问local,它是个变量。
最好的操作技巧
减少全局变量的数量。假设两个人在同一代码中不同的函数里,他们都想使用相同名字的全局变量。这是可能很容易导致不可预料的结果和难于调试的毛病。 总是把你的变量用var来声明。
这有一个非常有意思的例子指出局部变量和全局变量的一个重要区别。
var a = 123;
function f() {
alert(a);
var a = 1;
alert(a);
}
f();
你也许以为第一次alert()会显示123(全局变量a的值),第二次显示1(局部变量的值)。但事实上并非如此。第一次显 示’undefined’.那是因为在函数内部,局部变量作用域比全局作用域更加重要。因此局部变量会用相同的名字重写所有的全局变量。同时第一次调用 alert(),a没有定义(因此值是undefined),但是它却存在于局部变量作用域中.
函数就是数据
这是一个非常重要的概念,我们需要跟上来–javascript中的函数实际上就是数据,那就意味着使用下面两种方式定义函数是等价的:
function f(){return 1;}
var f = function(){return 1;}
第二种方式定义函数被称为函数文字标识。
当你使用typeof操作符在把函数作为值的变量时,它返回字符串”function”.
>>> function f(){return 1;}
>>> typeof f
“function”
因此javascript函数就是数据,但是是一种特殊数据,具有两个重要特点:
它们包含代码 它们是可执行的(可调用的) 你在前面已经看见,函数执行是靠在名字后面加上圆括号。下面例子表明,不管函数是如何定义的,它都工作。在这个例子中,你也可以看到函数如何被当成一个通常的变量–可以被复制给不同的变量甚至被删除掉。
>>> var sum = function(a, b) {return a + b;}
>>> var add = sum;
>>> delete sum
true
>>> typeof sum;
“undefined”
>>> typeof add;
“function”
>>> add(1, 2);
3
正因为函数是赋值给变量的数据,所以命名函数名称的规则和命名变量的规则是可以通用的–函数名称不能由数字开始,它可以是任意字母,数字和下划线的组合。
匿名函数
javascript中,在你程序中存在一片数据那是可以的。假设你有如下的代码。
>>> “test”; [1,2,3]; undefined; null; 1;
这些代码看起来有些单调,因为它们真正上没有做什么事,但是这些代码是有效的并且也不会导致错误。你可以说这些代码包含匿名数据–匿名是因为这些数据片断没有赋给任何变量,因此也没有一个名字。
到现在你应该知道,函数像其他变量一样,所以它们可以不给赋一个名字直接使用:
>>> function(a){return a;}
现在,这些匿名的数据片断分散在你的代码里没有真正作用,除非他们刚好产生作用。在这些情况,他们可以有两种优雅的用处:
你可以传入一个匿名函数作为另外一个函数的参数。接受函数使用传入的函数可以做很多有意义的事情。 你也可以定义一个匿名函数并马上执行它。 让我们看看匿名函数的这两个应用的更多例子。
回调函数
正因为函数就像任意其他赋给变量的数据,它可以被定义,删除,复制,那么为什么不能作为参数传给其他函数呢?
这里有一个例子接受两个函数作为参数,执行他们并返回他们两个函数返回值的和。
function invoke_and_add(a, b){
return a() + b();
}
再让我们定义两个简单的额外函数仅仅返回硬编码的值:
function _disibledevent=>
for(i = 0; i < 3; i++) {
ar = arguments * 2;
}
return ar;
}
function addOne(a) {
return a + 1;
}
到现在测试这些函数:
>>> multiplyByTwo(1, 2, 3);
[2, 4, 6]
>>> addOne(100)
101
让我们看看,我们想要一个数组myarr,它包含三个元素,并且它们每个元素被分别传入两个函数。首先,让我们调用multiplyByTwo().
>>> var myarr = [];
>>> myarr = multiplyByTwo(10, 20, 30);
[20, 40, 60]
然后在循环将每个元素传入addOne().
>>> for (var i = 0; i < 3; i++) {myarr = addOne(myarr);}
>>> myarr
[21, 41, 61]
正如我们所看到的,一切工作顺利,但是仍然有提高的空间。首先这里有两个循环。循环是比较昂贵的如果它们连续或者重复使用。我们可以使用一个循环来达到这个目的。这里是如何修改multiplyByTwo()以便它能接受回调函数并在每个迭代元素上使用回调函数:
function multiplyByTwo(a, b, c, callback) {
var i, ar = [];
for(i = 0; i < 3; i++) {
ar = callback(arguments * 2);
}
return ar;
}
使用修改的函数,整个工作现在仅仅需要一个函数调用,它传入起始值和回调函数。
>>> myarr = multiplyByTwo(1, 2, 3, addOne);
[3, 5, 7]
我们可以使用匿名函数替代使用 addOne() , 这也会节省一个额外的全局变量。
>>> myarr = multiplyByTwo(1, 2, 3, function(a){return a + 1});
[3, 5, 7]
匿名函数在需要的时候也很容易修改:
>>> myarr = multiplyByTwo(1, 2, 3, function(a){return a + 2});
[4, 6, 8]
自调用函数
到目前为止我们讨论了使用匿名函数作为回调函数。让我们再看匿名函数的另外一个应用–在函数定义完成时候直接调用。这里有一个例子:
(
function(){
alert(’boo’);
}
)()
乍一看,这个语法有点吓人,但是实际上很容易理解–你只是简单的放一个匿名函数在一对括号里,然后后面跟一对括号。第二对基本上始说”执行吧”,同时也是你的匿名函数要接受的参数的放置位置。
(
function(name){
alert(’Hello ‘ + name + ‘!’);
}
)(’dude’)
一个使用自调用匿名函数的好处是完成一项事情而且不创建全局变量。 一个缺点,当然是你不能执行同一函数两次(除非你把它放在一个循环里或者另外一个函数里). 这使得匿名回调函数非常适合于一次性的初始化任务中使用。
内部函数(私有函数)
在脑子里记着函数就像其他值一样,没有什么区别,不让你在另外一个函数内部定义一个函数.
function a(param) {
function b(theinput) {
return theinput * 2;
};
return ‘The result is ‘ + b(param);
};
使用函数的字面标识法,这个函数也可这样写:
var a = function(param) {
var b = function(theinput) {
return theinput * 2;
};
return ‘The result is ‘ + b(param);
};
当你调用全局函数a()的时候,它将在内部调用局部函数b().既然b()是局部函数,它是不可以在a()外面访问的,所以我们可以说它是一个私有函数.
>>> a(2);
“The result is 4″
>>> a(8);
“The result is 16″
>>> b(2);
b is not defined
私有函数的好处如下:
可以保证全局命名空间的干净(更小机会的命名冲突) 私有–你只讲你的功能暴露给你决定暴露的”外面世界”,保持自身功能并不意味着被其他部分的应用来消费. 返回函数的函数
正如在前面提到的,函数总会返回值,如果不明确的指定返回值,那么默认情况下会返回undefined.函数只能有一个返回值,当然这个值也可能恰好是另外一个函数.
function a() {
alert(’A!’);
return function(){
alert(’B!’);
};
}
在这个例子中函数a()做它自己的事情(says A!)并且返回另外一个函数做另外一件事情(says B!).你可以将返回的值赋给另外一个变量,然后使用这个变量作为通常的函数使用:
>>> var newFunc = a();
>>> newFunc();
首先会alert A!然后第二次将alert B!.
如果你想马上执行返回函数,而不想赋值给一个新的变量,你可以直接使用另外一对圆括号在后面.最终的结果都是一样的.
>>> a()();
函数,重写它们自己
因为函数可以返回函数,你可以使用新函数替换旧的.继续前面的例子,你可以用a()返回的值重写真正的a()函数:
>>> a = a();
上面的alerts A!, 但是你再调用a()它alerts B!.
这在函数需要一次性的初始化工作要做的时候是非常有用的.在第一次调用之后,会重写自己,然后可以避免在每次调用的时候执行不必要的重复性工作.
在上面的例子中,我们从外面重新定义了函数–我们得到返回值并赋值给函数自己.但是函数也可以真正在内部被覆写.
function a() {
alert(’A!’);
a = function(){
alert(’B!’);
};
}
如果调用这个函数第一次,它将:
Alert A! (把这个当作一次性的初始化暂时的工作) 重定义全局变量a,让一个新的函数赋值给它 在随后的时间里,每次调用函数都alert B!
这里还有另外一个例子结合了上面本章几块讲到的几个技术:
var a = function() {
function someSetup(){
var setup = ‘done’;
}
function actualWork() {
alert(’Worky-worky’);
}
someSetup();
return actualWork;
}();
在这个例子中:
有两个私有函数—someSetup()和actualWork(). 有一个自调用函数–a()在定义完成后就调用自己。 函数执行第一次的时候调用someSetup()然后返回一个真正工作的变量的应用,这里是个函数。注意这里返回的没有圆括号,因为这里返回的是函数的引用,而非函数调用的结果。 因为整段代码由var a =…开始,自调用函数返回的值被赋给了a. 如果你想测试你对讨论的话题的理解程度,回答下面的问题。什么结果将被下面的代码alert:
最初加载完成的时候alert什么? 之后调用a()呢? 这些技术在做一些浏览器环境的事情时是十分有用的。因为不同的浏览器可以用不同的方法达到相同的事情,并且你会知道浏览器的特点不会在函数调用过程 中发生变化,你需要一个函数决定最好的方式以便在当前的浏览器下工作最好,然后再重新定义自己,那么 浏览器嗅探器就只做一次。在本书的后面你将会看到相关的例子。
闭包
本章剩下的部分是关于闭包的(有什么好的办法结束本章呢?)闭包可能最初有点难于掌握,因此如果你在第一次阅读的时候没有明白千万不要受挫。你应该 读完剩下的章节和你自己经历的例子,但是如果你感觉不完全理解这些内容,你可以在以后再返回来,以便在前面讨论的一些内容有机会掌握到。
在开始闭包前,我们先复习和扩大一下javascript的上下文作用域。
作用域链
正如你已经知道的,在javascript中,不像其他语言一样,没有打括号范围,但是有一个函数作用域。定义在函数内部的变量在函数外部是不可见的。但是定义在一个代码块(if/for循环)中的变量是可以被外部可见的。
>>> var a = 1; function f(){var b = 1; return a;}
>>> f();
1
>>> b
b is not defined
变量a是全局变量,然而是在函数f()内部定义的,因此:
在f()内部,a,b均可见 在f()外部,a可见,b不可见。 如果你定义一个n(),那么n()将具有访问它自己作用域变量的权限,以及它作用域的”父作用域”.这就是所谓的作用域链,并且这个作用域可以根据你的需要达到尽可能的长。
var a = 1;
function f(){
var b = 1;
function n() {
var c = 3;
}
}
语义作用域(Lexical Scope)
Javascript中,函数有语义作用域.这意味着函数在定义的时候创建了它们自己的环境(作用域) , 而不是在执行的时候创建的。让我们看看这个例子:
>>> function f1(){var a = 1; f2();}
>>> function f2(){return a;}
>>> f1();
a is not defined
在函数f1()我们可以调用f2()。因为局部变量a也在f1()中,有人可能会认为f2()可以访问a,但事实并非如此。同时当f2()定义(而 不是被执行),这时是不可以看到a的。f2()和f1()一样,仅仅具有访问自己作用域以及全局作用域的权限。 f1()和f2()是不会共享私有的作用域的。
当函数被定义,它就”记住”了它的环境,它的作用链。这并不是指函数也记住了在它作用域里的每一个单独的变量。恰好相反–你可以增加,删除或者更新 函数作用域里的变量,并且函数将会看到最近的,最新状态的变量。如果继续上面的例子,并声明一个全局变量a,f2()可以看到它,因为f2()知道到全局 变量的路径,并能访问环境中的每个成员。同时注意f1()如何包含一个f2()的调用,并且它能工作,尽管f2()还没有定义。f1()所要知道的是它的 作用域,因此所有显示在它作用域的所有东西都直接变得可以让f1()使用。
>>> function f1(){var a = 1; return f2();}
>>> function f2(){return a;}
>>> f1();
a is not defined
>>> var a = 5;
>>> f1();
5
>>> a = 55;
>>> f1();
55
>>> delete a;
true
>>> f1();
a is not defined
这个行为给javascript很大的灵活性–你可以增加,删除变量,然后再增加,这样总是可以的。你可以尝试删除函数f2(),然后再用不同的代 码体定义它。最后,f1()仍然工作,因为在某些时候它所需要的是知道如何访问它的作用域,而不是这个作用域用于包含什么。继续上面的例子:
>>> delete f2;
true
>>> f1()
f2 is not defined
>>> var f2 = function(){return a * 2;}
>>> var a = 5;
5
>>> f1();
10
Tags: 

延伸阅读

最新评论

发表评论