ECMAScript 6(或者叫 ECMAScript 2015)是 ECMAScript 的最新標準,極大的提高了 JavaScript 中處理參數的能力。現在我們可以使用 rest 參數(rest parameters)、默認值(default values)和解構(destructuring)以及其他許多新的特性。本文我們將探索參數(arguments)和參數(parameter)的方方面面,看一下ES6是如何對他們改進和提升的。
Arguments 和 Parameters
arguments 和 Parameters 的含義通常是可以互換的。盡管如此,為了本文的目標,還是要做出區分。在大多數的標準中,函數聲明時給出的叫做 parameters(或者叫 formal parameters),而傳遞給函數的叫做的 arguments(或者叫 actual arguments),看下面的函數:
function foo(param1, param2) {// do something
}
foo(10, 20);
在這個函數中,param1
和 param2
是函數的 parameters,而傳遞給函數的值(10
和 20
)是 arguments。
譯者注:本文后面不再區分 arguments 和 parameters,統一譯作參數。
擴展運算符(...)
在 ES5 中,apply()
方法可以很方便將數組作為參數傳遞給函數,經常用于使用 Math.max()
來取得數組的最大值。看下面的代碼段:
var myArray = [5, 10, 50];
Math.max(myArray); // Error: NaN
Math.max.apply(Math, myArray); // 50
Math.max()
方法不支持數組,只接受數字作為參數。當數組傳遞給函數,函數會拋出錯誤。但是當使用 apply()
方法后,數組變成了一個個單獨的數組傳遞給了函數,所以 Math.max()
就能夠正確的執行了。
幸運的是,ES6 給我們帶來了擴展運算符,我們就不必再繼續使用 apply()
方法了。我們可以將表達式輕松的展開為多個參數。
var myArray = [5, 10, 50];
Math.max(...myArray); // 50
在這里我們通過擴展運算符將 myArray
展開成了一個個單獨的值。雖然 ES5 中我們可以通過 apply()
方法來模擬擴展運算符,但是語法上讓人迷惑,并且缺少可擴展性。擴展運算符不僅易于使用,還帶來了許多新的特性。比如,你可以在函數調用時多次使用擴展運算符,并且還可以和其他參數混合在一起。
function myFunction() {for(var i in arguments){console.log(arguments[i]);}
}
var params = [10, 15];
myFunction(5, ...params, 20, ...[25]); // 5 10 15 20 25
擴展運算符另一大好處就是他可以很容易的和構造函數(constructor)一起使用:
new Date(...[2016, 5, 6]); // Mon Jun 06 2016 00:00:00 GMT-0700 (Pacific Daylight Time)
當前我們可以使用 ES5 來重寫上面的代碼,不過我們需要一個復雜的方法來避免一個類型錯誤:
new Date.apply(null, [2016, 4, 24]); // TypeError: Date.apply is not a constructor
new (Function.prototype.bind.apply(Date, [null].concat([2016, 5, 6]))); // Mon Jun 06 2016 00:00:00 GMT-0700 (Pacific Daylight Time)
REST 參數
rest 參數和擴展運算符是一樣的語法,但是他不是將數組展開成一個個的參數,而是將一個個參數轉換為數組。
譯者注:rest 參數和擴展運算符雖然一樣的語法,在這里你就可以看出作者強調的 arguments 和 parameters 的區別了。擴展運算符用于函數調用的參數(arguments)中,而 rest 參數用于函數聲明的參數(parameters)中。
function myFunction(...options) {return options;
}
myFunction('a', 'b', 'c'); // ["a", "b", "c"]
如果沒有提供參數,rest 參數會被設置為空數組:
function myFunction(...options) {return options;
}
myFunction(); // []
當創建可見函數(接受數量可變的參數的函數)的時候,rest 參數就顯得十分有用。因為 rest 參數是一個數組,所以可以很方便的替換 arguments
對象(將會在下文討論)。看下面一個使用 ES5 編寫的方法:
function checkSubstrings(string) {for (var i = 1; i < arguments.length; i++) {if (string.indexOf(arguments[i]) === -1) {return false;}}return true;
}
checkSubstrings('this is a string', 'is', 'this'); // true
這個函數的作用是檢查一個字符串是否包含指定的一系列字符串。這個函數的第一個問題就是,我們必須查看函數體才知道函數接受多個參數。另外 arguments
的迭代必須從 1 開始,因為 arguments[0]
是第一個參數。如果我們稍后給第一參數之后再添加參數,或許我們就忘記更新這個循環了。使用 rest 參數,我們可以很輕易的避開這個問題:
function checkSubstrings(string, ...keys) {for (var key of keys) {if (string.indexOf(key) === -1) {return false;}}return true;
}
checkSubstrings('this is a string', 'is', 'this'); // true
函數的輸出和上一個函數一樣。再重復一次,string
參數作為第一個參數傳入,剩下的參數被塞進一個數組并且賦值給了變量 keys
。
使用 rest 參數代替 arguments
不僅提高了代碼的可讀性,并且避免了 JavaScript 中的性能問題。盡管如此,rest 參數并不能無限制使用,舉個例子,它只能是最后一個參數,否則會導致語法錯誤。
function logArguments(a, ...params, b) {console.log(a, params, b);
}
logArguments(5, 10, 15); // SyntaxError: parameter after rest parameter
另一個限制方法聲明時只允許一個 rest 參數:
function logArguments(...param1, ...param2) {
}
logArguments(5, 10, 15); // SyntaxError: parameter after rest parameter
默認值
ES5 中的默認參數
ES5 中 JavaScript 并不支持默認值,但這里有個很簡單的實現,使用 OR
運算符(||
),我們可以很容易的模擬默認參數,看下面的代碼:
function foo(param1, param2) {param1 = param1 || 10;param2 = param2 || 10;console.log(param1, param2);
}
foo(5, 5); // 5 5
foo(5); // 5 10
foo(); // 10 10
這個函數期望接收兩個參數,但當無參數調用時,它會使用默認值。在函數內,缺失的參數自動設置為 undefined,所以我們檢查這些參數,并給他們設置默認值。為了檢測缺失的參數并設置默認值,我們使用 OR
運算符(||
)。這個運算符首先檢查第一個值,如果是 truthy,運算符會返回它,否則返回第二個參數。
這種方法在函數內很常用,但也存在瑕疵。如果傳遞 0
或者 null
也會返回默認值。因為它們被認為是 falsy 值。所以如果我們確實需要給函數傳遞 0
或者 null
,我們需要換種方法來檢測參數是否缺失:
function foo(param1, param2) {if(param1 === undefined){param1 = 10;}if(param2 === undefined){param2 = 10;}console.log(param1, param2);
}
foo(0, null); // 0, null
foo(); // 10, 10
在這個函數中,通過檢查參數的類型是否為 undefined 來確定是否要賦予默認值。這種方法代碼量稍微大一些,但更安全,可以讓我們給函數傳遞 0
或者 null
。
ES6 中的默認參數
ES6 中,我們不必再檢查參數是否為 undefined 來模擬默認參數,我們可以直接將默認參數函數聲明中。
function foo(a = 10, b = 10) {console.log(a, b);
}
foo(5); // 5 10
foo(0, null); // 0 null
正如你所看到的,忽略參數返回了默認值,但傳遞 0
或者 null
并沒有。我們甚至可以使用函數來產生參數的默認值:
function getParam() {alert("getParam was called");return 3;
}
function multiply(param1, param2 = getParam()) {return param1 * param2;
}
multiply(2, 5); // 10
multiply(2); // 6 (also displays an alert dialog)
需要注意的是,只有缺少第二個參數的時候,gegParam
方法才會執行,所以當我們使用兩個參數 multiply()
的時候并不會彈出 alert。
默認參數另一個有意思的特性是在方法聲明是可以引用其他參數和變量作為默認參數:
function myFunction(a=10, b=a) {console.log('a = ' + a + '; b = ' + b);
}
myFunction(); // a=10; b=10
myFunction(22); // a=22; b=22
myFunction(2, 4); // a=2; b=4
甚至可以在函數聲明的時候執行操作符:
function myFunction(a, b = ++a, c = a*b) {console.log(c);
}
myFunction(5); // 36
注意:不像其他語言,JavaScript 是在調用時才計算默認參數的:
function add(value, array = []) {array.push(value);return array;
}
add(5); // [5]
add(6); // [6], not [5, 6]
解構賦值
解構賦值是 ES6 的新特性,讓我們可以從數組或者對象中提取值并賦值給變量,語法上類似于對象和數組字面量。當給函數傳參時,這種語法清晰且易于理解并且很實用。
在 ES5 中,經常使用配置對象來處理大量的的可選參數,尤其是屬性的順序無關緊要的時候,看下面的函數:
function initiateTransfer(options) {var protocol = options.protocol,port = options.port,delay = options.delay,retries = options.retries,timeout = options.timeout,log = options.log;// code to initiate transfer
}
options = {protocol: 'http',port: 800,delay: 150,retries: 10,timeout: 500,log: true
};
initiateTransfer(options);
這種模式 JavaScript 開發者經常使用,并且很好用。但我們必須進入函數體內才知道到底需要多少參數,使用解構參數賦值,我們可以在函數聲明時很清晰的指定需要的參數。
function initiateTransfer({protocol, port, delay, retries, timeout, log}) {// code to initiate transfer
};
var options = {protocol: 'http',port: 800,delay: 150,retries: 10,timeout: 500,log: true
}
initiateTransfer(options);
在這個函數中,我們使用了對象解構模式,而不是一個配置型對象,讓我們的代碼更加清晰易讀。
我們也可以混用解構參數和普通參數:
function initiateTransfer(param1, {protocol, port, delay, retries, timeout, log}) {// code to initiate transfer
}
initiateTransfer('some value', options);
需要注意,如果函數調用時解構參數缺失會拋出一個類型錯誤:
function initiateTransfer({protocol, port, delay, retries, timeout, log}) {// code to initiate transfer
}
initiateTransfer(); // TypeError: Cannot match against 'undefined' or 'null'
當我們的參數是必須的,這種行為我們是想要的,但是如果我們期望參數可選呢?為阻止這種錯誤,我們需要給解構參數賦一個默認值:
function initiateTransfer({protocol, port, delay, retries, timeout, log} = {}) {// code to initiate transfer
}
initiateTransfer(); // no error
在這個函數中,我們給解構參數賦了一個空對象作為默認值。現在如果函數調用時沒有賦予參數,不會拋出錯誤。
我們也可以給解構參數每個屬性都賦默認值:
function initiateTransfer({protocol = 'http',port = 800,delay = 150,retries = 10,timeout = 500,log = true
}) {// code to initiate transfer
}
在這個例子中,每個屬性都被賦予默認值,就無需在函數體內手動檢查 undefined 的參數再賦予默認值。
參數傳遞
函數傳參有兩種方式:引用傳遞和值傳遞。如果是引用傳遞,修改參數會引起全局的變化,如果是值傳遞,只會引起函數內的變化。
在一些語言中,像 Visual Basic 和 PowerShell,我們可以選擇聲明是值傳遞還是引用傳遞,但 JavaScript 不是這樣。
值傳遞
嚴格來說,JavaScript只能值傳遞。當我們通過值傳遞給函數傳參,就在函數作用域內創建了這個值得副本。所以任何值得變化都只會反映在函數內部。看下面的例子:
var a = 5;
function increment(a) {a = ++a;console.log(a);
}
increment(a); // 6
console.log(a); // 5
在這里,在函數內部修改修改參數并不會影響到原始值。所以在函數外打印這個變量,得到的結果始終是 5
。
引用傳遞
在 JavaScript 中,所有的都是值傳遞,但是當我們傳遞一個變量指向一個對象(包括數組),這個“值”就指向了這個對象,改變了對象的某個屬相也會引起其關聯對象的改變。
看這個函數:
function foo(param){param.bar = 'new value';
}
obj = {bar : 'value'
}
console.log(obj.bar); // value
foo(obj);
console.log(obj.bar); // new value
正如你看到的,對象的屬性在函數體內部被修改,但是卻影響到了函數外部的對象。
當我們傳遞一個非原始的值,像數組或者對象,程序會在內存中創建一個對象,指向原始地址。如果被修改,原始值也會隨之修改。
類型檢查和缺失或多余參數
在強類型的語言中,我們必須在函數聲明時聲明參數的類型,但 JavaScript 中沒有這種特性,在 JavaScript 中,并不關心傳遞給函數的參數的類型和個數。
假設我們有一個函數,僅接受一個參數。當我們調用這個函數的使用,我們并不限制到底傳遞給函數多少個參數,甚至可以選擇不傳,都不會產生錯誤。
參數的個數可以分為兩種情況:
-
參數缺失
缺失的變量賦值為 undefined
-
參數過多
多余的參數會被忽略,但可以從 arguments 變量中取到(下文即將討論)。
強制參數
函數調用中如果函數缺失,它會被設置為 undefined。我們可以利用這一點,如果參數缺失就拋出錯誤:
function foo(mandatory, optional) {if (mandatory === undefined) {throw new Error('Missing parameter: mandatory');}
}
在 ES6 中,我們可以更近一步,使用默認參數來設置強制參數:
function throwError() {throw new Error('Missing parameter');
}
function foo(param1 = throwError(), param2 = throwError()) {// do something
}
foo(10, 20); // ok
foo(10); // Error: missing parameter
arguments 對象
在 ES4 的時候默認參數就被加入,來代替 arguments
對象,但 ES4 并沒有實現。隨著 ES6 的發布,JavaScript 現在官方支持了默認參數。但并沒有取消支持 arguments
的計劃。
arguments
對象是一個類數組的對象,可以在所有的函數中取到。arguments
通過數字索引來獲取傳入的參數,而不是通過參數的名字。這個對象允許我們給函數傳入任意多的參數。看下面的代碼判斷:
function checkParams(param1) {console.log(param1); // 2console.log(arguments[0], arguments[1]); // 2 3console.log(param1 + arguments[0]); // 4
}
checkParams(2, 3);
這個函數期望傳入一個參數,當我們傳入兩個參數調用它的時候,我們通過 param1
或者 arguments[0]
來獲取第一個參數,但第二個參數只能通過 arguments[1]
獲取。也即是說,arguments
對象可以和有命名的參數一起使用。
arguments
對象包含了所有傳入函數的參數,并且索引的起始是 1
。當我們希望獲取更多的參數的時候,我們會使用 arguments[2]
、arguments[3]
等等。
我們可以跳過所有的參數命名設置,僅僅使用 arguments
對象:
function checkParams() {console.log(arguments[1], arguments[0], arguments[2]);
}
checkParams(2, 4, 6); // 4 2 6
實際上,命名的參數是一種方便,但不是必需的。同樣的,rest 參數也可以用來顯示傳入的參數:
function checkParams(...params) {console.log(params[1], params[0], params[2]); // 4 2 6console.log(arguments[1], arguments[0], arguments[2]); // 4 2 6
}
checkParams(2, 4, 6);
arguments
對象是一個類數組對象,但是缺少像 slice
和 foreach
等方法。為了在 arguments
對象上使用這些方法,需要將其轉換為真實的數組:
function sort() {var a = Array.prototype.slice.call(arguments);return a.sort();
}
sort(40, 20, 50, 30); // [20, 30, 40, 50]
在這個函數中,使用 Array.prototype.slice.call()
快速將 arguments
對象轉換為數組。然后使用 sort
方法進行排序。
ES6 有一種更直接的方法,Array.from()
,ES6 新增的方法,用來通過類數組對象創建一個新的數組。
function sort() {var a = Array.from(arguments);return a.sort();
}
sort(40, 20, 50, 30); // [20, 30, 40, 50]
length 屬性
雖然 arguments 對象并不是嚴格意義的數組,但它有一個 length
屬性,可以用來檢查傳遞給函數的參數的個數。
function countArguments() {console.log(arguments.length);
}
countArguments(); // 0
countArguments(10, null, "string"); // 3
通過使用 length
屬性,我們可以更好的控制參數的數量。比如說,如果一個函數需要兩個參數,我們就可以使用 length
屬性來檢查參數數量,如果少于期望數量就拋出錯誤。
function foo(param1, param2) {if (arguments.length < 2) {throw new Error("This function expects at least two arguments");} else if (arguments.length === 2) {// do something}
}
rest 參數是數組,所以他也有 length
屬性,我們用 ES6 來重寫上面的方法:
function foo(...params) {if (params.length < 2) {throw new Error("This function expects at least two arguments");} else if (params.length === 2) {// do something}
}
Callee 和 Caller 屬性
callee
屬性指向當前正在運行的函數,而 caller
指向調用當前正在運行函數的函數。在 ES5 嚴格模式下,這些屬性是被廢棄掉的,如果要訪問它們會拋出錯誤。
arguments.callee
屬性在遞歸函數(遞歸函數是一個普通函數,通過它的簽名指向自身)下很有用,尤其是函數的簽名不可用時(也就是匿名函數)。因為匿名函數沒有名字,唯一指向自身的方法就是通過 arguments.callee
。
var result = (function(n) {if (n <= 1) {return 1;} else {return n * arguments.callee(n - 1);}
})(4); // 24
嚴格模式和非嚴格模式下的 arguments
在 ES5 非嚴格模式下, arguments
對象有一個不常用的特性:它保持和命名參數值同步。
function foo(param) {console.log(param === arguments[0]); // truearguments[0] = 500;console.log(param === arguments[0]); // truereturn param
}
foo(200); // 500
在函數內部,一個新的值賦給 arguments[0]
。因為 arguments
一直和命名參數的值保持同步,arguments[0]
的改變也會引起 param
的改變。事實上,他們是同個變量的不同名稱。在 ES5 嚴格模式下,這種令人迷惑的特性被移除了:
"use strict";
function foo(param) {console.log(param === arguments[0]); // truearguments[0] = 500;console.log(param === arguments[0]); // falsereturn param
}
foo(200); // 200
這次,arguments[0]
的改變沒有影響到 param
,并且輸出和期望一樣。ES6下,輸出結果和 ES5 的嚴格模式是一致的。但是請記住,在函數聲明時使用了默認參數,arguments
不受影響。
function foo(param1, param2 = 10, param3 = 20) {console.log(param1 === arguments[0]); // trueconsole.log(param2 === arguments[1]); // trueconsole.log(param3 === arguments[2]); // falseconsole.log(arguments[2]); // undefinedconsole.log(param3); // 20
}
foo('string1', 'string2');
在這個函數中,盡管 param3
有默認值,但他和 arguments[2]
并不相等,因為只有兩個參數傳入了函數。也就是說,設置默認參數并不影響 arguments 對象。
結論
ES6 給 JavaScript 帶來了許多大大小小的改進。越來越多的開發者開始使用 ES6,而且很多所有的特性都可以無障礙使用。本文我們學習了 ES6 是如何提升JavaScript 處理參數的能力的。但我們僅僅學了 ES6 的一點皮毛。更多的有趣的特性等著我們去挖掘!
ECMAScript 6 Compatibility Table, Juriy Zaytsev
“ECMAScript 2015 Language Specification,” ECMA International
看下時間現在正好是23:23,幾乎用了一個下午和晚上把這篇文章讀完又翻譯完,這篇文章結合 ES5 和 ES6 來講解,收益頗多。不過翻譯水平有限,求多提意見多多指教 ~
原文地址: How To Use Arguments And Parameters In ECMAScript 6
小廣告
歡迎關注我們的微信公眾號: