JavaScript/函数
函数是解决特定问题的代码块,并将解决方案返回给调用语句。函数存在于它自己的上下文中。因此,函数将庞大的程序分解成更小的“砖块”,这些砖块构建了软件及其开发过程。
// define a function
function <function_name> (<parameters>) {
<function_body>
}
// call a function
<variable> = <function_name> (<arguments>);
JavaScript 支持软件开发范式 函数式编程。函数是派生自对象的 数据类型;它们可以绑定到变量,作为参数传递,并从其他函数返回,就像任何其他数据类型一样。
函数可以通过三种主要方式构造。第一种版本可以进一步简化;见下文。
传统方式
"use strict";
// conventional declaration (or 'definition')
function duplication(p) {
return p + "! " + p + "!";
}
// call the function
const ret = duplication("Go");
alert(ret);
通过变量和表达式进行构造
"use strict";
// assign the function to a variable
let duplication = function (p) {
return p + "! " + p + "!";
};
const ret = duplication("Go");
alert(ret);
通过new
操作符进行构造(此版本有点繁琐)
"use strict";
// using the 'new' constructor
let duplication = new Function ("p",
"return p + '! ' + p + '!'");
const ret = duplication("Go");
alert(ret);
对于函数的声明,我们已经看到了 3 种变体。对于它们的调用,也有 3 种变体。声明和调用是相互独立的,您可以随意组合它们。
传统的调用变体使用函数名后跟圆括号( )
。圆括号内是函数的参数(如果存在)。
"use strict";
function duplication(p) {
return p + "! " + p + "!";
}
// the conventional invocation method
const ret = duplication("Go");
alert(ret);
如果脚本在浏览器中运行,还有两种可能性。它们使用浏览器提供的window
对象。
"use strict";
function duplication(p) {
return p + "! " + p + "!";
}
// via 'call'
let ret = duplication.call(window, "Go");
alert(ret);
// via 'apply'
ret = duplication.apply(window, ["Go"]);
alert(ret);
提示:如果您使用函数名而不带圆括号()
,您将收到函数本身(脚本),而不是任何调用结果。
"use strict";
function duplication(p) {
return p + "! " + p + "!";
}
alert(duplication); // 'function duplication (p) { ... }'
函数会受到“提升”的影响。此机制会自动将函数的声明转移到其作用域的顶部。因此,您可以从源代码中比其声明更上层的位置调用函数。
"use strict";
// use a function above (in source code) its declaration
const ret = duplication("Go");
alert(ret);
function duplication(p) {
return p + "! " + p + "!";
}
到目前为止,我们已经看到了声明和调用这两个独立的步骤。还有一种语法变体允许将两者组合起来。它的特点是在函数声明周围使用圆括号,后面跟着()
来调用该声明。
"use strict";
alert( // 'alert' to show the result
// declaration plus invocation
(function (p) {
return p + "! " + p + "!";
})("Go") // ("Go"): invocation with the argument "Go"
);
alert(
// the same with 'arrow' syntax
((p) => {
return p + "! " + p + "!";
})("Gooo")
);
此语法被称为 立即调用函数表达式 (IIFE)。
当函数被调用时,声明阶段的参数被调用时的参数替换。在上面的声明中,我们使用变量名p
作为参数名。当调用函数时,我们大多使用字面量“Go”作为参数。在运行时,它会替换函数中所有p
的出现。
此类替换是 '按值' 而不是'按引用'完成的。参数原始值的副本被传递给函数。如果此复制的值在函数内被改变,则函数外部的原始值不会改变。
"use strict";
// there is one parameter 'p'
function duplication(p) {
// In this example, we change the parameter's value
p = "NoGo";
alert("In function: " + p);
return p + "! " + p + "!";
};
let x = "Go";
const ret = duplication(x);
// is the modification of the argument done in the function visible here? No.
alert("Return value: " + ret + " Variable: " + x);
对于对象(所有非基本数据类型),这种“按值调用”可能会产生惊人的效果。如果函数修改了对象的属性,则此更改在外部也能看到。
"use strict";
function duplication(p) {
p.a = 2; // change the property's value
p.b = 'xyz'; // add a property
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);
// is the modification of the argument done in the function visible here? Yes.
alert("Return value: " + ret + " Object: " + JSON.stringify(x));
当示例运行时,它显示在调用duplication
之后,函数所做的更改不仅在返回值中可见。此外,原始对象x
的属性也已改变。为什么会这样?它与基本数据类型的行为不同吗?不。
函数接收对该对象的引用的副本。因此,在函数内部,引用的是同一个对象。该对象本身只存在一次,但有两个(相同的)对该对象的引用。使用哪个引用修改对象的属性并没有区别。
另一个结果是(这可能与基本数据类型直观地相同?)对引用本身的修改(例如,通过创建新的对象)在外部例程中不可见。对新对象的引用存储在原始引用的副本中。现在我们不仅有两个(值不同的)引用,而且还有两个对象。
"use strict";
function duplication(p) {
// modify the reference by creating a new object
p = {};
p.a = 2; // change the property's value
p.b = 'xyz'; // add a property
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);
// is the modification of the argument done in the function visible here? No.
alert("Return value: " + ret + " Object: " + JSON.stringify(x));
注释 1:这种参数传递技术的名字在不同语言中并不一致。有时它被称为“按共享调用”。维基百科 对此进行了概述。
注释 2:JavaScript 参数传递带来的描述后果与使用关键字const
带来的后果类似,const
声明一个变量为常量。此类变量无法改变。然而,如果它们引用了一个对象,那么该对象的属性可以被改变。
如果函数被调用的参数少于它包含的参数,那么多余的参数将保持未定义。但是您可以通过在函数签名中分配一个值来为这种情况定义默认值。缺少的参数将获得这些值作为其默认值。
"use strict";
// two nearly identical functions; only the signature is slightly different
function f1(a, b) {
alert("The second parameter is: " + b)
};
function f2(a, b = 10) {
alert("The second parameter is: " + b)
};
// identical invocations; different results
f1(5); // undefined
f1(5, 100); // 100
f2(5); // 10
f2(5, 100); // 100
对于一些函数来说,它们接收不同数量的参数是“正常的”。例如,考虑一个显示姓名的函数。firstName
和familyName
必须始终提供,但也有可能需要显示academicTitle
或titleOfNobility
。JavaScript 提供了不同的方法来处理这种情况。
可以检查“正常”参数和附加参数,以确定它们是否包含值。
"use strict";
function showName(firstName, familyName, academicTitle, titleOfNobility) {
"use strict";
// handle required parameters
let ret = "";
if (!firstName || !familyName) {
return "first name and family name must be specified";
}
ret = firstName + ", " + familyName;
// handle optional parameters
if (academicTitle) {
ret = ret + ", " + academicTitle;
}
if (titleOfNobility) {
ret = ret + ", " + titleOfNobility;
}
return ret;
}
alert(showName("Mike", "Spencer", "Ph.D."));
alert(showName("Tom"));
每个可能没有提供的参数都必须单独检查。
如果可选参数的处理在结构上是相同的,则可以使用剩余运算符语法来简化代码——通常与循环结合使用。该功能的语法在函数签名中由三个点组成——就像在展开语法中一样。
它是如何运作的?作为函数调用的一部分,JavaScript 引擎会将给定的可选参数合并到一个数组中。(请注意,调用脚本并不会使用数组。)这个数组作为最后一个参数传递给函数。
"use strict";
// the three dots (...) introduces the 'rest syntax'
function showName(firstName, familyName, ...titles) {
// handle required parameters
let ret = "";
if (!firstName || !familyName) {
return "first name and family name must be specified";
}
ret = firstName + ", " + familyName;
// handle optional parameters
for (const title of titles) {
ret = ret + ", " + title;
}
return ret;
}
alert(showName("Mike", "Spencer", "Ph.D.", "Duke"));
alert(showName("Tom"));
调用中的第三个参数以及所有后续参数将被收集到一个数组中,该数组可在函数中作为最后一个参数使用。这允许使用循环并简化函数的源代码。
与 C 语言家族的其他成员一致,JavaScript 在函数中提供了 arguments
关键字。它是一个类似数组的对象,包含函数调用中给定的所有参数。您可以循环遍历它或使用它的 length
属性。
它的功能与上述的 *剩余语法* 相当。主要区别在于 arguments
包含 **所有** 参数,而 *剩余语法* 不一定影响所有参数。
"use strict";
function showName(firstName, familyName, academicTitles, titlesOfNobility) {
// handle ALL parameters with a single keyword
for (const arg of arguments) {
alert(arg);
}
}
showName("Mike", "Spencer", "Ph.D.", "Duke");
函数的目的是提供针对特定问题的解决方案。这个解决方案通过 return
语句返回给调用程序。
它的语法是 return <表达式>
,其中 <表达式>
是可选的。
函数运行直到它遇到这样的 return
语句(或发生未捕获的异常,或在最后一个语句之后)。<表达式>
可以是一个简单变量,可以是任何数据类型,例如 return 5
,也可以是一个复杂表达式,例如 return myString.length
,也可以完全省略:return
。
如果 return
语句中没有 <表达式>
,或者根本没有遇到 return
语句,则返回 undefined
。
"use strict";
function duplication(p) {
if (typeof p === 'object') {
return; // return value: 'undefined'
}
else if (typeof p === 'string') {
return p + "! " + p + "!";
}
// implicit return with 'undefined'
}
let arg = ["Go", 4, {a: 1}];
for (let i = 0; i < arg.length; i++) {
const ret = duplication(arg[i]);
alert(ret);
}
箭头函数是上面展示的传统函数语法的紧凑替代方案。它们缩写了一些语言元素,省略了其他元素,并且与原始语法相比,只有几个 语义区别。
它们始终是匿名函数,但可以被赋值给变量。
"use strict";
// original conventional syntax
function duplication(p) {
return p + "! " + p + "!";
}
// 1. remove keyword 'function' and function name
// 2. introduce '=>' instead
// 3. remove 'return'; the last value is automatically returned
(p) => {
p + "! " + p + "!"
}
// remove { }, if only one statement
(p) => p + "! " + p + "!"
// Remove parameter parentheses if it is only one parameter
// -----------------------------
p => p + "! " + p + "!" // that's all!
// -----------------------------
alert(
(p => p + "! " + p + "!")("Go")
);
以下是一个使用数组的例子。forEach
方法循环遍历数组,并依次生成一个数组元素。这被传递给箭头函数的单个参数 e
。箭头函数会显示 e
和简短文本。
"use strict";
const myArray = ['a', 'b', 'c'];
myArray.forEach(e => alert("The element of the array is: " + e));
其他编程语言在匿名函数或 lambda 表达式等术语下提供了箭头函数的概念。
函数可以调用其他函数。在实际应用中,这种情况很常见。
当函数调用自身时,就会出现特殊情况。这被称为 *递归调用*。当然,这意味着存在无限循环的风险。您必须更改参数以避免这种情况。
通常,当应用程序处理树状结构时,例如 物料清单、DOM 树 或 家谱信息 时,就会出现对这种递归调用的需求。这里,我们介绍了易于实现的数学问题 阶乘 计算。
阶乘是小于或等于某个数字 的所有正整数的乘积,写成 。例如, 。它可以通过从 到 的循环来解决,但它也存在递归解决方案。 的阶乘是已经计算过的 的阶乘乘以 ,或者用公式表示为: 。这种想法导致了相应的函数递归构造
"use strict";
function factorial(n) {
if (n > 0) {
const ret = n * factorial(n-1);
return ret;
} else {
// n = 0; 0! is 1
return 1;
}
}
const n = 4;
alert(factorial(n));
只要 大于 ,脚本会再次调用 factorial
函数,但这次传入的参数是 。因此,参数会逐渐收敛到 。当达到 时,这是 factorial
函数第一次不再被调用。它返回 的值。这个数字乘以之前 factorial
函数调用时传递的下一个较大的数字。乘法的结果将返回给之前调用 factorial
函数的函数......