ES6語法總結(7)--函數的擴展(1)

函數的擴展

  1. 函數參數的默認值
  2. rest 參數
  3. 嚴格模式
  4. name 屬性

1.函數參數的默認值

基本用法

ES6 以前,不能直接爲函數的參數指定默認值,只能採用變通的方法。javascript

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等於空字符,結果被改成默認值。java

爲了不這個問題,一般須要先判斷一下參數y是否被賦值,若是沒有,再等於默認值。web

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 }

除了簡潔,ES6 的寫法還有兩個好處:首先,閱讀代碼的人,能夠馬上意識到哪些參數是能夠省略的,不用查看函數體或文檔;其次,有利於未來的代碼優化,即便將來的版本在對外接口中,完全拿掉這個參數,也不會致使之前的代碼沒法運行。ide

參數變量是默認聲明的,因此不能用letconst再次聲明。svg

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

上面代碼中,參數變量x是默認聲明的,在函數體中,不能用letconst再次聲明,不然會報錯。函數

使用參數默認值時,函數不能有同名參數。fetch

// 不報錯
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。

與解構賦值默認值結合使用

參數默認值能夠與解構賦值的默認值,結合起來使用。

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的參數是一個對象時,變量xy纔會經過解構賦值生成。若是函數foo調用時沒提供參數,變量xy就不會生成,從而報錯。經過提供函數參數的默認值,就能夠避免這種狀況。

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]

參數默認值的位置

一般狀況下,定義了默認值的參數,應該是函數的尾參數。由於這樣比較容易看出來,到底省略了哪些參數。若是非尾部的參數設置默認值,實際上這個參數是無法省略的。

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

做用域

一旦設置了參數的默認值,函數進行聲明初始化時,參數會造成一個單獨的做用域(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
x // 1

上面代碼中,函數foo的參數造成一個單獨做用域。這個做用域裏面,首先聲明瞭變量x,而後聲明瞭變量yy的默認值是一個匿名函數。這個匿名函數內部的變量x,指向同一個做用域的第一個參數x。函數foo內部又聲明瞭一個內部變量x,該變量與第一個參數x因爲不是同一個做用域,因此不是同一個變量,所以執行y後,內部變量x和外部全局變量x的值都沒變。

若是將var x = 3var去除,函數foo的內部變量x就指向第一個參數x,與匿名函數內部的x是一致的,因此最後輸出的就是2,而外層的全局變量x依然不受影響。

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

foo() // 2
x // 1

應用

利用參數默認值,能夠指定某一個參數不得省略,若是省略就拋出一個錯誤。

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) { ··· }

2.rest 參數

ES6 引入 rest 參數(形式爲...變量名),用於獲取函數的多餘參數,這樣就不須要使用arguments對象了。rest 參數搭配的變量是一個數組,該變量將多餘的參數放入數組中。

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

上面代碼的add函數是一個求和函數,利用 rest 參數,能夠向該函數傳入任意數目的參數。

下面是一個 rest 參數代替arguments變量的例子。

// arguments變量的寫法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest參數的寫法
const sortNumbers = (...numbers) => numbers.sort();

上面代碼的兩種寫法,比較後能夠發現,rest 參數的寫法更天然也更簡潔。

arguments對象不是數組,而是一個相似數組的對象。因此爲了使用數組的方法,必須使用Array.prototype.slice.call先將其轉爲數組。rest 參數就不存在這個問題,它就是一個真正的數組,數組特有的方法均可以使用。下面是一個利用 rest 參數改寫數組push方法的例子。

function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)

注意,rest 參數以後不能再有其餘參數(即只能是最後一個參數),不然會報錯。

// 報錯
function f(a, ...b, c) {
  // ...
}

函數的length屬性,不包括 rest 參數。

(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

3.嚴格模式

從 ES5 開始,函數內部能夠設定爲嚴格模式。

function doSomething(a, b) {
  'use strict';
  // code
}

ES2016 作了一點修改,規定只要函數參數使用了默認值、解構賦值、或者擴展運算符,那麼函數內部就不能顯式設定爲嚴格模式,不然會報錯。

// 報錯
function doSomething(a, b = a) {
  'use strict';
  // code
}

// 報錯
const doSomething = function ({a, b}) {
  'use strict';
  // code
};

// 報錯
const doSomething = (...a) => {
  'use strict';
  // code
};

const obj = {
  // 報錯
  doSomething({a, b}) {
    'use strict';
    // code
  }
};

這樣規定的緣由是,函數內部的嚴格模式,同時適用於函數體和函數參數。可是,函數執行的時候,先執行函數參數,而後再執行函數體。這樣就有一個不合理的地方,只有從函數體之中,才能知道參數是否應該以嚴格模式執行,可是參數卻應該先於函數體執行。

// 報錯
function doSomething(value = 070) {
  'use strict';
  return value;
}

上面代碼中,參數value的默認值是八進制數070,可是嚴格模式下不能用前綴0表示八進制,因此應該報錯。可是實際上,JavaScript 引擎會先成功執行value = 070,而後進入函數體內部,發現須要用嚴格模式執行,這時纔會報錯。

雖然能夠先解析函數體代碼,再執行參數代碼,可是這樣無疑就增長了複雜性。所以,標準索性禁止了這種用法,只要參數使用了默認值、解構賦值、或者擴展運算符,就不能顯式指定嚴格模式。

兩種方法能夠規避這種限制。第一種是設定全局性的嚴格模式,這是合法的。

'use strict';

function doSomething(a, b = a) {
  // code
}

第二種是把函數包在一個無參數的當即執行函數裏面。

const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

4.name 屬性

函數的name屬性,返回該函數的函數名。

function foo() {}
foo.name // "foo"

這個屬性早就被瀏覽器普遍支持,可是直到 ES6,纔將其寫入了標準。

須要注意的是,ES6 對這個屬性的行爲作出了一些修改。若是將一個匿名函數賦值給一個變量,ES5 的name屬性,會返回空字符串,而 ES6 的name屬性會返回實際的函數名。

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

上面代碼中,變量f等於一個匿名函數,ES5 和 ES6 的name屬性返回的值不同。

若是將一個具名函數賦值給一個變量,則 ES5 和 ES6 的name屬性都返回這個具名函數本來的名字。

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"

Function構造函數返回的函數實例,name屬性的值爲anonymous

(new Function).name // "anonymous"

bind返回的函數,name屬性值會加上bound前綴。

function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "
相關文章
相關標籤/搜索