1.4.x 函数

定义一个函数并不会执行它,只是命名了该函数并指定了调用它时要执行的操作。

函数总会返回一个值,通常由 return 语句指定。当没有 return 语句时,就会返回默认值。对于使用 new 关键字调用的构造函数,返回的默认值是 this。对于其它所有情况,返回的默认值都是 undefined。

函数调用的参数(parameters)是函数的参数(arguments),可以是值(原始值),也可以是引用(对象)。

2. 调用函数

调用函数实际上就是用指定的参数去执行指定的操作。

在调用函数的时候,它必须在作用域内,而函数声明会被提升,如下:

console.log(square(5));
// 声明在调用之后
function square(n) {
  return n * n;
}

注:函数提升只适用于“函数声明”,并不适用于“函数表达式”定义的函数。

函数作用域就是声明它的函数。如果它是在顶层声明的,那它的作用域就是整个程序。

  1. 正常调用、递归调用(相关概念函数作用域,function scope)

    1. 函数声明(函数提升,function hoisting)

    2. 函数表达式

  2. 其他调用方法,比如在某些情况下需要:

    1. 动态调用函数

    2. 函数的参数数量不同

    3. 将函数调用的上下文设置为在运行时确定的特定对象

函数本身就是对象,相反,这些对象也有方法(函数),其中 apply() 方法可用于实现此目标。

functions are themselves objects

3. 函数作用域

在函数内定义的变量,在函数外是不能访问的。但是反过来,函数可以访问“在其定义范围内”定义的所有变量和函数。

A function can access all variables and functions (defined inside the scope) (in which it is defined).

  • 在全局范围内定义的函数,可以访问在全局范围内定义的所有变量

  • 在另一个函数中定义的函数,可以访问其父函数中定义的所有变量,以及父函数可以访问的任何其他变量

4. 作用域和函数栈

4.1 递归函数

函数体里的 this 关键字,并不指向当前正在执行的函数,所以必须按名称引用 Function 对象。

一个函数可以引用和调用它自己,有三种方式:

  1. 函数的名字

  2. arguments.callee

  3. 指向函数的变量

const foo = function bar() {
  // 以下三种方式是等价的
  bar();
  arguments.callee();
  foo();
};

递归函数,比如:

function walkTree(node) {
  if (node === null) {
    return;
  }
  // do something with node
  for (let i = 0; i < node.childNodes.length; i++) {
    walkTree(node.childNodes[i]);
  }
}

递归本身使用了一个栈:函数栈。

4.2 嵌套函数和闭包

我们可以将一个函数嵌套在另一个函数中。对于外部函数来说,内部的嵌套函数是私有的。

the inner function, the nested function the outer function, the containing function

嵌套函数会形成一个闭包,闭包是一个自带执行上下文(自由变量以及绑定这些变量的环境)的表达式(最常见的是函数)。 因为内部的嵌套函数是闭包,这就意味着(内部的)嵌套函数可以“继承”(外部的)包含函数的参数和变量,换句话说就是内部函数包含外部函数的作用域(scope)。

总结一下:

  1. 内部函数只能通过外部函数的语句访问

  2. 内部函数形成一个闭包:内部函数可以使用外部函数的参数和变量,而外部函数不能使用内部函数的参数和变量

嵌套函数,比如:

function addSquares(a, b) {
  function square(x) {
    return x * x;
  }
  return square(a) + square(b);
}
const a = addSquares(2, 3); // returns 13
const b = addSquares(3, 4); // returns 25
const c = addSquares(4, 5); // returns 41

嵌套函数,比如:

function outside(x) {
  function inside(y) {
    return x + y;
  }
  return inside;
}
const fnInside = outside(3); // Think of it like: give me a function that adds 3 to whatever you give it
const result1 = fnInside(5); // returns 8
const result2 = outside(3)(5); // returns 8

4.3 保留变量

闭包必须保留它引用的所有参数和变量,比如上面例子中在返回 inside 的时候,是会保留 x 的。 考虑到每次调用闭包的参数可能会不同,所以每调用一次 outside 都会创建一个新的闭包。 只有当返回的闭包 inside 不再可访问时,它的内存才会被释放。

闭包的这种工作方式和在其它对象中存储引用没什么不同,只是它的存储方式更不明显些,因为闭包没有直接设置引用也没法查看引用。

4.4 多重嵌套函数

函数可以多重嵌套。比如:

function A(x) {
  function B(y) {
    function C(z) {
      console.log(x + y + z);
    }
    C(3);
  }
  B(2);
}
A(1); // logs 6 (1 + 2 + 3)
  • 函数 A 包含函数 B,函数 B 包含函数 C

  • 此时,函数 B 和函数 C 都形成了闭包

    • B 形成了一个包含 A 的闭包,也就是说 B 可以访问 A 的参数和变量

    • C 形成了一个包含 B 的闭包,也就是说 C 可以访问 B 的参数和变量

  • → C 可以访问 B 和 A 的参数和变量

我们可以看到,闭包是可以包含多个作用域(scope)的,它们递归地包含“包含它的函数”的作用域,这称为作用域链(scope chaining)。

4.5 命名冲突

当闭包作用域内的两个参数或变量具有相同的名称时,就会发生命名冲突。 嵌套的越深,优先级就越高。 也就是说,最内部的作用域具有最高优先级,最外部的作用域具有最低优先级,这就是作用域链(the scope chain)。 作用域链上的第一个是最内层的作用域,最后一个是最外层的作用域。

比如:

function outside() {
  const x = 5;
  function inside(x) {
    return x * 2;
  }
  return inside;
}

outside()(10); // 返回 20

命名冲突发生在语句 return x * 2,此时的变量 x 到底是 inside 的参数 x 还是 outside 的变量 x。 分析:此时的作用域链是 {inside, outside, global object},所以 insidex=10outsidex=5 的优先级高。 所以是返回 20。

主要参考

Last updated