1.函數(shù)參數(shù)的默認(rèn)值
基本用法
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賦值了兔港,但是對(duì)應(yīng)的布爾值為false庸毫,則該賦值不起作用仔拟。就像上面代碼的最后一行,參數(shù)y等于空字符飒赃,結(jié)果被改為默認(rèn)值利花。
為了避免這個(gè)問(wèn)題,通常需要先判斷一下參數(shù)y是否被賦值载佳,如果沒有炒事,再等于默認(rèn)值。
if (typeof y === 'undefined') {
? y = 'World';
}
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
參數(shù)變量是默認(rèn)聲明的,所以不能用let或const再次聲明姑躲。
function foo(x = 5) {
? let x = 1; // error
? const x = 2; // error
}
上面代碼中睡扬,參數(shù)變量x是默認(rèn)聲明的,在函數(shù)體中黍析,不能用let或const再次聲明卖怜,否則會(huì)報(bào)錯(cuò)。
使用參數(shù)默認(rèn)值時(shí)阐枣,函數(shù)不能有同名參數(shù)马靠。
// 不報(bào)錯(cuò)
function foo(x, x, y) {
? // ...
}
// 報(bào)錯(cuò)
function foo(x, x, y = 1) {
? // ...
}
// SyntaxError: Duplicate parameter name not allowed in this context
另外奄抽,一個(gè)容易忽略的地方是,參數(shù)默認(rèn)值不是傳值的甩鳄,而是每次都重新計(jì)算默認(rèn)值表達(dá)式的值逞度。也就是說(shuō),參數(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。這時(shí)彬祖,每次調(diào)用函數(shù)foo茁瘦,都會(huì)重新計(jì)算x + 1,而不是默認(rèn)p等于 100储笑。
與解構(gòu)賦值默認(rèn)值結(jié)合使用
參數(shù)默認(rèn)值可以與解構(gòu)賦值的默認(rèn)值甜熔,結(jié)合起來(lái)使用。
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
上面代碼只使用了對(duì)象的解構(gòu)賦值默認(rèn)值突倍,沒有使用函數(shù)參數(shù)的默認(rèn)值腔稀。只有當(dāng)函數(shù)foo的參數(shù)是一個(gè)對(duì)象時(shí),變量x和y才會(huì)通過(guò)解構(gòu)賦值生成羽历。如果函數(shù)foo調(diào)用時(shí)沒提供參數(shù)焊虏,變量x和y就不會(huì)生成,從而報(bào)錯(cuò)秕磷。通過(guò)提供函數(shù)參數(shù)的默認(rèn)值诵闭,就可以避免這種情況。
function foo({x, y = 5} = {}) {
? console.log(x, y);
}
foo() // undefined 5
上面代碼指定澎嚣,如果沒有提供參數(shù)疏尿,函數(shù)foo的參數(shù)默認(rèn)為一個(gè)空對(duì)象。
如果傳入undefined易桃,將觸發(fā)該參數(shù)等于默認(rèn)值褥琐,null則沒有這個(gè)效果。
function foo(x = 5, y = 6) {
? console.log(x, y);
}
foo(undefined, null)
// 5 null
上面代碼中晤郑,x參數(shù)對(duì)應(yīng)undefined敌呈,結(jié)果觸發(fā)了默認(rèn)值,y參數(shù)等于null造寝,就沒有觸發(fā)默認(rèn)值磕洪。
作用域
一旦設(shè)置了參數(shù)的默認(rèn)值,函數(shù)進(jìn)行聲明初始化時(shí)匹舞,參數(shù)會(huì)形成一個(gè)單獨(dú)的作用域(context)褐鸥。等到初始化結(jié)束,這個(gè)作用域就會(huì)消失赐稽。這種語(yǔ)法行為叫榕,在不設(shè)置參數(shù)默認(rèn)值時(shí)浑侥,是不會(huì)出現(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í),參數(shù)形成一個(gè)單獨(dú)的作用域荞下。在這個(gè)作用域里面伶选,默認(rèn)值變量x指向第一個(gè)參數(shù)x,而不是全局變量x尖昏,所以輸出是2仰税。
再看下面的例子。
let x = 1;
function f(y = x) {
? let x = 2;
? console.log(y);
}
f() // 1
上面代碼中抽诉,函數(shù)f調(diào)用時(shí)陨簇,參數(shù)y = x形成一個(gè)單獨(dú)的作用域。這個(gè)作用域里面迹淌,變量x本身沒有定義河绽,所以指向外層的全局變量x。函數(shù)調(diào)用時(shí)唉窃,函數(shù)體內(nèi)部的局部變量x影響不到默認(rèn)值變量x耙饰。
var x = 1;
function foo(x = x) {
? // ...
}
foo() // ReferenceError: x is not defined
上面代碼中,參數(shù)x = x形成一個(gè)單獨(dú)作用域纹份。實(shí)際執(zhí)行的是let x = x苟跪,由于暫時(shí)性死區(qū)的原因,這行代碼會(huì)報(bào)錯(cuò)”x 未定義“矮嫉。
如果參數(shù)的默認(rèn)值是一個(gè)函數(shù)削咆,該函數(shù)的作用域也遵守這個(gè)規(guī)則。請(qǐng)看下面的例子蠢笋。
let foo = 'outer';
function bar(func = () => foo) {
? let foo = 'inner';
? console.log(func());
}
bar(); // outer
上面代碼中,函數(shù)bar的參數(shù)func的默認(rèn)值是一個(gè)匿名函數(shù)鳞陨,返回值為變量foo昨寞。函數(shù)參數(shù)形成的單獨(dú)作用域里面,并沒有定義變量foo厦滤,所以foo指向外層的全局變量foo援岩,因此輸出outer。
如果寫成下面這樣掏导,就會(huì)報(bào)錯(cuò)享怀。
function bar(func = () => foo) {
? let foo = 'inner';
? console.log(func());
}
bar() // ReferenceError: foo is not defined
上面代碼中,匿名函數(shù)里面的foo指向函數(shù)外層趟咆,但是函數(shù)外層并沒有聲明變量foo添瓷,所以就報(bào)錯(cuò)了梅屉。
var x = 1;
function foo(x, y = function() { x = 2; }) {
? var x = 3;
? y();
? console.log(x);
}
foo() // 3
x // 1
上面代碼中,函數(shù)foo的參數(shù)形成一個(gè)單獨(dú)作用域鳞贷。這個(gè)作用域里面坯汤,首先聲明了變量x,然后聲明了變量y搀愧,y的默認(rèn)值是一個(gè)匿名函數(shù)惰聂。這個(gè)匿名函數(shù)內(nèi)部的變量x,指向同一個(gè)作用域的第一個(gè)參數(shù)x咱筛。函數(shù)foo內(nèi)部又聲明了一個(gè)內(nèi)部變量x搓幌,該變量與第一個(gè)參數(shù)x由于不是同一個(gè)作用域,所以不是同一個(gè)變量迅箩,因此執(zhí)行y后鼻种,內(nèi)部變量x和外部全局變量x的值都沒變。
如果將var x = 3的var去除沙热,函數(shù)foo的內(nèi)部變量x就指向第一個(gè)參數(shù)x叉钥,與匿名函數(shù)內(nèi)部的x是一致的,所以最后輸出的就是2篙贸,而外層的全局變量x依然不受影響投队。
var x = 1;
function foo(x, y = function() { x = 2; }) {
? x = 3;
? y();
? console.log(x);
}
foo() // 2
x // 1
2.rest 參數(shù)
ES6 引入 rest 參數(shù)(形式為...變量名),用于獲取函數(shù)的多余參數(shù)爵川,這樣就不需要使用arguments對(duì)象了敷鸦。rest 參數(shù)搭配的變量是一個(gè)數(shù)組,該變量將多余的參數(shù)放入數(shù)組中寝贡。
function add(...values) {
? let sum = 0;
? for (var val of values) {
? ? sum += val;
? }
? return sum;
}
add(2, 5, 3) // 10
上面代碼的add函數(shù)是一個(gè)求和函數(shù)扒披,利用 rest 參數(shù),可以向該函數(shù)傳入任意數(shù)目的參數(shù)圃泡。
下面是一個(gè) rest 參數(shù)代替arguments變量的例子碟案。
// arguments變量的寫法
function sortNumbers() {
? return Array.prototype.slice.call(arguments).sort();
}
// rest參數(shù)的寫法
const sortNumbers = (...numbers) => numbers.sort();
下面是一個(gè)利用 rest 參數(shù)改寫數(shù)組push方法的例子。
function push(array, ...items) {
? items.forEach(function(item) {
? ? array.push(item);
? ? console.log(item);
? });
}
var a = [];
push(a, 1, 2, 3)
注意颇蜡,rest 參數(shù)之后不能再有其他參數(shù)(即只能是最后一個(gè)參數(shù))价说,否則會(huì)報(bào)錯(cuò)。
// 報(bào)錯(cuò)
function f(a, ...b, c) {
? // ...
}
函數(shù)的length屬性风秤,不包括 rest 參數(shù)鳖目。
4.name 屬性 § ?
函數(shù)的name屬性,返回該函數(shù)的函數(shù)名缤弦。
5.箭頭函數(shù)
1.如果箭頭函數(shù)的代碼塊部分多于一條語(yǔ)句领迈,就要使用大括號(hào)將它們括起來(lái),并且使用return語(yǔ)句返回。
由于大括號(hào)被解釋為代碼塊狸捅,所以如果箭頭函數(shù)直接返回一個(gè)對(duì)象衷蜓,必須在對(duì)象外面加上括號(hào),否則會(huì)報(bào)錯(cuò)薪贫。
// 報(bào)錯(cuò)
let getTempItem = id => { id: id, name: "Temp" };
// 不報(bào)錯(cuò)
let getTempItem = id => ({ id: id, name: "Temp" });
2.如果箭頭函數(shù)不需要參數(shù)或需要多個(gè)參數(shù)恍箭,就使用一個(gè)圓括號(hào)代表參數(shù)部分。
3.箭頭函數(shù)可以與變量解構(gòu)結(jié)合使用瞧省。
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
? return person.first + ' ' + person.last;
}
使用注意點(diǎn) § ?
箭頭函數(shù)有幾個(gè)使用注意點(diǎn)扯夭。
(1)函數(shù)體內(nèi)的this對(duì)象,就是定義時(shí)所在的對(duì)象鞍匾,而不是使用時(shí)所在的對(duì)象交洗。
(2)不可以當(dāng)作構(gòu)造函數(shù),也就是說(shuō)橡淑,不可以使用new命令构拳,否則會(huì)拋出一個(gè)錯(cuò)誤。
(3)不可以使用arguments對(duì)象梁棠,該對(duì)象在函數(shù)體內(nèi)不存在置森。如果要用,可以用 rest 參數(shù)代替符糊。
(4)不可以使用yield命令凫海,因此箭頭函數(shù)不能用作 Generator 函數(shù)。
上面四點(diǎn)中男娄,第一點(diǎn)尤其值得注意行贪。this對(duì)象的指向是可變的,但是在箭頭函數(shù)中模闲,它是固定的建瘫。
function foo() {
? setTimeout(() => {
? ? console.log('id:', this.id);
? }, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
上面代碼中,setTimeout的參數(shù)是一個(gè)箭頭函數(shù)尸折,這個(gè)箭頭函數(shù)的定義生效是在foo函數(shù)生成時(shí)啰脚,而它的真正執(zhí)行要等到 100 毫秒后。如果是普通函數(shù)翁授,執(zhí)行時(shí)this應(yīng)該指向全局對(duì)象window拣播,這時(shí)應(yīng)該輸出21。但是收擦,箭頭函數(shù)導(dǎo)致this總是指向函數(shù)定義生效時(shí)所在的對(duì)象(本例是{id: 42}),所以輸出的是42谍倦。
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è)置了兩個(gè)定時(shí)器,分別使用了箭頭函數(shù)和普通函數(shù)昼蛀。前者的this綁定定義時(shí)所在的作用域(即Timer函數(shù))宴猾,后者的this指向運(yùn)行時(shí)所在的作用域(即全局對(duì)象)圆存。所以,3100 毫秒之后仇哆,timer.s1被更新了 3 次沦辙,而timer.s2一次都沒更新。
由于箭頭函數(shù)沒有自己的this讹剔,所以當(dāng)然也就不能用call()油讯、apply()、bind()這些方法去改變this的指向延欠。
6.雙冒號(hào)運(yùn)算符
函數(shù)綁定運(yùn)算符是并排的兩個(gè)冒號(hào)(::)陌兑,雙冒號(hào)左邊是一個(gè)對(duì)象,右邊是一個(gè)函數(shù)由捎。該運(yùn)算符會(huì)自動(dòng)將左邊的對(duì)象兔综,作為上下文環(huán)境(即this對(duì)象),綁定到右邊的函數(shù)上面狞玛。
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);
}
7.尾調(diào)用優(yōu)化
尾調(diào)用(Tail Call)是函數(shù)式編程的一個(gè)重要概念软驰,本身非常簡(jiǎn)單,一句話就能說(shuō)清楚心肪,就是指某個(gè)函數(shù)的最后一步是調(diào)用另一個(gè)函數(shù)锭亏。
function f(x){
? return g(x);
}
// 情況三
function f(x){
? g(x);
}
上面代碼中,情況一是調(diào)用函數(shù)g之后蒙畴,還有賦值操作贰镣,所以不屬于尾調(diào)用,即使語(yǔ)義完全一樣膳凝。情況二也屬于調(diào)用后還有操作碑隆,即使寫在一行內(nèi)。情況三等同于下面的代碼蹬音。
function f(x){
? g(x);
? return undefined;
}
尾遞歸 § ?
函數(shù)調(diào)用自身上煤,稱為遞歸。如果尾調(diào)用自身著淆,就稱為尾遞歸劫狠。
遞歸非常耗費(fèi)內(nèi)存,因?yàn)樾枰瑫r(shí)保存成千上百個(gè)調(diào)用幀永部,很容易發(fā)生“棧溢出”錯(cuò)誤(stack overflow)独泞。但對(duì)于尾遞歸來(lái)說(shuō),由于只存在一個(gè)調(diào)用幀苔埋,所以永遠(yuǎn)不會(huì)發(fā)生“棧溢出”錯(cuò)誤懦砂。
function factorial(n) {
? if (n === 1) return 1;
? return n * factorial(n - 1);
}
factorial(5) // 120
上面代碼是一個(gè)階乘函數(shù),計(jì)算n的階乘,最多需要保存n個(gè)調(diào)用記錄荞膘,復(fù)雜度 O(n) 罚随。
如果改寫成尾遞歸,只保留一個(gè)調(diào)用記錄羽资,復(fù)雜度 O(1) 淘菩。
function factorial(n, total) {
? if (n === 1) return total;
? return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
還有一個(gè)比較著名的例子,就是計(jì)算 Fibonacci 數(shù)列屠升,也能充分說(shuō)明尾遞歸優(yōu)化的重要性潮改。
非尾遞歸的 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)化過(guò)的 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)化”對(duì)遞歸操作意義重大,所以一些函數(shù)式編程語(yǔ)言將其寫入了語(yǔ)言規(guī)格微服。ES6 是如此趾疚,第一次明確規(guī)定,所有 ECMAScript 的實(shí)現(xiàn)以蕴,都必須部署“尾調(diào)用優(yōu)化”糙麦。這就是說(shuō),ES6 中只要使用尾遞歸丛肮,就不會(huì)發(fā)生棧溢出赡磅,相對(duì)節(jié)省內(nèi)存。
遞歸函數(shù)的改寫
尾遞歸的實(shí)現(xiàn)宝与,往往需要改寫遞歸函數(shù)焚廊,確保最后一步只調(diào)用自身。做到這一點(diǎn)的方法习劫,就是把所有用到的內(nèi)部變量改寫成函數(shù)的參數(shù)咆瘟。比如上面的例子,階乘函數(shù) factorial 需要用到一個(gè)中間變量total诽里,那就把這個(gè)中間變量改寫成函數(shù)的參數(shù)袒餐。這樣做的缺點(diǎn)就是不太直觀,第一眼很難看出來(lái)谤狡,為什么計(jì)算5的階乘灸眼,需要傳入兩個(gè)參數(shù)5和1?
兩個(gè)方法可以解決這個(gè)問(wèn)題墓懂。方法一是在尾遞歸函數(shù)之外焰宣,再提供一個(gè)正常形式的函數(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
上面代碼通過(guò)一個(gè)正常形式的階乘函數(shù)factorial捕仔,調(diào)用尾遞歸函數(shù)tailFactorial宛徊,看起來(lái)就正常多了佛嬉。
函數(shù)式編程有一個(gè)概念逻澳,叫做柯里化(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) // 120
上面代碼通過(guò)柯里化苞氮,將尾遞歸函數(shù)tailFactorial變?yōu)橹唤邮芤粋€(gè)參數(shù)的factorial。
第二種方法就簡(jiǎn)單多了瓤逼,就是采用 ES6 的函數(shù)默認(rèn)值笼吟。
function factorial(n, total = 1) {
? if (n === 1) return total;
? return factorial(n - 1, n * total);
}
factorial(5) // 120
上面代碼中,參數(shù)total有默認(rèn)值1霸旗,所以調(diào)用時(shí)不用提供這個(gè)值贷帮。