參數(shù)默認(rèn)值
ES6 允許為函數(shù)的參數(shù)設(shè)置默認(rèn)值,即直接寫在參數(shù)定義的后面。
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
const p = new Point();
p // { x: 0, y: 0 }
參數(shù)變量是默認(rèn)聲明的,所以不能用let或const再次聲明。
- 與解構(gòu)賦值默認(rèn)值結(jié)合使用
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
這樣的好處是,當(dāng)沒有傳對象進(jìn)去的時(shí)候,也不會報(bào)錯(cuò)。
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
(如果沒有參數(shù),就為空對象,有對象,就設(shè)定初始值)
如果函數(shù)fetch的第二個(gè)參數(shù)是一個(gè)對象,就可以為它的三個(gè)屬性設(shè)置默認(rèn)值。這種寫法不能省略第二個(gè)參數(shù),如果結(jié)合函數(shù)參數(shù)的默認(rèn)值,就可以省略第二個(gè)參數(shù)。這時(shí),就出現(xiàn)了雙重默認(rèn)值。
通常情況下,定義了默認(rèn)值的參數(shù),應(yīng)該是函數(shù)的尾參數(shù)。因?yàn)檫@樣比較容易看出來,到底省略了哪些參數(shù)。如果非尾部的參數(shù)設(shè)置默認(rèn)值,實(shí)際上這個(gè)參數(shù)是沒法省略的。此時(shí)你需要顯式地傳入一個(gè)undefined來觸發(fā)默認(rèn)值(null不行)。
函數(shù)的length值
指定了默認(rèn)值以后,函數(shù)的length屬性,將返回沒有指定默認(rèn)值的參數(shù)個(gè)數(shù)。也就是說,指定了默認(rèn)值后,length屬性將失真。
如果設(shè)置了默認(rèn)值的參數(shù)不是尾參數(shù),那么length屬性也不再計(jì)入后面的參數(shù)了。
作用域
一旦設(shè)置了參數(shù)的默認(rèn)值,函數(shù)進(jìn)行聲明初始化時(shí),參數(shù)會形成一個(gè)單獨(dú)的作用域。等到初始化結(jié)束,這個(gè)作用域就會消失。這種語法行為,在不設(shè)置參數(shù)默認(rèn)值時(shí),是不會出現(xiàn)的。
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。
如果參數(shù)的默認(rèn)值是一個(gè)函數(shù),該函數(shù)的作用域也遵守這個(gè)規(guī)則。
很復(fù)雜的例子:
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的值都沒變。
(簡單來說有三層作用域分別有一個(gè)x)
應(yīng)用
- 利用參數(shù)默認(rèn)值,可以指定某一個(gè)參數(shù)不得省略,如果省略就拋出一個(gè)錯(cuò)誤。
(做法:在某個(gè)參數(shù)沒有傳入的時(shí)候的默認(rèn)值設(shè)為一個(gè)函數(shù),拋出一個(gè)錯(cuò)誤信息)
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
注意函數(shù)名throwIfMissing之后有一對圓括號,這表明參數(shù)的默認(rèn)值不是在定義時(shí)執(zhí)行,而是在運(yùn)行時(shí)執(zhí)行。如果參數(shù)已經(jīng)賦值,默認(rèn)值中的函數(shù)就不會運(yùn)行。
- 另外,可以將參數(shù)默認(rèn)值設(shè)為undefined,表明這個(gè)參數(shù)是可以省略的。
rest參數(shù)
ES6 引入 rest 參數(shù)(形式為...變量名),用于獲取函數(shù)的多余參數(shù),這樣就不需要使用arguments對象了。rest 參數(shù)搭配的變量是一個(gè)數(shù)組,該變量將多余的參數(shù)放入數(shù)組中。
實(shí)際上就是一個(gè)參數(shù)數(shù)組,用for of 循環(huán)來遍歷。
arguments對象不是數(shù)組,而是一個(gè)類似數(shù)組的對象。所以為了使用數(shù)組的方法,必須使用Array.prototype.slice.call先將其轉(zhuǎn)為數(shù)組。rest 參數(shù)就不存在這個(gè)問題,它就是一個(gè)真正的數(shù)組,數(shù)組特有的方法都可以使用。下面是一個(gè)利用 rest 參數(shù)改寫數(shù)組push方法的例子。
注意,rest 參數(shù)之后不能再有其他參數(shù)(即只能是最后一個(gè)參數(shù)),否則會報(bào)錯(cuò)。
函數(shù)的length屬性,不包括 rest 參數(shù)。
name屬性
- 函數(shù)的name屬性,返回該函數(shù)的函數(shù)名。
- Function構(gòu)造函數(shù)返回的函數(shù)實(shí)例,name屬性的值為anonymous。
- bind返回的函數(shù),name屬性值會加上bound前綴。
箭頭函數(shù)
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
如果箭頭函數(shù)不需要參數(shù)或需要多個(gè)參數(shù),就使用一個(gè)圓括號代表參數(shù)部分。
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
由于大括號被解釋為代碼塊,所以如果箭頭函數(shù)直接返回一個(gè)對象,必須在對象外面加上括號,否則會報(bào)錯(cuò)。
// 報(bào)錯(cuò)
let getTempItem = id => { id: id, name: "Temp" };
// 不報(bào)錯(cuò)
let getTempItem = id => ({ id: id, name: "Temp" });
箭頭函數(shù)可以與變量解構(gòu)結(jié)合使用。
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
箭頭函數(shù)的一個(gè)用處是簡化回調(diào)函數(shù)。
// 正常函數(shù)寫法
[1,2,3].map(function (x) {
return x * x;
});
// 箭頭函數(shù)寫法
[1,2,3].map(x => x * x);
使用箭頭函數(shù)的注意點(diǎn):
- 函數(shù)體內(nèi)的this對象,就是定義時(shí)所在的對象,而不是使用時(shí)所在的對象。
- 不可以當(dāng)作構(gòu)造函數(shù),也就是說,不可以使用new命令,否則會拋出一個(gè)錯(cuò)誤。
- 不可以使用arguments對象,該對象在函數(shù)體內(nèi)不存在。如果要用,可以用 rest 參數(shù)代替。
- 不可以使用yield命令,因此箭頭函數(shù)不能用作 Generator 函數(shù)。
箭頭函數(shù)可以讓setTimeout里面的this,綁定定義時(shí)所在的作用域,而不是指向運(yùn)行時(shí)所在的作用域。
箭頭函數(shù)可以讓this指向固定化,這種特性很有利于封裝回調(diào)函數(shù)。下面是一個(gè)例子,DOM 事件的回調(diào)函數(shù)封裝在一個(gè)對象里面。
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);
}
};
箭頭函數(shù)里面根本沒有自己的this,而是引用外層的this。
雙冒號運(yùn)算符
箭頭函數(shù)可以綁定this對象,大大減少了顯式綁定this對象的寫法(call、apply、bind)。但是,箭頭函數(shù)并不適用于所有場合,所以現(xiàn)在有一個(gè)提案,提出了“函數(shù)綁定”(function bind)運(yùn)算符,用來取代call、apply、bind調(diào)用。
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);
}
尾調(diào)用優(yōu)化
尾調(diào)用(Tail Call)是函數(shù)式編程的一個(gè)重要概念,本身非常簡單,一句話就能說清楚,就是指某個(gè)函數(shù)的最后一步是調(diào)用另一個(gè)函數(shù)。
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í)行時(shí),調(diào)用幀只有一項(xiàng),這將大大節(jié)省內(nèi)存。這就是“尾調(diào)用優(yōu)化”的意義。
我們知道,函數(shù)調(diào)用會在內(nèi)存形成一個(gè)“調(diào)用記錄”,又稱“調(diào)用幀”(call frame),保存調(diào)用位置和內(nèi)部變量等信息。如果在函數(shù)A的內(nèi)部調(diào)用函數(shù)B,那么在A的調(diào)用幀上方,還會形成一個(gè)B的調(diào)用幀。等到B運(yùn)行結(jié)束,將結(jié)果返回到A,B的調(diào)用幀才會消失。如果函數(shù)B內(nèi)部還調(diào)用函數(shù)C,那就還有一個(gè)C的調(diào)用幀,以此類推。所有的調(diào)用幀,就形成一個(gè)“調(diào)用?!保╟all stack)。
尾遞歸優(yōu)化
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
但代碼的語義化有點(diǎn)奇怪,下面代碼通過一個(gè)正常形式的階乘函數(shù)factorial,調(diào)用尾遞歸函數(shù)tailFactorial,看起來就正常多了。
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
/////////////////////////方法二:默認(rèn)值大法
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5) // 120
ES6 的尾調(diào)用優(yōu)化只在嚴(yán)格模式下開啟,正常模式是無效的。、