关于函数的理解和总结

101 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 19 天,点击查看活动详情

函数

函数实际上是对象,每个函数都是 Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。

函数可以通过函数声明和函数表达式来进行定义

// 函数表达式
function sum(num1, num2) {
  return num1 + num2;
}

// 函数表达式
const add = function (num1, num2) {
  return num1 + num2;
};

箭头函数

箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方都可以使用箭头函数

// 箭头函数
const sum = (num1, num2) => {
  return num1 + num2;
};
/**
 * 如果箭头函数只有一个参数,可以将括号省略
 * 如果箭头函数的函数体只有一条语句,可以省略大括号,省略大括号会将该条* 语句的结果作为函数的返回值
 */

const log = (str) => console.log(str);

注意: 箭头函数不能使用 argumentssupernew.target ,箭头函数不能作为构造函数,也没有 prototype 属性

函数名

函数作为一个对象(引用类型), 函数名实际上就是指向函数的指针,与其他包含对象指针的变量具有相同的行为。意味这同一个函数可以有多个名称

function sum(num1, num2) {
  return num1 + num2;
}
let add = sum;

sum(10, 10);
add(10, 10);

ES6 中所有函数对象都会暴露一个只读的 name 属性,多数情况下,这个属性保存的就是一个函数标识符,对于箭头函数,该值为空字符串。如果是使用 Function 构造函数创建的,则标识成 anonymous

function foo() {}

console.log(foo.name); // foo
console.log((() => {}).name); // (空字符串)
console.log(new Function().name); // anonymous

理解参数

ES 函数的参数,即不关心传入参数的个数,也不关心参数的数据类型。 对于使用 function 关键字定义的函数(非箭头函数),可以在函数内部方位 arguments 对象(类数组对象,不是 Array 的实例),从中取得传入的每个参数值

function sayHi() {
  console.log(argument[0]);
}

sayHi("hello world");

arguments 中的值始终会与对应的命名参数同步

function sum(num1, num2) {
  arguments[1] = 30; // num2 也被修改为 30
  return num1 + num2;
}

sum(10, 10); // 40

虽然 arguments 会与对应的命名参数的值保持同步,但是它们并不是访问同一个内存地址。另外 arguments 的长度是根据传入的参数个数决定的,而不是函数定义是命名参数个数确定的

function sum(num1, num2) {
  arguments[1] = 30;
  console.log(num2);
  return num1 + num2; // num2 为 undefined
}

sum(10); // NaN

没有重载

ES 中函数参数是由另个或者多个值的数组表示的,没有函数签名,自然也没有重载。如果定义了两个同名函数,则后定义的会覆盖先定义的

function sum(num) {
  return num + 100;
}

function sum(num1) {
  return num1 + 300;
}

console.log(sum(100)); // 400

默认参数值

ES6 之前,实现默认参数的一种常用方式就是检测摸个参数是否为 undefined ,如果是则给它赋一个值作为默认值

function makeKing(name) {
  name = typeof name !== "undefined" ? name : "Henry";
  return `King ${name} VIII`;
}

ES6 之后,支持显示的定义默认参数

function makeKing(name = "Henry") {
  return `King ${name} VIII`;
}

makeKing(); // King Henry VIII

默认参数作用域与暂时性死区

给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样。默认参数会按照定义他们的顺序依次被初始化,所以后面定义默认值的参数可以应用先定义的参数,反之由于暂时性死区的存在,则不行

function makeKing(name = "Henry", numerals = name) {
  return `King ${name} ${numerals}`;
}
console.log(makeKing()); // King Henry Henry

参数扩展和收集

ES6 中新增了扩展操作符,可以非常简介的操作和组合集合数据

扩展参数

对可迭代对象使用扩展操作符,可以将可迭代对象拆分成单独的项

let values = [1, 2, 3, 4];
console.log(values); // [1, 2, 3, 4]
for (let i = 0; i < values.length; i++) {
  console.log(values[i]);
}
// 1
// 2
// 3
// 4
console.log(...values); // 1 2 3 4

收集参数

在定义函数是,可以使用扩展操作符将不同长度的独立参数组合成一个数组,这将会得到一个 Array 实例

function getSum(...values) {
  console.log(values); // [1,2,3,4]
  console.log(typeof values); // object
  console.log(values instanceof Array); // true
}

getSum(1, 2, 3, 4);

函数声明和函数表达式

JavaScript 引擎在任何代码执行之前,会先读取函数声明(这里存在一个预编译的过程),并在执行上下文中生成函数定义(这个过程叫做函数声明提升)。而函数表达式必须等到代码执行到那一行,才会在执行上下文中生成函数定义。

sum(10, 10); // 20
function sum(a, b) {
  return a + b;
}

add(10, 10); // Uncaught TypeError: add is not a function
var add = function (a, b) {
  return a + b;
};

函数作为值

ES 中,函数名就是变量,因此函数可以在任何可以使用变量的地方使用。这以为着函数可以作为参数传递给另一个函数,也可以作为一个函数的返回值(这一类函数称为高阶函数

function callSomeFunction(someFunction, someArgument) {
  return someFunction(someArgument);
}

function createComparisonFunction(propertyName) {
  return function (object1, object2) {
    let value1 = object1[propertyName];
    let value2 = object2[propertyName];
    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  };
}

函数内部

ES 中,函数内部存在两个特殊的对象: argumentsthis 。 ES6 中新增了 new.target 属性

arguments

arguments 是一个类数组对象,包含调用函数传入的所有的参数,在箭头函数中没有该对象

arguments 中含有一个 callee 属性,该属性是一个指向 arguments 对象所在函数的指针

使用 arguments 的 callee 属性实现阶乘函数

function factorial(num) {
  if (num <= 1) {
    return 1;
  }
  // 使用 arguments.callee 可以不关心函数名称实现递归
  return num * arguments.callee(num - 1);
}

this

在标准函数中, this 引用的是把函数当成方法调用的上下文对象,称为 this 值(在网页的全局上下文中调用函数, this 指向 windows)

window.color = "red";
let o = {
  color: "green",
};

function sayColor() {
  console.log(this.color);
}

sayColor(); // red
o.sayColor = sayColor;
o.sayColor(); // green

// 函数名只是保存指针的变量,全局定义的 sayColor 和 o.sayColor 是同一个函数,只不过执行的上下文不同

this 指向必须到函数被调用时才能确定

在箭头函数中, this 引用的是定义箭头函数的上下文

window.color = "red";
let o = {
  color: "green",
};

let sayColor = () => console.log(this.color);

sayColor(); // red
o.sayColor = sayColor;
o.sayColor(); // red

caller

ES5 函数中的 caller 属性,引用的是调用当前函数的函数,如果实在全局作用域中调用则为 null

function outer() {
  inner();
}

function inner() {
  console.log(inner.caller); // 这里会打印 outer 函数的源代码
}

new.target

ES 中的函数可以作为构造函数实例化一个对象,也可以作为普通函数进行调用, ES6 中新增的 new.target 属性,可以检测是否使用 new 关键字调用函数。 如果函数是作为普通函数调用, new.targetundefined,如果使用 new 关键字调用, new.target 则引用被调用的构造函数

function Person() {
  console.log(this);
  console.log(new.target);
  console.log(this instanceof Person);
  console.log(new.target === Person);
  console.log(new.target instanceof Person);
}

Person();
// Window 对象
// undefined
// false
// false
// false
new Person();
// Person()
// function Person(){...}
// true
// true
// false

函数属性和方法

ES 中函数是对象,因此有属性和方法。每个函数都有两个属性: lengthprototype 。 其中, length 属性保存函数定义的命名参数的个数

function sayName(name) {
  console.log(name);
}
console.log(sayName.length); // 1

prototype 属性是保存引用类型所有实例方法的地方, toString()valueOf() 等方法都保存在 prototype 上,是的所有实例都能方法这些方法。 prototype 属性是不可枚举的

函数还有两个方法: applycall 。 这两个方法都可以以指定的 this 值来调用函数。 apply 接受两个参数:第一个参数为函数内的 this 值和一个参数数组(可以使 Array 的实例,也可以是 arguments 对象)。 call 第一个参数为 this 值,剩下的参数为传给被调用函数的参数, call 向函数传递参数,需要逐个列出

window.name = "zhang";
let o = {
  name: "li",
};

function sayName() {
  console.log(this.name);
}

sayName(); // zhang
sayName.apply(o); // li
sayName.call(o); // li

ES5 中,新增了一个方法 bind, 该方法会创建一个新的函数实例,其 this 值会被绑定到传给 bind 的对象

window.name = "zhang";
let o = {
  name: "li",
};

function sayName() {
  console.log(this.name);
}

let newSayName = sayName.bind(o);
// 即使在全局作用域中调用 newSayName, newSayName 函数内部的 this 依然指向对象 o
newSayName(); // li

函数表达式

定义函数有两种方式: 函数声明和函数表达式

函数声明的关键特点就是存在函数声明提升,即函数声明会在代码执行之前获得定义,意味这函数声明可以出现在调用之后

函数表达式看起来像是创建一个函数,然后将它赋值给一个变量,这样创建的函数叫做匿名函数(有时也成为兰姆达函数)

// 函数声明
function functionName() {}

// 函数表达式
const functionName = function () {};

递归

递归函数通常的形式就是通过函数名称调用自己

// 递归计算阶乘
function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}

执行以下代码时会出错

let anotherFactorial = factorial;
factorial = null;
console.log(anotherFactorial(4));

可以使用 arguments 对象中的 callee 属性解决上面的问题, arguments.callee 指向的是正在执行的函数

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}

但是在严格模式下, 不能访问 arguments.callee , 可以使用命名函数表达式来达到目录

const factorial = function f(num) {
  if (num <= 1) {
    return 1;
  }
  return num * f(num - 1);
};

尾调用优化

尾调用: 外部函数的返回值是一个内部函数的返回值

function outerFunction() {
  return innerFunction();
}

尾调用优化:

  • 执行到 outerFunction 函数体,第一个栈帧被推入栈中
  • 执行到 outerFunction 函数体中的 return 语句,为求值返回语句,必须先求值 innerFunction
  • js 引擎发现把第一个栈帧弹出栈也没有问题,因为 innerFunction 的返回值也是 outerFunction 的返回值
  • 弹出 outerFunction 的栈帧
  • 执行到 innerFunction 函数体,栈帧被推到栈上
  • 执行 innerFunction 函数,计算其返回值
  • innerFunction 的栈帧弹出栈外

尾调用优化条件

尾调用优化的条件就是确定外部栈帧真的没有必要存在了,涉及的条件如下

  • 代码在严格模式下执行
  • 外部函数的返回值是对尾部函数的调用
  • 尾调用函数返回后不需要执行额外的逻辑
  • 尾调用函数不是引用外部函数作用域中自有变量的闭包
"use strict";
// 有优化:栈帧销毁前执行参数计算
function outerFunction(a, b) {
  return innerFunction(a + b);
}
// 有优化:初始返回值不涉及栈帧
function outerFunction(a, b) {
  if (a < b) {
    return a;
  }
  return innerFunction(a + b);
}
// 有优化:两个内部函数都在尾部
function outerFunction(condition) {
  return condition ? innerFunctionA() : innerFunctionB();
}

尾调用优化的代码

通过计算斐波那契数列的函数来了解尾调用优化

function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
}

上面的代码不符合尾调用优化的条件,因为返回语句中有一个相加的操作。结果,fib(n) 的栈帧的内存复杂度为 O(2^n) 。 将其重构成满足尾调用优化的条件,使用两个嵌套的函数,外部函数作为基础框架,内部函数执行递归

"use strict";
function fib(n) {
  return finImpl(0, 1, n);
}

function fibImpl(a, b, n) {
  if (n === 0) {
    return a;
  }
  return fibImpl(b, a + b, n - 1);
}