“睡服”面试官系列第十三篇之函数的扩展(建议收藏学习)
目錄
?
1. 函數(shù)參數(shù)的默認值
1.1基本用法
1.2與解構賦值默認值結合使用
1.3參數(shù)默認值的位置
1.4函數(shù)的 length 屬性?
1.5作用域
1.6應用
2. rest 參數(shù)
3. 嚴格模式
4. name 屬性
5. 箭頭函數(shù)
5.1基本用法
5.2使用注意點
5.3嵌套的箭頭函數(shù)
6. 雙冒號運算符
7. 尾調用優(yōu)化
7.1什么是尾調用?
7.2尾調用優(yōu)化
7.3尾遞歸
7.4遞歸函數(shù)改寫
7.5嚴格模式
7.6尾遞歸優(yōu)化的實現(xiàn)
8. 函數(shù)參數(shù)的尾逗號
9. catch 語句的參數(shù)
總結
“睡服“面試官系列之各系列目錄匯總(建議學習收藏)
1. 函數(shù)參數(shù)的默認值
1.1基本用法
ES6 之前,不能直接為函數(shù)的參數(shù)指定默認值,只能采用變通的方法。
function log(x, y) { y = y || 'World'; console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello World上面代碼檢查函數(shù) log 的參數(shù) y 有沒有賦值,如果沒有,則指定默認值為 World 。這種寫法的缺點在于,如果參數(shù) y 賦值了,但是對應的布爾值為
 false ,則該賦值不起作用。就像上面代碼的最后一行,參數(shù) y 等于空字符,結果被改為默認值。
 為了避免這個問題,通常需要先判斷一下參數(shù) y 是否被賦值,如果沒有,再等于默認值
ES6 允許為函數(shù)的參數(shù)設置默認值,即直接寫在參數(shù)定義的后面
function log(x, y = 'World') { console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello可以看到,ES6 的寫法比 ES5 簡潔許多,而且非常自然。下面是另一個例子
function Point(x = 0, y = 0) { this.x = x; this.y = y; } const p = new Point(); p // { x: 0, y: 0 }除了簡潔,ES6 的寫法還有兩個好處:首先,閱讀代碼的人,可以立刻意識到哪些參數(shù)是可以省略的,不用查看函數(shù)體或文檔;其次,有利于將來的代碼
 優(yōu)化,即使未來的版本在對外接口中,徹底拿掉這個參數(shù),也不會導致以前的代碼無法運行。
 參數(shù)變量是默認聲明的,所以不能用 let 或 const 再次聲明。
上面代碼中,參數(shù)變量 x 是默認聲明的,在函數(shù)體中,不能用 let 或 const 再次聲明,否則會報錯。
 使用參數(shù)默認值時,函數(shù)不能有同名參數(shù)
另外,一個容易忽略的地方是,參數(shù)默認值不是傳值的,而是每次都重新計算默認值表達式的值。也就是說,參數(shù)默認值是惰性求值的
let x = 99; function foo(p = x + 1) { console.log(p); } foo() // 100 x = 100; foo() // 101上面代碼中,參數(shù) p 的默認值是 x + 1 。這時,每次調用函數(shù) foo ,都會重新計算 x + 1 ,而不是默認 p 等于 100。
1.2與解構賦值默認值結合使用
參數(shù)默認值可以與解構賦值的默認值,結合起來使用。
function foo({x, y = 5}) { console.log(x, y); } foo({}) // undefined 5 foo({x: 1}) // 1 5 foo({x: 1, y: 2}) // 1 2 foo() // TypeError: Cannot read property 'x' of undefined上面代碼只使用了對象的解構賦值默認值,沒有使用函數(shù)參數(shù)的默認值。只有當函數(shù) foo 的參數(shù)是一個對象時,變量 x 和 y 才會通過解構賦值生成。如果
 函數(shù) foo 調用時沒提供參數(shù),變量 x 和 y 就不會生成,從而報錯。通過提供函數(shù)參數(shù)的默認值,就可以避免這種情況
上面代碼指定,如果沒有提供參數(shù),函數(shù) foo 的參數(shù)默認為一個空對象。
 下面是另一個解構賦值默認值的例子
上面代碼中,如果函數(shù) fetch 的第二個參數(shù)是一個對象,就可以為它的三個屬性設置默認值。這種寫法不能省略第二個參數(shù),如果結合函數(shù)參數(shù)的默認
 值,就可以省略第二個參數(shù)。這時,就出現(xiàn)了雙重默認值。
上面代碼中,函數(shù) fetch 沒有第二個參數(shù)時,函數(shù)參數(shù)的默認值就會生效,然后才是解構賦值的默認值生效,變量 method 才會取到默認值 GET 。
 作為練習,請問下面兩種寫法有什么差別?
上面兩種寫法都對函數(shù)的參數(shù)設定了默認值,區(qū)別是寫法一函數(shù)參數(shù)的默認值是空對象,但是設置了對象解構賦值的默認值;寫法二函數(shù)參數(shù)的默認值是
 一個有具體屬性的對象,但是沒有設置對象解構賦值的默認值。
1.3參數(shù)默認值的位置
通常情況下,定義了默認值的參數(shù),應該是函數(shù)的尾參數(shù)。因為這樣比較容易看出來,到底省略了哪些參數(shù)。如果非尾部的參數(shù)設置默認值,實際上這個
 參數(shù)是沒法省略的。
上面代碼中,有默認值的參數(shù)都不是尾參數(shù)。這時,無法只省略該參數(shù),而不省略它后面的參數(shù),除非顯式輸入 undefined 。
 如果傳入 undefined ,將觸發(fā)該參數(shù)等于默認值, null 則沒有這個效果。
上面代碼中, x 參數(shù)對應 undefined ,結果觸發(fā)了默認值, y 參數(shù)等于 null ,就沒有觸發(fā)默認值。
1.4函數(shù)的 length 屬性?
指定了默認值以后,函數(shù)的 length 屬性,將返回沒有指定默認值的參數(shù)個數(shù)。也就是說,指定了默認值后, length 屬性將失真
(function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2?上面代碼中, length 屬性的返回值,等于函數(shù)的參數(shù)個數(shù)減去指定了默認值的參數(shù)個數(shù)。比如,上面最后一個函數(shù),定義了 3 個參數(shù),其中有一個參數(shù) c
 指定了默認值,因此 length 屬性等于 3 減去 1 ,最后得到 2 。
 這是因為 length 屬性的含義是,該函數(shù)預期傳入的參數(shù)個數(shù)。某個參數(shù)指定默認值以后,預期傳入的參數(shù)個數(shù)就不包括這個參數(shù)了。同理,后文的 rest
 參數(shù)也不會計入 length 屬性
如果設置了默認值的參數(shù)不是尾參數(shù),那么 length 屬性也不再計入后面的參數(shù)了
(function (a = 0, b, c) {}).length // 0 (function (a, b = 1, c) {}).length // 11.5作用域
一旦設置了參數(shù)的默認值,函數(shù)進行聲明初始化時,參數(shù)會形成一個單獨的作用域(context)。等到初始化結束,這個作用域就會消失。這種語法行為,
 在不設置參數(shù)默認值時,是不會出現(xiàn)的。
上面代碼中,參數(shù) y 的默認值等于變量 x 。調用函數(shù) f 時,參數(shù)形成一個單獨的作用域。在這個作用域里面,默認值變量 x 指向第一個參數(shù) x ,而不是全局
 變量 x ,所以輸出是 2 。
 再看下面的例子
上面代碼中,函數(shù) f 調用時,參數(shù) y = x 形成一個單獨的作用域。這個作用域里面,變量 x 本身沒有定義,所以指向外層的全局變量 x 。函數(shù)調用時,函
 數(shù)體內部的局部變量 x 影響不到默認值變量 x 。
 如果此時,全局變量 x 不存在,就會報錯
下面這樣寫,也會報錯。
var x = 1; function foo(x = x) { // ... } foo() // ReferenceError: x is not defined上面代碼中,參數(shù) x = x 形成一個單獨作用域。實際執(zhí)行的是 let x = x ,由于暫時性死區(qū)的原因,這行代碼會報錯”x 未定義“。
 如果參數(shù)的默認值是一個函數(shù),該函數(shù)的作用域也遵守這個規(guī)則。請看下面的例子
上面代碼中,函數(shù) bar 的參數(shù) func 的默認值是一個匿名函數(shù),返回值為變量 foo 。函數(shù)參數(shù)形成的單獨作用域里面,并沒有定義變量 foo ,所以 foo 指向
 外層的全局變量 foo ,因此輸出 outer 。
 如果寫成下面這樣,就會報錯
上面代碼中,匿名函數(shù)里面的 foo 指向函數(shù)外層,但是函數(shù)外層并沒有聲明變量 foo ,所以就報錯了。
下面是一個更復雜的例子
var x = 1; function foo(x, y = function() { x = 2; }) { var x = 3; y(); console.log(x); } foo() // 3 x // 1上面代碼中,函數(shù) foo 的參數(shù)形成一個單獨作用域。這個作用域里面,首先聲明了變量 x ,然后聲明了變量 y , y 的默認值是一個匿名函數(shù)。這個匿名函
 數(shù)內部的變量 x ,指向同一個作用域的第一個參數(shù) x 。函數(shù) foo 內部又聲明了一個內部變量 x ,該變量與第一個參數(shù) x 由于不是同一個作用域,所以不是同
 一個變量,因此執(zhí)行 y 后,內部變量 x 和外部全局變量 x 的值都沒變。
 如果將 var x = 3 的 var 去除,函數(shù) foo 的內部變量 x 就指向第一個參數(shù) x ,與匿名函數(shù)內部的 x 是一致的,所以最后輸出的就是 2 ,而外層的全局變量
 x 依然不受影響。
1.6應用
利用參數(shù)默認值,可以指定某一個參數(shù)不得省略,如果省略就拋出一個錯誤。
function throwIfMissing() { throw new Error('Missing parameter'); } function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided; } foo() // Error: Missing parameter上面代碼的 foo 函數(shù),如果調用的時候沒有參數(shù),就會調用默認值 throwIfMissing 函數(shù),從而拋出一個錯誤。
 從上面代碼還可以看到,參數(shù) mustBeProvided 的默認值等于 throwIfMissing 函數(shù)的運行結果(注意函數(shù)名 throwIfMissing 之后有一對圓括號),這表
 明參數(shù)的默認值不是在定義時執(zhí)行,而是在運行時執(zhí)行。如果參數(shù)已經(jīng)賦值,默認值中的函數(shù)就不會運行。
 另外,可以將參數(shù)默認值設為 undefined ,表明這個參數(shù)是可以省略的。
2. rest 參數(shù)
ES6 引入 rest 參數(shù)(形式為 ...變量名 ),用于獲取函數(shù)的多余參數(shù),這樣就不需要使用 arguments 對象了。rest 參數(shù)搭配的變量是一個數(shù)組,該變量
 將多余的參數(shù)放入數(shù)組中。
上面代碼的 add 函數(shù)是一個求和函數(shù),利用 rest 參數(shù),可以向該函數(shù)傳入任意數(shù)目的參數(shù)。
 下面是一個 rest 參數(shù)代替 arguments 變量的例子。
上面代碼的兩種寫法,比較后可以發(fā)現(xiàn),rest 參數(shù)的寫法更自然也更簡潔。
 arguments 對象不是數(shù)組,而是一個類似數(shù)組的對象。所以為了使用數(shù)組的方法,必須使用 Array.prototype.slice.call 先將其轉為數(shù)組。rest 參數(shù)就
 不存在這個問題,它就是一個真正的數(shù)組,數(shù)組特有的方法都可以使用。下面是一個利用 rest 參數(shù)改寫數(shù)組 push 方法的例子。
?注意,rest 參數(shù)之后不能再有其他參數(shù)(即只能是最后一個參數(shù)),否則會報錯。
// 報錯 function f(a, ...b, c) { // ... }函數(shù)的 length 屬性,不包括 rest 參數(shù)。
(function(a) {}).length // 1 (function(...a) {}).length // 0 (function(a, ...b) {}).length // 13. 嚴格模式
從 ES5 開始,函數(shù)內部可以設定為嚴格模式
function doSomething(a, b) { 'use strict'; // code }ES2016 做了一點修改,規(guī)定只要函數(shù)參數(shù)使用了默認值、解構賦值、或者擴展運算符,那么函數(shù)內部就不能顯式設定為嚴格模式,否則會報錯。
// 報錯 function doSomething(a, b = a) { 'use strict'; // code } // 報錯 const doSomething = function ({a, b}) { 'use strict'; // code }; // 報錯 const doSomething = (...a) => { 'use strict'; // code }; const obj = { // 報錯 doSomething({a, b}) { 'use strict'; // code這樣規(guī)定的原因是,函數(shù)內部的嚴格模式,同時適用于函數(shù)體和函數(shù)參數(shù)。但是,函數(shù)執(zhí)行的時候,先執(zhí)行函數(shù)參數(shù),然后再執(zhí)行函數(shù)體。這樣就有一個
 不合理的地方,只有從函數(shù)體之中,才能知道參數(shù)是否應該以嚴格模式執(zhí)行,但是參數(shù)卻應該先于函數(shù)體執(zhí)行
上面代碼中,參數(shù) value 的默認值是八進制數(shù) 070 ,但是嚴格模式下不能用前綴 0 表示八進制,所以應該報錯。但是實際上,JavaScript 引擎會先成功執(zhí)
 行 value = 070 ,然后進入函數(shù)體內部,發(fā)現(xiàn)需要用嚴格模式執(zhí)行,這時才會報錯。
 雖然可以先解析函數(shù)體代碼,再執(zhí)行參數(shù)代碼,但是這樣無疑就增加了復雜性。因此,標準索性禁止了這種用法,只要參數(shù)使用了默認值、解構賦值、或
 者擴展運算符,就不能顯式指定嚴格模式。
 兩種方法可以規(guī)避這種限制。第一種是設定全局性的嚴格模式,這是合法的。
第二種是把函數(shù)包在一個無參數(shù)的立即執(zhí)行函數(shù)里面。
const doSomething = (function () { 'use strict'; return function(value = 42) { return value; }; }());4. name 屬性
函數(shù)的 name 屬性,返回該函數(shù)的函數(shù)名
function foo() {} foo.name // "foo"這個屬性早就被瀏覽器廣泛支持,但是直到 ES6,才將其寫入了標準。
 需要注意的是,ES6 對這個屬性的行為做出了一些修改。如果將一個匿名函數(shù)賦值給一個變量,ES5 的 name 屬性,會返回空字符串,而 ES6 的 name 屬
 性會返回實際的函數(shù)名。
上面代碼中,變量 f 等于一個匿名函數(shù),ES5 和 ES6 的 name 屬性返回的值不一樣。
 如果將一個具名函數(shù)賦值給一個變量,則 ES5 和 ES6 的 name 屬性都返回這個具名函數(shù)原本的名字
Function 構造函數(shù)返回的函數(shù)實例, name 屬性的值為 anonymous
(new Function).name // "anonymous"bind 返回的函數(shù), name 屬性值會加上 bound 前綴。
function foo() {}; foo.bind({}).name // "bound foo" (function(){}).bind({}).name // "bound "5. 箭頭函數(shù)
5.1基本用法
ES6 允許使用“箭頭”( => )定義函數(shù)。
var f = v => v;上面的箭頭函數(shù)等同于
var f = function(v) { return v; };如果箭頭函數(shù)不需要參數(shù)或需要多個參數(shù),就使用一個圓括號代表參數(shù)部分
var f = () => 5; // 等同于 var f = function () { return 5 }; var sum = (num1, num2) => num1 + num2; // 等同于 var sum = function(num1, num2) { return num1 + num2; };如果箭頭函數(shù)的代碼塊部分多于一條語句,就要使用大括號將它們括起來,并且使用 return 語句返回
var sum = (num1, num2) => { return num1 + num2; }由于大括號被解釋為代碼塊,所以如果箭頭函數(shù)直接返回一個對象,必須在對象外面加上括號,否則會報錯。
// 報錯 let getTempItem = id => { id: id, name: "Temp" }; // 不報錯 let getTempItem = id => ({ id: id, name: "Temp" });如果箭頭函數(shù)只有一行語句,且不需要返回值,可以采用下面的寫法,就不用寫大括號了。
let fn = () => void doesNotReturn();箭頭函數(shù)可以與變量解構結合使用。
const full = ({ first, last }) => first + ' ' + last; // 等同于 function full(person) { return person.first + ' ' + person.last; }箭頭函數(shù)使得表達更加簡潔
const isEven = n => n % 2 == 0; const square = n => n * n;上面代碼只用了兩行,就定義了兩個簡單的工具函數(shù)。如果不用箭頭函數(shù),可能就要占用多行,而且還不如現(xiàn)在這樣寫醒目。
 箭頭函數(shù)的一個用處是簡化回調函數(shù)。
 ?
另一個例子是
// 正常函數(shù)寫法 var result = values.sort(function (a, b) { return a - b; }); // 箭頭函數(shù)寫法 var result = values.sort((a, b) => a - b);下面是 rest 參數(shù)與箭頭函數(shù)結合的例子。
const numbers = (...nums) => nums; numbers(1, 2, 3, 4, 5) // [1,2,3,4,5] const headAndTail = (head, ...tail) => [head, tail]; headAndTail(1, 2, 3, 4, 5) // [1,[2,3,4,5]]5.2使用注意點
箭頭函數(shù)有幾個使用注意點。
 (1)函數(shù)體內的 this 對象,就是定義時所在的對象,而不是使用時所在的對象。
 (2)不可以當作構造函數(shù),也就是說,不可以使用 new 命令,否則會拋出一個錯誤。
 (3)不可以使用 arguments 對象,該對象在函數(shù)體內不存在。如果要用,可以用 rest 參數(shù)代替。
 (4)不可以使用 yield 命令,因此箭頭函數(shù)不能用作 Generator 函數(shù)。
 上面四點中,第一點尤其值得注意。 this 對象的指向是可變的,但是在箭頭函數(shù)中,它是固定的。
上面代碼中, setTimeout 的參數(shù)是一個箭頭函數(shù),這個箭頭函數(shù)的定義生效是在 foo 函數(shù)生成時,而它的真正執(zhí)行要等到 100 毫秒后。如果是普通函
 數(shù),執(zhí)行時 this 應該指向全局對象 window ,這時應該輸出 21 。但是,箭頭函數(shù)導致 this 總是指向函數(shù)定義生效時所在的對象(本例是 {id: 42} ),所
 以輸出的是 42 。
 箭頭函數(shù)可以讓 setTimeout 里面的 this ,綁定定義時所在的作用域,而不是指向運行時所在的作用域。下面是另一個例子。
上面代碼中, Timer 函數(shù)內部設置了兩個定時器,分別使用了箭頭函數(shù)和普通函數(shù)。前者的 this 綁定定義時所在的作用域(即 Timer 函數(shù)),后者的
 this 指向運行時所在的作用域(即全局對象)。所以,3100 毫秒之后, timer.s1 被更新了 3 次,而 timer.s2 一次都沒更新。
 箭頭函數(shù)可以讓 this 指向固定化,這種特性很有利于封裝回調函數(shù)。下面是一個例子,DOM 事件的回調函數(shù)封裝在一個對象里面
上面代碼的 init 方法中,使用了箭頭函數(shù),這導致這個箭頭函數(shù)里面的 this ,總是指向 handler 對象。否則,回調函數(shù)運行時, this.doSomething 這一
 行會報錯,因為此時 this 指向 document 對象。
 this 指向的固定化,并不是因為箭頭函數(shù)內部有綁定 this 的機制,實際原因是箭頭函數(shù)根本沒有自己的 this ,導致內部的 this 就是外層代碼塊的
 this 。正是因為它沒有 this ,所以也就不能用作構造函數(shù)。
 所以,箭頭函數(shù)轉成 ES5 的代碼如下。
上面代碼中,轉換后的 ES5 版本清楚地說明了,箭頭函數(shù)里面根本沒有自己的 this ,而是引用外層的 this 。
 請問下面的代碼之中有幾個 this ?
上面代碼之中,只有一個 this ,就是函數(shù) foo 的 this ,所以 t1 、 t2 、 t3 都輸出同樣的結果。因為所有的內層函數(shù)都是箭頭函數(shù),都沒有自己的
 this ,它們的 this 其實都是最外層 foo 函數(shù)的 this 。
 除了 this ,以下三個變量在箭頭函數(shù)之中也是不存在的,指向外層函數(shù)的對應變量: arguments 、 super 、 new.target
上面代碼中,箭頭函數(shù)內部的變量 arguments ,其實是函數(shù) foo 的 arguments 變量。
 另外,由于箭頭函數(shù)沒有自己的 this ,所以當然也就不能用 call() 、 apply() 、 bind() 這些方法去改變 this 的指向
上面代碼中,箭頭函數(shù)沒有自己的 this ,所以 bind 方法無效,內部的 this 指向外部的 this 。
 長期以來,JavaScript 語言的 this 對象一直是一個令人頭痛的問題,在對象方法中使用 this ,必須非常小心。箭頭函數(shù)”綁定” this ,很大程度上解決
 了這個困擾。
5.3嵌套的箭頭函數(shù)
箭頭函數(shù)內部,還可以再使用箭頭函數(shù)。下面是一個 ES5 語法的多重嵌套函數(shù)。
function insert(value) { return {into: function (array) { return {after: function (afterValue) { array.splice(array.indexOf(afterValue) + 1, 0, value); return array; }}; }}; } insert(2).into([1, 3]).after(1); //[1, 2, 3]上面這個函數(shù),可以使用箭頭函數(shù)改寫。
let insert = (value) => ({into: (array) => ({after: (afterValue) => { array.splice(array.indexOf(afterValue) + 1, 0, value); return array; }})}); insert(2).into([1, 3]).after(1); //[1, 2, 3]下面是一個部署管道機制(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ù)還有一個功能,就是可以很方便地改寫 λ 演算
// λ演算的寫法 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)));上面兩種寫法,幾乎是一一對應的。由于 λ 演算對于計算機科學非常重要,這使得我們可以用 ES6 作為替代工具,探索計算機科學。
6. 雙冒號運算符
箭頭函數(shù)可以綁定 this 對象,大大減少了顯式綁定 this 對象的寫法( call 、 apply 、 bind )。但是,箭頭函數(shù)并不適用于所有場合,所以現(xiàn)在有一個
 提案,提出了“函數(shù)綁定”(function bind)運算符,用來取代 call 、 apply 、 bind 調用。
 函數(shù)綁定運算符是并排的兩個冒號( :: ),雙冒號左邊是一個對象,右邊是一個函數(shù)。該運算符會自動將左邊的對象,作為上下文環(huán)境(即 this 對
 象),綁定到右邊的函數(shù)上面。
如果雙冒號左邊為空,右邊是一個對象的方法,則等于將該方法綁定在該對象上面
var method = obj::obj.foo; // 等同于 var method = ::obj.foo; let log = ::console.log; // 等同于 var log = console.log.bind(console);雙冒號運算符的運算結果,還是一個對象,因此可以采用鏈式寫法
// 例一 import { map, takeWhile, forEach } from "iterlib"; getPlayers() ::map(x => x.character()) ::takeWhile(x => x.strength > 100) ::forEach(x => console.log(x)); // 例二 let { find, html } = jake; document.querySelectorAll("div.myClass") ::find("p") ::html("hahaha")7. 尾調用優(yōu)化
7.1什么是尾調用?
尾調用(Tail Call)是函數(shù)式編程的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函數(shù)的最后一步是調用另一個函數(shù)。
function f(x){ return g(x); }上面代碼中,函數(shù) f 的最后一步是調用函數(shù) g ,這就叫尾調用。
 以下三種情況,都不屬于尾調用。
上面代碼中,情況一是調用函數(shù) g 之后,還有賦值操作,所以不屬于尾調用,即使語義完全一樣。情況二也屬于調用后還有操作,即使寫在一行內。情況
 三等同于下面的代碼。
尾調用不一定出現(xiàn)在函數(shù)尾部,只要是最后一步操作即可
function f(x) { if (x > 0) { return m(x) } return n(x); }上面代碼中,函數(shù) m 和 n 都屬于尾調用,因為它們都是函數(shù) f 的最后一步操作。
7.2尾調用優(yōu)化
尾調用之所以與其他調用不同,就在于它的特殊的調用位置。
 我們知道,函數(shù)調用會在內存形成一個“調用記錄”,又稱“調用幀”(call frame),保存調用位置和內部變量等信息。如果在函數(shù) A 的內部調用函數(shù) B ,那
 么在 A 的調用幀上方,還會形成一個 B 的調用幀。等到 B 運行結束,將結果返回到 A , B 的調用幀才會消失。如果函數(shù) B 內部還調用函數(shù) C ,那就還有一
 個 C 的調用幀,以此類推。所有的調用幀,就形成一個“調用棧”(call stack)。
 尾調用由于是函數(shù)的最后一步操作,所以不需要保留外層函數(shù)的調用幀,因為調用位置、內部變量等信息都不會再用到了,只要直接用內層函數(shù)的調用
 幀,取代外層函數(shù)的調用幀就可以了
上面代碼中,如果函數(shù) g 不是尾調用,函數(shù) f 就需要保存內部變量 m 和 n 的值、 g 的調用位置等信息。但由于調用 g 之后,函數(shù) f 就結束了,所以執(zhí)行到最
 后一步,完全可以刪除 f(x) 的調用幀,只保留 g(3) 的調用幀。
 這就叫做“尾調用優(yōu)化”(Tail call optimization),即只保留內層函數(shù)的調用幀。如果所有函數(shù)都是尾調用,那么完全可以做到每次執(zhí)行時,調用幀只有
 一項,這將大大節(jié)省內存。這就是“尾調用優(yōu)化”的意義。
 注意,只有不再用到外層函數(shù)的內部變量,內層函數(shù)的調用幀才會取代外層函數(shù)的調用幀,否則就無法進行“尾調用優(yōu)化”。
上面的函數(shù)不會進行尾調用優(yōu)化,因為內層函數(shù) inner 用到了外層函數(shù) addOne 的內部變量 one
7.3尾遞歸
函數(shù)調用自身,稱為遞歸。如果尾調用自身,就稱為尾遞歸。
 遞歸非常耗費內存,因為需要同時保存成千上百個調用幀,很容易發(fā)生“棧溢出”錯誤(stack overflow)。但對于尾遞歸來說,由于只存在一個調用幀,
 所以永遠不會發(fā)生“棧溢出”錯誤。
上面代碼是一個階乘函數(shù),計算 n 的階乘,最多需要保存 n 個調用記錄,復雜度 O(n) 。
 如果改寫成尾遞歸,只保留一個調用記錄,復雜度 O(1)
還有一個比較著名的例子,就是計算 Fibonacci 數(shù)列,也能充分說明尾遞歸優(yōu)化的重要性。
 非尾遞歸的 Fibonacci 數(shù)列實現(xiàn)如下。
尾遞歸優(yōu)化過的 Fibonacci 數(shù)列實現(xiàn)如下
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由此可見,“尾調用優(yōu)化”對遞歸操作意義重大,所以一些函數(shù)式編程語言將其寫入了語言規(guī)格。ES6 是如此,第一次明確規(guī)定,所有 ECMAScript 的實
 現(xiàn),都必須部署“尾調用優(yōu)化”。這就是說,ES6 中只要使用尾遞歸,就不會發(fā)生棧溢出,相對節(jié)省內存。
7.4遞歸函數(shù)改寫
尾遞歸的實現(xiàn),往往需要改寫遞歸函數(shù),確保最后一步只調用自身。做到這一點的方法,就是把所有用到的內部變量改寫成函數(shù)的參數(shù)。比如上面的例
 子,階乘函數(shù) factorial 需要用到一個中間變量 total ,那就把這個中間變量改寫成函數(shù)的參數(shù)。這樣做的缺點就是不太直觀,第一眼很難看出來,為什么
 計算 5 的階乘,需要傳入兩個參數(shù) 5 和 1 ?
 兩個方法可以解決這個問題。方法一是在尾遞歸函數(shù)之外,再提供一個正常形式的函數(shù)。
上面代碼通過一個正常形式的階乘函數(shù) factorial ,調用尾遞歸函數(shù) tailFactorial ,看起來就正常多了。
 函數(shù)式編程有一個概念,叫做柯里化(currying),意思是將多參數(shù)的函數(shù)轉換成單參數(shù)的形式。這里也可以使用柯里化。
上面代碼通過柯里化,將尾遞歸函數(shù) tailFactorial 變?yōu)橹唤邮芤粋€參數(shù)的 factorial 。
 第二種方法就簡單多了,就是采用 ES6 的函數(shù)默認值。
上面代碼中,參數(shù) total 有默認值 1 ,所以調用時不用提供這個值。
 總結一下,遞歸本質上是一種循環(huán)操作。純粹的函數(shù)式編程語言沒有循環(huán)操作命令,所有的循環(huán)都用遞歸實現(xiàn),這就是為什么尾遞歸對這些語言極其重
 要。對于其他支持“尾調用優(yōu)化”的語言(比如 Lua,ES6),只需要知道循環(huán)可以用遞歸代替,而一旦使用遞歸,就最好使用尾遞歸。
7.5嚴格模式
ES6 的尾調用優(yōu)化只在嚴格模式下開啟,正常模式是無效的。
 這是因為在正常模式下,函數(shù)內部有兩個變量,可以跟蹤函數(shù)的調用棧。
 func.arguments :返回調用時函數(shù)的參數(shù)。
 func.caller :返回調用當前函數(shù)的那個函數(shù)。
 尾調用優(yōu)化發(fā)生時,函數(shù)的調用棧會改寫,因此上面兩個變量就會失真。嚴格模式禁用這兩個變量,所以尾調用模式僅在嚴格模式下生效。
7.6尾遞歸優(yōu)化的實現(xiàn)
尾遞歸優(yōu)化只在嚴格模式下生效,那么正常模式下,或者那些不支持該功能的環(huán)境中,有沒有辦法也使用尾遞歸優(yōu)化呢?回答是可以的,就是自己實現(xiàn)尾
 遞歸優(yōu)化。
 它的原理非常簡單。尾遞歸之所以需要優(yōu)化,原因是調用棧太多,造成溢出,那么只要減少調用棧,就不會溢出。怎么做可以減少調用棧呢?就是采用“循
 環(huán)”換掉“遞歸”。
 下面是一個正常的遞歸函數(shù)。
上面代碼中, sum 是一個遞歸函數(shù),參數(shù) x 是需要累加的值,參數(shù) y 控制遞歸次數(shù)。一旦指定 sum 遞歸 100000 次,就會報錯,提示超出調用棧的最大次
 數(shù)。
 蹦床函數(shù)(trampoline)可以將遞歸執(zhí)行轉為循環(huán)執(zhí)行
上面就是蹦床函數(shù)的一個實現(xiàn),它接受一個函數(shù) f 作為參數(shù)。只要 f 執(zhí)行后返回一個函數(shù),就繼續(xù)執(zhí)行。注意,這里是返回一個函數(shù),然后執(zhí)行該函數(shù),
 而不是函數(shù)里面調用函數(shù),這樣就避免了遞歸執(zhí)行,從而就消除了調用棧過大的問題。
 然后,要做的就是將原來的遞歸函數(shù),改寫為每一步返回另一個函數(shù)。
上面代碼中, sum 函數(shù)的每次執(zhí)行,都會返回自身的另一個版本。
 現(xiàn)在,使用蹦床函數(shù)執(zhí)行 sum ,就不會發(fā)生調用棧溢出
蹦床函數(shù)并不是真正的尾遞歸優(yōu)化,下面的實現(xiàn)才是。
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) // 100001上面代碼中, tco 函數(shù)是尾遞歸優(yōu)化的實現(xiàn),它的奧妙就在于狀態(tài)變量 active 。默認情況下,這個變量是不激活的。一旦進入尾遞歸優(yōu)化的過程,這個變
 量就激活了。然后,每一輪遞歸 sum 返回的都是 undefined ,所以就避免了遞歸執(zhí)行;而 accumulated 數(shù)組存放每一輪 sum 執(zhí)行的參數(shù),總是有值的,這
 就保證了 accumulator 函數(shù)內部的 while 循環(huán)總是會執(zhí)行。這樣就很巧妙地將“遞歸”改成了“循環(huán)”,而后一輪的參數(shù)會取代前一輪的參數(shù),保證了調用棧
 只有一層
8. 函數(shù)參數(shù)的尾逗號
ES2017 允許函數(shù)的最后一個參數(shù)有尾逗號(trailing comma)。
 此前,函數(shù)定義和調用時,都不允許最后一個參數(shù)后面出現(xiàn)逗號。
上面代碼中,如果在 param2 或 bar 后面加一個逗號,就會報錯。
 如果像上面這樣,將參數(shù)寫成多行(即每個參數(shù)占據(jù)一行),以后修改代碼的時候,想為函數(shù) clownsEverywhere 添加第三個參數(shù),或者調整參數(shù)的次序,
 就勢必要在原來最后一個參數(shù)后面添加一個逗號。這對于版本管理系統(tǒng)來說,就會顯示添加逗號的那一行也發(fā)生了變動。這看上去有點冗余,因此新的語
 法允許定義和調用時,尾部直接有一個逗號。
這樣的規(guī)定也使得,函數(shù)參數(shù)與數(shù)組和對象的尾逗號規(guī)則,保持一致了
9. catch 語句的參數(shù)
目前,有一個提案,允許 try...catch 結構中的 catch 語句調用時不帶有參數(shù)。這個提案跟參數(shù)有關,也放在這一章介紹。
 傳統(tǒng)的寫法是 catch 語句必須帶有參數(shù),用來接收 try 代碼塊拋出的錯誤。
新的寫法允許省略 catch 后面的參數(shù),而不報錯。
try { // ··· } catch { // ··· }新寫法只在不需要錯誤實例的情況下有用,因此不及傳統(tǒng)寫法的用途廣。
let jsonData; try { jsonData = JSON.parse(str); } catch { jsonData = DEFAULT_DATA; }上面代碼中, JSON.parse 報錯只有一種可能:解析失敗。因此,可以不需要拋出的錯誤實例。
總結
本博客源于本人閱讀相關書籍和視頻總結,創(chuàng)作不易,謝謝點贊支持。學到就是賺到。我是歌謠,勵志成為一名優(yōu)秀的技術革新人員。
歡迎私信交流,一起學習,一起成長。
推薦鏈接 其他文件目錄參照
“睡服“面試官系列之各系列目錄匯總(建議學習收藏)
總結
以上是生活随笔為你收集整理的“睡服”面试官系列第十三篇之函数的扩展(建议收藏学习)的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 计算机bios设置系统安装教程,U盘装系
 - 下一篇: 充值核销卡密恶意并发请求防止重复利用卡密