...
)undefined
會觸發預設值?arguments
!...
)
ECMAScript 6 中的參數處理已大幅升級。它現在支援參數預設值、剩餘參數(變數引數)和解構。
此外,展開運算子有助於函式/方法/建構函式呼叫和陣列文字。
預設參數值是透過等號(=
)為參數指定的。如果呼叫者未提供參數值,則使用預設值。在以下範例中,y
的預設參數值為 0
function
func
(
x
,
y
=
0
)
{
return
[
x
,
y
];
}
func
(
1
,
2
);
// [1, 2]
func
(
1
);
// [1, 0]
func
();
// [undefined, 0]
如果您在參數名稱之前加上 rest 運算子 (...
),該參數會透過陣列接收所有剩餘參數
function
format
(
pattern
,
...
params
)
{
return
{
pattern
,
params
};
}
format
(
1
,
2
,
3
);
// { pattern: 1, params: [ 2, 3 ] }
format
();
// { pattern: undefined, params: [] }
如果您在參數清單中使用物件樣式進行解構,您可以模擬命名參數
function
selectEntries
({
start
=
0
,
end
=-
1
,
step
=
1
}
=
{})
{
// (A)
// The object pattern is an abbreviation of:
// { start: start=0, end: end=-1, step: step=1 }
// Use the variables `start`, `end` and `step` here
···
}
selectEntries
({
start
:
10
,
end
:
30
,
step
:
2
});
selectEntries
({
step
:
3
});
selectEntries
({});
selectEntries
();
A 行中的 = {}
讓您可以不帶參數呼叫 selectEntries()
。
...
) 在函式和建構式呼叫中,散佈運算子會將可迭代值轉換為參數
>
Math
.
max
(
-
1
,
5
,
11
,
3
)
11
>
Math
.
max
(...[
-
1
,
5
,
11
,
3
])
11
>
Math
.
max
(
-
1
,
...[
-
5
,
11
],
3
)
11
在陣列文字中,散佈運算子會將可迭代值轉換為陣列元素
>
[
1
,
...[
2
,
3
],
4
]
[
1
,
2
,
3
,
4
]
ES6 處理參數的方式等同於透過正式參數解構實際參數。也就是說,下列函式呼叫
function
func
(
«
FORMAL_PARAMETERS
»
)
{
«
CODE
»
}
func
(
«
ACTUAL_PARAMETERS
»
);
大致等同於
{
let
[
«
FORMAL_PARAMETERS
»
]
=
[
«
ACTUAL_PARAMETERS
»
];
{
«
CODE
»
}
}
範例 – 下列函式呼叫
function
logSum
(
x
=
0
,
y
=
0
)
{
console
.
log
(
x
+
y
);
}
logSum
(
7
,
8
);
會變成
{
let
[
x
=
0
,
y
=
0
]
=
[
7
,
8
];
{
console
.
log
(
x
+
y
);
}
}
接著我們來看具體功能。
ECMAScript 6 讓您可以指定參數的預設值
function
f
(
x
,
y
=
0
)
{
return
[
x
,
y
];
}
略過第二個參數會觸發預設值
> f(1)
[1, 0]
> f()
[undefined, 0]
注意 – undefined
也會觸發預設值
> f(undefined, undefined)
[undefined, 0]
預設值會依需求計算,只在實際需要時才計算
> const log = console.log.bind(console);
> function g(x=log('x'), y=log('y')) {return 'DONE'}
> g()
x
y
'DONE'
> g(1)
y
'DONE'
> g(1, 2)
'DONE'
undefined
會觸發預設值? undefined
應該被解譯為遺失參數或遺失物件或陣列的一部分,這一點並不顯而易見。這樣做的理由是,它讓您可以委派預設值的定義。我們來看兩個範例。
在第一個範例中(來源:Rick Waldron 的 TC39 會議記錄,2012-07-24),我們不必在 setOptions()
中定義預設值,我們可以將這項任務委派給 setLevel()
。
function
setLevel
(
newLevel
=
0
)
{
light
.
intensity
=
newLevel
;
}
function
setOptions
(
options
)
{
// Missing prop returns undefined => use default
setLevel
(
options
.
dimmerLevel
);
setMotorSpeed
(
options
.
speed
);
···
}
setOptions
({
speed
:
5
});
在第二個範例中,square()
不必為 x
定義預設值,它可以將這項任務委派給 multiply()
function
multiply
(
x
=
1
,
y
=
1
)
{
return
x
*
y
;
}
function
square
(
x
)
{
return
multiply
(
x
,
x
);
}
預設值進一步強化了 undefined
的角色,表示某個東西不存在,而 null
表示空值。
在參數預設值中,你可以參照任何變數,包括其他參數
function
foo
(
x
=
3
,
y
=
x
)
{}
foo
();
// x=3; y=3
foo
(
7
);
// x=7; y=7
foo
(
7
,
2
);
// x=7; y=2
但是,順序很重要。參數從左到右宣告。在預設值「內部」,如果你存取尚未宣告的參數,你會得到一個 ReferenceError
function
bar
(
x
=
y
,
y
=
4
)
{}
bar
(
3
);
// OK
bar
();
// ReferenceError: y is not defined
預設值存在於它們自己的範圍中,這個範圍介於函式周圍的「外部」範圍與函式主體的「內部」範圍之間。因此,你無法從預設值存取「內部」變數
const
x
=
'outer'
;
function
foo
(
a
=
x
)
{
const
x
=
'inner'
;
console
.
log
(
a
);
// outer
}
如果在先前的範例中沒有外部 x
,預設值 x
會產生一個 ReferenceError
(如果觸發的話)。
如果預設值是閉包,這個限制可能會令人最為驚訝
const
QUX
=
2
;
function
bar
(
callback
=
()
=>
QUX
)
{
// returns 2
const
QUX
=
3
;
callback
();
}
bar
();
// ReferenceError
將剩餘運算子 (...
) 放在最後一個形式參數前面,表示它會在陣列中接收所有剩餘實際參數。
function
f
(
x
,
...
y
)
{
···
}
f
(
'a'
,
'b'
,
'c'
);
// x = 'a'; y = ['b', 'c']
如果沒有剩餘參數,剩餘參數會設定為空陣列
f
();
// x = undefined; y = []
arguments
! 剩餘參數可以完全取代 JavaScript 臭名昭著的特殊變數 arguments
。它們的優點是永遠都是陣列
// ECMAScript 5: arguments
function
logAllArguments
()
{
for
(
var
i
=
0
;
i
<
arguments
.
length
;
i
++
)
{
console
.
log
(
arguments
[
i
]);
}
}
// ECMAScript 6: rest parameter
function
logAllArguments
(...
args
)
{
for
(
const
arg
of
args
)
{
console
.
log
(
arg
);
}
}
arguments
的一個有趣功能是,你可以同時擁有正常參數和所有參數的陣列
function
foo
(
x
=
0
,
y
=
0
)
{
console
.
log
(
'Arity: '
+
arguments
.
length
);
···
}
在這種情況下,如果你將剩餘參數與陣列解構結合,就可以避免使用 arguments
。產生的程式碼較長,但更明確
function
foo
(...
args
)
{
let
[
x
=
0
,
y
=
0
]
=
args
;
console
.
log
(
'Arity: '
+
args
.
length
);
···
}
相同的技術適用於命名參數(選項物件)
function
bar
(
options
=
{})
{
let
{
namedParam1
,
namedParam2
}
=
options
;
···
if
(
'extra'
in
options
)
{
···
}
}
arguments
可迭代 arguments
在 ECMAScript 6 中是可迭代5,這表示您可以使用 for-of
和展開運算子
> (function () { return typeof arguments[Symbol.iterator] }())
'function'
> (function () { return Array.isArray([...arguments]) }())
true
在程式語言中呼叫函式(或方法)時,您必須將實際參數(由呼叫者指定)對應到形式參數(函式定義)。有兩種常見的方法可以這樣做
selectEntries
(
3
,
20
,
2
)
selectEntries
({
start
:
3
,
end
:
20
,
step
:
2
})
命名參數有兩個主要好處:它們提供函式呼叫中參數的描述,而且它們適用於選用參數。我將先說明這些好處,然後向您展示如何透過物件文字在 JavaScript 中模擬命名參數。
函式一旦有多個參數,您可能會對每個參數的用途感到困惑。例如,假設您有一個函式 selectEntries()
,它會從資料庫傳回資料庫項目。給定函式呼叫
selectEntries
(
3
,
20
,
2
);
這三個數字是什麼意思?Python 支援命名參數,它們可以輕鬆找出發生了什麼事
# Python syntax
selectEntries
(
start
=
3
,
end
=
20
,
step
=
2
)
選用位置參數只有在它們在最後被省略時才有效。在其他任何地方,您都必須插入 null
等佔位符,以便其餘參數具有正確的位置。
使用可選命名參數,這不是問題。您可以輕鬆省略任何一個。以下是幾個範例
# Python syntax
selectEntries
(
step
=
2
)
selectEntries
(
end
=
20
,
start
=
3
)
selectEntries
()
與 Python 和許多其他語言不同,JavaScript 不支援原生命名參數。但有一個相當優雅的模擬:每個實際參數都是物件文字中的屬性,其結果傳遞為單一形式參數給受呼叫者。當您使用此技術時,selectEntries()
的呼叫如下所示。
selectEntries
({
start
:
3
,
end
:
20
,
step
:
2
});
函數接收具有屬性 start
、end
和 step
的物件。您可以省略任何一個
selectEntries
({
step
:
2
});
selectEntries
({
end
:
20
,
start
:
3
});
selectEntries
();
在 ECMAScript 5 中,您會實作 selectEntries()
如下所示
function
selectEntries
(
options
)
{
options
=
options
||
{};
var
start
=
options
.
start
||
0
;
var
end
=
options
.
end
||
-
1
;
var
step
=
options
.
step
||
1
;
···
}
在 ECMAScript 6 中,您可以使用解構,如下所示
function
selectEntries
({
start
=
0
,
end
=-
1
,
step
=
1
})
{
···
}
如果您使用零個參數呼叫 selectEntries()
,解構會失敗,因為您無法將物件模式與 undefined
相符。這可透過預設值來修正。在以下程式碼中,如果缺少第一個參數,物件模式會與 {}
相符。
function
selectEntries
({
start
=
0
,
end
=-
1
,
step
=
1
}
=
{})
{
···
}
您也可以將位置參數與命名參數結合。後者通常放在最後
someFunc
(
posArg1
,
{
namedArg1
:
7
,
namedArg2
:
true
});
原則上,JavaScript 引擎可以最佳化這個模式,以便不會建立中間物件,因為呼叫網站中的物件文字和函數定義中的物件模式都是靜態的。
您可能大多會在 ECMAScript 6 中使用 for-of
迴圈,但陣列方法 forEach()
也受益於解構。或者更確切地說,其回呼會受益。
第一個範例:解構陣列中的陣列。
const
items
=
[
[
'foo'
,
3
],
[
'bar'
,
9
]
];
items
.
forEach
(([
word
,
count
])
=>
{
console
.
log
(
word
+
' '
+
count
);
});
第二個範例:解構陣列中的物件。
const
items
=
[
{
word
:
'foo'
,
count
:
3
},
{
word
:
'bar'
,
count
:
9
},
];
items
.
forEach
(({
word
,
count
})
=>
{
console
.
log
(
word
+
' '
+
count
);
});
ECMAScript 6 Map 沒有 map()
方法(像陣列一樣)。因此,必須
[key,value]
成對的陣列。map()
陣列。如下所示。
const
map0
=
new
Map
([
[
1
,
'a'
],
[
2
,
'b'
],
[
3
,
'c'
],
]);
const
map1
=
new
Map
(
// step 3
[...
map0
]
// step 1
.
map
(([
k
,
v
])
=>
[
k
*
2
,
'_'
+
v
])
// step 2
);
// Resulting Map: {2 -> '_a', 4 -> '_b', 6 -> '_c'}
工具方法 Promise.all()
的運作方式如下
解構有助於處理 Promise.all()
完成結果的陣列
const
urls
=
[
'http://example.com/foo.html'
,
'http://example.com/bar.html'
,
'http://example.com/baz.html'
,
];
Promise
.
all
(
urls
.
map
(
downloadUrl
))
.
then
(([
fooStr
,
barStr
,
bazStr
])
=>
{
···
});
// This function returns a Promise that is fulfilled
// with a string (the text)
function
downloadUrl
(
url
)
{
return
fetch
(
url
).
then
(
request
=>
request
.
text
());
}
fetch()
是 XMLHttpRequest
的 Promise 版本。它是 Fetch 標準 的一部分。
本節提到一些描述性參數定義的技巧。它們很聰明,但也有缺點:它們會增加視覺上的混亂,並可能讓您的程式碼更難理解。
有些參數沒有預設值,但可以省略。在這種情況下,我偶爾會使用預設值 undefined
,以清楚地表示該參數是選用的。這是多餘的,但具有描述性。
function
foo
(
requiredParam
,
optionalParam
=
undefined
)
{
···
}
在 ECMAScript 5 中,您有幾個選項可以確保提供必要的參數,這些選項都相當笨拙
function
foo
(
mustBeProvided
)
{
if
(
arguments
.
length
<
1
)
{
throw
new
Error
();
}
if
(
!
(
0
in
arguments
))
{
throw
new
Error
();
}
if
(
mustBeProvided
===
undefined
)
{
throw
new
Error
();
}
···
}
在 ECMAScript 6 中,您可以(濫用)預設參數值來達成更簡潔的程式碼(感謝:艾倫·維爾夫斯-布洛克的點子)
/**
* Called if a parameter is missing and
* the default value is evaluated.
*/
function
mandatory
()
{
throw
new
Error
(
'Missing parameter'
);
}
function
foo
(
mustBeProvided
=
mandatory
())
{
return
mustBeProvided
;
}
互動
> foo()
Error: Missing parameter
> foo(123)
123
本節介紹三種強制執行最大元數的方法。執行的範例是一個最大元數為 2 的函數 f
– 如果呼叫者提供超過 2 個參數,應擲回錯誤。
第一種方法是收集正式剩餘參數 args
中的所有實際參數,並檢查其長度。
function
f
(...
args
)
{
if
(
args
.
length
>
2
)
{
throw
new
Error
();
}
// Extract the real parameters
let
[
x
,
y
]
=
args
;
}
第二種方法依賴於正式剩餘參數 empty
中出現不需要的實際參數。
function
f
(
x
,
y
,
...
empty
)
{
if
(
empty
.
length
>
0
)
{
throw
new
Error
();
}
}
第三種方法使用一個哨兵值,如果存在第三個參數,則該值會消失。一個需要注意的地方是,如果存在值為 undefined
的第三個參數,預設值 OK
也會觸發。
const
OK
=
Symbol
();
function
f
(
x
,
y
,
arity
=
OK
)
{
if
(
arity
!==
OK
)
{
throw
new
Error
();
}
}
遺憾的是,這些方法每一個都會引入顯著的視覺和概念混亂。我傾向於建議檢查 arguments.length
,但我還希望 arguments
消失。
function
f
(
x
,
y
)
{
if
(
arguments
.
length
>
2
)
{
throw
new
Error
();
}
}
...
) 展開運算子 (...
) 看起來與 rest 運算子完全相同,但它恰好相反
Math.max()
是展示展開運算子如何在方法呼叫中運作的一個好範例。Math.max(x1, x2, ···)
傳回值最大的參數。它接受任意數量的參數,但無法套用於陣列。展開運算子修正了這個問題
> Math.max(-1, 5, 11, 3)
11
> Math.max(...[-1, 5, 11, 3])
11
與 rest 運算子相反,你可以在部分序列的任何地方使用展開運算子
> Math.max(-1, ...[5, 11], 3)
11
另一個範例是 JavaScript 沒有辦法將一個陣列的元素破壞性地附加到另一個陣列。然而,陣列確實有方法 push(x1, x2, ···)
,它會將所有參數附加到其接收器。以下程式碼顯示你可以如何使用 push()
將 arr2
的元素附加到 arr1
。
const
arr1
=
[
'a'
,
'b'
];
const
arr2
=
[
'c'
,
'd'
];
arr1
.
push
(...
arr2
);
// arr1 is now ['a', 'b', 'c', 'd']
除了函式和方法呼叫之外,展開運算子也適用於建構函式呼叫
new
Date
(...[
1912
,
11
,
24
])
// Christmas Eve 1912
展開運算子也可以用在陣列文字中
> [1, ...[2,3], 4]
[1, 2, 3, 4]
這為你提供了一個合併陣列的便捷方法
const
x
=
[
'a'
,
'b'
];
const
y
=
[
'c'
];
const
z
=
[
'd'
,
'e'
];
const
arr
=
[...
x
,
...
y
,
...
z
];
// ['a', 'b', 'c', 'd', 'e']
展開運算子的一個優點是它的運算元可以是任何可迭代值(與陣列方法 concat()
相反,後者不支援迭代)。
展開運算子讓你能夠將任何可迭代值轉換成陣列
const
arr
=
[...
someIterableObject
];
我們將一個 Set 轉換成陣列
const
set
=
new
Set
([
11
,
-
1
,
6
]);
const
arr
=
[...
set
];
// [11, -1, 6]
您自己的可迭代物件可以用相同的方式轉換為陣列
const
obj
=
{
*
[
Symbol
.
iterator
]()
{
yield
'a'
;
yield
'b'
;
yield
'c'
;
}
};
const
arr
=
[...
obj
];
// ['a', 'b', 'c']
請注意,就像 for-of
迴圈一樣,展開運算子僅對可迭代值有效。所有內建資料結構都是可迭代的:陣列、映射和集合。所有類陣列 DOM 資料結構也是可迭代的。
如果您遇到不可迭代但類陣列(索引元素加上屬性 length
)的項目,您可以使用 Array.from()
6 將其轉換為陣列
const
arrayLike
=
{
'0'
:
'a'
,
'1'
:
'b'
,
'2'
:
'c'
,
length
:
3
};
// ECMAScript 5:
var
arr1
=
[].
slice
.
call
(
arrayLike
);
// ['a', 'b', 'c']
// ECMAScript 6:
const
arr2
=
Array
.
from
(
arrayLike
);
// ['a', 'b', 'c']
// TypeError: Cannot spread non-iterable value
const
arr3
=
[...
arrayLike
];