1. 程式人生 > >ES6-函式的擴充套件-函式引數的預設值

ES6-函式的擴充套件-函式引數的預設值

1)基本用法

ES6 之前,不能直接為函式的引數指定預設值。

function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
上面程式碼檢查函式 log 的引數 y 有沒有賦值,如果沒有,則指定預設值為 world。這種寫法的缺點在於,如果引數 y 賦值了,但是對應的布林值為 false ,則該賦值不起作用,就像上面程式碼的最後一行,引數 y 等於空字元,結果被改為預設值。
為了避免這個問題,通常需要先判斷一下引數 y 是否被賦值,如果沒有,再等於預設值。
if (typeof y === 'undefined') {
  y = 'World';
}
ES6 允許為函式的引數設定預設值,即直接寫在引數定義的後面。
function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
可以看到 ES6 比 ES5 簡潔很多。
function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}

const p = new Point();
p // { x: 0, y: 0 }
這樣寫首先於閱讀程式碼的人,可以很清楚知道哪些引數是可以省略的,不用檢視函式體或文件。其次,有利於將來程式碼的優化,即使以後測地拿掉這個引數,也不會導致以前的diam無法執行。

引數變數是預設宣告的,所以不能用 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
上面程式碼中,引數 p 的預設值是 x+1 ,這時,每次呼叫函式 foo ,都會重新計算 x+1,而不是預設 p 等於 100。

2)與解構賦值預設值結合使用

引數預設值可以與解構賦值的預設值,結合起來使用。
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
上面程式碼只使用了物件的解構預設值,沒有使用函式引數的預設值。只有當函式 foo 的引數是一個物件時,變數 x 和 y 才會通過解構賦值生成。如果函式 foo 呼叫時沒有提供引數,變數 x 和 y 就不會生成,從而報錯。通過提供函式引數的預設值,就可以避免這種情況。
function foo({x, y = 5} = {}) {
  console.log(x, y);
}

foo() // undefined 5
上面程式碼指定,如果沒有提供引數,函式 foo 的引數預設為一個空物件。
下面是一個解構賦值的例子。
function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method);
}

fetch('http://example.com', {})
// "GET"

fetch('http://example.com')
// 報錯
上面程式碼中,如果函式 fetch 的第二個引數是一個物件,就可以為它的三個屬性設定預設值,這種寫法不能省略第二個引數,如果結合函式引數的預設值,就可以省略第二個引數,這是,就出現雙重預設值。
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
  console.log(method);
}

fetch('http://example.com')
// "GET"
上面程式碼中,函式fetch沒有第二個引數時,函式引數的預設值就會生效,然後才是解構賦值的預設值生效,變數method才會取到預設值GET
// 寫法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 寫法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}
上面兩種寫法都對函式的引數設定了預設值,區別是寫法函式的預設值是空物件,但是設定了物件解構賦值的預設值;寫法二函式引數的預設值是一個有具體屬性的物件,但是沒有設定解構賦值的預設值。
// 函式沒有引數的情況
m1() // [0, 0]
m2() // [0, 0]

// x 和 y 都有值的情況
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x 有值,y 無值的情況
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x 和 y 都無值的情況
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

3)引數預設值的位置

通常情況下,定義了預設值的引數,應該是函式的尾引數。因為這樣比較容易看出來,到底是省略了哪些引數。如果非尾部的引數設定預設值,實際上這個引數是沒法省略的。

// 例一
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]

上面程式碼中,有預設值的引數都不是尾引數。這時,無法只省略改引數,而不省略它後面的引數,除非顯式輸入 undefined。

如果傳入 undefined ,將觸發該引數等於預設值,null 則沒有這個效果。

function foo(x = 5, y = 6) {
  console.log(x, y);
}

foo(undefined, null)
// 5 null
上面程式碼中,x 引數對應 undefined,結果觸發了預設值,y 引數等於 null ,就沒有觸發預設值。

4)函式的 length 屬性

指定了預設值以後,函式 length 屬性,將返回沒有指定預設值的引數個數。也就是說,指定了預設值後,length 屬性將失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
上面程式碼中,length 屬性的返回值,等於函式的引數個數減去指定了預設值的引數個數。

length 屬性的含義是,該函式預期傳入的引數個數,某個引數指定預設值以後,預期傳入引數個數就不包括這個引數了,同理,後文的 rest 引數也不會計入 length 屬性。

(function(...args) {}).length // 0
如果設定了預設值的引數不是尾引數,那麼 length 屬性也不再計入後面的引數了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

5)作用域

一旦設定了引數的預設值,函式進行宣告初始化時,引數會形成一個單獨的作用域(context)。等到初始化結束後,這個作用域就會消失。這種語法行為,在不設定引數預設值時,是不會出現的。
var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2) // 2
上面程式碼中,引數 y 的預設值等於變數 x 。呼叫函式 f 時,引數形成一個單獨的作用域。在這個作用域裡面,預設值變數 x 指向第一個引數 x ,而不是全域性變數 x。所以輸出是 2。
let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1

上面程式碼中,函式 f 呼叫時,引數 y=x 形成一個單獨的作用域。這個作用域裡面,變數 x 本身沒有定義,所以指向外層的全域性變數 x。函式呼叫時,函式體內部的區域性變數 x 影響不到預設值變數 x。

如果此時 x 的變數不存在,就會報錯。

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // ReferenceError: x is not defined
這樣寫也報錯。
var x = 1;

function foo(x = x) {
  // ...
}

foo() // ReferenceError: x is not defined

上面的程式碼中,引數 x=x 形成一個單獨作用域。實際執行的是 let x=x由於暫時性死區的原因,這行程式碼會報錯“x未定義”。

如果引數的預設值是一個函式,該函式的作用域也遵守這個規則,請看下面的例子。
let foo = 'outer';

function bar(func = () => foo) {
  let foo = 'inner';
  console.log(func());
}

bar(); // outer
上面程式碼中,函式 bar 的引數 func 的預設值是一個匿名函式,返回值為變數 foo。函式引數形成的單獨作用域裡面,並沒有定義變數 foo,所以 foo 指向外層的全域性變數 foo,因此輸出 outer。

如果這麼寫就會報錯。

function bar(func = () => foo) {
  let foo = 'inner';
  console.log(func());
}

bar() // ReferenceError: foo is not defined
上面程式碼中,匿名函式裡面的 foo 指向函式外層,但是函式外層並沒有宣告變數 foo ,所以就報錯了。

下面是一個複雜的例子。

var x = 1;
function foo (x,y=function(){ x=2; }) {
	var x=3;
	y();
	console.log(x);
}

foo(); //3
console.log(x); //1
上面程式碼中,函式 foo 的引數形成一個單獨作用域。這個作用域裡面,首先聲明瞭變數 x,然後聲明瞭變數 y,y 的預設值是一個匿名函式。這個函式內部的變數 x,指向同一個作用域的第一個引數 x。函式 foo 內部又聲明瞭一個內部變數 x,該變數與第一個引數 x 由於不是同一作用域,所以不是同一個變數,因此執行 y 後,內部變數 x 和外部全域性變數 x 的值都沒變。
如果將var x=3 的 var 去掉,函式 foo 的內部變數 x ,與匿名函式內部的 x 是一致的,所以最後輸出就是2,而外層的全域性變數 x 依然不受影響。
var x = 1;
function foo(x, y = function() { x = 2; }) {
  x = 3;
  y();
  console.log(x);
}

foo() // 2
x // 1

6) 應用

利用引數預設值,可以指定某一引數不得省略,如果省略就丟擲一個錯誤。
function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter
上面程式碼的 foo 函式,如果呼叫的時候沒有引數,就會呼叫預設值 throwIfMissing 函式,從而丟擲錯誤資訊。

從上面程式碼還可以看到,引數 mustBeProvided 的預設值等於 throwIfMissing 函式的執行結果(注意函式名throwIfMissing 之後又一對圓括號),這表明引數的預設值不是在定義時執行,而是在執行時執行。如果引數已經賦值,預設值中的函式就不會執行。

另外,可以將引數預設值設為 undefined,表明這個引數是可以省略的。

function foo(optional = undefined) { ··· }