函数可以用来结构化大段程序;减少重复;为子程序命名,将子程序彼此分开。
编程语言有越多预先精确定义的函数,灵活性就越低,所以需要程序员自己编写函数而不是语言作者内置越多的函数就越好。
定义函数
关键字function,可选的参数,必不可少的花括号。有些函数可以返回值,有些则没有,他们只产生side effect。
return 关键字会立刻跳出函数,将表达式返回给函数调用的代码,如果return后面没有表达式则返回undefined。
var makeNoise = function() {
console.log("Pling!");
};
makeNoise();
// → Pling!
var power = function(base, exponent) {
var result = 1;
for (var count = 0; count < exponent; count++)
result *= base;
return result;
};
console.log(power(2, 10));
// → 1024
参数与作用域
参数就像普通变量,只是他们的值由调用函数的代码赋予,不是函数本身。
函数最重要的特性是,所有在函数内部声明的变量和他们的参数都是局部变量,局部变量在每次函数调用时都会被创建,并且这些“化身"不会彼此干扰。上例中,power内部的result变量,每次调用都会被创建。
相反,在函数外部声明的变量是全局变量,他们可以被整个程序包括函数内部访问到,只要函数内部没有同名的局部变量。
var x = "outside";
var f1 = function() {
var x = "inside f1";
};
f1();
console.log(x);
// → outside
var f2 = function() {
x = "inside f2";
};
f2();
console.log(x);
// → inside f2
上面的代码定义并且调用了2个函数,f1函数内部声明了一个局部变量x,它修改的是局部变量x。f2函数没有声明局部变量,所以函数内部的x引用的是全局变量x。
嵌套的作用域
javascript可以在函数内部创建函数,这产生了作用域的嵌套。
var landscape = function() {
var result = "";
var flat = function(size) {
for (var count = 0; count < size; count++)
result += "_";
};
var mountain = function(size) {
result += "/";
for (var count = 0; count < size; count++)
result += "'";
result += "\\";
};
flat(3);
mountain(4);
flat(6);
mountain(1);
flat(1);
return result;
};
console.log(landscape());
// → ___/''''\______/'\_
flat函数和mountain函数可以访问result变量,因为他们都是处于一个作用域中,landscape中。flat和mountain无法访问彼此的count变量,因count在不同的函数中定义。
所有的局部变量可以访问它所在的函数内的其他局部变量(兄弟变量互相可见),函数内的变量可见性由该函数在程序中的位置决定。所有处在包围函数定义的代码块中的变量都是可见的(可访问上层变量),也就是所在函数体和包围该函数体的上层区域都是可见的,这叫做词法作用域。
其他语言中,花括号会产生新的作用域,在javascript中,只有函数能产生作用域。花括号只是用来括住if语句和循环体的。
var something = 1;
{
var something = 2;
// Do stuff with variable something...
}
// Outside of the block again...
//something其实是同一个变量,花括号无实际作用
javascript下一版将引入let,它能根据封闭的代码块创建变量,不再根据函数。
函数作为值
函数名也视同为一种“值”,可以将它用于表达式,也可以将它存储在其他地方,或作为参数传递给其他函数。类似的,一个存储了函数的变量仍然是普通变量,可以对他重新赋值。
函数声明
函数声明会被提前到代码中最顶部,可以在该作用域中任意地方访问。这个特性导致我们可以随意安排函数声明的位置,不用担心在第一次使用函数时没有定义它。
不要在if语句或循环体中定义函数。
Javascript语言标准禁止这样做,实际上各个平台表现并不同。
函数调用栈
计算机存储的函数调用上下文叫做调用栈,每当有函数被调用,当前所处的上下文环境就被存储到调用栈顶部,当函数执行完毕,读取调用栈顶部的环境继续运行程序。
调用栈需要内存,当调用栈变得巨大,计算机会提示“栈空间不足”或者“过多的递归”。下面的代码导致栈空间无限增长,最终因内存不足被毁掉。
function chicken() {
return egg();
}
function egg() {
return chicken();
}
console.log(chicken() + " came first.");
// → ??
可选的参数
javascript对参数要求不严格,如果你传多了,函数会忽略多余的参数,如果传少了,函数会得到undefined。
这个特性的坏处是你可能传递了错误数量的参数,而执行环境并不会告诉你。好处是你可以实现带有可选参数的函数,比如这样。
function power(base, exponent) {
if (exponent == undefined)
exponent = 2;
var result = 1;
for (var count = 0; count < exponent; count++)
result *= base;
return result;
}
console.log(power(4));
// → 16
console.log(power(4, 3));
// → 64
下一章有种方法能让函数接收任意数量的参数,这很有用。
console.log("R", 2, "D", 2);
// → R 2 D 2
闭包
函数调用每次都重新创建局部变量,可以同时存在多个局部变量的实例,互不干扰。
function wrapValue(n) {
var localVariable = n;
return function() { return localVariable; };
}
var wrap1 = wrapValue(1);
var wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2
这种特性--在一个封闭的函数中,引用一个特定的局部变量的实例--叫做闭包。一个“封闭包裹”局部变量的函数叫做一个闭包。
这个特性可以加以利用:
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
var twice = multiplier(2);
console.log(twice(5));
// → 10
显式的声明一个局部变量localVariable不是必须的,因为参数本身就是一个局部变量。可以这样理解,function关键字就像封印咒语,把函数体封印起来,然后赋值给一个函数值。当碰见return function(...) {...}这类代码时,想象它返回一个等待解封的暗号,以后解除封印后可以执行代码。
上述代码中,multiplier函数返回一个封印的代码块,然后存储在twice中。console.log(twice(5));调用了twice中的值,导致被封印的代码(return number * factor;)封印解除。twice借助multiplier的函数调用(创建了它)可以访问factor,在解封时,它又通过它的参数number获得了传递给他的参数,5。
递归
函数可以调用自己,这叫做递归。
function power(base, exponent) {
if (exponent == 0)
return 1;
else
return base * power(base, exponent - 1);
}
console.log(power(2, 3));
// → 8
在javascript中,递归大概要比循环慢10倍。
在编程中,不要太考虑效率问题,直到你真的发现程序变慢了,那么去找出那部分的问题,解决它。
虽然递归比循环慢,但有时递归解决特定的问题要更容易,尤其是那些能产生“分支",分支继续产生分支的问题。
有这样一个问题,给你一个数字,从1开始,或者加5,或者乘以3,怎么才能得到这个数字?
递归方法:
function findSolution(target) {
function find(start, history) {
if (start == target)
return history;
else if (start > target)
return null;
else
return find(start + 5, "(" + history + " + 5)") ||
find(start * 3, "(" + history + " * 3)");
}
return find(1, "1");
}
console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)
我并不要求这段程序一定要找到最短的解决答案,找到任意一种就可以。
find函数是实际递归的函数,它接收2个参数,一个是当前计算的数,一个用来记录如何产生目标数字的字符串。
函数采取3个动作,如果当前数字就是目标数字,则当前历史就是产生目标数字的方法,直接返回历史。如果当前数字大于目标数字,则没有必要继续计算了,返回null。如果当前数字小于目标数字,则函数尝试2种可能,调用它本身2次来加和乘,如果第一次调用没有返回null,则返回它,否则返回第二次调用,而不管是字符串还是null。
用13举例,看一下函数调用的过程:
find(1, "1")
find(6, "(1 + 5)")
find(11, "((1 + 5) + 5)")
find(16, "(((1 + 5) + 5) + 5)")
too big
find(33, "(((1 + 5) + 5) * 3)")
too big
find(18, "((1 + 5) * 3)")
too big
find(3, "(1 * 3)")
find(8, "((1 * 3) + 5)")
find(13, "(((1 * 3) + 5) + 5)")
found!
第一次调用的结果是调用它本身2次。第一次调用产生(1+5),然后尝试2种可能,来计算是否小于或等于13,由于没有找到,所以给第一次调用返回null,||操作符使探索1*3成为可能。这次探索通过另一个递归调用,幸运的找到了13.最内部的递归返回了一个字符串,沿着中间的||操作符一路返回,最终给出了解答。
增长的函数
随着功能或程序使用的情景的增加或变更,原先的函数可能变得越来越大,也可能变成只适合特定情境才有的程序。这丧失了函数本质的优点,功能独立,减少重复。
例子:
想要打印农场的牛和鸡的数量,数字要求用0填充成3位数字,像这样
007 Cows
011 Chickens
function printFarmInventory(cows, chickens) {
var cowString = String(cows);
while (cowString.length < 3)
cowString = "0" + cowString;
console.log(cowString + " Cows");
var chickenString = String(chickens);
while (chickenString.length < 3)
chickenString = "0" + chickenString;
console.log(chickenString + " Chickens");
}
printFarmInventory(7, 11);
现在也要统计牛,为了不复制相同的代码,我重新写了函数。
function printZeroPaddedWithLabel(number, label) {
var numberString = String(number);
while (numberString.length < 3)
numberString = "0" + numberString;
console.log(numberString + " " + label);
}
function printFarmInventory(cows, chickens, pigs) {
printZeroPaddedWithLabel(cows, "Cows");
printZeroPaddedWithLabel(chickens, "Chickens");
printZeroPaddedWithLabel(pigs, "Pigs");
}
printFarmInventory(7, 11, 3);
现在,printZeroPaddedWithLabel变得复杂,且通用性很差。
再次重写
function zeroPad(number, width) {
var string = String(number);
while (string.length < width)
string = "0" + string;
return string;
}
function printFarmInventory(cows, chickens, pigs) {
console.log(zeroPad(cows, 3) + " Cows");
console.log(zeroPad(chickens, 3) + " Chickens");
console.log(zeroPad(pigs, 3) + " Pigs");
}
printFarmInventory(7, 16, 3);
函数最好写得简洁纯粹一点。不管是简单的3位填充型数字,还是复杂的数字格式化系统,始终保持纯粹,而不是一个函数却带有很多的功能,这些功能可能大部分人都用不上。
函数与副作用(side effect)
函数可以分为2类,一类是返回值的函数,一类是产生“副作用(side effect)"的函数.当然我们确实可以让一个函数既产生副作用也返回值。
第一版函数printZeroPaddedWithLabel是一个产生副作用的函数(打印一行内容),第二版zeroPad函数是返回值的函数。第二个比第一个有用不是偶然的,能够产生值的函数要比产生副作用的函数有更多的用法。
一个纯粹的函数不止没有副作用,而且也不依赖其他函数的副作用,比如它不会依赖一个可能被偶然改变的全局变量。一个纯粹的函数,有一个优点,只要输入相同的参数,永远返回相同的结果(不做其他任何事情)。调用这类函数,可以直接用它产生的结果代替。一个纯粹的函数只要在一处环境有效,它就在任意情况下都有效,非纯粹的函数则根据不同因素产生不同的结果,并且有副作用(对主调用函数产生附加的影响。例如修改全局变量或修改参数),这些将导致追踪和调试变得困难。
没有必要发起“纯函数”的圣战或大革命,副作用常常是有用的,没有办法写一个console.log的纯函数版,console.log也毫无疑问是有用的。而且对于某些操作,使用副作用要更加高效,所以为了计算速度而避免纯函数说得通的。
总结
function用在表达式中,可以声明函数(创建函数值),用在语句中,可以声明变量并将函数赋值给它。
// Create a function value f
var f = function(a) {
console.log(a + 2);
};
// Declare g to be a function
function g(a, b) {
return a * b * 3.5;
}
了解函数重要的是了解作用域。
参数和局部变量在每次调用函数时会重建。