JavaScript函数篇

24 阅读7分钟

JavaScript函数篇学习


本文是记录一下JavaScript官网部分的函数篇学习

函数是 JavaScript 中的基本组件之一。JavaScript 中的函数类似于过程——一组执行任务或计算值的语句。但要成为函数,这个过程应该接受输入并返回与输入存在某些明显关系的输出。

调用函数

定义的函数并不会自动执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。

调用函数才会以给定的参数真正执行这些动作。

函数一定要处于调用它们的作用域中,但是函数的声明可以被提升(出现在调用语句之后)。函数声明的范围是声明它的函数(或者,如果它是在顶层声明的,则为整个程序)之内。

函数提升
console.log(square(5)); // 25

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

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

// 所有函数声明实际上都位于作用域的顶部
function square(n) {
  return n * n;
}

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

函数提升仅适用于函数声明,而不适用于函数表达式

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

在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的作用域内定义。相对应的,一个函数可以访问定义在其范围内的任何变量和函数。

换言之,定义在全局域中的函数可以访问所有定义在全局域中的变量。在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量。

// 下面的变量定义在全局作用域中
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"
作用域和函数栈

递归

一个函数可以指向并调用自身。有三种方法可以达到这个目的:

  1. 函数名
  2. arguments.callee
  3. 作用域内一个指向该函数的变量名

在这个函数体内,以下的语句是等价的:

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

调用自身的函数我们称之为递归函数

闭包

闭包是 JavaScript 中最强大的特性之一。JavaScript 允许函数嵌套,并且内部函数具有定义在外部函数中的所有变量和函数(以及外部函数能访问的所有变量和函数)的完全访问权限。

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

此外,由于内部函数可以访问外部函数的作用域,因此当内部函数生存周期大于外部函数时,外部函数中定义的变量和函数的生存周期将比内部函数执行的持续时间要长。当内部函数以某一种方式被任何一个外部函数之外的任何作用域访问时,就会创建闭包。

// 外部函数定义了一个名为“name”的变量
const pet = function (name) {
  const getName = function () {
    // 内部函数可以访问外部函数的“name”变量
    return name;
  };
  return getName; // 返回内部函数,从而将其暴露给外部作用域
};
const myPet = pet("Vivie");

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

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

const getCode = (function () {
  const apiCode = "0]Eal(eh&2"; // 我们不希望外部能够修改的代码......

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

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

如果一个闭包的函数定义了一个和外部的某个变量名称相同的变量,那么这个闭包将无法引用外部作用域中的这个变量。(内部作用域的变量“覆盖”外部作用域,直至程序退出内部作用域。可以将其视作命名冲突。)

const createPet = function (name) {
  // 外部函数定义了一个名为“name”的变量。
  return {
    setName(name) {
      // 闭包函数还定义了一个名为“name”的变量。
      name = name; // 我们如何访问外部函数定义的“name”?
    },
  };
};
使用 arguments 对象

函数的实际参数会被保存在一个类似数组的 arguments 对象中。在函数内,你可以按如下方式找出传入的参数:

arguments[i];

其中 i 是参数的序号,从 0 开始。所以第一个传入函数的参数会是 arguments[0]。参数的数量由 arguments.length 表示。

使用 arguments 对象,你可以处理比声明更多的参数来调用函数。这在你事先不知道会需要将多少参数传递给函数时十分有用。你可以用 arguments.length 来获得实际传递给函数的参数的数量,然后用 arguments 对象来访问每个参数。

例如,考虑有一个用来连接字符串的函数。唯一正式的参数是在连接后的字符串中用来分隔各个连接部分的字符。

function myConcat(separator) {
  let result = ""; // 初始化列表
  // 迭代 arguments
  for (let i = 1; i < arguments.length; i++) {
    result += arguments[i] + separator;
  }
  return result;
}

arguments 变量只是“类数组”,而不是数组。它与数组类似,有索引编号和 length 属性。尽管如此,它并不具备 Array 对象的所有数组操作方法。

函数参数

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

默认参数

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

在过去,用于设定默认参数的一般策略是在函数的主体中测试参数值是否为 undefined,如果是则赋予这个参数一个默认值。

在下面的示例中,如果调用函数时没有给 b 提供值,那么它的值就是 undefined,在执行 a*b 时,调用乘法通常会返回 NaN。但是,这已经被示例的第二行所避免了:

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

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

使用默认参数,在函数体的手动检查就不再必要了。现在,你可以在函数头简单地把 1 设定为 b 的默认值:

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

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

剩余参数

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

在下面的示例中,multiply 函数使用剩余参数收集从第二个参数开始到最后的参数。

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无绑定性

更简洁的函数

在一些函数模式中,更简洁的函数很受欢迎。对比一下:

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;在作为“对象方法”调用的函数中指向这个对象;等等)。事实证明,这对于面向对象的编程风格来说并不理想。

function Person() {
  // 构造函数 Person() 将 `this` 定义为自身。
  this.age = 0;

  setInterval(function growUp() {
    // 在非严格模式下,growUp() 函数将 `this` 定义为“全局对象”,
    // 这与 Person() 定义的 `this` 不同。
    this.age++;
  }, 1000);
}

const p = new Person();