函数签名(或类型签名或方法签名)定义函数或方法的输入和输出,签名可以包括:
- 函数或方法的名称及作用对象
- 参数及其类型
- 返回值和类型
- 可能抛出或传回的异常
- 有关面向对象程序中方法的可用性的信息
函数定义
广义上来说,JavaScript 有四种函数:
- 常规函数(Regular function):可以返回任何内容;调用后始终运行至完成。
// 函数声明
function name(
param0, param1, /* …, */ paramN
) { statements }
// 函数表达式
const myFunction = function [name] (
param0, param1, /* …, */ paramN
) { statements }
// 箭头函数表达式
const myFunction = (
param0, param1, /* …, */ paramN
) => { statements }
// 立即调用函数表达式
(function [name] (
param0, param1, /* …, */ paramN
) { statements })(
param0, param1, /* …, */ paramN
)
// 构造函数
const myFunction = [new] Function(
arg0, arg1, /* …, */ argN, functionBody
)
- 生成器函数(Generator function):返回一个
Generator
对象;可以使用yield
运算符暂停和恢复。
// 函数声明
function* name (
param0, param1, /* …, */ paramN
) { statements }
// 函数表达式
const myFunction = function* [name] (
param0, param1, /* …, */ paramN
) { statements }
// 构造函数
const GeneratorFunction = function* () {}.constructor;
const myFunction = [new] GeneratorFunction (
arg1, arg2, ... argN, functionBody
)
- 异步函数(Async function):返回一个
Promise
;可以使用await
运算符暂停和恢复。
// 函数声明
async function name (
param0, param1, /* …, */ paramN
) { statements }
// 函数表达式
const myFunction = async function [name] (
param0, param1, /* …, */ paramN
) { statements }
// 构造函数
const AsyncFunction = async function () {}.constructor
const myFunction = [new] AsyncFunction(
arg0, arg1, /* …, */ argN, functionBody
)
- 异步生成器函数(Async generator function):返回一个
AsyncGenerator
对象;await
和yield
运算符都可以使用。
// 函数声明
async function* name (
param0, param1, /* …, */ paramN
) { statements }
// 函数表达式
const myFunction = async function* [name] (
param0, param1, /* …, */ paramN
) { statements }
// 构造函数
const AsyncGeneratorFunction = async function* () {}.constructor
const myFunction = [new] AsyncGeneratorFunction(
arg0, arg1, /* …, */ argN, functionBody
)
对于每种函数,都有三种定义方法:
- 函数声明(Declaration):
function
,function*
,async function
,async function*
function multiply(x, y) {
return x * y
}
- 函数表达式(Expression):
function
,function*
,async function
,async function*
var multiply = function (x, y) {
return x * y
}
- 构造函数(Constructor):
Function()
,GeneratorFunction()
,AsyncFunction()
,AsyncGeneratorFunction()
var multiply = new Function('x', 'y', 'return x * y')
如果一个函数中没有使用 return
语句,则它默认返回 undefined
。要想返回一个特定的值,则函数必须使用 return
语句来指定一个要返回的值。(使用 new
关键字调用一个构造函数除外)。
方法函数
方法函数定义是对象的一个属性,它使用 function
关键字,后跟对象名称和括号。从 ECMAScript 2015 开始,你可以用更短的语法定义自己的方法,类似于 getters
和 setters
。
// Object
const o = {
[Symbol('method')]() {}
}
// funciton
function F () {
}
F.staticMethod = function () {}
F.prototype.method = function () {}
// class
class C {
method() {}
}
块级函数
从 ECMAScript 6 开始,在严格模式下,块里的函数作用域为这个块。ECMAScript 6 之前不建议块级函数在严格模式下使用。
"use strict"
function f() {
return 1
}
{
function f() {
return 2
}
}
console.log(f() === 1) // true
// console.log(f() === 2) // 在非严格模式下
在非严格模式下,块中的函数声明表现奇怪。一句话:不要用。
if (shouldDefineZero) {
function zero() {
// DANGER: 兼容性风险
console.log('This is zero.')
}
}
函数参数
调用函数时,传递给函数的值被称为函数的实参(值传递),对应位置的函数参数名叫作形参。如果实参是一个包含原始值 (数字,字符串,布尔值) 的变量,则就算函数在内部改变了对应形参的值,返回后,该实参变量的值也不会改变。如果实参是一个对象引用,则对应形参会和该实参指向同一个对象。假如函数在内部改变了对应形参的值,返回后,实参指向的对象的值也会改变。
默认参数
函数默认参数允许在没有值或 undefined
被传入时使用默认形参。
function multiply(a, b = 1) {
return a * b
}
multiply(5, 2) // 10
multiply(5) // 5
剩余参数
剩余参数语法允许我们将一个不定数量的参数表示为一个数组。
function add(...values) {
let sum = 0
for (var val of values) {
sum += val
}
return sum
}
add(2, 6, 7) // 15
剩余参数可以被解构,这意味着他们的数据可以被解包到不同的变量中。
function f(...[a, b, c]) {
return a + b + c;
}
f(1, 2, 3) // 6
arguments对象
arguments 是一个对应于传递给函数的参数的类数组对象。
function printArgs() {
console.log(arguments)
}
printArgs('a', 'b', 'c')
// [Arguments] { '0': 'a', '1': 'b', '2': 'c', length: 3 }
arguments
对象不是一个 Array
。它类似于 Array
,但除了 length
属性和索引元素之外没有任何 Array
属性。但是它可以被转换为一个真正的Array:
var args = Array.from(arguments)
var args = [...arguments]
如果调用的参数多于正式声明接受的参数,则可以使用 arguments
对象。这种技术对于可以传递可变数量的参数的函数很有用。使用 arguments.length
来确定传递给函数参数的个数,然后使用 arguments
对象来处理每个参数。要确定函数签名中(输入)参数的数量,请使用 Function.length
属性。
剩余参数和 arguments对象的区别
- 剩余参数只包含那些没有对应形参的实参,而
arguments
对象包含了传给函数的所有实参。 arguments
对象不是一个真正的数组,而剩余参数是真正的Array
实例,也就是说能够在它上面直接使用所有的数组方法,比如 sort,map,forEach或pop。arguments
对象还有一些附加的属性(如callee属性)。
arguments
对象可以与剩余参数、默认参数和解构赋值参数结合使用。
- 在严格模式下,剩余参数、默认参数和解构赋值参数的存在不会改变
arguments
对象的行为,但是在非严格模式下就有所不同了。当非严格模式中的函数没有包含剩余参数、默认参数和解构赋值,那么arguments
对象中的值会跟踪参数的值(反之亦然)。 - 当非严格模式中的函数有包含剩余参数、默认参数和解构赋值,那么
arguments
对象中的值不会跟踪参数的值(反之亦然)。相反,arguments
反映了调用时提供的参数:
调用函数
定义的函数并不会自动执行它,仅仅是赋予函数以名称并明确函数被调用时该做些什么。调用函数才会以给定的参数真正执行这些动作。
函数一定要处于调用它们的作用域中,但是函数的声明可以被提升(出现在调用语句之后)。函数声明的范围是声明它的函数(或者,如果它是在顶层声明的,则为整个程序)之内。
函数可以调用其本身,这被称为递归。递归函数通常用于解决数学问题,比如计算阶乘。
function factorial(n) {
return n === 0 || n === 1 ? 1 : (n * factorial(n - 1))
}
还有其他的方式来调用函数。常见的一些情形是某些地方需要动态调用函数,或者函数的实参数量是变化的,或者调用函数的上下文需要指定为在运行时确定的特定对象。
函数提升仅适用于函数声明,而不适用于函数表达式。
函数作用域
在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的作用域内定义。相对应的,一个函数可以访问定义在其范围内的任何变量和函数。
换言之,定义在全局域中的函数可以访问所有定义在全局域中的变量。在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量。
const num1 = 20
const num2 = 3
const name = 'Chamakh'
function multiply () {
return num1 * num2
}
console.log(multiply()) // 60
function getScore () {
const num1 = 2
const num2 = 3
function add() {
return `${name} 的得分为 ${num1 + num2}`
}
return add()
}
console.log(getScore()) // 'Chamakh 的得分为 5'
作用域和函数栈
一个函数可以指向并调用自身,有三种方法可以达到这个目的:
- 函数名
- arguments.callee
- 作用域内一个指向该函数的变量名
调用自身的函数我们称之为递归函数。在某种意义上说,递归近似于循环。两者都重复执行相同的代码,并且两者都需要一个终止条件(避免无限循环,或者在这种情况下更确切地说是无限递归)。
// 循环
(function loop(x) {
while (x < 10) { x++ }
})(0)
// 递归
function recursion(x) {
if (x >= 10) return
recursion(x + 1)
}
recursion(0)
不过,有些算法并不能简单的用迭代来实现。例如,获取树结构中所有的节点时,使用递归实现要容易得多。
function walkTree(node) {
if (node === null) return
for (let i = 0; i < node.childNodes.length; i++) {
walkTree(node.childNodes[i])
}
}
将递归算法转换为非递归算法是可能的,不过逻辑上通常会更加复杂,而且需要使用栈。事实上,递归本身就使用了栈:函数栈。
嵌套函数和闭包
可以在一个函数里面嵌套另外一个函数,嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包(closure),闭包是可以拥有独立变量以及绑定了这些变量的环境(“封闭”了表达式)的表达式(通常是函数)。既然嵌套函数是一个闭包,就意味着一个嵌套函数可以“继承”容器函数的参数和变量。换句话说,内部函数包含外部函数的作用域。
- 内部函数只可以在外部函数中访问。
- 它可以访问外部函数的参数和变量,但是外部函数却不能使用它的参数和变量。
函数可以被多层嵌套,因此,闭包可以包含多个作用域;它们递归地包含了所有包含它的函数作用域。这个称之为作用域链。
function A(x) {
function B(y) {
function C(z) {
console.log(x + y + z)
}
C(3)
}
B(2)
}
A(1) // 6
B
形成了一个包含A
的闭包(B
可以访问A
的参数和变量)C
形成了一个包含B
的闭包。C
的闭包包含B
,且B
的闭包包含A
,所以C
的闭包也包含A
。这意味着C
同时可以访问B
和A
的参数和变量。换言之,C
用这个顺序链接了B
和A
的作用域。
反过来却不是这样。A
不能访问 C
,因为 A
不能访问 B
中的参数和变量,C
是 B
中的一个变量,所以 C
是 B
私有的。
命名冲突
当同一个闭包作用域下两个参数或者变量同名时,就会产生命名冲突。更近的作用域有更高的优先权,所以最近的优先级最高,最远的优先级最低。这就是作用域链。链的第一个元素就是最里面的作用域,最后一个元素便是最外层的作用域。
function outside() {
const x = 5
function inside(x) {
return x * 2
}
return inside
}
console.log(outside()(10)) // 20(而不是 10)
命名冲突发生在语句 return x * 2
上,inside
的参数 x
和 outside
的变量 x
发生了冲突。这里的作用链域是 {inside
、outside
、全局对象}。因此 inside
的 x
优先于 outside
的 x
,因此返回 20
(inside
的 x
)而不是 10
(outside
的 x
)。