JavaScript 编程精解 (3)

第三章 函数

  • 函数是JavaScript中不可或缺的组成部分
  • 函数是构造大型程序的工具,可以用于减少重复性工作、为子程序命名并隔离各个子程序的运行。

3.1 定义函数

创建函数的表达式以关键字function开头。

var fun = function (parameter1, parameter2, ...) {//参数可以为空,也可以为一或多个
    // 函数体,哪怕只有一条语句,也要包含在大括号中,语句会在调用函数时执行
    ...
    // return语句决定了函数的返回值,后面不跟表达式时返回undefined
    return;
}

3.2 参数和作用域

  • 函数的参数初始值由函数调用者提供。

  • 函数内部创建的变量和参数都属于函数的局部变量,这种隔离机制确保了函数间不会相互干扰。

3.3 嵌套作用域

  • 可以在函数中创建其他函数,并产生不同程度的局部作用域。

  • 任何局部作用域都可以访问到包含它的局部作用域

  • 函数内部变量的可见性取决于函数在代码当中的位置,在包含了一个函数定义的代码块中,这个函数可以访问到代码块中的所有变量,即函数上层的代码块中的变量和函数内部的变量。这种控制变量的方法称为词法作用域

  • let关键字作用与var相同,只不过变量作用域是块作用域而非函数局部作用域。

3.4 函数值

函数和函数名的区别函数是一种叫做function引用类型的实例,因此函数是一个对象。对象是保存在内存中的,函数名则是指向这个对象的指针。

JavaScript中函数是一等公民,可以作为参数传入别的函数,也可以作为一个函数的返回值,也可以被重新赋值。

3.5 符号声明

函数声明还有一种更为简洁的方式:

function fun(parameter) {
    //todo
}
  • 函数声明不遵循一般的从上到下的流控制规则。
console.log('are you ok ?', ans());

function ans() {
    return 'yeah, i\'m ok';
}
  • 为了确保函数在不同环境下运行的行为一致,应在最外层的函数或程序作用域中进行函数声明。

3.6 调用栈

由于函数需要在执行结束后跳转回调用该函数的代码位置,因此计算机必须记住函数调用的上下文。我们将计算机存储这个上下文的区域称之为调用栈

  • 当函数调用时,当前的上下文信息就会被存储在栈顶

  • 当函数返回时,系统会删除存储在栈顶的上下文信息,并使用该信息继续执行程序。

// 若计算机空间无限大,循环调用会一直执行下去,但事实上是该程序会耗尽内存空间,导致“栈空间溢出”。
function chicken() {
    return egg();
}
function egg() {
    return chicken();
}
console.log(chicken() + ' came first.');

3.7 可选参数

JavaScript对传送函数的参数数量几乎不做任何限制。如果你传递了过多参数,多余的参数就会被忽略,而如果你传递的参数过少,遗漏的参数将会被赋值成undefined。

缺点:你可能恰好向函数传递了错误数量的参数,但没有人会告诉你这个错误。

优点: 我们可以利用这种行为来让函数接收可选参数。

3.8 闭包

如果函数已经执行结束,那么这些由函数创建的局部变量会如何处理呢?

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

可以把关键字function当做一种“冻结”代码并将其打包成函数值的模型。所以当看到“return function(...) {...}”这样的代码时,可以将其理解为一个句柄,其中句柄指向一段包装好的计算代码。

3.9 递归

函数完全可以自己调用自己,只要避免栈溢出的问题即可。我们把函数调用自身的行为称为递归

function power(base, exponent) {
    if (exponent == 0)
      return 1;
    else
      return base * power(base, exponent-1);
}

console.log(power(2, 3));
// 8

需要注意的是,在标准的JavaScript实现当中,递归写法的函数执行效率比循环写法的函数慢了大约10倍。如何权衡性能与优雅是一个值得考虑的问题,但有一条基本原则:除非程序执行速度确实太慢,否则先不要关注效率问题。

对于某些问题来说,递归相较于循环更能解决问题。这类问题通常需要执行和处理多个分支,而每个分支又会引出更多的执行分支。

3.10 添加新函数

两种常用的引入函数的方法:

  1. 找出程序中多次出现的相似代码。

  2. 写新功能代码,觉得一些代码应该包含在一个函数时。甚至可以先编写调用函数的代码,然后再具体实现调用的函数。

给函数起名的难易程度取决于我们封装的函数的用途是否明确

3.11 函数及其副作用

可以将函数分为两类:一类调用后产生副作用,而另一类则产生返回值(当然也可以定义同时产生副作用和返回值的函数)。

相比于直接产生副作用的函数,产生返回值的函数更容易集成到新的环境当中使用。但在副作用的帮助下,有些操作则更易、更快实现,因此考虑到运算速度,有时候纯函数并不可取。

3.12 本章小结

  1. 对于关键字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;
}
  1. 要理解函数的含义,就必须理解局部作用域的概念。对于一个函数来说,其参数及其内部声明的变量都是局部变量,每当调用函数时,这些变量都会被重新创建,而且对外并不可见。而在函数作用域当中声明的函数,可以访问其外部函数的局部作用域。

  2. 将程序中的任务划分到不同的函数中的做法是非常有用的,而且有助于提高代码的可读性。

你可能感兴趣的:(JavaScript 编程精解 (3))