函數的擴展
函數參數的默認值
基本用法
ES6提供了函數的指定默認值的寫法,例子如下
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
const p = new Point();
p // { x: 0, y: 0 }
優點:①讀代碼的人容易知道哪些參數可以省略。②以後代碼優化,把這參數去掉也不會導致代碼無法運行。
有幾個注意點
①參數是默認聲明,不能使用let
或const
再次聲明
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
②函數參數不能有同名參數
// 不報錯
function foo(x, x, y) {
// ...
}
// 報錯
function foo(x, x, y = 1) {
// ...
}
// SyntaxError: Duplicate parameter name not allowed in this context
③參數的值是重新計算默認值的表達式的值。也就是說,參數是惰性求值
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
與解構賦值默認值結合使用
參數默認值可以與解構賦值結合起來使用,實現參數的自動賦值以及對一些配置項進行省略。
例子如下
// 寫法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 寫法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
第一種寫法由於第二種,因爲第一種寫法當傳過來的參數爲undefined時,參數是有值的,而第二種參數是無值的,因爲當有參數傳過來的時候,就不會使用默認值。
參數默認值的位置
有默認值的參數一般都是放在函數參數的尾部,因爲這樣比較容易看出來,哪些參數是可以省略的,而如果設置了默認值的參數不再尾部的話,這個參數其實是不能省略的。
例子
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 報錯
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 報錯
f(1, undefined, 2) // [1, 5, 2]
函數的length屬性
指定了默認值之後,函數的length
屬性,將放回沒有指定默認值的參數個數,所以它失準了。
作用域
設置了參數默認值後,在函數進行聲明初始化時,參數會形成一個單獨的作用域,初始化結束都,作用域小時。
例子
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
上面例子,f函數會有參數作用域,所以y是賦予傳進來的2,而不是1.
再看一個例子
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
例子中,參數作用域沒有聲明x,所以它會到外層的全局作用域中找到x=1賦值給y,如果外層作用域找不到x會報錯。
應用
使用參數默認值,我們可以指定一個參數不得省略,省略就報錯,如下例子
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
把參數的默認值設定爲一個拋出錯誤的函數,如果調用這個函數的時候沒有傳這個參數,這個參數就會自動報錯,而如果你想忽略某個參數,就把參數默認設置爲undefined
rest參數
es6引入了rest參數(形式爲...變量名
),作用是獲取多餘的函數,它與一個數組搭配起來,多餘的變量就是放入這個數組中
例子如下
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
上面是實現一個加法的函數,參數可以傳任意個,都會存進values裏面
另外,可以用rest代替arguments變量,使代碼更加簡潔。
// arguments變量的寫法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest參數的寫法
const sortNumbers = (...numbers) => numbers.sort();
注意:rest參數之後不能有其他參數
嚴格模式
es6開始,如果函數參數使用了默認值,解構賦值,或者擴展運算符,那麼函數內部就不能設定爲嚴格模式,否則會報錯。
兩種解決辦法:設定全局性的嚴格模式。
'use strict';
function doSomething(a, b = a) {
// code
}
函數包在一個無參數的立即執行函數中
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
name屬性
函數name屬性會返回函數名
function foo() {}
foo.name // "foo"
需要注意,如果將一個變量賦值爲匿名函數,調用name屬性,es5會返回空字符串,es6會返回變量名
Function
構造函數返回的函數實例,name
屬性的值爲anonymous
。
(new Function).name // "anonymous"
bind
返回的函數,name
屬性值會加上bound
前綴。
unction foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
箭頭函數
基本用法
ES6 允許使用“箭頭”(=>
)定義函數。
var f = v => v;
// 等同於
var f = function (v) {
return v;
};
如果箭頭函數不需要參數或需要多個參數,就使用一個圓括號代表參數部分。
var f = () => 5;
// 等同於
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同於
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭頭函數的代碼塊部分多於一條語句,就要使用大括號將它們括起來,並且使用return
語句返回。
var sum = (num1, num2) => { return num1 + num2; }
由於大括號被解釋爲代碼塊,所以如果箭頭函數直接返回一個對象,必須在對象外面加上括號,否則會報錯。
// 報錯
let getTempItem = id => { id: id, name: "Temp" };
// 不報錯
let getTempItem = id => ({ id: id, name: "Temp" });
箭頭函數可以與變量解構結合使用。
const full = ({ first, last }) => first + ' ' + last;
// 等同於
function full(person) {
return person.first + ' ' + person.last;
}
下面是 rest 參數與箭頭函數結合的例子。
const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]
使用注意點
- 函數體內的
this
對象,就是定義時所在的對象,而不是使用時所在的對象。 - 不可以當作構造函數,也就是說,不可以使用
new
命令,否則會拋出一個錯誤。 - 不可以使用
arguments
對象,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。 - 不可以使用
yield
命令,因此箭頭函數不能用作 Generator 函數。
其中。第一點想說的是,箭頭函數可以讓this固定化
例子
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
上面代碼中,setTimeout
的參數是一個箭頭函數,這個箭頭函數的定義生效是在foo
函數生成時,而它的真正執行要等到 100 毫秒後。如果是普通函數,執行時this
應該指向全局對象window
,這時應該輸出21
。但是,箭頭函數導致this
總是指向函數定義生效時所在的對象(本例是{id: 42}
),所以輸出的是42
。
this
指向的固定化,並不是因爲箭頭函數內部有綁定this
的機制,實際原因是箭頭函數根本沒有自己的this
,導致內部的this
就是外層代碼塊的this
。正是因爲它沒有this
,所以也就不能用作構造函數。
除了this
,arguments
super
new.target
在匿名函數中都是不存在的。
箭頭函數不適用的場景
-
定義對象的方法,且該方法內部包括
this
const cat = { lives: 9, jumps: () => { this.lives--; } }
上面代碼中,cat.jumps()
方法是一個箭頭函數,這是錯誤的。調用cat.jumps()
時,如果是普通函數,該方法內部的this
指向cat
;如果寫成上面那樣的箭頭函數,使得this
指向全局對象,因此不會得到預期結果。這是因爲對象不構成單獨的作用域,導致jumps
箭頭函數定義時的作用域就是全局作用域。
-
需要動態this的時候,也不應該使用箭頭函數。
var button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on'); });
上面代碼運行時,點擊按鈕會報錯,因爲button
的監聽函數是一個箭頭函數,導致裏面的this
就是全局對象。如果改成普通函數,this
就會動態指向被點擊的按鈕對象。
尾調用優化
什麼是尾調用
尾調用就是指某個函數的最後一步是調用另一個函數。例子如下
function f(x){
return g(x);
}
下面例子不屬於尾調用
function f(x){
g(x);
}
以爲它等同於
function f(x){
g(x);
return undefined;
}
尾調用優化
函數調用會在內存形成一個“調用記錄”,又稱“調用幀”(call frame),保存調用位置和內部變量等信息。如果在函數A
的內部調用函數B
,那麼在A
的調用幀上方,還會形成一個B
的調用幀。等到B
運行結束,將結果返回到A
,B
的調用幀纔會消失。如果函數B
內部還調用函數C
,那就還有一個C
的調用幀,以此類推。所有的調用幀,就形成一個“調用棧”
尾調用由於是函數的最後一步操作,所以不需要保留外層函數的調用幀,因爲調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就可以了。
例子
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同於
function f() {
return g(3);
}
f();
// 等同於
g(3);
上面代碼中,如果函數g
不是尾調用,函數f
就需要保存內部變量m
和n
的值、g
的調用位置等信息。但由於調用g
之後,函數f
就結束了,所以執行到最後一步,完全可以刪除f(x)
的調用幀,只保留g(3)
的調用幀
所以,“尾調用優化”,即只保留內層函數的調用幀。如果所有函數都是尾調用,那麼完全可以做做到每次執行時,調用幀只有一項,這大大節省內存。
注意,目前只有 Safari 瀏覽器支持尾調用優化,Chrome 和 Firefox 都不支持。
尾遞歸
尾遞歸:就是尾調用函數自身。
遞歸非常耗費內存,因爲需要同時保存成千上百個調用幀,很容易發生“棧溢出”錯誤(stack overflow)。但對於尾遞歸來說,由於只存在一個調用幀,所以永遠不會發生“棧溢出”錯誤。
階乘例子
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
計算n
的階乘,最多需要保存n
個調用記錄,複雜度 O(n)
尾遞歸
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
只保留一個調用記錄,複雜度O(1) 。
遞歸函數的改寫
尾遞歸的實現,往往需要改寫遞歸函數,確保最後一步只調用自身。做到這一點的方法,就是把所有用到的內部變量改寫成函數的參數
但能會造成代碼不直觀
解決辦法
- 函數式編程柯里化:意思是將多參數的函數轉換成單參數的形式。
- 使用ES6的函數默認值
嚴格模式
ES6 的尾調用優化只在嚴格模式下開啓,正常模式是無效的。
函數參數的尾逗號
ES2017允許函數的最後一個參數有尾逗號
以前,函數定義或調用時,不允許最後一個參數後面有逗號
function clownsEverywhere(
param1,
param2
) { /* ... */ }
clownsEverywhere(
'foo',
'bar'
);
現在是可以的
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
clownsEverywhere(
'foo',
'bar',
);
Function.protorype.toString()
ES2019對函數實例的toString()
做了修改。
toString()方法返回函數代碼本身,以前會省略註釋和空格
以前
function /* foo comment */ foo () {}
foo.toString()
// function foo() {}
如今
function /* foo comment */ foo () {}
foo.toString()
// "function /* foo comment */ foo () {}"
catch命令的參數省略
ES2019中,try….catch的catch代碼塊後面可以不接參數
try {
// ...
} catch {
// ...
}