函数

函数是解决特定问题的一段代码,它返回解决方案给调用的语句。函数存在于自己的上下文中,因此,函数将庞大的程序分解成更小的“模块”,这有助于组织软件结构和软件开发过程。

// 定义一个函数
function <function_name> (<parameters>) {
  <function_body>
}

// 调用一个函数
<variable> = <function_name> (<arguments>);

JavaScript 支持函数式编程(Functional Programming)这一软件开发范式。函数是从 Object 派生出来的数据类型;它们可以绑定到变量上,可以作为参数传递,也可以从其他函数中返回,就像任何其他数据类型一样。

函数声明

函数可以通过三种主要方式来构造。第一种版本可以进一步简化;见下文。

传统的声明方式:

"use strict";

// 传统声明(或‘定义’)
function duplication(p) {
  return p + "! " + p + "!";
}

// 调用函数
const ret = duplication("Go");
alert(ret);

通过变量和表达式构造:

"use strict";

// 将函数赋值给变量
let duplication = function (p) {
  return p + "! " + p + "!";
};

const ret = duplication("Go");
alert(ret);

使用 new 操作符构造(这个版本有点繁琐):

"use strict";

// 使用 'new' 构造器
let duplication = new Function ("p",
  "return p + '! ' + p + '!'");

const ret = duplication("Go");
alert(ret);

函数调用

对于函数声明,我们已经看到了三种变体。同样,对于它们的调用,也有三种变体。声明和调用是独立的,你可以任意组合它们。

传统的调用方式

传统的调用方法使用函数名后跟一对圆括号 ()。在括号内传递函数的参数(如果有的话)。

"use strict";

function duplication(p) {
  return p + "! " + p + "!";
}

// 传统的调用方式
const ret = duplication("Go");
alert(ret);

如果脚本在浏览器中运行,还可以使用以下两种方式,它们利用了浏览器提供的 window 对象:

"use strict";

function duplication(p) {
  return p + "! " + p + "!";
}

// 通过 'call' 调用
let ret = duplication.call(window, "Go");
alert(ret);

// 通过 'apply' 调用
ret = duplication.apply(window, ["Go"]);
alert(ret);

提示:如果你使用函数名而不加圆括号 (),你将得到函数本身(即函数体的脚本),而不是调用的结果。

"use strict";

function duplication(p) {
  return p + "! " + p + "!";
}

alert(duplication); // 'function duplication (p) { ... }'

提升(Hoisting)

函数会受到“提升”(Hoisting)的影响。这一机制会自动将函数的声明提升到其作用域的顶部。因此,你可以在函数声明之前的地方调用该函数。

"use strict";

// 在函数声明之前使用该函数
const ret = duplication("Go");
alert(ret);

function duplication(p) {
  return p + "! " + p + "!";
}

立即调用函数(Immediately Invoked Function Expression,IIEF)

到目前为止,我们已经看到了声明和调用两个独立的步骤。还有一种语法变体,允许将两者结合在一起。它的特点是将函数声明放在圆括号中,并紧跟着圆括号 () 来调用该声明。

"use strict";

alert(  // 'alert' 用于显示结果
  // 声明加调用
  (function (p) {
    return p + "! " + p + "!";
  })("Go")   // ("Go"): 使用参数 "Go" 来调用
);

alert(
  // 使用 '箭头函数' 语法的相同示例
  ((p) => {
    return p + "! " + p + "!";
  })("Gooo")
);

这种语法称为 立即调用函数表达式(Immediately Invoked Function Expression,IIEF)。

参数

当函数被调用时,声明阶段的参数会被调用时传入的实参所替代。在上述声明中,我们使用变量名 p 作为参数名。当调用函数时,我们通常使用字面量 "Go" 作为实参。运行时,它会替换函数中所有 p 的出现。上述例子展示了这一技术。

值传递(Call-by-value)

这些替换是通过“值传递”进行的,而不是“引用传递”。传递给函数的是参数原始值的副本。如果在函数内部修改了这个副本,外部的原始值不会被改变。

"use strict";

// 函数有一个参数 'p'
function duplication(p) {
  // 在这个例子中,我们修改了参数的值
  p = "NoGo";
  alert("In function: " + p);
  return p + "! " + p + "!";
};

let x = "Go";
const ret = duplication(x);

// 函数内部对参数的修改在这里可见吗?不可以。
alert("Return value: " + ret + " Variable: " + x);

对于对象(所有非原始数据类型),这种“值传递”有可能带来惊讶的效果。如果函数修改了对象的属性,这个改变在外部也是可见的。

"use strict";

function duplication(p) {
  p.a = 2;       // 修改对象的属性值
  p.b = 'xyz';   // 添加一个新属性
  alert("In function: " + JSON.stringify(p));
  return JSON.stringify(p) + "! " + JSON.stringify(p) + "!";
};

let x = {a: 1};
alert("Object: " + JSON.stringify(x));
const ret = duplication(x);

// 函数内部对对象的修改在这里可见吗?是的。
alert("Return value: " + ret + " Object: " + JSON.stringify(x));

当这个例子运行时,它显示了在调用 duplication 后,函数所做的修改不仅在返回值中可见,而且原始对象 x 的属性也发生了变化。这是因为函数接收到的是对象引用的副本。在函数内部,仍然引用的是相同的对象。该对象本身只存在一次,但有两个(相同的)引用指向它。无论是通过哪个引用修改对象的属性,修改都对外部可见。

修改引用本身

另一个后果是(这可能与原始数据类型的行为类似?)修改引用本身(例如,通过创建一个新对象)在外部是不可见的。新对象的引用存储在原始引用的副本中。此时,不仅有两个引用(值不同),而且有两个对象。

"use strict";

function duplication(p) {
  // 通过创建新对象来修改引用
  p = {};

  p.a = 2;       // 修改对象的属性值
  p.b = 'xyz';   // 添加新属性
  alert("In function: " + JSON.stringify(p));
  return JSON.stringify(p) + "! " + JSON.stringify(p) + "!";
};

let x = {a: 1};
alert("Object: " + JSON.stringify(x));
const ret = duplication(x);

// 函数内部对引用的修改在这里可见吗?不可以。
alert("Return value: " + ret + " Object: " + JSON.stringify(x));

注意

  1. 这种参数传递技术的命名在不同的语言中不一致。有时它被称为“共享传递”(call-by-sharing)。Wikipedia 给出了概述。

  2. JavaScript 的参数传递方式与使用 const 关键字声明常量的后果相似。这样的变量无法修改。然而,如果这些变量引用一个对象,仍然可以修改对象的属性。

默认值

如果函数调用时传入的参数数量少于函数的参数数量,那么多余的参数将保持 undefined。不过,你可以通过在函数签名中为这些参数赋予默认值来解决这个问题。缺少的参数将会获得这些默认值。

"use strict";

// 两个几乎相同的函数,只有签名略有不同
function f1(a, b) {
  alert("第二个参数是: " + b);
}

function f2(a, b = 10) {
  alert("第二个参数是: " + b);
}

// 相同的调用,不同的结果
f1(5);        //   undefined
f1(5, 100);   //   100

f2(5);        //   10
f2(5, 100);   //   100

可变数量的参数

对于某些函数,接收不同数量的参数是“正常”的。例如,想象一个显示名字的函数,firstNamefamilyName 必须传递,但也有可能需要显示 academicTitletitleOfNobility。JavaScript 提供了不同的方式来处理这种情况。

单独检查

可以检查“正常”参数以及额外的参数,以确定它们是否包含值。

"use strict";

function showName(firstName, familyName, academicTitle, titleOfNobility) {
  "use strict";

  // 处理必需的参数
  let ret = "";
  if (!firstName || !familyName) {
    return "必须指定名字和姓氏";
  }
  ret = firstName + ", " + familyName;

  // 处理可选参数
  if (academicTitle) {
    ret = ret + ", " + academicTitle;
  }
  if (titleOfNobility) {
    ret = ret + ", " + titleOfNobility;
  }

  return ret;
}

alert(showName("Mike", "Spencer", "Ph.D."));
alert(showName("Tom"));

每个可能未提供的参数都必须单独检查。

'Rest' 参数

如果可选参数的处理方式结构上是相同的,可以通过使用 rest 操作符语法来简化代码——通常与循环结合使用。这个功能的语法是函数签名中的三个点(...),与扩展语法相似。

它是如何工作的呢?在函数调用时,JavaScript 引擎将传入的可选参数合并成一个数组(请注意,调用脚本并不使用数组)。这个数组作为最后一个参数传递给函数。

"use strict";

// 三个点(...)引入了 'rest' 语法
function showName(firstName, familyName, ...titles) {

  // 处理必需的参数
  let ret = "";
  if (!firstName || !familyName) {
    return "必须指定名字和姓氏";
  }
  ret = firstName + ", " + familyName;

  // 处理可选参数
  for (const title of titles) {
    ret = ret + ", " + title;
  }

  return ret;
}

alert(showName("Mike", "Spencer", "Ph.D.", "Duke"));
alert(showName("Tom"));

调用中的第三个及以后的参数会被收集成一个数组,并作为函数的最后一个参数提供。这使得可以使用循环,并简化了函数的源代码。

'arguments' 关键字

与其他 C 系列语言相似,JavaScript 在函数中提供了 arguments 关键字。它是一个类似数组的对象,包含函数调用中所有传入的参数。你可以遍历它,或者使用它的 length 属性。

它的功能与上面的 rest 语法类似。主要区别是,arguments 包含所有的参数,而 rest 语法不一定会影响所有参数。

"use strict";

function showName(firstName, familyName, academicTitles, titlesOfNobility) {

  // 使用单一关键字处理所有参数
  for (const arg of arguments) {
    alert(arg);
  }
}

showName("Mike", "Spencer", "Ph.D.", "Duke");

返回值

函数的目的是提供一个解决特定问题的方案,并通过 return 语句将该方案返回给调用程序。

return 语句的语法是 return <expression>,其中 <expression> 是可选的。

一个函数会一直执行,直到遇到 return 语句(或发生未捕获的异常,或执行完最后一条语句)。<expression> 可以是一个简单的变量,例如 return 5,也可以是一个复杂的表达式,例如 return myString.length,或者完全省略 return,此时将返回 undefined

"use strict";

function duplication(p) {
  if (typeof p === 'object') {
    return;  // 返回值: 'undefined'
  }
  else if (typeof p === 'string') {
    return p + "! " + p + "!";
  }
  // 隐式返回 'undefined'
}

let arg = ["Go", 4, {a: 1}];
for (let i = 0; i < arg.length; i++) {
  const ret = duplication(arg[i]);
  alert(ret);
}

箭头函数 (=>)

箭头函数是传统函数语法的一种简洁替代方式。它省略了一些语言元素,去掉了其他一些,并且与原始语法相比,语义上只有少数几个区别。

箭头函数总是匿名的,但可以赋值给一个变量。

"use strict";

// 原始的传统语法
function duplication(p) {
    return p + "! " + p + "!";
}

// 1. 去掉关键字 'function' 和函数名
// 2. 用 '=>' 替代
// 3. 去掉 'return',最后的值会自动返回
(p) => {
     p + "! " + p + "!"
}

// 如果只有一个语句,可以去掉 {} 
(p) =>  p + "! " + p + "!"

// 如果只有一个参数,可以去掉参数括号
// -----------------------------
      p => p + "! " + p + "!"     // 就这么简单!
// -----------------------------

alert(
  (p => p + "! " + p + "!")("Go")
);

这里是一个使用数组的例子。forEach 方法遍历数组并一个接一个地产生数组元素。这个元素被传递给箭头函数的单个参数 e,箭头函数将 e 和一段简短文本一起显示。

"use strict";

const myArray = ['a', 'b', 'c'];

myArray.forEach(e => alert("数组元素是: " + e));

其他编程语言也提供了箭头函数的概念,如匿名函数或 Lambda 表达式。

递归调用

函数可以调用其他函数,在实际应用中,通常会出现这种情况。

当函数调用自己时,称为递归调用。当然,这可能会导致无限循环的风险。因此,你必须修改参数以避免这一问题。

通常需要递归调用的情况是,当应用程序处理树形结构时,比如物料清单、DOM 树或家谱信息。我们这里展示一个简单的数学问题——阶乘计算,来说明递归的实现。

阶乘是小于或等于某个数 n 的所有正整数的乘积,表示为 n!。例如,4! = 4 × 3 × 2 × 1 = 24。它可以通过从 1n 的循环来计算,但也可以通过递归来解决。n! 等于 (n-1)! × n,因此可以通过递归构造函数来实现:

"use strict";

function factorial(n) {
  if (n > 0) {
    const ret = n * factorial(n - 1);
    return ret;
  } else {
    // n = 0;  0! 为 1
    return 1;
  }
}

const n = 4;
alert(factorial(n));

只要 n > 0,脚本就会再次调用 factorial,但这次传入的参数是 n-1。因此,参数最终会收敛到 0。当 0 被传递时,factorial 函数第一次不再被调用,而是返回 1。然后,这个值将与前一次调用 factorial 的返回值相乘,乘积将返回给前一次调用的 factorial,依此类推。

Last modified: Monday, 13 January 2025, 3:05 PM