參數(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ù)默認(rèn)值時胆胰,函數(shù)不能有同名參數(shù)狞贱。
// 不報(bào)錯
function foo(x, x, y) {
// ...
}
// 報(bào)錯
function foo(x, x, y = 1) {
// ...
}
// SyntaxError: Duplicate parameter name not allowed in this context
同名參數(shù)只會取最后的參數(shù)名
作用域
一旦設(shè)置了參數(shù)的默認(rèn)值,函數(shù)進(jìn)行聲明初始化時蜀涨,參數(shù)會形成一個單獨(dú)的作用域(context)瞎嬉。等到初始化結(jié)束,這個作用域就會消失厚柳。這種語法行為氧枣,在不設(shè)置參數(shù)默認(rèn)值時,是不會出現(xiàn)的别垮。
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
上面代碼中便监,參數(shù)y的默認(rèn)值等于變量x。調(diào)用函數(shù)f時,參數(shù)形成一個單獨(dú)的作用域烧董。在這個作用域里面毁靶,默認(rèn)值變量x指向第一個參數(shù)x,而不是全局變量x逊移,所以輸出是2预吆。
再看下面的例子。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
上面代碼中胳泉,函數(shù)f調(diào)用時拐叉,參數(shù)y = x形成一個單獨(dú)的作用域。這個作用域里面胶背,變量x本身沒有定義巷嚣,所以指向外層的全局變量x。函數(shù)調(diào)用時钳吟,函數(shù)體內(nèi)部的局部變量x影響不到默認(rèn)值變量x廷粒。
如果此時,全局變量x不存在红且,就會報(bào)錯坝茎。
function f(y = x) {
let x = 2;
console.log(y);
}
f() // ReferenceError: x is not defined
下面這樣寫,也會報(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 未定義“次酌。
應(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)行
rest 參數(shù)
ES6 引入 rest 參數(shù)(形式為...變量名),用于獲取函數(shù)的多余參數(shù)因块,這樣就不需要使用arguments對象了橘原。rest 參數(shù)搭配的變量是一個數(shù)組,該變量將多余的參數(shù)放入數(shù)組中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
箭頭函數(shù)
ES6 允許使用“箭頭”(=>)定義函數(shù)趾断。
var f = v => v;
等同于:
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;
}
使用注意點(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ù)中耍鬓,它是固定的
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
上面代碼中,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)行時所在的作用域鸦概。下面是另一個例子。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭頭函數(shù)
setInterval(() => this.s1++, 1000);
// 普通函數(shù)
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函數(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ù)封裝在一個對象里面指厌。
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);
}
};
上面代碼的init方法中刊愚,使用了箭頭函數(shù),這導(dǎo)致這個箭頭函數(shù)里面的this踩验,總是指向handler對象鸥诽。否則,回調(diào)函數(shù)運(yùn)行時箕憾,this.doSomething這一行會報(bào)錯牡借,因?yàn)榇藭rthis指向document對象。
this指向的固定化袭异,并不是因?yàn)榧^函數(shù)內(nèi)部有綁定this的機(jī)制钠龙,實(shí)際原因是箭頭函數(shù)根本沒有自己的this,導(dǎo)致內(nèi)部的this就是外層代碼塊的this御铃。正是因?yàn)樗鼪]有this碴里,所以也就不能用作構(gòu)造函數(shù)。
請問下面的代碼之中有幾個this上真?
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
上面代碼之中咬腋,只有一個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疏日。
什么是尾調(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)用侵俗。
// 情況一
function f(x){
let y = g(x);
return y;
}
// 情況二
function f(x){
return g(x) + 1;
}
// 情況三
function f(x){
g(x);
}
上面代碼中,情況一是調(diào)用函數(shù)g之后丰刊,還有賦值操作隘谣,所以不屬于尾調(diào)用,即使語義完全一樣啄巧。情況二也屬于調(diào)用后還有操作寻歧,即使寫在一行內(nèi)。情況三等同于下面的代碼秩仆。
function f(x){
g(x);
return undefined;
}
尾調(diào)用不一定出現(xiàn)在函數(shù)尾部码泛,只要是最后一步操作即可。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
尾調(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)用幀就可以了。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
上面代碼中局待,如果函數(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)化”
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
上面的函數(shù)不會進(jìn)行尾調(diào)用優(yōu)化镜硕,因?yàn)閮?nèi)層函數(shù)inner用到了外層函數(shù)addOne的內(nèi)部變量one
尾遞歸
函數(shù)調(diào)用自身,稱為遞歸返干。如果尾調(diào)用自身兴枯,就稱為尾遞歸。
遞歸非常耗費(fèi)內(nèi)存矩欠,因?yàn)樾枰瑫r保存成千上百個調(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ù),計(jì)算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ù)列實(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ù)列實(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)存妆艘。
轉(zhuǎn)自 阮一峰 ECMAScript 6 入門