Skip to content

函数

1. 定义函数

1.1 函数声明

  • 函数声明语句会被提升至当前作用域顶部,因此函数调用代码可以出现在函数声明之前。
  • 如果没有 return 语句或者 return 后没跟表达式,则函数返回 undefined
javascript
function printprops(o) {
  for (let p in o) {
    console.log(`${p}: ${o[p]}\n`);
  }
}

1.2 函数表达式

  • 函数表达式与函数声明类似,区别在于函数声明一定要有函数名,函数表达式函数名是可选的,一般会没有函数名,并将函数表达式赋值给变量或常量。
javascript
const square = function(x) {
  return x * x;
}
  • 函数表达式可以有函数名,一般在调用自己(递归)时使用,但不建议,因为可以用赋值的变量来调用,更加简洁。
javascript
const f = function fact(x) {
  if (x <= 1) return 1;
  else return x * fact(x - 1);
}
// 等价于
const f = function(x) {
  if (x <= 1) return 1;
  else return x * f(x - 1);
}
  • 函数表达式不能在定义之前被调用,因为在被求值之前是不存在的。

1.3 箭头函数

ES6 提出的箭头函数更加简洁,不需要 function 关键字和函数名:

javascript
const sum = (x, y) => { return x + y; }

如果只有一个 return 语句,则可以省略 return 关键字及花括号:

javascript
const sum = (x, y) => x + y;

如果只有一个参数,则可以省略参数列表的圆括号:

javascript
const polynomial = x => x * x + 2 * x + 3;

注意

  • 无参情况下圆括号不能省略。
  • 函数体只有一个 return 语句且返回对象字面量时,要么不省略 return 和 花括号,要么把对象字面量用圆括号包裹后返回。
javascript
const f = x => { return { value: x } }
const g = x => ({ value: x}) // 圆括号包裹

特性

  • 箭头函数从环境继承 this 的值,而不是像其它函数那样定义自己的调用上下文。
  • 箭头函数没有 prototype 属性,因此不能作为构造函数。

1.4 嵌套函数

重要的是理解变量作用域规则:作用域链逐层寻找

2. 调用函数

2.1 函数调用

  • 在一次调用中,每个实参表达式都会被求值,结果作为实参,并赋值给函数中定义的命名形参。
  • 在 ES2020 中,可以使用条件式调用函数,从而只在函数不是 nullundefined 时调用函数。
javascript
const a = sum?.(1, 2);
  • 非严格模式下函数调用上下文(this 值)是全局对象(globalThis),严格模式下,调用上下文是 undefined。对于箭头函数来说,总是继承自身定义所在环境的 this 值。

2.2 方法调用

函数作为对象的属性就是方法,调用与普通函数类似,区别在于调用上下文,方法调用的 this 就是该调用对象,在方法体内可以通过 this 引用该对象。

javascript
let calculator = {
  operand1: 1,
  operand2: 1,
  add() {
    this.result = this.operand1 + this.operand2; // this 指向包含对象
  }
}
calculator.add()
calculator.result // 2

一般方法通过点号进行属性访问,但使用方括号的属性访问表达式也可以实现方法调用:

javascript
o["m"](x, y) // o.m(x, y) 的另一种写法

除了箭头函数,嵌套函数不会继承包含函数的 this 值,如果嵌套函数被当作方法调用,this 值就是调用它的对象,如果被当作函数来调用,this 值要么是全局对象,要么是 undefined

javascript
let o = {
  m: function() {
    let self = this;
    this === o // true
    f();

    function f() {
      this === o // false: this 是全局对象或undefined
      self === o // true
    }
  }
}

上面代码演示了一种常见技巧:把外层函数的 this 值赋值给另一个变量,并在嵌套内部函数内部使用该变量而非 this

2.2.1 方法调用链

前提:如果方法返回对象,基于该方法的返回值还可以调用其它方法。

说明

方法调用链是一种编程风格,即对象的方法统一返回 this,则可以连续调用该对象的方法。

javascript
new Square().x(100).y(100).size(50).outline('red').fill('blue').draw()

2.3 构造函数调用

  • 如果函数或方法调用前加上 new 关键字,就是构造函数调用。
  • 构造函数调用时会先创造一个空对象,这个对象继承构造函数 prototype 属性指定的对象,构造函数中的 this 指向新创建的对象。
  • 构造函数不需要 return,默认会返回新创建的对象,如果用 return 显式返回了一个对象,则该对象替换新创建的对象被返回,如果 return 非对象值,则仍返回新创建的对象。

2.4 间接调用

  • JavaScript 函数有两个方法 call()apply(),通过它们可以间接调用函数。
  • call()apply() 允许指定调用时的 this 值,意味着可以将任何函数作为任何对象的方法来调用。

2.5 隐式调用

  • 如果对象有获取方法或设置方法,则查询或设置其属性值可能会调用这些方法。
  • 当对象在字符串上下文中使用时(比如当拼接对象与字符串时),会调用对象的 toString() 方法。类似地,当对象用于数值上下文时,则会调用它的 valueOf() 方法。
  • 遍历可迭代对象时,会涉及一系列方法调用。
  • 标签模板字面量是一种伪装的函数调用。
  • 代理对象。

3. 函数实参与形参

3.1 可选形参与默认值

  • 当调用函数时传入的实参少于声明的形参,额外的形参会获得默认值,通常是 undefined
  • 在设置有可选参数的函数时,一定要把可选参数放在参数列表的最后,这样调用时才能省略。
  • ES6 之后,可以直接在参数列表中给参数指定默认值。
  • 形参默认值可以是常量,也可以是变量或函数调用计算默认值。
javascript
// 不传 a 则创建个新数组并返回
function getPropertyNames(o, a = []) {
  for (let property in o) a.push(property);
  return a;
}

3.2 剩余形参与可变长度实参列表

剩余形参的作用:可以在调用时传入比形参多任意数量的实参。

javascript
function max(first = -Infinity, ...rest) {
  let maxValue = first;
  for (let n of rest) {
    if (n > maxValue) {
      maxValue = n;
    }
  }
  return maxValue;
}

max(1, 10, 100, 2, 3, 1000, 4, 5, 6) // 1000
  • 剩余形参前面有三个点 ...,且必须在参数列表的最后。传入的实参首先赋值给非剩余形参,然后剩余的实参保存到一个数组中赋值给剩余形参。
  • 在函数体内,剩余形参的值永远是数组。

3.3 Arguments 对象

  • Arguments 对象是一个类数组对象,它保存了传给函数的所有实参值。
  • 严格模式下,arguments 会被当作保留字。
javascript
 function max(x) {
   let maxValue = -Infinity;
   for (let i = 0; i < arguments.length; i++) {
     if (arguments[i] > maxValue) {
       maxValue = arguments[i];
     }
   }
   return maxValue;
 }

max(1, 10, 100) // 100

3.4 函数调用中使用拓展操作符

在函数调用时,可以使用展开运算符 ... 展开可迭代对象。

javascript
let numbers = [5, 2, 10];
Math.min(...numbers) // 2

3.5 把函数实参解构为形参

如果函数形参名包含在方括号中,那么调用时传入的数组实参会被解构赋值为单独的命名形参:

javascript
function vectorAdd([x1, y1], [x2, y2]) {
  return [x1 + x2, y1 + y2];
}

vectorAdd([1, 2], [3, 4]) // [4, 6]

类似的,如果函数需要一个对象实参,也可以把传入的对象解构赋值给形参:

javascript
function vectorMultiply({x, y}, scalar) {
  return { x: x * scalar, y: t * scalar }
}
vectorMultiply({ x: 1, y: 2 }, 2) // { x: 2, y: 4 }

在解构赋值中可以为形参设置默认值:

javascript
function vectorMultiply({x, y, z = 0}, scalar) {
  return { x: x * scalar, y: t * scalar, z: z * scalar }
}
vectorMultiply({ x: 1, y: 2 }, 2) // { x: 2, y: 4, z: 0 }

同正常数组和对象解构一样,形参的解构也可以使用剩余参数:

javascript
// 需注意解构中剩余语法与形参中剩余参数的区别
function f([x, y, ...records], ...rest) {
  return [x + y, ...rest, ...records]; // 这里是拓展操作符
}
f([1, 2, 3, 4], 5, 6) // [3, 5, 6, 3, 4]

// 对象解构使用剩余语法,ES2018开始支持
function vectorMultiply({x, y, z = 0, ...props}, scalar) {
  return { x: x * scalar, y: t * scalar, z: z * scalar, ...props }
}
vectorMultiply({ x: 1, y: 2, w: -1 }, 2) // { x: 2, y: 4, z: 0, w: -1 }

3.6 参数类型

JavaScript 会自动进行类型转换,但最好在一开始就判断参数类型,避免因类型不对而报错,减少不必要的运算。

javascript
function sum(a) {
  let total = 0;
  for (let ele of a) {
    if (typeof ele !== 'number') {
      throw new TypeError('element must be number')
    }
    total += ele;
  }
  return total;
}

sum([1, 2, 3]) // 6
sum(['a', 1, 2]) // TypeError

4. 函数作为值

  • 函数可以作为值赋值给其它变量或传递给函数调用。
  • 函数甚至可以没有名字,匿名函数也可以调用(立即调用表达式):
javascript
((x) => x * x)(3); // 9

let a = [x => x * x, 20];
a[0](a[1]); // 400

4.1 自定义函数属性

如果一个函数需要一个静态变量,且每次调用时都能访问到该变量,通常可以将该变量定义为函数自身的一个属性:

javascript
uniqueInteger.counter = 0;

function uniqueInteger() {
  return uniqueInteger.counter++;
}

uniqueInteger(); // 0
uniqueInteger(); // 1

5. 函数作为命名空间

函数体内声明的变量在函数外部不可见。可以把函数作为临时的命名空间,保证其中定义的变量不会污染全局命名空间。

6. 闭包

  • 函数对象与作用域组合起来解析函数变量的机制叫闭包,更简单点说就是能够访问其它函数内部变量的函数。
  • 闭包由两部分组成:一个是函数本身,一个是函数创建时的词法环境,这个环境包括了闭包创建时所能访问的所有局部变量。
  • 闭包的作用是读取函数内部的变量,同时让这些变量的值始终保存在内存中,不会在外部函数调用后自动清除。
javascript
// 利用闭包改写4.1的函数,防止自定义属性被恶意修改
let uniqueInteger = (function() {
  let counter = 0;
  return function() {
    return counter++;
  }
})();

uniqueInteger(); // 0
uniqueInteger(); // 1
javascript
function counter() {
  let n = 0;
  return {
    count: function() { return n++; },
    reset: function() { n = 0; }
  }
}

let c = counter(), d = counter(); // 每次调用都会创建新的作用域,互相独立
c.count() // 0
d.count() // 0
c.reset() // reset 和 count 共享状态
c.count() // 0
d.count() // 1

7. 函数属性、方法和构造函数

7.1 length

只读属性,表示参数列表中的形参个数,不计算剩余形参。

7.2 name

只读属性,即函数定义时使用的名字,如果是未命名的函数,返回第一次创建时赋值的变量名。

7.3 prototype

除了箭头函数外,所有函数都有 prototype 属性。

7.4 call() 和 apply()

  • call()apply() 允许一个对象间接调用一个函数,就仿佛该函数是该对象的一个方法。
  • call()apply() 第一个参数是要调用这个函数的对象,在函数体内变成 this 的值。
  • 如果对箭头函数调用这两个方法,则传入的第一个参数会被省略。
  • 对于 call() 方法,除了第一个参数,后续的参数都会传给被调用的函数,apply() 方法与 call() 类似,只不过传给函数的参数需要以数组的形式提供:
javascript
f.call(o, 1, 2);
f.apply(o, [1, 2]);

// 通过apply将数组传给需要多个参数的函数
let bigger = Math.max.apply(Math, [1, 2, 3])

7.5 bind()

  • bind() 方法的主要目的是把函数绑定到对象身上,即确定函数内部 this 值。如果在函数 f 上调用 bind() 并传入对象 o,会返回一个新函数。该新函数内部 this 就指向 o
javascript
function f(y) { return this.x + y }
let o = { x: 1 }
let g = f.bind(o);
g(2); // 3
let p = { x: 10, g }
p.g(2) // 3, g 仍然绑定在 o 上
  • 传递给新函数的所有参数都会传给原始函数。
  • bind() 也可以执行“部分应用”,即在第一个参数之后传给 bind() 的参数也会随 this 值一起被绑定,这种技术有时候也称为柯里化:
javascript
let sum = (x, y) => x + y;
let succ = sum.bind(null, 1); // 把第一个参数绑定为1
succ(2) // 3 

function f(y, z) {
  return this.x + y + z;
}
let g = f.bind({ x: 1}, 2);
g(3) // 6

7.6 toString()

函数也有 toString() 方法,ECMAScript 规范要求该方法返回一个符合函数声明的字符串。

7.7 Function 构造函数

Function 构造函数可以用来创建新函数,通过构造函数创建的是匿名函数:

javascript
const f = new Function('x', 'y', 'return x + y;')

Function 构造函数可以接收任意多个字符串参数,最后一个参数是函数体文本,前面的参数都是形参名。如果函数没有参数,可以只给构造函数传一个参数作为函数体。

注意

  • Function 构造函数允许在运行时动态创建和编译函数。
  • 每次调用时都会创建一个新的函数对象。
  • 创建的函数不使用词法作用域,始终编译为同顶级函数一样。

8. 函数式编程

8.1 高阶函数

高阶函数就是操作函数的函数,它接收一个或多个函数作为参数,并返回一个新函数。

javascript
function not(f) {
  return function(...args) {
    let result = f.apply(this, args);
    return !result;
  }
}

const even = x => x % 2 === 0;
const odd = not(even); // 确定数值是不是偶数的新函数
[1, 1, 3, 5, 5].every(odd) // true

8.2 函数的部分应用

  • 通过 bind() 方法可以实现函数的部分应用,即将一个函数绑定到一个对象上,并指定部分参数,返回一个新函数。
  • bind() 方法在左侧应用参数,也可以通过高阶函数实现左侧或右侧的部分应用:
javascript
// 传给这个函数的参数会部分应用到左边
function partialLeft(f, ...outArgs) {
  return function (...innerArgs) {
    let args = [...outArgs, ...innerArgs]
    return f.apply(this, args);
  }
}
// 传给这个函数的参数会部分应用到右边
function partialRight(f, ...outArgs) {
  return function (...innerArgs) {
    let args = [...innerArgs, ...outArgs]
    return f.apply(this, args);
  }
}

8.3 函数记忆

在函数式编程中,如果函数拥有缓存数据,则称为函数记忆,如:

javascript
function memoize(f) {
  // 缓存
  const cache = new Map();

  return function(...args) {
    let key = args.length + args.join('+');
    if (cache.has(key)) {
      return cache.get(key);
    } else {
      let result = f.apply(this, args);
      cache.set(key, result);
      return result;
    }
  }
}

说明

memoize 函数创建了一个 map 对象作为缓存,并赋值给一个局部变量,在闭包中成为被返回函数的私有变量。