Skip to content

函数的高阶理解

函数参数

默认参数

js
const createBooking = function (
  flightNum,
  numPassengers = 1, // 设定默认参数 //[!code highlight]
  price = 199, // 设定默认参数
) {
  const booking = {
    flightNum,
    numPassengers,
    price,
  };
  bookings.push(booking);
};

传参

js
const flight = 'BY747';
const person = {
  name: 'jonas',
  age: 27,
};

const createPassenger = function (flightnum, passenger) {
  flightnum = 'LY233'; 
  passenger.name = 'passenger'; 
};

createPassenger(flight, person);

console.log(flight); // BY747
console.log(person); // {name: 'passenger', age: 27}

按值传递与按引用传递:JS 没有按引用传递的说法,它的所有传参都是按值传递的,只是有的值本身就是其他值的地址/引用罢了,比如上面例子中传入的 person

因此这里要非常注意,我们传入的到底是数据本身还是其他数据的引用,我们是否希望函数中的操作影响到我们传入的参数。

一等函数与高阶函数

一等函数(first-class functions):

  • 函数是“一等公民”。
  • 函数只是值:这是由于函数是对象,而对象就是值。
  • 就像其他数据一样,函数能被赋值,被传递。
  • 函数可以调用方法。

高阶函数(higher-order functions):

  • 接收别的函数作为参数,比如 addEventListener
  • 返回一个函数。

用一个例子来理解高阶函数:

js
// 去掉英文字符串中的所有空格并将所有字母变为小写
const oneWord = function (str) {
  return str.replace(/ /g, '').toLowerCase();
};

// 将一句英文字母的第一个单词变为大写
const upperFirstWord = function (str) {
  const [first, ...others] = str.split(' ');
  return [first.toUpperCase(), ...others].join(' ');
};

// 高阶函数
const transformer = function (str, fn) {
  return fn(str); // 用传入的函数去处理传入的字符串
};

console.log(
  transformer('JavaScript is the best!', upperFirstWord),
); // JAVASCRIPT is the best!

console.log(
  transformer('JavaScript is the best!', oneWord),
); // javascriptisthebest!

这里也体现了面向对象编程中的多态。回调函数允许我们创建抽象abstract

返回一个函数:

js
const greet = function (greeting) {
  return function (name) {
    console.log(`${greeting}, ${name}`);
  };
};

const heyGreeting = greet('Hey');
heyGreeting('jonas'); // Hey, jonas
heyGreeting('maggie'); // Hey, maggie

greet('hello')('jiaqi'); // hello, jiaqi

这上面是一种函数柯里化的用法,同时它能够生效是因为闭包的存在。

改变 this

call apply

  • 改变函数的 this 指向为传入 callapply 的第一个参数。
  • call 从第二个参数开始传入连续的调用函数的参数,apply 传入调用函数参数数组。
js
const lufthansa = {
  airline: 'Lufthansa',
  iataCode: 'LH',
  bookings: [],
  book(flightNum, name) {
    console.log(
      `${name} booked a seat on ${this.airline} flight ${this.iataCode}${flightNum}`,
    );
    this.bookings.push({
      flight: `${this.iataCode}${this.flightNum}`,
      name,
    });
  },
};

// 函数作为方法被调用,this 指向调用该方法的对象
lufthansa.book(239, 'jonas'); // jonas booked a seat on Lufthansa flight LH239

const eurowings = {
  airline: 'Eurowings',
  iataCode: 'EW',
  bookings: [],
  // 这里的 book 方法和上面完全相同,但是我们不想简单的复制过来
};

// 保存 lufthansa 的 book 方法
const book = lufthansa.book;
book.call(eurowings, 23, 'jiaqi'); // jiaqi booked a seat on Eurowings flight EW23
book.apply(eurowings, [23, 'jiaqi']); // jiaqi booked a seat on Eurowings flight EW23

apply 变得没那么通用了,因为通过数组解构也完全可以在 call 中实现 apply 的功能。

js
book.call(eurowings, ...[23, 'jiaqi']); // jiaqi booked a seat on Eurowings flight EW23

bind 是另一个改变 this 指向的方法,它的第一个参数就是 this 指向的对象,并且它不会立即执行,因此需要把它存起来:

js
const bookEW = book.bind(eurowings); 
bookEW(33, 'steven'); // steven booked a seat on Eurowings flight EW33

bind 还有其他的用法,比如设定一个固定函数的某个参数的值,并将其作为一个新的函数返回,如:

js
const addTax = (rate, value) => value + value * rate;

const addVAT = addTax.bind(null, 0.1); 
console.log(addVAT(300)); // 330

使用闭包的思想也可以完成上面的功能:

js
const addTax = function (rate) {
  return function (value) {
    return value + value * rate;
  };
};

addTax(0.1)(300); // 330

立即调用的函数表达式 IIFE

Imediately Invoked Function Expression (IIFE)

调用一次后立刻销毁的函数。

我们先来看一下它的格式:

js
// IIFE
(function () {
  console.log('This will never run again');
})(); // This will never run again

箭头函数也一样:

js
// IIFE
(() => {
  console.log('This will ALSO never run again');
})(); // This will ALSO never run again

它创造了一个函数作用域,在 ES6 之前能够起到规范作用域的功能;但现在已经不需要了。但它的只执行一次的函数表达式的特性依然被需要。

闭包

首先分析一个例子:

js
// 👇下面这个函数将创建闭包
const secureBooking = function () {
  let passengerCount = 0;

  return function () {
    passengerCount++;
    console.log(`${passengerCount} passengers`);
  };
};

// booker 当前也是一个函数了
const booker = secureBooking(); 

我们分析上面那句高亮的代码发生了什么:

  1. 首先,我们的代码在全局执行上下文中运行,当前,全局执行上下文中只有 secureBooking
  2. 然后,secureBooking 运行,调用栈顶中存放该函数的执行上下文,它的执行上下文中有局部变量环境,存放着该函数内部的所有局部变量。在这里,也就是说里面有 passengerCount = 0
  3. 接下来,全局执行上下文中创建 booker 变量。
  4. secureBooking 执行并返回的函数存放到 booker 变量中,之后 secureBooking 弹出调用栈并消失。

就像上图所展示的一样:

  • scope chain 的顺序是由代码写的顺序决定的;
  • 而调用栈中的顺序是由函数被调用的顺序决定的。

并且要非常明确的一点是,当函数执行完毕,它在调用栈中就已经消失了。

当我们执行 booker 函数的时候:

js
booker(); // 1 passengers
booker(); // 2 passengers
booker(); // 3 passengers

这里面就有一个非常神奇的事情,当前 passengerCount 这个变量已经早就不在调用栈中了,为什么调用的时候还是可以拿到他?

这就是闭包的作用。

TIP

闭包使一个函数能够记住当它被创建的时候,它的出生地的所有存在的变量。

让我们继续回到刚才的代码中:

当我们执行 booker() 的时候,它的执行上下文就会被存放到调用栈中,同时它的作用域指向了全局作用域,作用域链就形成了。

此时,我们就看到了一个问题,passengerCount 并不在它的作用域链中,那么它要怎么拿到这个变量?

我们再回顾一遍闭包的作用:

TIP

任何函数总是能访问它被创建的时候的执行上下文中的变量,即使那个执行上下文已经被销毁。

并且!闭包内变量的优先级是要高于作用域链的,也就是说,即使有一个全局变量 passengerCount,闭包中的 passengerCount 依然会被优先使用。

它的定义是这样的:

A closure is the closed-over variable environment of the execution context in which a function was created, even after that execution context is gone.

中文解释:

闭包

闭包是当函数被创建的时候,它所在的执行上下文中的封闭的变量环境

有两种通俗的理解方式:

  1. 一个函数永远不会和它出生地存在的变量失去联系。
  2. 当函数出生的时候,它就被上了一个背包,这个背包里装着所有当前执行上下文中的变量,此后,无论这个函数去到哪里,它都背着这个背包。当它需要用到一些变量的时候,它会先翻翻自己的背包里有没有这个变量,有就直接拿出来用,没有才会顺着作用域链去找。

我们无法显式地获取闭包中的变量,我们能做的只是在外面看看它。

闭包的一大应用便是解决了异步函数执行时失去其父级作用域变量的问题。