ES6標(biāo)準(zhǔn)入門讀書筆記6(函數(shù)的擴(kuò)展)

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è)問題,通常需要先判斷一下參數(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á)式的值。也就是說,參數(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é)合起來使用。

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ì)通過解構(gòu)賦值生成。如果函數(shù)foo調(diào)用時(shí)沒提供參數(shù),變量x和y就不會(huì)生成,從而報(bào)錯(cuò)。通過提供函數(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ì)消失。這種語法行為,在不設(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ù)的代碼塊部分多于一條語句,就要使用大括號(hào)將它們括起來,并且使用return語句返回。

由于大括號(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ù),也就是說,不可以使用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è)重要概念,本身非常簡單,一句話就能說清楚,就是指某個(gè)函數(shù)的最后一步是調(diào)用另一個(gè)函數(shù)。

function f(x){
? return g(x);
}
// 情況三
function f(x){
? g(x);
}

上面代碼中,情況一是調(diào)用函數(shù)g之后,還有賦值操作,所以不屬于尾調(diào)用,即使語義完全一樣。情況二也屬于調(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ì)于尾遞歸來說,由于只存在一個(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ù)列,也能充分說明尾遞歸優(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)化過的 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ù)式編程語言將其寫入了語言規(guī)格。ES6 是如此,第一次明確規(guī)定,所有 ECMAScript 的實(shí)現(xiàn),都必須部署“尾調(diào)用優(yōu)化”。這就是說,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)就是不太直觀,第一眼很難看出來,為什么計(jì)算5的階乘,需要傳入兩個(gè)參數(shù)5和1?

兩個(gè)方法可以解決這個(gè)問題。方法一是在尾遞歸函數(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

上面代碼通過一個(gè)正常形式的階乘函數(shù)factorial,調(diào)用尾遞歸函數(shù)tailFactorial,看起來就正常多了。

函數(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

上面代碼通過柯里化,將尾遞歸函數(shù)tailFactorial變?yōu)橹唤邮芤粋€(gè)參數(shù)的factorial。

第二種方法就簡單多了,就是采用 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è)值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。