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