ES6之主要知識點(五)函數
函數參數的默認值
作用域
var x = 1; function f(x, y = x) { console.log(y); } f(2) // 2
let x = 1; function f(y = x) { let x = 2; console.log(y); } f() // 1
var x = 1; function foo(x, y = function() { x = 2; }) { var x = 3; y(); console.log(x); } foo() // 3 x // 1
var x = 1; function foo(x, y = function() { x = 2; }) { x = 3; y(); console.log(x); } foo() // 2 x // 1
應用
function throwIfMissing() { throw new Error(‘Missing parameter‘); } function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided; } foo() // Error: Missing parameter
上面代碼的foo
函數,如果調用的時候沒有參數,就會調用默認值throwIfMissing
函數,從而拋出一個錯誤。
2.rest 參數
ES6 引入 rest 參數(形式為...變量名
),用於獲取函數的多余參數,這樣就不需要使用arguments
對象了。
rest 參數搭配的變量是一個數組,該變量將多余的參數放入數組中。
function push(array, ...items) { items.forEach(function(item) { array.push(item); console.log(item); }); } var a = [];
註意,rest 參數之後不能再有其他參數(即只能是最後一個參數),否則會報錯。
3.箭頭函數
x => x * x
上面的箭頭函數相當於:
function (x) { return x * x; }
如果參數不是一個,就需要用括號()
括起來:
// 兩個參數: (x, y) => x * x + y * y // 無參數: () => 3.14 // 可變參數: (x, y, ...rest) => { var i, sum = x + y; for (i=0; i<rest.length; i++) { sum += rest[i]; } return sum; }
如果要返回一個對象,就要註意,如果是單表達式,這麽寫的話會報錯:
// SyntaxError: x => { foo: x }
因為和函數體的{ ... }
有語法沖突,所以要改為:
// ok: x => ({ foo: x })
箭頭函數看上去是匿名函數的一種簡寫,但實際上,箭頭函數和匿名函數有個明顯的區別:箭頭函數內部的this
是詞法作用域,由上下文確定。
回顧前面的例子,由於JavaScript函數對this
綁定的錯誤處理,下面的例子無法得到預期結果:
var obj = { birth: 1990, getAge: function () { var b = this.birth; // 1990 var fn = function () { return new Date().getFullYear() - this.birth; // this指向window或undefined }; return fn(); }
現在,箭頭函數完全修復了this
的指向,this
總是指向詞法作用域,也就是外層調用者obj
:
var obj = { birth: 1990, getAge: function () { var b = this.birth; // 1990 var fn = () => new Date().getFullYear() - this.birth; // this指向obj對象 return fn(); } }; obj.getAge(); // 25
使用註意點
箭頭函數有幾個使用註意點。
(1)函數體內的this
對象,就是定義時所在的對象,而不是使用時所在的對象。
(2)不可以當作構造函數,也就是說,不可以使用new
命令,否則會拋出一個錯誤。
(3)不可以使用arguments
對象,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。
(4)不可以使用yield
命令,因此箭頭函數不能用作 Generator 函數。
function Timer() { this.s1 = 0; this.s2 = 0; // 箭頭函數 setInterval(() => this.s1++, 1000); // 普通函數 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
上面代碼中,Timer
函數內部設置了兩個定時器,分別使用了箭頭函數和普通函數。前者的this
綁定定義時所在的作用域(即Timer
函數)
,後者的this
指向運行時所在的作用域(即全局對象)。所以,3100毫秒之後,timer.s1
被更新了3次,而timer.s2
一次都沒更新。
另外,由於箭頭函數沒有自己的this
,所以當然也就不能用call()
、apply()
、bind()
這些方法去改變this
的指向。
4.綁定 this
函數綁定運算符是並排的兩個冒號(::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象,
作為上下文環境(即this對象),綁定到右邊的函數上面。
foo::bar; // 等同於 bar.bind(foo); foo::bar(...arguments); // 等同於 bar.apply(foo, arguments); const hasOwnProperty = Object.prototype.hasOwnProperty; function hasOwn(obj, key) { return obj::hasOwnProperty(key); }
5.尾調用優化
尾調用(Tail Call)是函數式編程的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函數的最後一步是調用另一個函數。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
尾遞歸
function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120
上面代碼是一個階乘函數,計算n
的階乘,最多需要保存n
個調用記錄,復雜度 O(n) 。如果改寫成尾遞歸,只保留一個調用記錄,復雜度 O(1) 。
function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120
嚴格模式
ES6 的尾調用優化只在嚴格模式下開啟,正常模式是無效的。
這是因為在正常模式下,函數內部有兩個變量,可以跟蹤函數的調用棧。
func.arguments
:返回調用時函數的參數。func.caller
:返回調用當前函數的那個函數。
尾調用優化發生時,函數的調用棧會改寫,因此上面兩個變量就會失真。嚴格模式禁用這兩個變量,所以尾調用模式僅在嚴格模式下生效
在非嚴格模式時候我們可以自己模擬
蹦床函數(trampoline)可以將遞歸執行轉為循環執行。
function trampoline(f) { while (f && f instanceof Function) { f = f(); } return f; }
上面就是蹦床函數的一個實現,它接受一個函數f
作為參數。只要f
執行後返回一個函數,就繼續執行。
註意,這裏是返回一個函數,然後執行該函數,而不是函數裏面調用函數,這樣就避免了遞歸執行,從而就消除了調用棧過大的問題。
然後,要做的就是將原來的遞歸函數,改寫為每一步返回另一個函數。
function sum(x, y) { if (y > 0) { return sum.bind(null, x + 1, y - 1); } else { return x; } }
上面代碼中,sum
函數的每次執行,都會返回自身的另一個版本。
現在,使用蹦床函數執行sum
,就不會發生調用棧溢出。
trampoline(sum(1, 100000)) // 100001
蹦床函數並不是真正的尾遞歸優化,下面的實現才是。
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
ES6之主要知識點(五)函數