一、函數(shù)參數(shù)的默認(rèn)值
1.1、基本用法
ES6 允許為函數(shù)的參數(shù)設(shè)置默認(rèn)值,直接寫在參數(shù)定義的后面
function log(x, y = 'World') {
console.log(x, y)
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
ES6 的寫法還有兩個好處:
- 閱讀代碼的人可以l立刻意識到哪些參數(shù)是可以省略的,不用查看函數(shù)體或文檔;
- 有利于將來的代碼優(yōu)化,即使未來的版本徹底拿掉這個參數(shù),也不會導(dǎo)致以前的代碼無法運行
使用默認(rèn)參數(shù)有以下幾點需要注意:
- 參數(shù)變量是默認(rèn)聲明的,所以不能用 let 或 const 再次聲明
function fn(x = 5) {
let x = 1 //error
const x = 1 // error
}
- 使用參數(shù)默認(rèn)值時,函數(shù)不能有同名參數(shù)。
function fn(x, x, y = 1) {
// todo
}
// Uncaught SyntaxError: Duplicate parameter name not allowed in this context
- 參數(shù)默認(rèn)值不是傳值的,而是每次都重新計算默認(rèn)值表達(dá)式的值。也就是說,參數(shù)默認(rèn)值是惰性求值的。
lett x = 99
function fn(p = x + 1) {
console.log(p)
}
fn() // 100
x = 100
fn() // 101
1.2、與解構(gòu)賦值默認(rèn)值結(jié)合使用
參數(shù)默認(rèn)值可以與結(jié)構(gòu)賦值的默認(rèn)值結(jié)合起來使用
function fn({x, y = 5}) {
console.log(x, y)
}
fn({}) // undefined, 5
fn({x: 1}) // 1, 5
fn({x: 1, y: 2}) // 1, 2
fn() // TypeError: Cannot destructure property `x` of 'undefined' or 'null'.
如果函數(shù) fn 調(diào)用時參數(shù)不是對象,變量 x 和 y 就不會生成,從而報錯。如果結(jié)合函數(shù)參數(shù)默認(rèn)值,就可以省略這個參數(shù),這是,就出現(xiàn)了雙重默認(rèn)值。
function fn({x, y = 5} = {}) {
console.log(x, y)
}
fn() // undefined, 5
當(dāng)沒有參數(shù)時,函數(shù)參數(shù)的默認(rèn)值就會生效,然后才是解構(gòu)賦值的默認(rèn)值生效。
對比下面兩種寫法有什么差別:
// 寫法一
function m1({x = 0, y = 0} = {}) {
return [x, y]
}
// 寫法二
function m2({x, y} = {x: 0, y: 0}) {
return [x, y]
}
上面兩種寫法的區(qū)別在于:
- 寫法一中 函數(shù)參數(shù)的默認(rèn)值是空對象,但是設(shè)置了對象結(jié)構(gòu)賦值的默認(rèn)值;
- 寫法二中 函數(shù)參數(shù)的默認(rèn)值是一個有具體屬性的函數(shù),但是沒有設(shè)置對象結(jié)構(gòu)賦值的默認(rèn)值
下面是幾種不同的調(diào)用方式:
// 函數(shù)沒有參數(shù)的情況下
m1() // [0, 0]
m2() // [0, 0]
// x 和 y都有值的情況下
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]
// x有值,y無值的情況
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]
// x 和 y都無值的情況
m1({}) // [0, 0]
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
1.3、參數(shù)默認(rèn)值的位置
通常情況下,定義了默認(rèn)值的參數(shù)應(yīng)該是函數(shù)的尾參數(shù)。韹為這樣比較容易看出到底省略了那些參數(shù)。如果非尾部的參數(shù)設(shè)置默認(rèn)值,實際上這個參數(shù)是無法省略的。
function fn(x = 5, y) {
return [x, y]
}
fn(undefined, 1) // [5, 1]
fn(, 1) // 報錯
如果傳入 undefined,將觸發(fā)該參數(shù)等于默認(rèn)值,null沒有這個效果
1.4、函數(shù)的 length 屬性
指定了默認(rèn)值以后,函數(shù)的 length 屬性將返回沒有指定默認(rèn)值的參數(shù)個數(shù)。也就是說,指定了默認(rèn)值后,length 屬性將失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 3) {}).length // 2
這是因為 length 屬性的含義是該函數(shù)預(yù)期傳入的參數(shù)個數(shù)。某個參數(shù)指定默認(rèn)值后,預(yù)期傳入的參數(shù)個數(shù)就不包括這個參數(shù)了。同理,rest 參數(shù)也不會計入 length 屬性。
(function (...args) {}).length // 0
如果設(shè)置了默認(rèn)值的參數(shù)不是尾參數(shù),那么length 屬性也不再計入后面的參數(shù)。
(function (a = 0, b , c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
1.5、作用域
一旦設(shè)置了參數(shù)的默認(rèn)值,函數(shù)進行聲明初始化時,參數(shù)會形成一個單獨的作用域(context)。等到初始化結(jié)束,這個作用域就會消失。這種語法行為在不設(shè)置參數(shù)默認(rèn)值時是不會出現(xiàn)的。
var x = 1
function fn(x, y = x) {
console.log(y)
}
fn(2) // 2
上面的代碼中,參數(shù) y 的默認(rèn)值等于變量x。調(diào)用函數(shù)f 時,參數(shù)形成一個單獨的作用域。在這個作用域里面,默認(rèn)值變量x 指向第一個參數(shù)x,而不是全局變量x,所以輸入 2
let x = 1
function fn(y = x) {
let x = 2
console.log(y)
}
fn() // 1
上面的代碼中,函數(shù) fn 調(diào)用時,參數(shù) y = x 形成了一個單獨的作用域。在這個作用域里面,變量 x 本身沒有定義,所以指向外層的全局變量x。函數(shù)調(diào)用時,函數(shù)體內(nèi)部的局部變量 x 影響不到默認(rèn)值變量 x。
如果全局變量 x 不存在,就會報錯
如果參數(shù)的默認(rèn)值是一個函數(shù),該函數(shù)的作用域也遵守這個規(guī)則。
let foo = 'outer'
function bar(func = x => foo) {
let foo = 'inner'
console.log(func())
}
bar() // outer
函數(shù)參數(shù)形成的單獨作用域里面并沒有定義變量 foo,所以 foo 指向外層的全局變量 foo,因此輸出 outer。
因此寫成下面這樣,就會報錯
function bar(func = () => foo) {
let foo = 'inner'
console.log(func())
}
bar() // ReferenceError: foo is not defined
下面是一個更為復(fù)雜的例子:
var x = 1
function foo(x, y = function() {x = 2}) {
var x = 3
y()
console.log(x)
}
foo() // 3
x // 1
y 的默認(rèn)值是一個匿名函數(shù),這個匿名函數(shù)內(nèi)部的變量 x 指向同一個作用域的第一個參數(shù) x,函數(shù) foo 內(nèi)部又聲明了一個內(nèi)部變量x,改變量與第一個參數(shù) x 由于不是同一個作用域,所以不是同一個變量,因此執(zhí)行 y 后,內(nèi)部變量 x 和外部變量 x 的值都沒有變。
如果將 var x = 3 的var 去除,函數(shù) foo 的內(nèi)部變量 x 就指向第一個參數(shù)x,與匿名函數(shù)內(nèi)部的 x 是一致的。
var x = 1
function foo(x, y = function() {x = 2}) {
x = 3
y()
console.log(x)
}
foo() // 3
x // 1
最后輸出的就是2,而外層的全局變量x 依然不受影響。
1.6、應(yīng)用
利用參數(shù)默認(rèn)值可以指定某一個參數(shù)不得省略,如果省略就拋出一個錯誤
function throwIfMissing() {
throw new Error('Missing parameter')
}
function foo(mustBeProvided = throwIfMissing() ) {
return mustBeProvided
}
foo()
另外,可以將參數(shù)默認(rèn)值設(shè)為 undefined,表明這個參數(shù)是可以省略的
function foo(optional = undefined) {...}
二、rest 參數(shù)
ES6 引入 rest 參數(shù)(形式為“...變量名”),用于獲取函數(shù)的多余參數(shù),這樣就不需要使用 arguments 對象了。rest 參數(shù)搭配的變量是一個數(shù)組,該變量將多余的參數(shù)放入其中。
function add(...values) {
let sum = 0
for (var val of values) {
sum += val
}
return sum
}
add(2 ,3, 4, 5, 5)
下面是一個 rest 參數(shù) 代替 arguments 變量的例子。
// arguments 變量的寫法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort()
}
// rest 參數(shù)的寫法
const sortNumbers = (...numbers) => numbers.sort()
下面是關(guān)于 rest 參數(shù)的注意事項:
- rest 參數(shù)中的變量代表一個數(shù)組,所以數(shù)組特有的方法都可以用于這個變量。
- rest 參數(shù)之后不能再有其他參數(shù)(即只能是最后一個參數(shù)),否則會報錯。
- 函數(shù)的 length 屬性不包括 rest 參數(shù)
三、嚴(yán)格模式
從 ES5 開始,函數(shù)內(nèi)部可以設(shè)定為嚴(yán)格模式。
function doSometing(a, b) {
'use strict'
// todo
}
ES2016 做了一點修改,規(guī)定只要函數(shù)參數(shù)使用了默認(rèn)值、解構(gòu)賦值或者擴展運算符,那么函數(shù)內(nèi)部就不能顯示設(shè)定為 嚴(yán)格模式,否則會報錯。
// 報錯
function doSomething(a, b = a) {
'use strict'
// todo
}
// 報錯
function doSomething ({a, b}) {
'use strict'
// todo
}
// 報錯
const doSomething = (...a) => {
'use strict'
// todo
}
這樣規(guī)定的原因是,函數(shù)內(nèi)部的嚴(yán)格模式同時適用于 函數(shù)體 和 函數(shù)參數(shù)。但是,函數(shù)執(zhí)行時,先執(zhí)行函數(shù)參數(shù),然后再執(zhí)行函數(shù)體。這樣就有一個不合理的地方:只有從函數(shù)體之中才能知道參數(shù)是否應(yīng)該以嚴(yán)格模式執(zhí)行,但是參數(shù)卻應(yīng)該先于函數(shù)體執(zhí)行。
// 報錯
function doSomething(value = 070) {
'use strict'
return value
}
上面代碼中,參數(shù) value 的默認(rèn)值是 八進制數(shù) 070,但是嚴(yán)格模式下不能用前綴 0 表示八進制,所以應(yīng)該報錯。但實際上,JavaScript 引擎會先成功執(zhí)行 value = 070,然后進入函數(shù)體內(nèi)部,發(fā)現(xiàn)需要用嚴(yán)格模式時才會報錯。
雖然可以先解析函數(shù)體代碼,再執(zhí)行參數(shù)代碼,但是這樣無疑增加了復(fù)雜性。因此,標(biāo)準(zhǔn)索性禁止了這種寫法,只要參數(shù)使用了默認(rèn)值、解構(gòu)賦值、擴展運算符,就不能夠顯示指定嚴(yán)格模式
有兩種方法可以規(guī)避這種限制:
1. 設(shè)定全局的嚴(yán)格模式
'use strict'
function doSomething(a, b = a) {
// todo
}
2. 把函數(shù)抱在一個無參數(shù)的立執(zhí)行函數(shù)里面
const doSomething = (function() {
'use strict'
return function(value = 42) {
return value
}
} ())
四、name 屬性
函數(shù)的 name 屬性返回該函數(shù)的函數(shù)名
function foo() {}
foo.name // "foo"
ES6 對這個屬性的行為做出了一些修改。如果將一個匿名函數(shù)賦值給一個變量,ES5 的 name 屬性會返回空字符串,而ES6 的 name 屬性會返回實際的函數(shù)名
var f = function() {}
// ES5
f.name // ""
// ES6
f.name // "f"
如果將一個具名函數(shù)賦值給一個變量,則ES5 和 ES6 的name屬性都返回這個具名函數(shù)原本的名字
const bar = function baz() {}
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
Function 構(gòu)造函數(shù)返回的函數(shù)實例,name 屬性的值為 anonymous
(new Function).name // "anonymous"
bind 返回的函數(shù),name 屬性值 會加上 bound 前綴
function foo() {}
foo.bind({}).name // "bound foo"
(function() {}.bind({}) ).name // "bound"
五、箭頭函數(shù)
5.1、 基本用法
ES6 允許使用 “箭頭”(=>) 定義函數(shù)
var f = v => v
等同于以下代碼
var f = function(v) {
return v
}
下面是箭頭函數(shù)的一些寫法:
- 如果箭頭函數(shù)不需要或需要多個參數(shù),就使用圓括號代表參數(shù)部分
var f = () => 5
- 如果箭頭函數(shù)的代碼塊多余一條語句,就要失業(yè)大括號將其括起來,并使用 return 語句返回
var sum = (num1, num2) => {return num1 + num2}
- 由于大括號被解釋為代碼塊,所以如果箭頭函數(shù)直接返回一個對象,必須在對象外面加上括號
var getTempItem = id => ({id: id, name: 'Temp'})
- 箭頭函數(shù)可以與變量解構(gòu)結(jié)合使用
const full = ({first, last}) => first + ' ' + last
- 一個參數(shù)時,如果是rest參數(shù),需要加上括號
// 報錯
const fn = ...number => 1
// 正確寫法
const fn = (...numbers) => 1
5.2、注意事項
箭頭函數(shù)有以下幾個使用注意事項:
- 函數(shù)體內(nèi)的 this 對象就是定義時所在的對象,而不是使用時所在的對象。
- 不可以當(dāng)作構(gòu)造函數(shù)。也就是說,不可以使用 new 命令符,否則會拋出一個錯誤。
- 不可以使用 arguments 對象,該對象在函數(shù)體內(nèi)部不存在。如果要用,可以用 rest 參數(shù)代替。
- 不可以使用 yield 命令,因為箭頭函數(shù)不能用作 Generator 函數(shù)。
其中,第一點尤其值得注意。this 對象的指向是可以變的,但在箭頭函數(shù)中它是固定的。
箭頭函數(shù)可以讓 this 指向固定化,這種特性非常有利于封裝回調(diào)函數(shù)。
var handler = {
id: '123456',
init: function() {
document.addEventListener('click', event => this.doSomething(event.type), false)
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id)
}
}
this指向的固定化并不是因為 箭頭函數(shù)內(nèi)部有綁定 this 的機制,實際原因是箭頭函數(shù)根本沒有自己的this,導(dǎo)致內(nèi)部的 this 就是外層代碼塊的 this。正是因為 它沒有 this,所以不能用作構(gòu)造函數(shù)。
除了 this,以下 3 個變量在 箭頭函數(shù)中也是不存在的,分別指向外層函數(shù)的對于變:arguments、super、new.target
function foo() {
setTimeout(() => {
console.log('args:', arguments)
}, 100)
}
foo([2, 3, 4, 5])
// args: [2, 3, 4, 5]
上面的代碼中,箭頭函數(shù)內(nèi)部的變量 arguments 其實是 函數(shù) foo 的arguments 變量。
另外,由于箭頭函數(shù)沒有自己的this,當(dāng)然也就不能用 call()、apply()、bind() 這些方法去改變 this 的指向。
(function () {
return [
(() => this.x).bind({x: 'inner'})()
]
}).call({x: 'outer'})
// ["outer"]
長期以來,JavaScript 語言的 this 對象一直是一個令人頭痛的問題,在對象方法中使用 this 必須非常小心。箭頭函數(shù)“綁定” this,很大程度上解決了這個困擾。
5.3、嵌套的箭頭函數(shù)
箭頭函數(shù)內(nèi)部還可以再使用箭頭函數(shù)。
下面是一個 部署管道機制(pipeline)的例子,即前一個函數(shù)的輸出是后一個函數(shù)的輸入。
const pipeline = (...funcs) =>
val => funcs.reduce((a, b) => b(a), val)
const plus1 = a => a + 1
const mult2 = a => a * 2
const addThenMult = pipeline(plus1, mult2)
addThenMult(5)
// 12
如果覺得上面的寫法可讀性比較差,可以采用下面的寫法
const plus1 = a => a + 1
const mult2 = a => a * 2
mult2(plus1(5))
// 12
箭頭函數(shù) 還有一個功能,就是可以很方便的地改寫 λ 演算
六、綁定 this
箭頭函數(shù)可以綁定 this 對象,大大減少了顯示綁定 this 對象的寫法(call、apply、bind)。但是,箭頭函數(shù)并非適合于所有場合,所以 ES7 提出了 “函數(shù)綁定”(function bind)運算符,用來取代 call、apply、bind調(diào)用。雖然該語法還是 ES7 的一個提案,但是Babel轉(zhuǎn)碼器已經(jīng)支持。
函數(shù)綁定運算符是并排的 雙冒號(::),雙冒號左邊是一個對象,右邊是一個函數(shù)。該運算符會自動將左邊對象作為上下文環(huán)境(即this 對象)綁定到右邊的函數(shù)上。
foo::bar
// 等同于
bar.bind(foo)
如果雙冒號左邊為空,右邊是一個對象的方法,則等于該方法綁定在該對象上。
var method = obj::obj.foo
// 等同于
var method = ::obj.foo
var log = ::console.log
// 等同于
var log = console.log.bind(console)
由于 雙冒號運算符返回的還是原對象,因此可以采用鏈?zhǔn)綄懛?/p>
let { find, html } = jake
document.querySelectorAll('div.myClass')
::find('p')
::html('hahha')
七、尾調(diào)用優(yōu)化
7.1、什么是尾調(diào)用
尾調(diào)用(Tail Call)是函數(shù)式編程的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函數(shù)的最后一步是調(diào)用另一個函數(shù)。
function f(x) {
return g(x)
}
如果在尾調(diào)用之后還要賦值操作,就不屬于尾調(diào)用,即使語義完全一樣。
尾調(diào)用不一定出現(xiàn)在函數(shù)尾部,只要是最后一步操作即可。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x)
}
7.2、尾調(diào)用優(yōu)化
函數(shù)調(diào)用會在內(nèi)存形成一個“調(diào)用記錄”,又稱“調(diào)用幀”(call frame),保存調(diào)用位置和 內(nèi)部變量等信息。如果在函數(shù) A 的內(nèi)部調(diào)用函數(shù) B,那么 在 A 的調(diào)用幀上方還會形成一個 B 的調(diào)用幀。等到 B 運行結(jié)束,將結(jié)果返回到 A,B的調(diào)用幀才會消失。如果函數(shù) B 內(nèi)部還調(diào)用 函數(shù)C,那就還有一個 C 的調(diào)用幀,以此類推。所有的調(diào)用幀就形成一個“調(diào)用?!保╟all stack)
尾調(diào)用由于是函數(shù)的最后一步操作,所以不需要保留外層函數(shù)的調(diào)用幀,因為調(diào)用位置內(nèi)部變量等信息都不會再用到了,直接用內(nèi)存函數(shù)的調(diào)用幀取代外層函數(shù)的即可。
function f() {
let m = 1
let n = 2
return g(m + n)
}
f()
// 等同于
function f() {
return g(3)
}
f()
// 等同于
g(3)
由于調(diào)用 g 之后,函數(shù) f 就結(jié)束了,所以執(zhí)行到最后一步,完全可以刪除 f(x) 的調(diào)用幀,只保留 g(3) 的調(diào)用幀
這就叫作“尾調(diào)用優(yōu)化”(Tail CallOptimization),即在保留內(nèi)層函數(shù)的調(diào)用幀。如果所有函數(shù)都是尾調(diào)用,那么完全可以做到每次執(zhí)行時調(diào)用幀只有一項,這將大大節(jié)省內(nèi)存。這就是“尾調(diào)用優(yōu)化”的意義。
注意:只有不再用到外層函數(shù)的內(nèi)部變量,內(nèi)層函數(shù)的調(diào)用幀才會取代外層函數(shù)的調(diào)用幀,否則就無法進行“尾調(diào)用優(yōu)化”。
function addOne(a) {
var one = 1
function inner(b) {
return b + one
}
return inner(a)
}
上面的函數(shù)不會進行尾調(diào)用優(yōu)化,因為內(nèi)層函數(shù) inner 用到了外層函數(shù) addOne 的內(nèi)部變量 one
7.3、尾遞歸
函數(shù)調(diào)用自身稱為遞歸。如果尾調(diào)用自身就成為尾遞歸
遞歸非常消耗內(nèi)存,因為需要同時保存成百上千調(diào)用幀,很容易發(fā)生“棧溢出”錯誤(stack overflow)。但對于尾遞歸來說,由于只存在一個調(diào)用幀,所以永遠(yuǎn)不會發(fā)生“棧溢出”錯誤。
function factorial(n) {
if (n===1) return 1
return n * factorial(n - 1)
}
factorial(5) // 120
上面是一個階乘函數(shù),計算 n 的階乘,最多需要保存 n 個調(diào)用記錄,復(fù)雜度為 O(n)。如果改寫成尾遞歸,只保留一個調(diào)用記錄,則復(fù)雜度為 O(1)。
function factorial(n, total) {
if (n === 1) return total
return factorial(n - 1, n * total)
}
// factorial(5, 1) // 120
還有一個比較著名的例子——計算Fibonacci 數(shù)列,也能充分說明尾遞歸優(yōu)化的重要性。
非尾遞歸的 Fibonacci 數(shù)列實現(xiàn)如下:
function Fibonacci(n) {
if (n <= 1) return 1
return Fibonacci(n - 1) + Fibonacci(n - 2)
}
Fibonacci(10) // 89
Fibonacci(100) // 堆棧溢出
Fibonacci(500) // 堆棧溢出
尾遞歸優(yōu)化的 Fibonacci 數(shù)列:
function Fibonacci(n, ac1 = 1, ac2 = 1) {
if ( n <= 1) return ac2
return Fibonacci(n -1, ac2, ac1 + ac2)
}
Fibonacci(100) // 573147844013817200000
Fibonacci(1000) // 7.0330367711422765e+208
由此可見,“尾調(diào)用優(yōu)化” 對遞歸操作意義重大,所以一些函數(shù)式編程語言將其寫入了語言規(guī)格。ES6 也是如此,第一次明確規(guī)定,所有 ECMAScript 的實現(xiàn)都必須部署“尾調(diào)用 優(yōu)化”。也就是說,在ES6 中,只要使用尾遞歸。就不會發(fā)生棧溢出,相對節(jié)省空間。
7.4、遞歸函數(shù)的改寫
尾遞歸的實現(xiàn)往往需要改寫遞歸函數(shù),卻白最后一步只調(diào)用自身。做到這一點的方法,就是把所有用到的內(nèi)部變量改寫成函數(shù)的參數(shù)。
比如上面的勵志。階乘函數(shù) factorial 需要用到第一個中間變量 total,那就把這個中間變量改寫成函數(shù)的參數(shù)。這樣做的缺點是不太直觀,第一眼很難看出來,為什么計算 5 的階乘需要傳入兩個參數(shù) 5 和 1
有兩個方法可以解決這個問題。
1. 在尾遞歸函數(shù)之外再提供一個正常形式的函數(shù)
function tailFactorial(n, total) {
if (n === 1) return total
return tailFactorial(n - 1, n * total)
}
function factorial(n) {
return tailFactorial(n, 1)
}
factorial(5) // 120
函數(shù)柯里化
函數(shù)式編程有一個概念,叫做柯里化(currying),意思就是將多參數(shù)的函數(shù)轉(zhuǎn)換成單參數(shù)的形式。這里也可以使用柯里化。
function currying(fn, n) {
return function(m) {
return fn.call(this, m, n)
}
}
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n -1, n * total)
}
const factorial = currying(tailFactorial, 1)
factorial(5)
2. 采用ES6 的函數(shù)默認(rèn)值
function factorial(n, total = 1) {
if (n === 1) return total
return factorial(n - 1, n * total)
}
factorial(5)
遞歸本質(zhì)上是一種循環(huán)操作。純粹的函數(shù)式編程語言沒有循環(huán)操作命令,所有的循環(huán)都用遞歸實現(xiàn),這就是為什么尾遞歸對這些語言極其重要。對于其他支出“尾調(diào)用優(yōu)化” 的語言(比如 Lua、ES6),只需要知道循環(huán)可以用遞歸代替,而一旦使用遞歸,就最好使用尾遞歸。
7.5、嚴(yán)格模式
ES6 的尾調(diào)用優(yōu)化只在嚴(yán)格模式下開啟,正常模式下無效的。
這是因為,在正常模式下函數(shù)內(nèi)部有兩個變量,可以跟蹤函數(shù)的調(diào)用棧
- func.arguments:返回調(diào)用時函數(shù)的參數(shù)
- func.caller:返回調(diào)用當(dāng)前函數(shù)的那個函數(shù)。
尾調(diào)用優(yōu)化發(fā)生時,函數(shù)的調(diào)用棧會改寫,因此上面兩個變量就會失真。嚴(yán)格模式禁用這兩個變量,所以尾調(diào)用模式僅在嚴(yán)格模式下生效。
function restricted() {
'use strict'
restricted.caller // 報錯
restricted.arguments // 報錯
}
restricted()
7.6、尾遞歸優(yōu)化的實現(xiàn)
尾遞歸優(yōu)化只在嚴(yán)格模式下生效,那么在正常模式下,或者在那些不支持該功能的環(huán)境中,要靠自己實現(xiàn)尾遞歸優(yōu)化。
實現(xiàn)原理:
尾遞歸之所以需要優(yōu)化,原因是調(diào)用棧太多造成溢出,那么只要減少調(diào)用棧就不會溢出。方法就是 采用 “循環(huán)” 替換 “遞歸”
下面是一個正常的遞歸函數(shù):
function sum(x, y) {
if (y > 0) {
return sum(x + 1, y -1)
} else {
return x
}
}
sum(1, 100000)
// RangeError: Maximum call stack size exceeded
上面的代碼中,sum 是一個遞歸函數(shù),參數(shù) x 是需要累積的值,參數(shù) y 控制遞歸次數(shù)。一旦指定 sum 遞歸100000 次,就會報錯,提示超出調(diào)用棧的最大次數(shù)。
蹦床函數(shù)(trampoline)可以將遞歸執(zhí)行轉(zhuǎn)為循環(huán)執(zhí)行
function trampoline(f) {
while(f && f instanceof Function) {
f = f()
}
return f
}
以上代碼就是蹦床函數(shù)的一個實現(xiàn),它接受函數(shù) f 作為參數(shù)。只要 f 執(zhí)行后返回一個 函數(shù)就繼續(xù)執(zhí)行。
這里是返回一個函數(shù),然后執(zhí)行函數(shù),而不是在函數(shù)里面調(diào)用函數(shù),這樣就避免了遞歸執(zhí)行,從而消除了調(diào)用棧過大的問題。
然后要做的是將原來的遞歸函數(shù)改寫為每一步返回另一個函數(shù)
function sum(x, y) {
if (y > 0) {
return sum.bind(null, x + 1, y -1)
} else {
return x
}
}
上面代碼中,sum 函數(shù)的每次執(zhí)行都會返回自身的另一個版本。
使用蹦床函數(shù)執(zhí)行 sum 就不會發(fā)生調(diào)用棧溢出。
trampoline(sum(1, 100000))
蹦床函數(shù)并不是真正的尾遞歸優(yōu)化,下面的實現(xiàn)才是
function tco(f) {
var value,
active = false,
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) // 100001
上面的代碼中,tco 函數(shù)時尾遞歸優(yōu)化的實現(xiàn),它的奧妙就在于狀態(tài)變量 active。默認(rèn)情況下,這個變量是不激活的。一旦進入尾遞歸優(yōu)化過程,這個變量就被激活了。然后,每一輪遞歸 sum 返回的都是 undefined,所以就避免了遞歸執(zhí)行;而 accumulated 數(shù)組存放每一輪 sum 執(zhí)行的參數(shù),總是有值的,這就保證了 accumulator 函數(shù)內(nèi)部的 while 循環(huán)總會執(zhí)行,很巧妙地將“遞歸” 改成了 “循環(huán)”,而后一輪的參數(shù)會取代前一輪的參數(shù),保住了調(diào)用棧只有一層
八、函數(shù)參數(shù)的尾逗號
ES2017 中有一個提案,允許函數(shù)的最后一個參數(shù)有尾逗號(trailing comma)
function clownsEverywhere(
param1,
param2
) { /* .... */}
clownsEverywhere(
'foo',
'bar'
)
像上面這樣,修改代碼時,若想為函數(shù)添加參數(shù),或者調(diào)整參數(shù)的次序,勢必要在原來最后一個參數(shù)后面添加一個逗號。這對于版本管理系統(tǒng)來說,就會顯示添加逗號的那一行發(fā)生了變動,看上去有點冗余,因此新天允許定義和調(diào)用時尾部有一個逗號
function clownsEverywhere(
param1,
param2,
) { /* .... */}
clownsEverywhere(
'foo',
'bar',
)
這樣的規(guī)定也使得函數(shù)參數(shù)與數(shù)組和對象的尾逗號規(guī)則可以保持一致。