Skip to content
On this page

ES6 中的函数

普通函数的两种声明方式:

js
// 函数声明
function sum(a, b) {
  return a + b
}
// 函数表达式
const sum = function (a, b) {
  return a + b
}

箭头函数:

js
const sum = (a, b) => a + b

ES6 函数的变化

  • 默认参数使得 arguments 和具名参数分离,arguments 始终是实参的初始情况
js
function mixArgs(first, second = 2) {
  console.log(arguments.length) // 1
  console.log(first) // a
  console.log(arguments[0]) // a
  console.log(first === arguments[0]) //  true
  console.log(second === arguments[1]) // true
  first = 'c'
  second = 'd'
  console.log(arguments[0]) // a
  console.log(first === arguments[0]) // false
  console.log(second === arguments[1]) // false
}
mixArgs('a', 'b')
  • 剩余参数对 arguments 的影响

arguments 始终反映初始实参数情况,无视剩余参数。

js
function checkArgs(...args) {
  console.log(args.length)
  console.log(arguments.length)
  console.log(args[0], arguments[0])
  console.log(args[1], arguments[1])
}
checkArgs('a', 'b')
  • 函数名称 --- name 属性

匿名函数调试困难,es6 进一步明确了函数的名称。

js
function a() {}
const b = function () {}
const c = function d() {}
console.log(a.name) // a
console.log(b.name) // b
console.log(c.name) // d
const person = {
  get firstName() {
    return 'Nicholas'
  },
  sayName: function () {},
}
const description = Object.getOwnPropertyDescriptor(person, 'firstName')
console.log(description.get.name) // get firstName
console.log(a.bind().name) // bound a
// eslint-disable-next-line no-new-func
console.log(new Function().name) // anonymous

① 函数表达式中的函数有名字,优先级高于接收函数的变量名称;

② getter setter 带有 set get 以区别于普通函数;

③ bind 返回的函数名称前有 bound;

④ new Function 创建的函数为 anonymous。

  • length 属性 -- 必需参数的个数
js
function a(b, c, d = 0, ...rest) {
  console.log(a.length) // 2
  console.log(arguments.length) // 5
}
a(1, 2, 3, 4, 5)

当函数没有默认参数或者剩余参数时,arguments.length 和 name.length 相等。

函数的双重职责

① 普通函数,普通函数调用时,调用的是 [[Call]] 属性

② 构造函数,new 调用时,调用的是 [[Constructor]] 属性

js
function Person(name, age) {
  this.name = name
  this.age = age
}
const p = new Person('JackChou', 25)
console.log(p)
console.log(Person)
console.log(Person.prototype.constructor)

new 做了哪些事情?

① 新建一个对象,设置该对象的属性,返回对象;

② 将对象的原型指向构造函数的原型对象;

③ 构造函数的 this 指向该对象;

js
function Person(name, age) {
  const p = {}
  p.name = name
  p.age = age
  // Person.call(p) // 爆栈
  return p
}
const p = Person('JackChou', 29)
Person.call(p)
p.__proto__ = Person.prototype
console.log(p)
console.log(Person.prototype)

如何判断使用 new 调用?

this instanceof Person

可靠吗? 不可靠,this 可改变。

new.target 判断 new 调用。

new 调用,new.target 是构造函数,当成普通函数调用,new.target 是 undefined。

js
function Person(name, age) {
  if (new.target === void 0) {
    throw new Error('Must call Person with new!')
  }
  this.name = name
  this.age = age
}
const p = new Person('JackChou', 25)
console.log(p)
console.log(Person)

只能在函数内部使用 new.target,否则报错。

箭头函数和普通函数的重要区别

  • 没有 this、arguments、super、new.target,它们由最靠近箭头函数的非箭头函数决定;
  • 不能作为构造函数:没 [[Constructor]];
  • 没有原型:不能作为构造函数,自然没有prototype属性了;
  • this 不能被改变,这点很用,不再为 this 苦恼
  • 参数名字不能重复,可使用剩余参数代替 arguments;
  • 不能定义生成器。

箭头函数的 this --- 最近的非箭头函数决定

js
const myObject = {
  myMethod(items) {
    console.log(this) // myObject
    const callback = () => {
      console.log(this) // myObject
    }
    items.forEach(callback)
  },
}

myObject.myMethod([1, 2, 3])

下面的代码输出多少?

js
let n = 0
const obj = {
  n: 1,
  fn: () => {
    console.log(this.n)
  },
}
obj.fn() // undefined

原因:箭头函数的 this 由最近的非箭头函数决定。fn 最近的非箭头函数是脚本,this 是 window,而 let 变量不会放在 window 对象上,故输出 undefined。

js
var n = 0
const obj = {
  n: 1,
  fn: () => {
    console.log(this.n)
  },
}
obj.fn() // 0

类使用fields语法声明的方法,this 绑定到当前对象。

js
class Person {
  constructor(name) {
    this.name = name
  }

  sayHi() {
    console.log(this.name)
  }
}
const p = new Person('JackChou')
p.sayHi() // JackChou
setTimeout(p.sayHi, 1000) // undefined

改成 fields 语法:

js
class Person {
  constructor(name) {
    this.name = name
  }

  sayHi = () => {
    console.log(this.name)
  }
}
const p = new Person('JackChou')
p.sayHi() // JackChou
setTimeout(p.sayHi, 1000) // JackChou

何时不该使用箭头函数?

  • 对象方法,应该使用对象属性简写。

例子在上面。

类中的实例方法应该使用箭头函数,即 fields 语法。

  • 原型中、构造函数;

箭头函数没有自己的 this。

  • 回调函数的动态 this,比如 事件处理器;
js
const button = document.getElementById('myButton')
button.addEventListener('click', () => {
  console.log(this === window) // => true
  this.innerHTML = 'Clicked button'
})

正确的写法:

js
const button = document.getElementById('myButton')
button.addEventListener('click', function () {
  console.log(this === window) // button
  this.innerHTML = 'Clicked button'
})
  • 复杂函数。
js
const multiply = (a, b) => (b === undefined ? b => a * b : a * b)
const double = multiply(2)
double(3) // => 6
multiply(2, 3) // => 6

复杂函数使用箭头函数,可读性差,写成普通函数更加可读。

js
function multiply(a, b) {
  if (b === undefined) {
    return function (b) {
      return a * b
    }
  }
  return a * b
}
  • ts 中能不使用箭头函数的地方,尽量避免使箭头函数

原因:ts 中要求类型,类型使得函数变得复杂,箭头函数会让有类型的函数可读性降低,尤其当参数超过 3 个。

尾递归

当满足以下条件时,尾调用优化会清除当前栈帧并再次利用它,而不是为尾调用创建新的栈帧:

  1. 尾调用不能引用当前栈帧中的变量(意味着该函数不能是闭包);

  2. 进行尾调用的函数在尾调用返回结果后不能做额外操作;

  3. 尾调用的结果作为当前函数的返回值---返回一个函数的调用。

js
function doSomething() {
  // 启用尾递归优化
  return doSome()
}

不返回函数调用,尾递归优化失效:

js
function doSomething() {
  // 尾递归优化失效
  doSome()
}

尾调用返回后操作有其他操作,尾递归优化失效:

js
function doSomething() {
  // 尾递归优化失效
  return 1 + doSome()
}
// 或者 保存尾调用的返回值,再返回
function doSomething() {
  // 尾递归优化失效
  const v = doSome()
  return v
}

有闭包,尾调用失效:

js
function doSomething() {
  const v = 1
  const fn = () => v // 闭包
  return fn()
}

尾递归优化在递归中的性能提升最明显:

js
function factorial(n) {
  if (n <= 1) {
    return 1
  } else {
    // 未被优化:在返回之后还要执行乘法
    return n * factorial(n - 1)
  }
}

尾递归优化:

js
function factorial(n, p = 1) {
  if (n <= 1) {
    return 1 * p
  } else {
    const result = n * p
    return factorial(n - 1, result)
  }
}

启用尾递归优化后,性能提高了一个数量级。

关键:将递归结果传入递归函数。

参考

When 'Not' to Use Arrow Functions

Released under the MIT License.