函数

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

在 JavaScript 中,函数是一等对象,因为它们可以传递给其他函数、从函数返回以及分配给变量和属性。它们还可以像任何其他对象一样拥有属性和方法。将它们与其他对象区分开来的因素是函数可以被调用。

有关更多示例和说明,请参阅关于函数的 JavaScript 指南

描述

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

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

返回值

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

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

传递参数

参数和实参 的含义略有不同,但在 MDN 网页文档中,我们经常互换使用它们。供快速参考

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 运算符

对于每种类型的函数,都有多种定义方式

声明

functionfunction*async functionasync function*

表达式

functionfunction*async functionasync 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为 false,则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 语言规范
# sec-function-definitions

浏览器兼容性

BCD 表格仅在浏览器中加载

另请参阅