ES6入門 ___ 函數(shù)的擴展

一、函數(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 的寫法還有兩個好處:

  1. 閱讀代碼的人可以l立刻意識到哪些參數(shù)是可以省略的,不用查看函數(shù)體或文檔;
  2. 有利于將來的代碼優(yōu)化,即使未來的版本徹底拿掉這個參數(shù),也不會導(dǎo)致以前的代碼無法運行

使用默認(rèn)參數(shù)有以下幾點需要注意:

  1. 參數(shù)變量是默認(rèn)聲明的,所以不能用 let 或 const 再次聲明
function fn(x = 5) {
  let x = 1 //error
  const x = 1 // error
}
  1. 使用參數(shù)默認(rèn)值時,函數(shù)不能有同名參數(shù)。
function fn(x, x, y = 1) {
  // todo
}
// Uncaught SyntaxError: Duplicate parameter name not allowed in this context
  1. 參數(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ù)的注意事項:

  1. rest 參數(shù)中的變量代表一個數(shù)組,所以數(shù)組特有的方法都可以用于這個變量。
  2. rest 參數(shù)之后不能再有其他參數(shù)(即只能是最后一個參數(shù)),否則會報錯。
  3. 函數(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ù)有以下幾個使用注意事項:

  1. 函數(shù)體內(nèi)的 this 對象就是定義時所在的對象,而不是使用時所在的對象。
  2. 不可以當(dāng)作構(gòu)造函數(shù)。也就是說,不可以使用 new 命令符,否則會拋出一個錯誤。
  3. 不可以使用 arguments 對象,該對象在函數(shù)體內(nèi)部不存在。如果要用,可以用 rest 參數(shù)代替。
  4. 不可以使用 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ī)則可以保持一致。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 函數(shù)參數(shù)的默認(rèn)值 基本用法 在ES6之前,不能直接為函數(shù)的參數(shù)指定默認(rèn)值,只能采用變通的方法。 上面代碼檢查函數(shù)l...
    陳老板_閱讀 512評論 0 1
  • 函數(shù)參數(shù)的默認(rèn)值 基本用法 在ES6之前,不能直接為函數(shù)的參數(shù)指定默認(rèn)值,只能采用變通的方法。 上面代碼檢查函數(shù)l...
    呼呼哥閱讀 3,703評論 0 1
  • 1.函數(shù)參數(shù)的默認(rèn)值 ES6 之前,不能直接為函數(shù)的參數(shù)指定默認(rèn)值,只能采用變通的方法。 ES6 允許為函數(shù)的參數(shù)...
    如果俞天陽會飛閱讀 269評論 0 0
  • 1.函數(shù)參數(shù)的默認(rèn)值 (1).基本用法 在ES6之前,不能直接為函數(shù)的參數(shù)指定默認(rèn)值,只能采用變通的方法。
    趙然228閱讀 830評論 0 0
  • 1.函數(shù)參數(shù)的默認(rèn)值 基本用法 ES6 之前,不能直接為函數(shù)的參數(shù)指定默認(rèn)值,只能采用變通的方法。 functio...
    Masami_9e88閱讀 558評論 0 0

友情鏈接更多精彩內(nèi)容