函数

Baseline 广泛可用 *

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2015 年 7 月⁩以来,各浏览器均已提供此特性。

* 此特性的某些部分可能存在不同级别的支持。

通常来说,函数是一种“子程序”,可以由函数外部(或在递归的情况下由函数内部)的代码调用。与程序本身一样,函数由一系列被称为函数体的语句组成。值可以作为参数传递给函数,函数将返回一个值。

在 JavaScript 中,函数是一等对象,因为它们可以作为参数传递给其他函数,从函数中返回,并分配给变量和属性。它们也可以像其他任何对象一样拥有属性和方法。与其他对象的区别在于函数可以被调用。

有关更多示例和解释,请参阅JavaScript 函数指南

描述

函数值通常是 Function 的实例。有关 Function 对象的属性和方法的信息,请参阅 Function。可调用值会导致 typeof 返回 "function" 而不是 "object"

注意:并非所有可调用值都是 instanceof Function。例如,Function.prototype 对象是可调用的,但不是 Function 的实例。你还可以手动设置函数的原型链,使其不再继承自 Function.prototype。然而,这种情况极其罕见。

返回值

默认情况下,如果函数的执行没有以 return 语句结束,或者 return 关键字后没有表达式,则返回值为 undefinedreturn 语句允许你从函数中返回任意值。一次函数调用只能返回一个值,但你可以通过返回一个对象或数组并解构结果来模拟返回多个值的效果。

注意:使用 new 调用的构造函数有一套不同的逻辑来确定它们的返回值。

传递参数

参数 (Parameters) 和实参 (arguments) 的含义略有不同,但在 MDN web docs 中,我们经常互换使用它们。快速参考:

js
function formatNumber(num) {
  return num.toFixed(2);
}

formatNumber(2);

在这个例子中,num 变量被称为函数的形参:它在函数定义中的圆括号列表中声明。函数期望 num 形参是一个数字——尽管在 JavaScript 中,如果不编写运行时验证代码,这是无法强制执行的。在 formatNumber(2) 调用中,数字 2 是函数的实参:它是函数调用中实际传递给函数的值。实参值可以通过相应的形参名称或 arguments 对象在函数体内部访问。

实参总是按值传递,从不按引用传递。这意味着如果函数重新分配一个形参,其值在函数外部不会改变。更准确地说,对象实参是按共享传递,这意味着如果对象的属性被修改,这种改变会影响函数外部。例如:

js
function updateBrand(obj) {
  // Mutating the object is visible outside the function
  obj.brand = "Toyota";
  // Try to reassign the parameter, but this won't affect
  // the variable's value outside the function
  obj = null;
}

const car = {
  brand: "Honda",
  model: "Accord",
  year: 1998,
};

console.log(car.brand); // Honda

// Pass object reference to the function
updateBrand(car);

// updateBrand mutates car
console.log(car.brand); // Toyota

this 关键字指向函数被访问的对象——它不指向当前执行的函数,因此你必须通过名称来引用函数值,即使在函数体内部也是如此。

定义函数

广义地说,JavaScript 有四种函数:

  • 普通函数:可以返回任何值;调用后总是运行到完成
  • 生成器函数:返回一个 Generator 对象;可以使用 yield 运算符暂停和恢复
  • 异步函数:返回一个 Promise;可以使用 await 运算符暂停和恢复
  • 异步生成器函数:返回一个 AsyncGenerator 对象;可以同时使用 awaityield 运算符

对于每种函数,都有多种定义方式:

声明

function, function*, async function, async function*

表达式

function, function*, async function, async function*

构造函数

Function(), GeneratorFunction(), AsyncFunction(), AsyncGeneratorFunction()

此外,还有定义箭头函数方法的特殊语法,它们为使用提供了更精确的语义。在概念上不是函数(因为它们在没有 new 的情况下调用时会抛出错误),但它们也继承自 Function.prototype 并且 typeof MyClass === "function"

js
// Constructor
const multiply = new Function("x", "y", "return x * y");

// Declaration
function multiply(x, y) {
  return x * y;
} // No need for semicolon here

// Expression; the function is anonymous but assigned to a variable
const multiply = function (x, y) {
  return x * y;
};
// Expression; the function has its own name
const multiply = function funcName(x, y) {
  return x * y;
};

// Arrow function
const multiply = (x, y) => x * y;

// Method
const obj = {
  multiply(x, y) {
    return x * y;
  },
};

所有语法的功能大致相同,但存在一些细微的行为差异。

  • Function() 构造函数、function 表达式和 function 声明语法创建的是完整的函数对象,可以使用 new 进行构造。然而,箭头函数和方法不能被构造。异步函数、生成器函数和异步生成器函数无论使用何种语法都不可构造。
  • function 声明创建的函数是提升的。其他语法不提升函数,函数值只在定义后可见。
  • 箭头函数和 Function() 构造函数总是创建匿名函数,这意味着它们不能轻易地递归调用自身。递归调用箭头函数的一种方法是将其赋值给一个变量。
  • 箭头函数语法无法访问 argumentsthis
  • Function() 构造函数无法访问任何局部变量——它只能访问全局作用域。
  • Function() 构造函数会导致运行时编译,并且通常比其他语法慢。

对于 function 表达式,函数名与函数被赋值的变量之间存在区别。函数名不能更改,而函数被赋值的变量可以被重新赋值。函数名可以与函数被赋值的变量不同——它们之间没有关系。函数名只能在函数体内部使用。尝试在函数体外部使用它会导致错误(或者如果其他地方声明了同名变量,则获取另一个值)。例如:

js
const y = function x() {};
console.log(x); // ReferenceError: x is not defined

另一方面,函数被赋值的变量仅受其作用域限制,该作用域保证包含函数声明的作用域。

函数声明还会创建一个与函数名同名的变量。因此,与通过函数表达式定义的函数不同,通过函数声明定义的函数可以通过其名称在其定义的作用域中以及在其自身体内访问。

通过 new Function 定义的函数会动态地组装其源代码,这在将其序列化时是可见的。例如,console.log(new Function().toString()) 会输出:

js
function anonymous(
) {

}

这是用于编译函数的实际源代码。然而,尽管 Function() 构造函数会创建名为 anonymous 的函数,但这个名称并没有添加到函数体的作用域中。函数体只能够访问全局变量。例如,下面的代码会导致错误:

js
new Function("alert(anonymous);")();

通过函数表达式或函数声明定义的函数继承当前作用域。也就是说,函数形成一个闭包。另一方面,通过 Function 构造函数定义的函数不继承任何除全局作用域以外的作用域(所有函数都继承全局作用域)。

js
// p is a global variable
globalThis.p = 5;
function myFunc() {
  // p is a local variable
  const p = 9;

  function decl() {
    console.log(p);
  }
  const expr = function () {
    console.log(p);
  };
  const cons = new Function("\tconsole.log(p);");

  decl();
  expr();
  cons();
}
myFunc();

// Logs:
// 9 (for 'decl' by function declaration (current scope))
// 9 (for 'expr' by function expression (current scope))
// 5 (for 'cons' by Function constructor (global scope))

通过函数表达式和函数声明定义的函数只解析一次,而通过 Function 构造函数定义的函数在每次调用构造函数时都会解析传递给它的字符串。尽管函数表达式每次都会创建一个闭包,但函数体不会被重新解析,因此函数表达式仍然比 new Function(...) 快。因此,应尽可能避免使用 Function 构造函数。

当函数声明出现在表达式上下文中时,可能会无意中变为函数表达式。

js
// A function declaration
function foo() {
  console.log("FOO!");
}

doSomething(
  // A function expression passed as an argument
  function foo() {
    console.log("FOO!");
  },
);

另一方面,函数表达式也可能变成函数声明。一个表达式语句不能以 functionasync function 关键字开头,这是实现 IIFE(立即执行函数表达式)时常见的错误。

js
function () { // SyntaxError: Function statements require a function name
  console.log("FOO!");
}();

function foo() {
  console.log("FOO!");
}(); // SyntaxError: Unexpected token ')'

相反,请以其他方式开始表达式语句,以便 function 关键字明确地开始一个函数表达式。常见的选项包括分组和使用 void

js
(function () {
  console.log("FOO!");
})();

void function () {
  console.log("FOO!");
}();

函数参数

每个函数参数都是一个简单的标识符,你可以在局部作用域中访问它。

js
function myFunc(a, b, c) {
  // You can access the values of a, b, and c here
}

有三种特殊的参数语法:

  • 默认参数允许在没有传递值或传递 undefined 时,用默认值初始化形式参数。
  • 剩余参数允许将不定数量的实参表示为数组。
  • 解构允许将数组中的元素或对象中的属性解包到不同的变量中。
js
function myFunc({ a, b }, c = 1, ...rest) {
  // You can access the values of a, b, c, and rest here
}

如果使用了上述非简单参数语法中的一种,则会产生一些后果:

  • 你不能将 "use strict" 应用于函数体——这会导致语法错误
  • 即使函数不在严格模式下,某些严格模式下的函数特性也适用,包括 arguments 对象停止与命名参数同步,访问 arguments.callee 会抛出错误,并且不允许重复的参数名。

arguments 对象

你可以通过使用 arguments 对象在函数内部引用函数的实参。

arguments

一个类数组对象,包含传递给当前执行函数的实参。

arguments.callee

当前正在执行的函数。

arguments.length

传递给函数的实参数量。

getter 和 setter 函数

你可以为任何支持添加新属性的标准内置对象或用户定义对象定义访问器属性。在对象字面量中,你可以使用特殊语法来定义访问器属性的 getter 和 setter。

get

将对象属性绑定到一个函数,当该属性被查找时,该函数将被调用。

set

将对象属性绑定到一个函数,当试图设置该属性时,该函数将被调用。

请注意,这些语法创建的是一个对象属性,而不是一个方法。getter 和 setter 函数本身只能通过反射 API(如 Object.getOwnPropertyDescriptor())进行访问。

块级函数

严格模式下,块内的函数作用域是该块。在 ES2015 之前,严格模式下禁止块级函数。

js
"use strict";

function f() {
  return 1;
}

{
  function f() {
    return 2;
  }
}

f() === 1; // true

// f() === 2 in non-strict mode

非严格代码中的块级函数

一句话:不要使用。

在非严格代码中,块内的函数声明行为怪异。例如:

js
if (shouldDefineZero) {
  function zero() {
    // DANGER: compatibility risk
    console.log("This is zero.");
  }
}

这在严格模式下的语义是明确的——zero 仅存在于 if 块的作用域内。如果 shouldDefineZero 为假,则 zero 永远不应被定义,因为该块从不执行。然而,历史上,这并未明确指定,因此不同浏览器在非严格模式下的实现方式不同。有关更多信息,请参阅function 声明参考。

一种更安全地有条件地定义函数的方法是将函数表达式赋值给一个变量:

js
// Using a var makes it available as a global variable,
// with closer behavior to a top-level function declaration
var zero;
if (shouldDefineZero) {
  zero = function () {
    console.log("This is zero.");
  };
}

示例

返回格式化数字

以下函数返回一个字符串,其中包含一个用前导零填充的数字的格式化表示。

js
// This function returns a string padded with leading zeros
function padZeros(num, totalLen) {
  let numStr = num.toString(); // Initialize return value as string
  const numZeros = totalLen - numStr.length; // Calculate no. of zeros
  for (let i = 1; i <= numZeros; i++) {
    numStr = `0${numStr}`;
  }
  return numStr;
}

以下语句调用 padZeros 函数。

js
let result;
result = padZeros(42, 4); // returns "0042"
result = padZeros(42, 2); // returns "42"
result = padZeros(5, 4); // returns "0005"

确定函数是否存在

你可以使用 typeof 运算符来确定函数是否存在。在以下示例中,执行测试以确定 window 对象是否有一个名为 noFunc 的属性是一个函数。如果是,则使用它;否则,采取其他操作。

js
if (typeof window.noFunc === "function") {
  // use noFunc()
} else {
  // do something else
}

请注意,在 if 测试中,使用的是对 noFunc 的引用——函数名后没有括号 (),因此不会实际调用函数。

规范

规范
ECMAScript® 2026 语言规范
# sec-function-definitions

浏览器兼容性

另见