ES 函数基础 —— 定义
定义函数的四种方式
使用函数声明语法
function sum(num1, num2) {
return num1 + num2
}
使用函数表达式
const sum = function (num1, num2) {
return num1 + num2
}
使用箭头函数表达式:
const sum = (num1, num2) => {
return num1 + num2
}
箭头函数没有
this
绑定,其this
只能通过静态作用域来确定。
使用 Function 构造函数 (不建议使用)
const sum = new Function('num1', 'num2', 'return num1 + num2')
Function
构造函数可以接受任一数量的参数,最后一个参数被看成是函数体,而前面的参数则枚举出函数的参数。
从技术角度上来讲,这也是一个函数表达式。但是,不推荐使用这种方法定义函数,因为这种方法会导致两次解析:
- 解析常规的 ECMAScript 代码。
- 解析传入构造函数的字符串。
这显然会影响性能。
不过,这种语法对于理解 函数是对象,函数名是指针 的概念倒是挺直观的。
函数名只是一个包含函数引用的变量,它与包含对象引用的其他变量并没有什么不同。所以,一个函数可能会有多个名字,比如:
function sum(num1, num2) {
return num1 + num2
}
sum(10, 10) // 20
const anotherSum = sum
anotherSum(10, 10) // 20
sum = null
anotherSum(10, 10) // 20
函数声明提升
既然不建议 使用 Function 构造函数,就不再介绍它了。
- 函数声明:函数声明具有函数声明提升(Function Declaration Hoisting)这一特性 —— 执行代码前会先读取函数声明,使其在执行任何代码之前可用。
- 函数表达式:函数表达式与其他表达式一样,使用前必须先赋值。
console.log(sum(10, 10))
function sum(num1, num2) {
return num1 + num2
}
以上代码完全可以运行。因为在代码开始执行之前,解析器就已经通过一个名为函数声明提升的过程,读取并将函数声明添加到执行环境中了。
console.log(sum(10, 10)) // ReferenceError: sum is not defined
const sum = function (num1, num2) {
return num1 + num2
}
以上代码在运行期间发生了错误。原因是在调用 sum()
时,sum()
还不存在。
参数
参数的默认值 [ES2015]
function log(x, y = 'World') {
console.log(x, y)
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
通常情况下,会为函数尾部的参数设置默认值,因为这样容易辨识带默认值的参数,也可以将带默认值的参数省略。 如果为非尾部参数设置默认值,这些参数是无法省略的。
// 1
function f(x = 1, y) {
return [x, y]
}
f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 报错
// 2
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
// 如果传入 undefined,将触发该参数等于默认值,null 没有这个效果
f(1, undefined, 2); // [1, 5, 2]
f(1, null, 2); // [1, null, 2]
指定了默认值后,函数的 length
属性将返回没有设置默认值的参数个数。这是因为 length
属性的含义是,该函数预期接受的参数的个数。某个参数被设置默认值后,预期传入的参数中,就不包含这个参数了。 同理,rest 参数也不会被计入 length
属性。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function(...args) {}).length // 0
如果设置了默认值的参数不是尾参数,那么 length
属性不会再计入之后的参数了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
基于以上几点,带有默认值的参数还是放在尾部,这样比较稳妥。
rest 参数 [ES2015]
引入 rest 参数,是为了获取函数的多余参数,这样就不需要使用 arguments
对象了。 rest 参数会被放到一个数组中:
function showArgs(...args) {
return args
}
const args = showArgs(1, 9, 9, 2)
console.log(Array.isArray(args)); // true
console.log(args); // [ 1, 9, 9, 2 ]
注意,rest 参数只能是最后一个参数,否则会报错:
// SyntaxError: Rest parameter must be last formal parameter
function f(a, ...b, c) {
// ...
}
函数的 length
属性,不包括 rest 参数:
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
定义函数时可以使用的内部属性
arguments
ES 函数的参数与大多数语言中函数的参数有所不同。ES 函数不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型。之所以会这样,是因为传入函数中的所有参数都是用一个类数组结构(尽管在语法上它有 Array 相关的属性和方法,但它不是 Array,实际上是一个对象)来保存的。 怎么在函数体内访问这个类数组结构,通过 arguments
。
将
arguments
这个类数组,转化成为真正的数组:Array.prototype.slice.call(arguments)
。这个转化比较慢,在性能不好的代码中不推荐这么做。
function sayHi(name, message) {
console.log('Hello ' + name + ',' + message)
}
function sayHi() {
console.log('Hello ' + arguments[0] + ',' + arguments[1]);
}
这说明了 ES 函数的一个重要特点:定义函数时使用参数,虽然提供了便利,但不是必需的。
arguments.callee
(不建议使用)
-
严格模式中,禁止使用
arguments.callee
-
在非严格模式下,也不推荐使用
arguments.callee
arguments
的主要用途是保存函数参数,但这个对象还有一个 callee
的属性,该属性是一个指针,指向拥有这个 arguments
对象的函数,这个属性对于递归调用很有用处。 递归函数是在一个函数通过名字调用自身时构成的。比如:
function factorial(num) {
if (num <= 1) {
return 1
} else {
return num * factorial(num-1)
}
}
这是一个经典的递归阶乘函数。这个函数看起来没啥问题,但是如果像下面这样使用就会出错:
const anotherFactorial = factorial
factorial = null
console.log(anotherFactorial(4))
以上代码切断了 factorial
变量与函数对象之间的联系,但是函数内部还会调用 factorial
,所以就导致了错误。这个时候,就可以使用 arguments.callee
解决这个问题。 阶乘函数都要用到递归算法。如上面的代码所示,在函数有名字,而且这个名字再也不会变更的情况下,这样定义并没有问题。但是这样的话,函数的执行就和函数名 factorial
耦合在一起了,为了消除这种紧密耦合的现象,就可以使用 arguments.callee
:
function factorial(num) {
if (num <=1 ) {
return 1
} else {
return num * arguments.callee(num-1)
}
}
重写后的 factorial()
函数的函数体内,没有再引用函数名 factorial
。这样,无论引用函数的时候使用什么名字,都可以保证正常完成递归调用。对比以下两段代码就能明白:
function factorial(num) {
if (num <=1 ) {
return 1
} else {
return num * arguments.callee(num-1)
}
}
const trueFactorial = factorial
factorial = function () {
return 0
}
console.log(trueFactorial(5)) // 120
console.log(factorial(5)) // 0
function factorial(num) {
if (num <=1 ) {
return 1;
} else {
return num * factorial(num-1);
}
}
const trueFactorial = factorial
factorial = function () {
return 0;
}
console.log(trueFactorial(5)); // 0
console.log(factorial(5)); // 0
可是,在严格模式和非严格模式,都不推荐使用 arguments.callee
,这要怎么办?使用函数表达式并对其进行命名。
const factorial = function f(num) {
if (num <=1) {
return 1
} else {
return num * f(num-1)
}
}
上面的代码创建了一个名为 f
的命名函数表达式,然后将它赋值给变量 factorial
。即使把函数赋值给了另一个变量,函数的名字 f
仍然有效,所以递归调用照样能正确完成。这种方法在严格模式和非严格模式下都能使用。
arguments.caller
(不建议使用)
严格模式下,禁止使用
arguments.caller
。
arguments.caller
保存着调用当前函数的函数的引用。如果在全局作用域中调用当前函数,它的值为 null。
function outer() {
inner()
}
function inner() {
console.log(inner.caller)
}
outer() // [Function: outer]
因为 outer()
调用了 inter()
,所以 inner.caller
就指向了 outer()
。为了降低耦合程度,可以通过 arguments.callee.caller
替换 inner.caller
:
function outer() {
inner()
}
function inner() {
console.log(arguments.callee.caller)
}
outer()
函数返回值与 return
-
位于
return
语句之后的代码永远不会执行 -
return
语句不带任何返回值时,会返回 =undefined=
推荐的做法是要么让函数始终都返回一个值,要么永远都不要返回值。否则,如果函数有时候返回值,有时候有不返回值,会给调试代码带来不便。
严格模式对定义函数的限制
-
不能把函数命名为
eval
或arguments
-
不能把参数命令为
eval
或arguments
- 不能出现两个命名参数同名的情况