ES6 中的函数
普通函数的两种声明方式:
// 函数声明
function sum(a, b) {
return a + b
}
// 函数表达式
const sum = function (a, b) {
return a + b
}
箭头函数:
const sum = (a, b) => a + b
ES6 函数的变化
- 默认参数使得 arguments 和具名参数分离,arguments 始终是实参的初始情况
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 始终反映初始实参数情况,无视剩余参数。
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 进一步明确了函数的名称。
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 属性 -- 必需参数的个数
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]] 属性
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 指向该对象;
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。
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 --- 最近的非箭头函数决定
const myObject = {
myMethod(items) {
console.log(this) // myObject
const callback = () => {
console.log(this) // myObject
}
items.forEach(callback)
},
}
myObject.myMethod([1, 2, 3])
下面的代码输出多少?
let n = 0
const obj = {
n: 1,
fn: () => {
console.log(this.n)
},
}
obj.fn() // undefined
原因:箭头函数的 this 由最近的非箭头函数决定。fn 最近的非箭头函数是脚本,this 是 window,而 let 变量不会放在 window 对象上,故输出 undefined。
var n = 0
const obj = {
n: 1,
fn: () => {
console.log(this.n)
},
}
obj.fn() // 0
类使用
fields
语法声明的方法,this 绑定到当前对象。
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 语法:
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,比如 事件处理器;
const button = document.getElementById('myButton')
button.addEventListener('click', () => {
console.log(this === window) // => true
this.innerHTML = 'Clicked button'
})
正确的写法:
const button = document.getElementById('myButton')
button.addEventListener('click', function () {
console.log(this === window) // button
this.innerHTML = 'Clicked button'
})
- 复杂函数。
const multiply = (a, b) => (b === undefined ? b => a * b : a * b)
const double = multiply(2)
double(3) // => 6
multiply(2, 3) // => 6
复杂函数使用箭头函数,可读性差,写成普通函数更加可读。
function multiply(a, b) {
if (b === undefined) {
return function (b) {
return a * b
}
}
return a * b
}
- ts 中能不使用箭头函数的地方,尽量避免使箭头函数
原因:ts 中要求类型,类型使得函数变得复杂,箭头函数会让有类型的函数可读性降低,尤其当参数超过 3 个。
尾递归
当满足以下条件时,尾调用优化会清除当前栈帧并再次利用它,而不是为尾调用创建新的栈帧:
尾调用不能引用当前栈帧中的变量(意味着该函数不能是闭包);
进行尾调用的函数在尾调用返回结果后不能做额外操作;
尾调用的结果作为当前函数的返回值---返回一个函数的调用。
function doSomething() {
// 启用尾递归优化
return doSome()
}
不返回函数调用,尾递归优化失效:
function doSomething() {
// 尾递归优化失效
doSome()
}
尾调用返回后操作有其他操作,尾递归优化失效:
function doSomething() {
// 尾递归优化失效
return 1 + doSome()
}
// 或者 保存尾调用的返回值,再返回
function doSomething() {
// 尾递归优化失效
const v = doSome()
return v
}
有闭包,尾调用失效:
function doSomething() {
const v = 1
const fn = () => v // 闭包
return fn()
}
尾递归优化在递归中的性能提升最明显:
function factorial(n) {
if (n <= 1) {
return 1
} else {
// 未被优化:在返回之后还要执行乘法
return n * factorial(n - 1)
}
}
尾递归优化:
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p
} else {
const result = n * p
return factorial(n - 1, result)
}
}
启用尾递归优化后,性能提高了一个数量级。
关键:将递归结果传入递归函数。