1. 程式人生 > >【ES6】函式的擴充套件(1)

【ES6】函式的擴充套件(1)

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';<span style="font-family: Arial, Helvetica, sans-serif;">}</span>

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;
}

var p = new Point();
p // { x: 0, y: 0 }
除了簡潔,ES6的寫法還有兩個好處:首先,閱讀程式碼的人,可以立刻意識到哪些引數是可以省略的,不用檢視函式體或文件;其次,有利於將來的程式碼優化,即使未來的版本在對外介面中,徹底拿掉這個引數,也不會導致以前的程式碼無法執行。
引數變數是預設宣告的,所以不能用let或const再次宣告。
function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}
上面程式碼中,引數變數x是預設宣告的,在函式體中,不能用let或const再次宣告,否則會報錯。

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

引數預設值可以與解構賦值的預設值,結合起來使用。

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就不會生成,從而報錯。如果引數物件沒有y屬性,y的預設值5才會生效。
下面是另一個物件的解構賦值預設值的例子。
function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method);
}

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

fetch('http://example.com')
// 報錯
上面程式碼中,如果函式fetch的第二個引數是一個物件,就可以為它的三個屬性設定預設值。
上面的寫法不能省略第二個引數,如果結合函式引數的預設值,就可以省略第二個引數。這時,就出現了雙重預設值。
function fetch(url, { method = 'GET' } = {}) {
  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]


引數預設值的位置

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

// 例一
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,就沒有觸發預設值。

函式的length屬性 

指定了預設值以後,函式的length屬性,將返回沒有指定預設值的引數個數。也就是說,指定了預設值後,length屬性將失真。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
上面程式碼中,length屬性的返回值,等於函式的引數個數減去指定了預設值的引數個數。比如,上面最後一個函式,定義了3個引數,其中有一個引數c指定了預設值,因此length屬性等於3減去1,最後得到2。
這是因為length屬性的含義是,該函式預期傳入的引數個數。某個引數指定預設值以後,預期傳入的引數個數就不包括這個引數了。同理,rest引數也不會計入length屬性。
(function(...args) {}).length // 0
如果設定了預設值的引數不是尾引數,那麼length屬性也不再計入後面的引數了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1


作用域

一個需要注意的地方是,如果引數預設值是一個變數,則該變數所處的作用域,與其他變數的作用域規則是一樣的,即先是當前函式的作用域,然後才是全域性作用域。

var x = 1;

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

f(2) // 2
上面程式碼中,引數y的預設值等於x。呼叫時,由於函式作用域內部的變數x已經生成,所以y等於引數x,而不是全域性變數x。
如果呼叫時,函式作用域內部的變數x沒有生成,結果就會不一樣
let x = 1;

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

f() // 1

上面程式碼中,函式呼叫時,y的預設值變數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
上面程式碼中,函式foo的引數x的預設值也是x。這時,預設值x的作用域是函式作用域,而不是全域性作用域。由於在函式作用域中,存在變數x,但是預設值在x賦值之前先執行了,所以這時屬於暫時性死區(參見《let和const命令》一章),任何對x的操作都會報錯。
如果引數的預設值是一個函式,該函式的作用域是其宣告時所在的作用域。請看下面的例子。
let foo = 'outer';

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

bar();
上面程式碼中,函式bar的引數func的預設值是一個匿名函式,返回值為變數foo。這個匿名函式宣告時,bar函式的作用域還沒有形成,所以匿名函式裡面的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
上面程式碼中,函式foo的引數y的預設值是一個匿名函式。函式foo呼叫時,它的引數x的值為undefined,所以y函式內部的x一開始是undefined,後來被重新賦值2。但是,函式foo內部重新聲明瞭一個x,值為3,這兩個x是不一樣的,互相不產生影響,因此最後輸出3。
如果將var x = 3的var去除,兩個x就是一樣的,最後輸出的就是2。
var x = 1;
function foo(x, y = function() { x = 2; }) {
  x = 3;
  y();
  console.log(x);
}

foo() // 2