Appearance
函数扩展
参数默认值
ES6 - 定义函数可以指定默认值。
- 参数变量是默认声明,不可用
let或const再次声明 - 函数不能有同名参数
- 参数为惰性求值,每次都要重新计算
- 传入
undefined,将触发该参数等于默认值,null则没有这个效果
js
let x = 99
function foo(p = x + 1) {
console.log(p)
}
foo() // 100
x = 100
foo() // 101参数默认值与解构
下面写法:不能省略第二个参数,对象中的属性有默认值,但是对象本身没有
js
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method)
}
// 正确写法
// { body = '', method = 'GET', headers = {} } = {}js
// 解构的值赋予默认值
{x = 0, y = 0} = {} // {} - 0,0
// 解构的对象赋予默认值
{x, y} = { x: 0, y: 0 } // {} - undefined,undefined参数默认值位置
定义了默认值的参数,应该是尾参数。非尾参数,用undefined占位
js
function f( x = 1, y ){}
f() // 1 undefined
f(,1) // 报错
f(undefined,1) // 1 1
f(2,1) // 2 1函数 length 属性
函数length属性:将返回没有指定默认值的参数个数。
rest参数也不会计入 length。
默认值参数不是尾参数,length 不会计入后面的参数
js
;(function (a) {}
.length(
// 1
function (a = 5) {}
)
.length(
// 0
function (a, b, c = 5) {}
)
.length(
// 2
function (...args) {}
)
.length(
// 0
function (a = 0, b, c) {}
)
.length(
// 0
function (a, b = 1, c) {}
).length) // 1作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。
js
// y = x 形成 单独作用域
// 实际执行 let y = x
function f(y = x) {
let x = 0
console.log(x, y)
}
f(1)js
function bar(func = () => foo) {
let foo = 'inner'
console.log(func())
}
bar() // ReferenceError: foo is not definedy默认值为匿名函数,其中x指向同作用域第一个参数x。
foo函数内部变量 x 是与 foo 参数变量 x 不同
执行 y() 只会影响参数变量,外部变量和内部变量都不会改变
js
var x = 1
function foo(
x,
y = function () {
x = 2 // closure
}
) {
// local x - undefined
var x = 3
// local x - 3
y()
// local x - 3
console.log(x)
}
foo()
// global x - 1与上面不同的是,没有内部变量,此时x=3中的x为参数变量。
js
var x = 1
function foo(
x,
y = function () {
x = 2
}
) {
// x - undefined
x = 3
// x - 3
y()
// x - 2
console.log(x)
}
foo()
x // x - 1应用
参数默认值在运行时执行,利用参数默认值,让不填参数就报错
js
function throwIfMissing() {
throw new Error('Missing parameter')
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided
}
foo()
// Error: Missing parameterrest 参数
ES6 中,引入 rest 参数 (...变量名),用于获取多余参数,代替arguments
arguments:类数组对象
rest:数组,无需转换
js
// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort()
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort()rest 参数后不能有其他参数,即rest 参数为最后一个参数
js
function f(a, ...b, c) {} // 报错函数length属性,不包括 rest 参数
js
;(function (a) {}
.length(
// 1
function (...a) {}
)
.length(
// 0
function (a, ...b) {}
).length) // 1严格模式
ES5,内部可以设定严格模式
js
function doSomething(a, b) {
'use strict'
// code
}ES2016,函数参数使用默认值,解构赋值,扩展运算符后,不能显示定义为 严格模式
严格模式适用于 函数内部和函数参数
原因:只有在函数体内才能知道是否以严格模式执行,但参数执行先于函数内部
js
// 报错
function doSomething(value = 070) {
'use strict';
return value;
}报错分析:严格模式下不能使用前缀 0表示八进制。执行value=070,进入函数内部,发现严格模式执行,所以报错。
解决方案
全局性严格模式
js
'use strict'
function doSomething(value = 070) {
return value
}包裹在无参数的立即执行函数里面
js
const doSomething = (function () {
'use strict'
return function (value = 42) {
return value
}
})()name 属性
匿名函数
js
var f = function () {}
// ES5
f.name // ""
// ES6
f.name // "f"具名函数
赋值给变量,name属性为函数名(ES5,ES6 相同)
js
const bar = function baz() {}
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"对象方法
使用了取值函数(getter)和存值函数(setter),函数名前加 get 和 set
js
const obj = {
get foo() {},
set foo(x) {}
}
obj.foo.name
// TypeError: Cannot read property 'name' of undefined
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo')
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"对象方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述
js
const key1 = Symbol('description')
const key2 = Symbol()
let obj = {
[key1]() {},
[key2]() {}
}
obj[key1].name // "[description]"
obj[key2].name // ""构造函数:返回函数实例,name属性为anonymous
js
new Function().name // "anonymous"bind:返回函数,name属性加上bound前缀
js
function foo() {}
foo
.bind({})
.name(
// "bound foo"
function () {}
)
.bind({}).name // "bound "箭头函数
ES6:箭头(=>)定义函数
基础用法
函数参数:不需要参数或多个参数,圆括号代表参数部分
js
;() => 5
;(x, y) => x + y代码块:多于一条语句,对象外加{},并且 return 语句返回
js
;(num1, num2) => {
return num1 + num2
}返回对象:因为大括号解析为代码块,箭头函数直接返回一个对象时,外层加上()
js
() => { id: id, name: "Temp" } // 报错
() => ({ id: id, name: "Temp" }) // 正确特殊情况:{}被解析为代码块,函数内部执行 a:1,无返回值
js
let foo = () => {
a: 1
}
foo() // undefined无需返回值:单行语句,且不需要返回值
js
let fn = () => void doesNotReturn()解构、rest 参数
js
;({ first, last }) => first + ' ' + last
;(...nums) => nums注意点
- this 指向固定,定义时所在对象,而不是使用者所在对象
- 箭头函数没有自己的 this,导致内部 this 指向外部 this。没有 this,不能用 call apply bind 方法改变 this 指向
- 不可做构造函数,不可以使用
new命令 - 没有
arguments对象(不存在),使用rest参数代替 - 不可以使用
yield命令,因此箭头函数不能用作 Generator 函数 super、new.target在箭头函数中也是不存在,指向外层变量typeof判断箭头函数 结果为functioninstanceof判断是否Function实例 结果为 true
js
function Timer() {
this.s1 = 0
this.s2 = 0
// 箭头函数 this -> timer
setInterval(() => this.s1++, 1000)
// 普通函数 this -> window
setInterval(function () {
this.s2++
}, 1000)
}
var timer = new Timer()
setTimeout(() => console.log('s1: ', timer.s1), 3100)
setTimeout(() => console.log('s2: ', timer.s2), 3100)
// s1: 3
// s2: 0下面代码有几个this?
js
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id)
}
}
}
}
var f = foo.call({ id: 1 })
var t1 = f.call({ id: 2 })()() // id: 1
var t2 = f().call({ id: 3 })() // id: 1
var t3 = f()().call({ id: 4 }) // id: 1只有一个this,即foo的this,无论怎么嵌套,都会去获取 foo 中的 id
不适用场合
以下两种方法应使用传统写法,而非箭头对象
定义对象的方法
对象中使用箭头函数:函数会指向对象所在作用域,此处是window,普通函数指向obj
js
globalThis.s = 21 // globalThis 此处 -> window
const obj = {
s: 42,
m: () => console.log(this.s)
}
obj.m() // 21动态 this
this 应该动态指向,使用箭头函数后,this 固化。
js
var button = document.getElementById('press')
button.addEventListener('click', () => {
this.classList.toggle('on')
})嵌套箭头函数
部署管道机制(pipeline)
js
const pipeline =
(...funcs) =>
(val) =>
funcs.reduce((a, b) => b(a), val)[?] λ 演算
js
// λ演算的写法
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
// ES6的写法
var fix = f => (x => f(v => x(x)(v)))
(x => f(v => x(x)(v)));尾调用优化
尾调用
尾调用(Tail Call):函数的最后一步是调用另一个函数
js
function f(x) {
return g(x)
}尾调用不一定出现在函数尾部,只要是最后一步操作
js
function f(x) {
if (x > 0) {
return m(x)
}
return n(x)
}非尾调用
js
// 情况一 - 函数之后还有别的操作
function f(x) {
let y = g(x)
return y
}
// 情况二 - 函数之后还有别的操作
function f(x) {
return g(x) + 1
}
// 情况三
function f(x) {
g(x)
// return undefined
}尾调用优化
调用帧:函数调用会在内存形成调用记录,保存调用位置和内部变量信息。
调用栈:函数 A 调用 B,会在 A 上方形成 B 调用帧。B 返回结果后,B 调用帧消失。如果 B 调用 C,会 B 上方形成 C 调用帧。所有调用帧组成调用栈。
尾调用是函数最后一步操作,无需保留调用帧。因为调用位置、内部变量等信息都不会再用到。
尾调用优化(Tail call optimization):保留内层函数的调用帧。如果所有函数都是尾调用,每次执行时,调用帧只有一项,这将大大节省内存。
注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
尾递归
问题:递归需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)
解决:函数尾调用自身,就称为尾递归。
尾递归关键:每次递归都在收集结果,避免线性递归展开消耗内存,只存在一个调用帧。
尾递归:只有一个调用帧
js
function Fibonacci2(n, ac1 = 1, ac2 = 1) {
if (n <= 1) {
return ac2
}
return Fibonacci2(n - 1, ac2, ac1 + ac2)
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity非尾递归:2 的 n 次方个调用帧
js
function Fibonacci(n) {
if (n <= 1) {
return 1
}
return Fibonacci(n - 1) + Fibonacci(n - 2)
}
Fibonacci(10) // 89
Fibonacci(100) // 超时
Fibonacci(500) // 超时递归函数改写
所有用到的 内部变量 改写成 函数的参数。
缺点:不太直观,计算 5 阶乘,为何传 1
js
function tailFactorial(n, total) {
if (n === 1) return total
return tailFactorial(n - 1, n * total)
}
tailFactorial(5, 1) // 120方案 1:外部提供正常函数
js
function factorial(n) {
return tailFactorial(n, 1)
}
factorial(5) // 120函数柯里化:多参数函数转为单参数函数
js
function currying(fn, n) {
return function (m) {
return fn.call(this, m, n)
}
}
const factorial = currying(tailFactorial, 1)
factorial(5) // 120方案 2:ES6 默认值
js
function factorial(n, total = 1) {
if (n === 1) return total
return factorial(n - 1, n * total)
}
factorial(5) // 120严格模式
尾调用优化之只在严格模式下有用
因为正常模式下,存在func.arugments和func.caller,尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。
func.arguments:返回调用时函数的参数。func.caller:返回调用当前函数的那个函数。
尾递归优化实现
问题:尾递归优化只在严格模式下生效
解决:正常模式下,或者那些不支持该功能的环境中。通过采用 “循环” 换掉 “递归” ,减少调用栈
js
function sum(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
} else {
return x
}
}
sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)蹦床函数(trampoline)可以将递归执行转为循环执行。这里是返回一个函数,然后执行函数,并非函数中调用函数,避免了递归执行。
js
function trampoline(f) {
while (f && f instanceof Function) {
f = f()
}
return f
}通过蹦床函数优化问题代码
js
function sum(x, y) {
if (y > 0) {
return sum.bind(null, x + 1, y - 1)
} else {
return x
}
}通过 sum 使每次返回一个新的函数,不会发生栈溢出
js
trampoline(sum(1, 100000)) // 100001==@DIF 尾调用优化:active 原理==
js
function tco(f) {
var value
var active = false
var accumulated = []
return function accumulator() {
accumulated.push(arguments)
if (!active) {
active = true
while (accumulated.length) {
value = f.apply(this, accumulated.shift())
}
active = false
return value
}
}
}
var sum = tco(function (x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
} else {
return x
}
})
sum(1, 100000)函数参数的尾逗号
ES2017 允许函数的最后一个参数有尾逗号(trailing comma)
Function.prototype.toString()
Function.prototype.toString():返回函数代码本身,以前会省略注释和空格,ES2019 - 后不会省略注释。
js
function /* foo comment */ foo() {}
foo.toString()
// "function /* foo comment */ foo () {}"catch 命令的参数省略
catch代码块参数不可省略。
js
try {
// ...
} catch (err) {
// 处理错误
}ES2019 - 允许catch语句省略参数。
js
try {
// ...
} catch {
// ...
}