函数
Baseline 广泛可用 *
通常来说,函数是一种“子程序”,可以由函数外部(或在递归的情况下由函数内部)的代码调用。与程序本身一样,函数由一系列被称为函数体的语句组成。值可以作为参数传递给函数,函数将返回一个值。
在 JavaScript 中,函数是一等对象,因为它们可以作为参数传递给其他函数,从函数中返回,并分配给变量和属性。它们也可以像其他任何对象一样拥有属性和方法。与其他对象的区别在于函数可以被调用。
有关更多示例和解释,请参阅JavaScript 函数指南。
描述
函数值通常是 Function 的实例。有关 Function 对象的属性和方法的信息,请参阅 Function。可调用值会导致 typeof 返回 "function" 而不是 "object"。
注意:并非所有可调用值都是 instanceof Function。例如,Function.prototype 对象是可调用的,但不是 Function 的实例。你还可以手动设置函数的原型链,使其不再继承自 Function.prototype。然而,这种情况极其罕见。
返回值
默认情况下,如果函数的执行没有以 return 语句结束,或者 return 关键字后没有表达式,则返回值为 undefined。return 语句允许你从函数中返回任意值。一次函数调用只能返回一个值,但你可以通过返回一个对象或数组并解构结果来模拟返回多个值的效果。
注意:使用 new 调用的构造函数有一套不同的逻辑来确定它们的返回值。
传递参数
参数 (Parameters) 和实参 (arguments) 的含义略有不同,但在 MDN web docs 中,我们经常互换使用它们。快速参考:
function formatNumber(num) {
return num.toFixed(2);
}
formatNumber(2);
在这个例子中,num 变量被称为函数的形参:它在函数定义中的圆括号列表中声明。函数期望 num 形参是一个数字——尽管在 JavaScript 中,如果不编写运行时验证代码,这是无法强制执行的。在 formatNumber(2) 调用中,数字 2 是函数的实参:它是函数调用中实际传递给函数的值。实参值可以通过相应的形参名称或 arguments 对象在函数体内部访问。
实参总是按值传递,从不按引用传递。这意味着如果函数重新分配一个形参,其值在函数外部不会改变。更准确地说,对象实参是按共享传递,这意味着如果对象的属性被修改,这种改变会影响函数外部。例如:
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对象;可以同时使用await和yield运算符
对于每种函数,都有多种定义方式:
此外,还有定义箭头函数和方法的特殊语法,它们为使用提供了更精确的语义。类在概念上不是函数(因为它们在没有 new 的情况下调用时会抛出错误),但它们也继承自 Function.prototype 并且 typeof MyClass === "function"。
// 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()构造函数总是创建匿名函数,这意味着它们不能轻易地递归调用自身。递归调用箭头函数的一种方法是将其赋值给一个变量。 - 箭头函数语法无法访问
arguments或this。 Function()构造函数无法访问任何局部变量——它只能访问全局作用域。Function()构造函数会导致运行时编译,并且通常比其他语法慢。
对于 function 表达式,函数名与函数被赋值的变量之间存在区别。函数名不能更改,而函数被赋值的变量可以被重新赋值。函数名可以与函数被赋值的变量不同——它们之间没有关系。函数名只能在函数体内部使用。尝试在函数体外部使用它会导致错误(或者如果其他地方声明了同名变量,则获取另一个值)。例如:
const y = function x() {};
console.log(x); // ReferenceError: x is not defined
另一方面,函数被赋值的变量仅受其作用域限制,该作用域保证包含函数声明的作用域。
函数声明还会创建一个与函数名同名的变量。因此,与通过函数表达式定义的函数不同,通过函数声明定义的函数可以通过其名称在其定义的作用域中以及在其自身体内访问。
通过 new Function 定义的函数会动态地组装其源代码,这在将其序列化时是可见的。例如,console.log(new Function().toString()) 会输出:
function anonymous(
) {
}
这是用于编译函数的实际源代码。然而,尽管 Function() 构造函数会创建名为 anonymous 的函数,但这个名称并没有添加到函数体的作用域中。函数体只能够访问全局变量。例如,下面的代码会导致错误:
new Function("alert(anonymous);")();
通过函数表达式或函数声明定义的函数继承当前作用域。也就是说,函数形成一个闭包。另一方面,通过 Function 构造函数定义的函数不继承任何除全局作用域以外的作用域(所有函数都继承全局作用域)。
// 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 构造函数。
当函数声明出现在表达式上下文中时,可能会无意中变为函数表达式。
// A function declaration
function foo() {
console.log("FOO!");
}
doSomething(
// A function expression passed as an argument
function foo() {
console.log("FOO!");
},
);
另一方面,函数表达式也可能变成函数声明。一个表达式语句不能以 function 或 async function 关键字开头,这是实现 IIFE(立即执行函数表达式)时常见的错误。
function () { // SyntaxError: Function statements require a function name
console.log("FOO!");
}();
function foo() {
console.log("FOO!");
}(); // SyntaxError: Unexpected token ')'
相反,请以其他方式开始表达式语句,以便 function 关键字明确地开始一个函数表达式。常见的选项包括分组和使用 void。
(function () {
console.log("FOO!");
})();
void function () {
console.log("FOO!");
}();
函数参数
每个函数参数都是一个简单的标识符,你可以在局部作用域中访问它。
function myFunc(a, b, c) {
// You can access the values of a, b, and c here
}
有三种特殊的参数语法:
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。
请注意,这些语法创建的是一个对象属性,而不是一个方法。getter 和 setter 函数本身只能通过反射 API(如 Object.getOwnPropertyDescriptor())进行访问。
块级函数
在严格模式下,块内的函数作用域是该块。在 ES2015 之前,严格模式下禁止块级函数。
"use strict";
function f() {
return 1;
}
{
function f() {
return 2;
}
}
f() === 1; // true
// f() === 2 in non-strict mode
非严格代码中的块级函数
一句话:不要使用。
在非严格代码中,块内的函数声明行为怪异。例如:
if (shouldDefineZero) {
function zero() {
// DANGER: compatibility risk
console.log("This is zero.");
}
}
这在严格模式下的语义是明确的——zero 仅存在于 if 块的作用域内。如果 shouldDefineZero 为假,则 zero 永远不应被定义,因为该块从不执行。然而,历史上,这并未明确指定,因此不同浏览器在非严格模式下的实现方式不同。有关更多信息,请参阅function 声明参考。
一种更安全地有条件地定义函数的方法是将函数表达式赋值给一个变量:
// 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.");
};
}
示例
返回格式化数字
以下函数返回一个字符串,其中包含一个用前导零填充的数字的格式化表示。
// 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 函数。
let result;
result = padZeros(42, 4); // returns "0042"
result = padZeros(42, 2); // returns "42"
result = padZeros(5, 4); // returns "0005"
确定函数是否存在
你可以使用 typeof 运算符来确定函数是否存在。在以下示例中,执行测试以确定 window 对象是否有一个名为 noFunc 的属性是一个函数。如果是,则使用它;否则,采取其他操作。
if (typeof window.noFunc === "function") {
// use noFunc()
} else {
// do something else
}
请注意,在 if 测试中,使用的是对 noFunc 的引用——函数名后没有括号 (),因此不会实际调用函数。
规范
| 规范 |
|---|
| ECMAScript® 2026 语言规范 # sec-function-definitions |
浏览器兼容性
加载中…