函数

函数是 JavaScript 中的基本构建块之一。JavaScript 中的函数类似于过程——一组执行任务或计算值的语句,但要使过程成为函数,它应该接收一些输入并返回一些输出,其中输入和输出之间存在明显的关联。要使用函数,必须在要调用它的作用域中定义它。

有关 JavaScript 函数的详细信息,请参阅 有关 JavaScript 函数的详尽参考章节

定义函数

函数声明

**函数定义**(也称为**函数声明**或**函数语句**)由 function 关键字、其后是

  • 函数的名称。
  • 函数参数的列表,用括号括起来,并用逗号分隔。
  • 定义函数的 JavaScript 语句,用大括号括起来,{ /* … */ }

例如,以下代码定义了一个名为 square 的简单函数

js
function square(number) {
  return number * number;
}

函数 square 接收一个参数,名为 number。该函数包含一条语句,表示返回函数的参数(即 number)乘以自身。 return 语句指定函数返回的值,即 number * number

参数本质上是按**值传递**给函数的——因此,如果函数体内的代码将完全新的值分配给传递给函数的参数,则**更改不会反映在全局范围内,也不会反映在调用该函数的代码中**。

当您将对象作为参数传递时,如果函数更改了对象的属性,则该更改在函数外部可见,如下面的示例所示

js
function myFunc(theObject) {
  theObject.make = "Toyota";
}

const mycar = {
  make: "Honda",
  model: "Accord",
  year: 1998,
};

console.log(mycar.make); // "Honda"
myFunc(mycar);
console.log(mycar.make); // "Toyota"

当您将数组作为参数传递时,如果函数更改了数组的任何值,则该更改在函数外部可见,如下面的示例所示

js
function myFunc(theArr) {
  theArr[0] = 30;
}

const arr = [45];

console.log(arr[0]); // 45
myFunc(arr);
console.log(arr[0]); // 30

函数表达式

虽然上面的函数声明在语法上是一个语句,但函数也可以通过 函数表达式 创建。

这样的函数可以是**匿名**的;它不必有名称。例如,函数 square 可以定义为

js
const square = function (number) {
  return number * number;
};

console.log(square(4)); // 16

但是,函数表达式可以提供一个名称。提供名称允许函数引用自身,并且还可以使调试器堆栈跟踪中更容易识别函数

js
const factorial = function fac(n) {
  return n < 2 ? 1 : n * fac(n - 1);
};

console.log(factorial(3)); // 6

将函数作为参数传递给另一个函数时,函数表达式很方便。以下示例显示了一个 map 函数,它应该接收一个函数作为第一个参数,并将一个数组作为第二个参数

js
function map(f, a) {
  const result = new Array(a.length);
  for (let i = 0; i < a.length; i++) {
    result[i] = f(a[i]);
  }
  return result;
}

在以下代码中,函数接收一个由函数表达式定义的函数,并对接收到的第二个参数(数组)的每个元素执行它

js
function map(f, a) {
  const result = new Array(a.length);
  for (let i = 0; i < a.length; i++) {
    result[i] = f(a[i]);
  }
  return result;
}

const cube = function (x) {
  return x * x * x;
};

const numbers = [0, 1, 2, 5, 10];
console.log(map(cube, numbers)); // [0, 1, 8, 125, 1000]

在 JavaScript 中,可以根据条件定义函数。例如,以下函数定义仅在 num 等于 0 时定义 myFunc

js
let myFunc;
if (num === 0) {
  myFunc = function (theObject) {
    theObject.make = "Toyota";
  };
}

除了按这里描述的方式定义函数之外,您还可以使用 Function 构造函数在运行时从字符串创建函数,这与 eval() 非常类似。

**方法**是对象的属性,它是一个函数。有关对象和方法的更多信息,请阅读 使用对象

调用函数

定义函数不会执行它。定义它会命名函数并指定在调用函数时要执行的操作。

**调用**函数实际上使用指定的参数执行指定的操作。例如,如果您定义了函数 square,您可以按如下方式调用它

js
square(5);

前面的语句使用 5 作为参数调用函数。函数执行其语句并返回 25 值。

函数在被调用时必须在作用域内,但函数声明可以被提升(出现在代码中的调用下方)。函数声明的作用域是声明它的函数(或者如果是在顶层声明的,则是整个程序)。

函数的参数不限于字符串和数字。您可以将整个对象传递给函数。showProps() 函数(在 使用对象 中定义)是接收对象作为参数的函数的示例。

函数可以调用自身。例如,这是一个递归计算阶乘的函数

js
function factorial(n) {
  if (n === 0 || n === 1) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

然后可以按如下方式计算 15 的阶乘

js
console.log(factorial(1)); // 1
console.log(factorial(2)); // 2
console.log(factorial(3)); // 6
console.log(factorial(4)); // 24
console.log(factorial(5)); // 120

还有其他调用函数的方法。通常情况下,函数需要动态调用,或者函数的参数数量不同,或者需要将函数调用的上下文设置为运行时确定的特定对象。

事实证明,函数本身是对象——反过来,这些对象有方法。(参见 Function 对象。) call()apply() 方法可以用来实现这个目标。

函数提升

考虑以下示例

js
console.log(square(5)); // 25

function square(n) {
  return n * n;
}

尽管在声明之前调用了 square() 函数,但这段代码没有错误。这是因为 JavaScript 解释器会将整个函数声明提升到当前作用域的顶部,因此上面的代码等效于

js
// All function declarations are effectively at the top of the scope
function square(n) {
  return n * n;
}

console.log(square(5)); // 25

函数提升仅适用于函数声明——不适用于函数表达式。以下代码将无法正常工作

js
console.log(square(5)); // ReferenceError: Cannot access 'square' before initialization
const square = function (n) {
  return n * n;
};

函数作用域

在函数内部定义的变量无法从函数外部的任何地方访问,因为该变量仅在函数的作用域内定义。但是,函数可以访问在其定义的作用域内定义的所有变量和函数。

换句话说,在全局作用域中定义的函数可以访问在全局作用域中定义的所有变量。在另一个函数内部定义的函数也可以访问其父函数中定义的所有变量,以及父函数可以访问的任何其他变量。

js
// The following variables are defined in the global scope
const num1 = 20;
const num2 = 3;
const name = "Chamakh";

// This function is defined in the global scope
function multiply() {
  return num1 * num2;
}

console.log(multiply()); // 60

// A nested function example
function getScore() {
  const num1 = 2;
  const num2 = 3;

  function add() {
    return `${name} scored ${num1 + num2}`;
  }

  return add();
}

console.log(getScore()); // "Chamakh scored 5"

作用域和函数栈

递归

函数可以引用并调用自身。函数有三种方法可以引用自身

  1. 函数的名称
  2. arguments.callee
  3. 引用函数的处于作用域内的变量

例如,考虑以下函数定义

js
const foo = function bar() {
  // statements go here
};

在函数体中,以下都是等效的

  1. bar()
  2. arguments.callee()
  3. foo()

一个调用自身的函数被称为递归函数。在某种程度上,递归类似于循环。两者都多次执行相同的代码,并且两者都需要一个条件(以避免无限循环,或者更确切地说,在这种情况下避免无限递归)。

例如,考虑以下循环

js
let x = 0;
// "x < 10" is the loop condition
while (x < 10) {
  // do stuff
  x++;
}

它可以转换为递归函数声明,然后调用该函数

js
function loop(x) {
  // "x >= 10" is the exit condition (equivalent to "!(x < 10)")
  if (x >= 10) {
    return;
  }
  // do stuff
  loop(x + 1); // the recursive call
}
loop(0);

但是,一些算法不能是简单的迭代循环。例如,获取树结构(例如DOM)的所有节点,通过递归更容易

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

与函数loop相比,每个递归调用本身在这里进行许多递归调用。

可以将任何递归算法转换为非递归算法,但逻辑通常更复杂,并且这样做需要使用堆栈。

实际上,递归本身使用堆栈:函数堆栈。堆栈式的行为可以在以下示例中看到

js
function foo(i) {
  if (i < 0) {
    return;
  }
  console.log(`begin: ${i}`);
  foo(i - 1);
  console.log(`end: ${i}`);
}
foo(3);

// Logs:
// begin: 3
// begin: 2
// begin: 1
// begin: 0
// end: 0
// end: 1
// end: 2
// end: 3

嵌套函数和闭包

你可以在另一个函数中嵌套函数。嵌套的(内部)函数对包含它的(外部)函数是私有的。

它也形成一个闭包。闭包是一个表达式(最常见的是一个函数),它可以包含自由变量以及绑定这些变量的环境(“封闭”表达式)。

由于嵌套函数是一个闭包,这意味着嵌套函数可以“继承”包含它的函数的参数和变量。换句话说,内部函数包含外部函数的范围。

总结

  • 只能从外部函数中的语句访问内部函数。
  • 内部函数形成闭包:内部函数可以使用外部函数的参数和变量,而外部函数不能使用内部函数的参数和变量。

以下示例显示了嵌套函数

js
function addSquares(a, b) {
  function square(x) {
    return x * x;
  }
  return square(a) + square(b);
}

console.log(addSquares(2, 3)); // 13
console.log(addSquares(3, 4)); // 25
console.log(addSquares(4, 5)); // 41

由于内部函数形成闭包,你可以调用外部函数并为内外函数指定参数

js
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
console.log(fnInside(5)); // 8
console.log(outside(3)(5)); // 8

变量的保存

注意当inside被返回时x是如何保存的。闭包必须保存它引用的所有范围中的参数和变量。由于每次调用都提供可能不同的参数,因此对于每次对outside的调用都会创建一个新的闭包。只有当返回的inside不再可访问时,内存才能被释放。

这与在其他对象中存储引用没有什么不同,但通常不太明显,因为你没有直接设置引用,也不能检查它们。

多重嵌套函数

函数可以进行多重嵌套。例如

  • 一个函数 (A) 包含一个函数 (B),而B自身包含一个函数 (C)。
  • 函数BC在这里都形成闭包。因此,B可以访问AC可以访问B
  • 此外,由于C可以访问B,而B可以访问A,因此C也可以访问A

因此,闭包可以包含多个范围;它们递归地包含包含它的函数的范围。这被称为作用域链。(为什么它被称为“链式”将在后面解释)。

考虑以下示例

js
function A(x) {
  function B(y) {
    function C(z) {
      console.log(x + y + z);
    }
    C(3);
  }
  B(2);
}
A(1); // Logs 6 (which is 1 + 2 + 3)

在这个示例中,C访问了ByAx

这是因为

  1. B形成一个包括A的闭包(即,B可以访问A的参数和变量)。
  2. C形成一个包括B的闭包。
  3. 由于C的闭包包括B,而B的闭包包括A,那么C的闭包也包括A。这意味着C可以访问两者BA的参数和变量。换句话说,C该顺序链式BA的范围。

然而,反过来就不成立。A不能访问C,因为A不能访问B的任何参数或变量,而CB的变量。因此,C仍然只是对B私有的。

命名冲突

当闭包范围内的两个参数或变量具有相同的名称时,就会发生命名冲突。更深的嵌套范围优先。因此,最内层的范围优先级最高,而最外层的范围优先级最低。这就是作用域链。链式中的第一个是最内层的范围,最后一个是最外层的范围。考虑以下

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

console.log(outside()(10)); // 20 (instead of 10)

命名冲突发生在语句return x * 2中,并且发生在inside的参数xoutside的变量x之间。此处的范围链为{inside, outside, 全局对象}。因此,insidex优先于outsidex,并且返回20 (insidex) 而不是10 (outsidex)。

闭包

闭包是 JavaScript 最强大的功能之一。JavaScript 允许嵌套函数,并允许内部函数完全访问外部函数内部定义的所有变量和函数(以及外部函数可以访问的所有其他变量和函数)。

但是,外部函数不能访问内部函数内部定义的变量和函数。这为内部函数的变量提供了一种封装。

此外,由于内部函数可以访问外部函数的范围,因此如果内部函数在外部函数的生命周期结束后存活下来,那么外部函数中定义的变量和函数将比外部函数执行的持续时间更长。当内部函数以某种方式对外部函数之外的任何范围可用时,就会创建闭包。

js
// The outer function defines a variable called "name"
const pet = function (name) {
  const getName = function () {
    // The inner function has access to the "name" variable of the outer function
    return name;
  };
  return getName; // Return the inner function, thereby exposing it to outer scopes
};
const myPet = pet("Vivie");

console.log(myPet()); // "Vivie"

它可以比上面的代码复杂得多。可以返回一个包含用于操作外部函数内部变量的方法的对象。

js
const createPet = function (name) {
  let sex;

  const pet = {
    // setName(newName) is equivalent to setName: function (newName)
    // in this context
    setName(newName) {
      name = newName;
    },

    getName() {
      return name;
    },

    getSex() {
      return sex;
    },

    setSex(newSex) {
      if (
        typeof newSex === "string" &&
        (newSex.toLowerCase() === "male" || newSex.toLowerCase() === "female")
      ) {
        sex = newSex;
      }
    },
  };

  return pet;
};

const pet = createPet("Vivie");
console.log(pet.getName()); // Vivie

pet.setName("Oliver");
pet.setSex("male");
console.log(pet.getSex()); // male
console.log(pet.getName()); // Oliver

在上面的代码中,外部函数的name变量对内部函数是可访问的,除了通过内部函数之外,没有其他方法可以访问内部变量。内部函数的内部变量充当外部参数和变量的安全存储。它们为内部函数提供“持久”和“封装”的数据。这些函数甚至不必分配给变量,也不必有名称。

js
const getCode = (function () {
  const apiCode = "0]Eal(eh&2"; // A code we do not want outsiders to be able to modify…

  return function () {
    return apiCode;
  };
})();

console.log(getCode()); // "0]Eal(eh&2"

注意:使用闭包时,有一些陷阱需要注意!

如果封闭函数定义了一个与外部范围中的变量同名的变量,那么就没有办法再次引用外部范围中的变量。(内部范围变量“覆盖”外部变量,直到程序退出内部范围。可以认为它是一个命名冲突)。

js
const createPet = function (name) {
  // The outer function defines a variable called "name".
  return {
    setName(name) {
      // The enclosed function also defines a variable called "name".
      name = name; // How do we access the "name" defined by the outer function?
    },
  };
};

使用 arguments 对象

函数的参数保存在一个类似数组的对象中。在函数内部,你可以像这样访问传递给它的参数

js
arguments[i];

其中i是参数的序数,从0开始。因此,传递给函数的第一个参数将是arguments[0]。参数的总数由arguments.length指示。

使用arguments对象,你可以使用比函数正式声明接受的参数更多的参数调用函数。这在你不事先知道将传递给函数多少个参数时非常有用。你可以使用arguments.length确定实际上传递给函数的参数数量,然后使用arguments对象访问每个参数。

例如,考虑一个将多个字符串连接起来的函数。该函数的唯一正式参数是一个字符串,指定用于连接要连接的项目的字符。该函数定义如下

js
function myConcat(separator) {
  let result = ""; // initialize list
  // iterate through arguments
  for (let i = 1; i < arguments.length; i++) {
    result += arguments[i] + separator;
  }
  return result;
}

你可以将任意数量的参数传递给此函数,它将每个参数连接到一个字符串“列表”中

js
console.log(myConcat(", ", "red", "orange", "blue"));
// "red, orange, blue, "

console.log(myConcat("; ", "elephant", "giraffe", "lion", "cheetah"));
// "elephant; giraffe; lion; cheetah; "

console.log(myConcat(". ", "sage", "basil", "oregano", "pepper", "parsley"));
// "sage. basil. oregano. pepper. parsley. "

注意:arguments变量是“类似数组的”,但不是数组。它类似于数组,因为它具有编号索引和length属性。但是,它拥有所有数组操作方法。

有关更多信息,请参阅 JavaScript 参考中的Function对象。

函数参数

有两种特殊类型的参数语法:默认参数剩余参数

默认参数

在 JavaScript 中,函数的参数默认值为undefined。但是,在某些情况下,设置不同的默认值可能很有用。这正是默认参数的作用。

在过去,设置默认值的通用策略是在函数体中测试参数值,如果它们为undefined,则分配一个值。

在以下示例中,如果b没有提供任何值,那么在计算a*b时它的值为undefined,并且通常会返回NaNmultiply调用。但是,这由本示例中的第二行阻止

js
function multiply(a, b) {
  b = typeof b !== "undefined" ? b : 1;
  return a * b;
}

console.log(multiply(5)); // 5

使用默认参数,不再需要在函数体中进行手动检查。你可以在函数头中将1作为b的默认值

js
function multiply(a, b = 1) {
  return a * b;
}

console.log(multiply(5)); // 5

有关更多详细信息,请参阅参考中的默认参数

剩余参数

the 剩余参数 语法允许我们将不确定的参数数量表示为数组。

在以下示例中,函数multiply使用剩余参数来收集从第二个参数到最后一个参数的所有参数。然后,该函数将这些参数乘以第一个参数。

js
function multiply(multiplier, ...theArgs) {
  return theArgs.map((x) => multiplier * x);
}

const arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]

箭头函数

一个箭头函数表达式(也称为胖箭头,以区别于未来 JavaScript 中假设的->语法)与函数表达式相比具有更短的语法,并且没有它自己的thisargumentssupernew.target。箭头函数始终是匿名的。

两个因素影响了箭头函数的引入:更短的函数不绑定this

更短的函数

在某些函数模式中,更短的函数受到欢迎。比较

js
const a = ["Hydrogen", "Helium", "Lithium", "Beryllium"];

const a2 = a.map(function (s) {
  return s.length;
});

console.log(a2); // [8, 6, 7, 9]

const a3 = a.map((s) => s.length);

console.log(a3); // [8, 6, 7, 9]

没有单独的 this

在箭头函数出现之前,每个新定义的函数都有它自己的this值(对于构造函数来说是新对象,对于严格模式函数调用来说是undefined,对于以“对象方法”形式调用的函数来说是基对象,等等)。事实证明,对于面向对象的编程风格来说,这并不理想。

js
function Person() {
  // The Person() constructor defines `this` as itself.
  this.age = 0;

  setInterval(function growUp() {
    // In nonstrict mode, the growUp() function defines `this`
    // as the global object, which is different from the `this`
    // defined by the Person() constructor.
    this.age++;
  }, 1000);
}

const p = new Person();

在 ECMAScript 3/5 中,这个问题通过将this中的值分配给可以闭包的变量来解决。

js
function Person() {
  // Some choose `that` instead of `self`.
  // Choose one and be consistent.
  const self = this;
  self.age = 0;

  setInterval(function growUp() {
    // The callback refers to the `self` variable of which
    // the value is the expected object.
    self.age++;
  }, 1000);
}

或者,可以创建一个绑定函数,以便将正确的this值传递给growUp()函数。

箭头函数没有它自己的this;它使用封闭执行上下文的this值。因此,在以下代码中,传递给setInterval的函数中的this与封闭函数中的this具有相同的值

js
function Person() {
  this.age = 0;

  setInterval(() => {
    this.age++; // `this` properly refers to the person object
  }, 1000);
}

const p = new Person();