JavaScript 中的一等函数

1,295 阅读4分钟

Javascript 作为一等函数。是一个高级开发者应该掌握的。为什么会被称作一等?不仅仅是它可以被当做变量一样处理。下面有三个关键点代表了 Javascript 一等函数的特点:

  1. 函数可以被复制给变量(函数声明与函数表达式的区别)
  2. 函数可以被当做参数传递给另一个函数(高阶函数)
  3. 函数可以被另一个函数返回(高阶函数,闭包)

针对以上特点,我们来做几个真实的复杂的例子。

给变量赋值一个函数

我们创建一个返回文本 “Hello” 的函数,然后把该函数赋值给变量 sayHello

const sayHello = () => {
  return 'Hello';
};

console.log(sayHello());
// "Hello"

这里有个区别是关于函数声明和函数表达式的区别。这个 StackOverflow 问题已经将其解释的清清楚楚。推荐有英文基础的同学了解一下。其实答案也很简单,他们之间的区别在于变量提升(hoisting)。具体到复杂的生成环境中,这个情况还是需要多多注意的,加上代码结构的影响,何时使用函数声明,何时使用表达式,是一个需要多多思考的问题。比如:在 if 表达式中,使用函数声明是无效的,会被提升到 if 之外,而使用 let 相关的表达式则可以限定该函数的作用范围。

作为参数传递函数

使用之前提到的 sayHello 方法,把该方法当做参数传递给 sayHelloToPerson。在函数 sayHelloToPerson 中,变量 greeter 将会指向 sayHello 方法的内存地址。当我们调用 greeter()时,sayHello就被调用了。

const sayHelloToPerson = (greeter, person) => {
  return greeter() + ' ' + person;
};

console.log(sayHelloToPerson(sayHello, 'John'));
// Hello John

单纯的说这个作为参数传递函数有点简单了。我们再举一个大家一定用过的例子:

let array = [1,2,3,4,5,6];
array.map(item => item * 2);

当你想对一个数组进行一些格式化或者改写的时候, map 是最容易想到的方法。map 里面其实就是接受了一个匿名函数,这个函数就是被当做参数传入的,只是匿名而已。同时,map 的使用方法也是一种高阶函数的表现。怎么样,是不是觉得一下子高大上了许多。关于数组的方法,最值得研究的 reduce,这里不展开讨论,不过可以出个题目供大家在留言区尝试解决一下:如何使用reduce实现一个map?

从函数中返回一个函数

如果我们不想输出 Hello,而是想自定义创建任何输出的形式。那么我们需要一个可以创建输出语的函数。

const greeterMaker = greeting => {
  return person => {
    return greeting + ' ' + person;
  };
};

const sayHelloToPerson = greeterMaker('Hello');
const sayHowdyToPerson = greeterMaker('Howdy');

console.log(sayHelloToPerson('Joanne'));
// "Hello Joanne"

console.log(sayHowdyToPerson('Joanne'));
// "Howdy Joanne"

上图中的 greeterMaker 接受了一个参数并且把该参数作为返回的一个匿名函数的参数。通过传递的参数来定义问候语。我们在使用 jQuery 时,常常看到的 $(this).find().eq(0) 这样链式调用的写法,其关键在于每个方法都返回了当前的 this 对象。这个思路与上面返回函数也有一定的相似之处。

真实的案例

对象校验

假设有一组由对象构成的用户信息数据,需要我们校验。我们可以通过校验函数来判断,这些数据是否合法。

const usernameLongEnough = obj => {
  return obj.username.length >= 5;
};

const passwordsMatch = obj => {
  return obj.password === obj.confirmPassword;
};

const objectIsValid = (obj, ...funcs) => {
  for (let i = 0; i < funcs.length; i++) {
    if (funcs[i](obj) === false) {
      return false;
    }
  }

  return true;
};

const obj1 = {
  username: 'abc123',
  password: 'foobar',
  confirmPassword: 'foobar',
};

const obj1Valid = objectIsValid(obj1, usernameLongEnough, passwordsMatch);
console.log(obj1Valid);
// true

const obj2 = {
  username: 'joe555',
  password: 'foobar',
  confirmPassword: 'oops',
};

const obj2Valid = objectIsValid(obj2, usernameLongEnough, passwordsMatch);
console.log(obj2Valid);
// false

API key 闭包

假设需要使用 API key 来调用 API。我们需要给每一个请求添加这个 key。那么我们可以创建一个方法把这个 API key 作为参数保存在一个方法中,然后返回这个方法。

const apiConnect = apiKey => {
  const getData = route => {
    return axios.get(`${route}?key=${apiKey}`);
  };

  const postData = (route, params) => {
    return axios.post(route, {
      body: JSON.stringify(params),
      headers: {
        Authorization: `Bearer ${apiKey}`,
      },
    });
  };

  return { getData, postData };
};

const api = apiConnect('my-secret-key');

// No need to include the apiKey anymore
api.getData('http://www.example.com/get-endpoint');
api.postData('http://www.example.com/post-endpoint', { name: 'Joe' });

总结

Javascript 中的函数还承担着实现类的功能。尽管 Javascript 中没有真正的类,它是基于原型继承的方式来实现封装、继承和多态的特性。关于这里的只是,在【Javascript高级程序设计】一书中有详细讲解。函数是一类特殊的对象。我们来看看下面的例子来解释这个原因:

console.log(Function instanceof Object);
// true
console.log(Object instanceof Function);
// true

函数是一类特殊的对象,那么 Function 作为 Object 的一个实例是正常的。不仅是 FunctionStringArrayNumber 等,都是 Object 的实例。然而 Object instanceof Function 该做如何解释? 实际上,所有的构造函数,本质上还是函数。一个函数必然是 Function 的实例啦!上面这个问题曾经是我遇到的面试题。但是没有答对。还是理解不深刻。在这里总结反思一下,查漏补缺。

参考资料

  1. first-class-functions-in-javascript

pic