01、第一堂函数课

306 阅读10分钟

JavaScript中最关键的概念是:函数是第一类对象,或者说一等公民。函数与对象共存,函数也可以被视为其他任意类型的JavaScript对象。函数和那些更普通的JavaScript数据类型一样,它能被变量引用,能以字面量方式声明,能被作为函数参数传递。面向函数编程带来的差异,会发现,在需要调用某函数的位置定义该函数,能让我们编写更紧凑、更易懂的代码

函数式的不同点

函数及函数式概念之所以重要,其原因之一在于函数是程序执行过程中的主要模块单元。除了全局JavaScript代码是在页面构建的阶段执行的,所有的脚本代码都将在一个函数中执行。

首先在JavaScript中对象有以下几种常用功能

  • 对象可通过字面量来创建{}
  • 对象可以赋值给变量、数组项、其他对象的属性
  • 对象可以作为参数传递
  • 对象可以作为函数的返回值
  • 对象能够具有动态创建和分配的属性

其实,在JavaScript中,我们几乎能够用函数来实现同样的事

函数是第一类对象

JavaScript中函数能够拥有对象的所有能力,也因此函数可被作为任意其他类型对象对待,因此函数也能实现以下功能

  • 通过字面量创建
  • 赋值给变量、数组项、其他对象的属性
  • 作为参数传递
  • 作为函数的返回值
  • 具有动态创建和分配的属性

对象能做的任何一件事,函数也能做。函数也是对象,唯一的特殊之处在于它是可调用的

第一类对象的特点之一是它能够作为参数传入函数。对于函数而言,这项特性也表明:如果我们将某个函数作为参数传人另一个函数,传入函数会在应用程序执行的未来某个时间点才执行。这个概念就是回调函数

回调函数

每当我们建立一个将在随后调用的函数时,无论是在事件处理阶段通过浏览器还是通过其他代码,都是在建立一个回调。

举个🌰

const text = 'Domo arigato!';
console.log("Before defining functions");
​
function useless(ninjaCallback) {
  console.log("In useless function");
  return ninjaCallback();
}
​
function getText() {
  console.log("In getText function");
  return text;
}
​
console.log("Before making all the calls");
​
assert(useless(getText) === text, "The useless function works! " + text);
​
console.log("After the calls have been made"); 
​
function assert(value, text){
  value&&console.log(text)
}
​
// Before defining functions
// Before making all the calls
// In useless function
// In getText function
// The useless function works! Domo arigato!
// After the calls have been made

上面代码的执行:

  1. getText函数作为参数传入useless函数
  2. useless函数体内通过Callback参数可以获取getText函数的引用
  3. 回到函数的调用让getText函数得到执行,也就是我们传入的getText函数通过useless函数被回调

过程很容易,表现在JavaScript函数式本质是让我们把函数作为第一类对象。

JavaScript的重要特征之一是可以在表达式出现的任意位置创建函数,除此之外这种方式能使代码更紧凑和易于理解(把函数定义放在函数使用处附近)。当一个函数不会在代码的多处位置被调用时,该特性可以避免用非必须的名字污染全局命名空间。

函数作为对象的乐趣

在集合中存储函数使我们轻易管理相关联的函数。例如,某些特定情况下必须调用的回调函数。记忆函数能记住上次计算得到的值,从而提高后续调用的性能。

存储函数

某些场景中我们需要存储元素唯一的函数集合。当我们向这样的集合中添加函数是会面临两个问题:那个函数对于这个集合来说是一个新函数,从而需要被加入到该集合中?又是那个函数已经存在于集合中,从而不需要再次加入到集合中?

一般来说,管理回调函数集合时,我们并不希望存在重复函数,否则一个事件会导致一个回调函数被多次调用。

举个🌰

var store = {
  nextId: 1,
  cache: {},
  add: function(fn) {
    if (!fn.id) {
      fn.id = this.nextId++;
      this.cache[fn.id] = fn;
      return true;
    }
  }
};
​
function ninja(){}
​
assert(store.add(ninja),"Function was safely added.");
assert(!store.add(ninja),"But it was only added once.");
function assert(value, text){
  value&&console.log(text)
}

以上代码中,创建了一个对象赋值给变量store。这个对象有两个数据属性:nextId是下一个可用的id,cache为缓存已经保存的函数。函数通过add()方法添加到缓存中

在add函数内部,首先检查该函数是否存在id属性,如果当前函数已经存在id属性,则假设该函数被处理过了,从而忽略该函数,否则为该函数分配一个id。同时增加nextId,保证下个ID不会有重复,然后将该函数增加到cache上,id作为属性名。

代码运行时,程序尝试两次添加函数,而该函数只被添加一次到缓存中

自记忆函数

记忆化是一种构建函数的处理过程,能够记住(缓存)上次计算结果。当函数计算得到结果时就将该结果按照参数存储起来。采用这种方式,如果另外一个调用也使用相同的参数,我们则可以直接返回上次存储的结果而不是再计算一遍,这样避免即重负又复杂的计算可以显著的提高性能。

举个🌰

计算素数(素数:素数一般指质数。 质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。)

function isPrime(value) {
  if (!isPrime.answers) {
    isPrime.answers = {};
  }
​
  if (isPrime.answers[value] !== undefined) {
    return isPrime.answers[value];
  }
​
  var prime = value !== 1;
​
  for (var i = 2; i < value; i++) {
    if (value % i === 0) {
      prime = false;
      break;
    }
  }
​
  return isPrime.answers[value] = prime;
}
​
assert(isPrime(5), "5 is prime!" );
assert(isPrime(5), "5 is prime!" );
assert(isPrime.answers[5], "The answer was cached!" );
function assert(value, text){
  value&&console.log(text)
}
​
// 5 is prime!
// 5 is prime!
// The answer was cached!

以上代码执行流程:

  1. 在isPrime函数中,首先检查answers属性来确认是否创建了一个缓存,如果没有创建,则创建一个
  2. 只有第一次函数调用才会创建这个初始空对象,之后之歌缓存就已经存在了。然后检查传入的值是否存储在缓存中
  3. 这个缓存会针对参数中的值value来存储该值是否为素数(true或false)。如果在缓存中找到该值,函数会直接返回。
  4. 由于缓存是函数自身的一个属性,所以只要该函数还存在,缓存也就存在

函数定义

JavaScript中提供了几种定义函数的方式,可以分为四类

  • 函数定义和函数表达式(最常用)

    function myFun(a,b){return a+b}
    
  • 箭头函数

    (a,b)=>a+b
    
  • 函数构造函数(最不常用)

    new Function('a','b','return a+b')
    
  • 生成器函数—ES6新增功能,不同于普通函数的函数,在执行过程中,这种函数能够退出再重新进入

    function* myGen(){yield 1}
    

函数声明和函数表达式

JavaScript中定义函数最常用的方式是函数声明和函数表达式。

函数声明

以强制性的function开头,紧接着强制性的函数名,以及括号和括号内一系列逗号分隔的可选参数名。函数体是一列可以为空的表达式,这些表达式必须包含在花括号内。

每个函数声明必须包含一个条件:作为一个单独的JavaScript语句,函数声明必须独立

举个🌰

function samurai() {
  return "samurai here";
}
​
function ninja() {
  function hiddenNinja() {
    return "ninja here";
  }
​
  return hiddenNinja();
}

函数表达式

JavaScript中函数是第一类对象,除此之外也就意味着它们可以通过字面量创建,可以赋值给变量和属性,可以作为参数或返回值。

总是是其他表达式的一部分的函数叫做函数表达式(作为赋值表达式的右值,或者作为其他函数的参数)

举个🌰

var myFunc = function(){};
myFunc(function(){
  return function(){};
});

箭头函数

箭头函数是函数表达式的简化版。

举个🌰

var greet = name => "Greetings " + name;
​
var anotherGreet = function(name){
  return "Greetings " + name;
}

箭头函数的定义以一串可选参数名列表开头,参数名以逗号分隔,当中有一个参数时,括号不是必须的,否则必须有括号或者将参数包裹在括号内

箭头操作符后面有两种可选方式。

  • 如果要创建一个简单函数,那么可以把表达式放在这里,该函数的返回值即为此表达式的返回值
  • 当箭头函数没那么简单从而需要更多代码时,箭头操作符则可以跟一个代码块。当代码块中没有return语句,返回值时undefined,否则就是return表达式的值

函数的实参和形参

形参时定义函数时所列举的变量

实参时调用函数时所传递给函数的值

当函数调用时提供了一系列实参,这些实参就会以形参在函数中定义的顺序被赋值到形参上。

如果实参的数量大于形参,那么额外的实参不会赋值给任何形参

如果形参的数量大于实参,那么没有对应实参的形参会被设为undefined

剩余参数

为函数的最后一个命名参数前加上参略号(...)前缀,这个参数就变成了一个叫做剩余参数的数组。数组内包含着传入的剩余的参数

举个🌰

获取第一个参数的值与余下参数中最大值的数做乘法

function multiMax(first, ...remainingNumbers){
  var sorted = remainingNumbers.sort(function(a, b){
    return b - a;
  });
  return first * sorted[0];
}
assert(multiMax(3, 1, 2, 3) == 9, "3*3=9 (First arg, by largest.)");
function assert(value, text){
  value&&console.log(text)
}

以上代码中,用四个参数调用了multiMax函数,在multiMax函数内第一个参数的值3被复制给了第一个形参first。由于第二个形参时剩余参数,所以剩余所有的参数都被放在数组remainingNumbers中。

默认参数

函数中的形参可以设定默认的值,当对应的实参没有传入的时候,就会采用默认的值,当对应的实参传入时会采用传入的实参

举个🌰

function performAction(ninja, action = "skulking"){
  return ninja + " " + action;
}
assert(performAction("Fuma") === "Fuma skulking","The default value is used for Fuma");
​
assert(performAction("Yoshi") === "Yoshi skulking","The default value is used for Yoshi");
​
 assert(performAction("Hattori") === "Hattori skulking","The default value is used for Hattori");
​
assert(performAction("Yagyu", "sneaking") === "Yagyu sneaking","Yagyu can do whatever he pleases, even sneak!");
​
// The default value is used for Fuma
// The default value is used for Yoshi
// The default value is used for Hattori
// Yagyu can do whatever he pleases, even sneak!

以上代码可以看到创建默认参数的方式是为函数的形参赋值

总结

把JavaScript看作函数式语言你就能书写复杂的代码。作为第一类对象,函数和JavaScript其他对象一样,具有以下功能

  • 通过字面量创建
  • 赋值给变量或属性
  • 作为参数传递
  • 作为函数返回值结果
  • 赋值给属性和方法

回调函数是被代码在未知情况回来调用的函数

函数具有属性,而且这些属性能够被存储任何信息,可以利用这个特性存储函数,或者缓存结果减少不必要的计算

有很多不同类型的函数:函数声明、函数表达式、箭头函数、函数生成器

函数声明和函数表达式是两种最主要的函数类型。函数声明必须具有函数名,在代码中也必须作为一个独立的语句存在。函数表达式可以不必有函数名,但此时它就i必须作为其他语句的一部分。

箭头函数是JavaScript新增特性,可以使用更简洁的方式定义函数

形参试函数定义时列出的变量,实参试函数调用时传递的值

形参列表和实参列表长度可以不同

未赋值的形参求值为undefined

传入的额外的实参不会赋值给任何一个命名形参

剩余参数和默认参数是JavaScript的新特性

剩余参数-不与任何形参名相匹配的额外实参可以通过剩余参数来引用

默认参数-函数调用时,若没传入参数,默认参数可以给函数提供缺省的参数值