函数

  • 函数表达式、函数声明及箭头函数
  • 默认参数及扩展操作符
  • 使用函数实现递归
  • 使用闭包实现私有变量

函数是一等公民

函数实际上是对象,每个函数都是 Function 类型的实例。函数名是指向函数对象的指针。

函数定义

  • 函数声明
  • 函数表达式
  • 箭头函数
  • 使用 Function 构造函数 // 代码会被解释两次,不推荐

箭头函数

ECMAScript 6 新增了使用胖箭头 => 语法定义函数表达式的能力。 很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。 任何可以使用函数表达式的地方,都可以使用箭头函数。

用法:

// 箭头函数
let arrowSum = (a, b) => a + b

// 函数表达式
let funcExpressionSum = function(a, b) {
  return a + b
}

⚠️注意:

  • 箭头函数不能使用 arguments、super、new.target,也不能用于构造函数。
  • 箭头函数也没有 prototype 属性。

函数名

函数名就是指向函数的指针,所以一个函数可以有多个名称。

ECMAScript 6 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。

多数情况下,name 属性保存一个函数标识符,或一个字符串化的变量名。即使没有名称,也会显示空字符串。 如果使用 Function 构造函数创建,则会标识为 "anonymous"

const fn = () => {}
const fn2 = fn

console.log(fn.name, fn2.name)
// fn, fn

console.log((() => {}).name)
// ''

console.log((new Function()).name)
// anonymous

如果函数是一个获取函数、设置函数,或使用 bind() 实例化,那么标识符前面会加上一个前缀。

function foo() {}

console.log(foo.bind(null).name)
// bound foo

let dog = {
  years: 1,
  get age() {
    return this.years
  },
  set age(newAge) {
    this.years = newAge
  }
}

let propertyDescriptor = Object.getownPropertyDescriptor(dog, 'age')
console.log(propertyDescriptor.get.name) // get age
console.log(propertyDescriptor.set.name) // set age

理解参数

使用 function 关键字定义函数时,可以在函数内部访问 arguments 对象,从中取得传进来的每个参数值。 可以使用 arguments 的 length 属性检查传入的参数个数。

普通情况下,arguments 和 入参变量 使用不同的内存空间,但修改 arguments 中的值,若对应位置存在变量,则会同步对应位置变量的值。 严格模式下,arguments 和 函数入参变量 独立治理,不会同步。

箭头函数中的参数,只能通过定义的命名参数访问。

没有重载

ECMAScript 函数不能像传统编程那样重载。同名函数,后定义的会覆盖之前的。

默认参数值

ECMAScript 6 之后,支持显示定义默认参数。

使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数。 修改命名参数也不会影响 arguments 对象,使用以调用函数时传入的值为准。

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

参数初始化顺序遵循 暂时性死区 规则,前面定义的参数不能引用后面定义的。

参数扩展与收集

... 扩展操作符,函数支持可迭代对象的扩展操作符形式的参数

const values = [1,2,3,4,5]

function getSum() {
  let sum = 0
  for(let i = 0; i < arguments.length; i++) {
    sum += arguments[i]
  }
  return sum
}

getSum(-1, ...values)

收集参数

function getSum(first = 0, ...values) {
  return values.reduce((x, y) => x + y, first)
}

函数声明和函数表达式

JS 引擎在加载数据时对它们是区分的。

JS 引擎在任何代码执行前,会先读取函数声明,并在执行上下文中生成函数定义。 而函数表达式必须等待代码执行到它那一行,才会在执行上下文中生成函数定义。

函数声明提升:函数声明会在任何代码执行前先被读取并添加到执行上下文

函数作为值

函数是一等公民,函数名在 ECMAScript 中就是变量,所以函数可以用在任何变量可以使用的地方。 传参时接收一个函数参数,或返回值为一个函数,都是可以的。

函数内部

  • ES5 中 arguments 和 this
  • ES6 新增 new.target 属性

arguments

类数组对象,包含调用函数时传入的所有参数,只有以 function 关键字定义函数时才会有。

arguments 对象有一个 callee 属性,指向 arguments 对象所在函数的指针。

function factorial(num) {
  if (num < 1) {
    return 1
  } else {
    return num * factorial(num - 1)
  }
}

// 使用 arguments.callee 实现函数逻辑和函数名解耦
function newFactorial(num) {
  if (num < 1) {
    return 1
  } else {
    return num * arguments.callee(num - 1)
  }
}

this

标准函数中,this 引用的是把函数当成方法调用的上下文对象。

window.color = 'red'
let o = {
  color: 'blue'
}

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

sayColor() // red

o.sayColor = sayColor
o.sayColor() // blue

在箭头函数中,this 引用的是定义箭头函数的上下文,不会改变 this 指向。

window.color = 'red'
let o = {
  color: 'blue'
}

const 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()
// 返回调用了 inner 函数的 outer 函数源代码

new.target

ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通对象被调用。

ECMAScript 6 新增了检测函数是否使用了 new 关键字调用的 new.target 属性。如果函数是正常调用,则为 undefined;如果使用了 new 关键字,则 new.target 将引用被调用的构造函数。

function King() {
  if(!new.target) {
    throw 'King must be instantiated using "new"'
  }
  console.log('King instantiated using "new"')
}

new King() // King instantiated using "new"
King() // Error: King must be instantiated using "new"

函数属性与方法

每个函数都有两个属性:length、prototype。

  • length:保存函数定义的命名参数的个数
  • prototype:保存引用类型所有实例方法。不可枚举,for-in 不会返回

还有两个方法:

  • apply(): 设置调用函数时函数体内的 this 对象的值,接受两个参数:函数内 this 的值和一个参数数组
  • call(): 与 apply() 作用一致,接受参数不同,第一个参数为 this 值,后续参数逐个传递。

函数表达式

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

函数声明的关键特点是 函数声明提升,会在代码执行前获得定义。

函数表达式:看起来像一个普通的变量定义和赋值。创建一个匿名函数再赋值给变量。

递归

递归函数:一个函数通过名称调用自身。

function factorial(num) {
  if (num < 1) {
    return 1
  } else {
    return num * factorial(num - 1)
  }
}

let anotherFactorial = factorial
factorial = null
anotherFactorial(4) // 报错,因为内部使用的 factorial 已经设为 null

// 使用 arguments.callee 实现函数逻辑和函数名解耦
function newFactorial(num) {
  if (num < 1) {
    return 1
  } else {
    return num * arguments.callee(num - 1)
  }
}
let anotherNewFactorial = newFactorial
newFactorial = null
anotherNewFactorial(4) // 24

在递归函数中,arguments.callee 是引用当前函数的首选。

不过,严格模式下不能访问 arguments.callee,可以使用命名函数表达式。

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

尾调用优化

ECMAScript 6 规范新增了一项内存管理优化机制,让 JS 引擎在满足条件时可以重用栈帧。

尾调用优化,就是减少了栈帧数量。

当 外部函数在最后 return 内部函数 时,此时 内部函数的返回值 即为 外部函数的返回值,此时 JS 引擎会将第一个栈帧弹出,执行 内部函数,将内部函数栈帧推到栈上。

优化条件:

  • 代码在严格模式下执行
  • 外部函数的返回值是对尾调用函数的调用
  • 尾调用函数返回后不需要执行额外操作
  • 尾调用函数不是引用外部函数作用域中自由变量的闭包

闭包 Closure

闭包:引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

详细了解闭包 -> 点击此处

立即调用的函数表达式

IIFE:创建之后立即调用,执行后不留下对函数的引用。

私有变量

JS 没有私有对象属性的概念,可以使用闭包实现公共方法,访问位于包含作用域中定义的变量。