第二章 函数
2.1 剖析函数定义
① 基本形式:
function name(...) {
...
return x*x; // 若return后面没有表达式,函数将返回 undefined。
}
② 关键字function 通常用于创建新函数。
③ 当代码遇到return语句时,立即跳出当前的函数,将返回值传递给调用该函数的代码。
2.1.1 定义顺序
① 定义函数可以在调用函数之后(拥有不同时间轴)。
② 原理:计算机在开始执行语句前,找到所有定义的function ,然后保存相关的function。我们不必考虑多个函数定义的顺序,使用函数允许它们之间相互调用,而不考虑哪个函数在第一个位置定义。
2.1.2 局部变量
① 函数一个非常重要的特性就是:内部创建的变量是函数的局部变量(在每次调用函数时创建,退出时则不再存在)。
② 如果没有定义同名的局部变量,函数内部则可能访问全局(非局部)变量。
③ 局部变量必须用 var 定义,否则视为全局变量。
④ 没有包含return语句,函数的实际返回值是undefined。
2.1.3 嵌套作用域
① 在JavaScript中,仅区分全局变量和局部变量还不够。实际上,变量作用域可以有任意层级(或嵌套)。其他函数内部定义的函数可以调用父函数的局部变量,而内部函数里定义的函数则不仅可以调用父函数的局部变量,还可以调用祖父函数的局部变量,以此类推。
② 函数内部的变量集是否可见,取决于函数在程序中的位置,在函数“上面”定义的所有变量都是可见的,也就是存在于函数体内并包含函数定义的以及位于程序顶级的变量。这种方式的变量可访问性称为词法作用域。
③JavaScript 不提供代码块(大括号之间)里产生新的局部环境。函数是唯一能创建新作用域的地方。(很多人认为这是JavaScript 设计者的小失误,新版本将增加此功能)
2.1.4 栈(先进后出)
① 当函数调用时,程序控制权交给函数体,当函数体返回后,继续调用函数之后的语句。因此,当函数体运行时,计算机必须记住调用该函数的上下文,以便知道之后从哪里继续。存放上下文的地方就是栈。
② 栈之所以存在,与一个函数体可以再次调用另外一个函数有关。每次一个函数被调用的时候,另外一个上下文就要被存储起来。(每次函数调用,当前上下文压入栈顶,函数返回时,在顶部的上下文就会被弹出栈重新获得)
③ 栈需要存储于计算机的内存空间里,若栈增长得太大,计算机就会抛出“堆栈溢出”或“太多递归”。(例如:两个函数互调的死循环)
2.1.5 函数值
① JavaScript 里的所有东西都是值,包括function 函数。这就是说:定义的函数名可以像普通的变量一样使用,而且其内容可以传递给表达式并用于更大的表达式。
② 特例说明:
var a = null;
function b() {return "B";}
(a||b)(); // 结果:"B"
-->若(a||b)()表达式产生的不是函数,则调用产生错误;一旦产生的是函数,调用一切正常(如本例)。
var a = null;
(a ||function() {return "B";})(); // 结果:"B"
-->代码产生效果与上例一样(除了没有定义函数名b)。
③ 在第5章中,我们将进一步探讨函数第一型的特征(通常是“函数都是值”这一概念的术语),并利用其特性编写一些灵活的代码。
2.1.6 闭包 ???
函数栈的特性及其将函数用作值的能力带来了一个有趣的问题:如果创建局部变量的函数调用不在栈上了,那局部变量会发生什么?
function createFunction() {
var local = 100;
return function() {return local;};
} // 返回值:100
如何处理这一情况就是向上函数变元问题(upwards Funarg problem)(许多旧编程语言禁用了这种形式),JavaScript 中,只要这个局部变量是可达的,就会尽力保存局部变量。
这种特性称为闭包。包裹一些局部变量的一个函数叫做一个闭包。该行为不仅让我们不用担心变量是否依然存在,而且还可以创造性地使用函数值。
例如:(下面的函数可以动态创建函数值:将函数的参数加上指定的数字。)
function makeAdder(amount) {
return function(number) {
return number + amount;
};
}
var addTwo = makeAdder(2);
addTwo(3); // 等价于function(3) (amount在内存中一直存在,值为2) ==> 结果:5;
例2:
function a() {
var i=0;
function b() {
alert(++i);
}
retrun b;
}
var c = a();
c(); //等价于b(); (i的值一直在内存中)
c(); //等价于b(); (i的值一直在内存中)
① 在JavaScript中,如果一个对象不再被引用,就会被GC(垃圾回收机制)回收。如果两个对象相互引用,而不再被第三个对象引用,那么相互引用的对象也会被回收。
② 例2中,因 a()被 b()引用,b()又被 a()外的 c 引用,所以a执行后不会被回收。
③ 例2中,闭包 a()中变量会存在内存中,闭包a的内部函数 b()中的变量被回收。
④ 例2中,c(); 相当于调用 b 函数,因为 a();的返回值是 b 函数。
2.1.7 可选参数
① JavaScript 不会限制传入函数的参数数目。
② 若传入的参数过多,多余的参数则被忽略掉。
③ 若传入的参数过少,缺失的参数默认为 undefined。
④ 这样做的好处是:可以使用函数接受“可选参数”。坏处:意外传入的错误参数不会提示。
2.2 技巧
2.2.1 避免重复
① 发明函数的原因就是为了代码复用。
② 定义函数条件:
a. 代码可能在不同项目中使用。
b. 尽量不要在函数内包括print 行为。更为灵活,而且print(XX(...))不会比printXX(...)更难输入。
c. 在自己明确自己需要该函数前,不要定义函数(引诱我们为每个小功能编写复杂框架的陷阱,并且这些功能没有任何实际作用)。
2.2.2 纯函数
① 用Purity 描述函数,指这个函数是否存在副作用。
② 纯函数在数理上指:当使用函数的时候,同样的参数总是返回同样的值,而没有副作用。
③ 纯函数和非纯函数主要区别:在代码设计和思维层面上。
a. 纯函数调用,可以用结果直接替换而不需要改变代码的意义。当不确定函数是否正常,只需调用测试即可。
b. 非纯函数,因各种因素可能返回不同的值并产生副作用,我们可能很难测试和理解这些副作用。
④ 多数情况下,我们需要非函数。少数情况,纯函数也可以解决,但非纯函数会更方便、更有效。
原则:若我们可以很自然地使用纯函数,那么就用纯函数,否则我们使用非纯函数(不用觉得使用非纯函数很低级)。
2.2.3 递归
一个函数调用自身叫做递归。
① 在大多数JavaScript 实现里,使用递归会比循环慢10倍。在Javascript 里,运行简单的循环比多次调用一个函数方便的多。
② 如果再该函数上使用一个足够打的幂数,将会导致栈溢出。
③ 很多开发人员反复的基本原则:只有在证明程序运行太慢时采取关注效率问题。一旦出现这种情况,找出占用最多时间的代码,然后将这些美观的代码改成高效的代码。
④ 有些问题用递归解决比循环要简单得多。大部分是一些需要探测或处理几个分支情况(每个分支又可能生成更多分支)的问题。
例如:从数字1开始,重复执行加5或者乘3这个步骤,编写函数,找出指定数字的运算序列。
function findSequence(goal) {
function find(start,history) {
if(start == goal)
retrun history;
else if(start > goal)
return null;
else
return find(start + 5, "(" + history + "+5)") ||
find(start * 3, "(" + history + "*3)");
}
return find(1,"1");
}
findSequence(24); // 结果:(((1 * 3) + 5) * 3)

浙公网安备 33010602011771号