ES 函数基础 —— 定义

published

定义函数的四种方式

使用函数声明语法

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 构造函数可以接受任一数量的参数,最后一个参数被看成是函数体,而前面的参数则枚举出函数的参数。

从技术角度上来讲,这也是一个函数表达式。但是,不推荐使用这种方法定义函数,因为这种方法会导致两次解析:

  1. 解析常规的 ECMAScript 代码。
  2. 解析传入构造函数的字符串。

这显然会影响性能。

不过,这种语法对于理解 函数是对象,函数名是指针 的概念倒是挺直观的。

函数名只是一个包含函数引用的变量,它与包含对象引用的其他变量并没有什么不同。所以,一个函数可能会有多个名字,比如:

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=

推荐的做法是要么让函数始终都返回一个值,要么永远都不要返回值。否则,如果函数有时候返回值,有时候有不返回值,会给调试代码带来不便。

严格模式对定义函数的限制

  • 不能把函数命名为 evalarguments
  • 不能把参数命令为 evalarguments
  • 不能出现两个命名参数同名的情况