用Node.js学JavaScript(三)语言函数、参数与闭包
在本期当中,重庆逍遥子科技有限公司将介绍 JavaScript 当中最有趣的一个领域,那就是「函数」(function) 的用法,还有「闭包」 (closure) 这种相当特别的概念。
函数的宣告
在 JavaScript 当中,函数的宣告方法大致有两种,第一种的宣告方法就和一般程序语言 (C/C++, Python, Java) 等差不多,是采用 f(a,b,c…) 这种方式宣告的,但是必须在前面加上 function 这个关键词。
在以下范例中, sub(a,b) 就是采用这种方式宣告的一个范例。
档案:function.js
// 第一种写法,直接宣告函数
function sub(a,b) {
return a-b;
}
// 第二种写法,将匿名函数指定给变量。
var add = function(a,b) {
return a+b;
}
console.log("add(3,5)=", add(3,5), " sub(7,2)=", sub(7,2));
执行结果:
D:\js\code>node function.js
add(3,5)= 8 sub(7,2)= 5
但是、在 JavaScript 当中,还有一种比较特别函数宣告方式,是在宣告了一个「匿名函数」之后,再把这个函数「塞给」一个变量。就像上述 `var add = function(a,b) …` 的做法,这样我们就可以用 add(3,5) 这样的方式去呼叫该函数了。
函数型态的参数
在上面的 add 范例中,我们将「函数」塞给一个变量,而且还可以直接把该变量当作函数来呼叫。
那么、我们能不能将函数当作参数来传递呢?
关于这点、当然是可以的,以下是一个将「函数当作参数」的范例。
档案: fptr.js
function sub(a,b) {
return a-b;
}
function f5(f, a) {
return f(a, 5);
}
console.log("sub(8,5)="+sub(8, 5));
console.log("f5(sub,8)="+f5(sub,8));
执行结果
D:\Dropbox\Public\web\js\code>node fptr
sub(8,5)=3
f5(sub,8)=3
您可以看到,函数 `f5(f, a)` 的参数 f,其实又是一个函数,因为我们在 f(a,5) 当中把 f 当作函数来呼叫。
所以、当我们呼叫 f5(sub, 8) 的时候,该函数会传回 3,因为当我们将 f5(sub, 8) 的内容 return f(a, 5) 里面的 f 取代为 sub,而 a 取代为 8 时,就会发现 return 语句的 f(a,5) 其实就是呼叫 sub(8,5),所以当然就会传回 3 啰!
参数的存取
对于一般的函数,参数个数是固定的,例如上述范例的 add(a,b) 与 sub(a,b) ,都很明确的有两个参数,因此直接用 a, b 就可以存取该参数。
但是、对于那种有不确定参数个数的函数,就没有对应名称可以用来存取这些参数了。
还好,javascript 在呼叫每个函数时,都会将参数放到一个称为 arguments 的变量里,arguments 是一个类似数组形态,我们可以透过 arguments 来存取每一个参数,以下是一个范例。
档案:arg.js
function print() {
for (var i in arguments) {
console.log(i, ":", arguments[i]);
}
}
print(3, 2.71828, "hello");
执行结果:
D:\js\code>node arg.js
0 : 3
1 : 2.71828
2 : hello
这种变动参数个数的函数,有时候很有用。例如、若我们要写一个可以找出最小值的函数,就可以用下列的 min() 函数。
档案:min.js
function min() {
var m = arguments[0];
for (var i in arguments) {
if (arguments[i] < m)
m = arguments[i];
}
return m;
}
var x = min(3, 7, 2, 9, 1, 5, 8);
console.log("x=min(3, 7, 2, 9, 1, 5, 8)=", x);
执行结果
D:\Dropbox\Public\web\js\code>node min.js
x=min(3, 7, 2, 9, 1, 5, 8)= 1
变量的领域范围
在上述的 min.js 程序中,您可以看到我们经常会用 var 这个关键词来宣告变量。但事实上,即使我们不用 var 宣告,该程序也能正常运作。以下是一个完全没有 var 宣告的版本。
档案:min2.js
function min() {
m = arguments[0];
for (i in arguments) {
if (arguments[i] < m)
m = arguments[i];
}
return m;
}
x = min(3, 7, 2, 9, 1, 5, 8);
console.log("x=min(3, 7, 2, 9, 1, 5, 8)=", x);
但是、上述这个没有 var 的版本 min2.js ,与那个有 var 的版本其实在某些细微处有所不同,因为采用 var 宣告时,该变量将会是一个局部变量,而没有采用 var 宣告就直接指定的方式,则会是一个「全局」变量,这种全局变量有可能造成更多的冲突问题,所以在一般的情况下,我们都会加上 var 宣告。
关于是否该为变量加上 var 的更详细描述,可以参考下列文章:
JavaScript 语言核心(3)你的变数 var 了吗?
闭包 (Closure)
对于很多 C/C++、Java、C#、VB 等语言的「程序人」而言,「闭包」是个很奇特而难以理解的概念,但对于 JavaScript、Lua、Python、Ruby 等动态语言来说,「闭包」却是个很自然的用法,一点都不神秘。
其实、是「闭包」 (Closure) 这个词给人的感觉太深奥了,我们不需要迷惑于这个名词的神秘感,请让我们先来看一个范例。
档案: closure.js
function sub(a,b) {
return a-b;
}
function sub5(a) {
return sub(a, 5);
}
function fsub5(a) {
return function() {
return sub(a, 5);
};
}
console.log("sub(8,5)="+sub(8, 5));
console.log("sub5(8)="+sub5(8));
console.log("fsub5(8)="+fsub5(8));
console.log("fsub5(sub,8)()="+fsub5(8)());
执行结果:
D:\Dropbox\Public\web\js\code>node closure
sub(8,5)=3
sub5(8)=3
fsub5(8)=function () {
return sub(a, 5);
}
fsub5(sub,8)()=3
在上述范例中,我们看到 sub(a,b) 是个很正常的函数,当我们呼叫 sub(8,5) 时会传回 3。
如果我们运用 sub(a,b) 定义一个传回 sub(a,5) 的函数为 sub5(a),那么 sub5(8) 同样也会传回 3。
上述程是最后的 fsub5 函数,则不像前面的 sub5 一样传回一个值,而是传回一个函数,这个函数的内容如下:
return function() {
return sub(a, 5);
}
这下问题就来了,fsub5 所传回的是一个函数,而这个函数里的 a 到底是什么东西呢?
这时,请让我们把眼光放大一点点:
function fsub5(a) {
return function() {
return sub(a, 5);
};
}
您会发现,原来所传回来的那个函数里的 a ,应该就是 fsub5(a) 的参数 a,这种「把外层变数一起包进来」的机制,就称为「闭包」。
换句话说、只要直接在函数里引用外层的变量,然后当我们将「函数封闭起来传回」时,该函数仍然可以正常使用,这就是闭包的概念了。
结语
在本文中,我们介绍了 JavaScript 中的「函数、参数与闭包」等观念,这些观念在我们进行模块化或撰写大型程序的时候,将会是非常重要的根基。
JavaScript 当中的函数,可以被塞进变量里,然后再将变量当作函数来呼叫。也可以放在参数里,拿来传递给另一个函数使用,这种方式有点像 C 语言当中的函数指针,只是感觉更精简,更有弹性而已。
而那个感觉有点神秘的「闭包」观念,也只不过是「在传回一整个函数时、顺便把外层的变量给包进来而已」,并不真的那么神秘啊!