函數的擴展

文章編寫參考 阮一峯《ECMAScript 6 入門》


1. 函數參數的默認值

1.1 基本用法

在ES6之前如果要給函數賦值一般採用以下這樣的方式

function fun(x, y) {
    y = y || "Blue";
    console.log(x, y);
}
fun("Hi")   //Hi Blue
fun("Hi", "Lucky")  //Hi Lucky

上面代碼中爲參數【y】指定了默認值,但是這樣的寫法在遇到【y】如果是布爾類型的值的時候,加入我爲【y】賦值爲false,那麼會發現沒法賦值成功。

爲了避免上面的問題,我們將上面的函數進行改寫

function fun(x, y) {
    if (typeof y === 'undefined') {
        y = y || "Blue";
    }
    console.log(x, y);
}

有了ES6之後就方便,我們可以直接在函數參數列表中進行參數默認值的定義

function fun(x, y = 'Blue') {
    console.log(x, y);
}
fun("Hi")   //Hi Blue
fun("Hi", "Lucky")  //Hi Lucky

上面的例子看出ES6的寫法比ES5的簡潔了很多,而且不存在布爾值的問題,下面是另外一個【構造函數】的例子

function fun(x = 'Hi', y = 'Blue') {
    this.x = x;
    this.y = y;
}
var foo = new fun();    //{ x: 'Hi', y: 'Blue' }

【注意】:函數的參數是默認聲明的,所以在函數作用域中不能存在與參數相同名稱的變量聲明

function fun(x = 'Blue') {
  let x = 'Crazy'; // error
  const x = 'Jack'; // error
}

上面代碼片段中,x 爲參數變量,在該函數作用域中進行再次聲明引起報錯。

函數參數的默認值可以使用表達式,但是表達式是【惰性求值】的,也就是說在函數調用的時候重新計算默認值。

let x = 10;
function fun(r = x + 1) {
    console.log(r);
}
fun();  //11
x = 100;
fun();  //101

上面代碼中函數默認值是一個【簡單表達式】,可以看出表達式在賦值的時候是在應用的時候才進行的。

let add = (x, y) => x + y;
let sub = (x, y) => x - y;
let fun = (r = add(1, 2)) => console.log(r);
fun();  //3
fun(sub(4, 2)); //2

上面代碼中函數默認值使用的是【函數賦值】,可以更加清晰的看出函數默認值的賦值時【惰性求值】的。

1.2 函數默認值與解構賦值結合使用

既然函數參數實際上也是一個變量聲明的過程,那麼函數默認值也可以使用【解構賦值】

let fun = ({ x, y = 'Blue' }) => console.log(x, y);

fun({});    //undefined Blue

fun({ x: "Hi" });   //Hi Blue

fun({ x: 'Hi', y: 'Crazy' });   //Hi Crazy

fun();  //// TypeError: Cannot read property 'x' of undefined

上面解構賦值代碼中,只有當fun參數是一個對象時,默認值的解構賦值纔會生效,最後一行代碼沒有傳參就報錯了。

我們看看對上面例子的改造

let fun = ({ x, y = 'Blue' } = {}) => console.log(x, y);

fun();  //undefined 'Blue'

上面代碼中函數未給予參數仍然能夠正常的運行。

【注意】看一看下面兩種寫法的差別,就明白上面兩個例子的差異在哪兒了

//寫法一
let fun = ({ x = 'Hi', y = 'Blue' } = {}) => console.log(x, y);

//寫法二
let fun1 = ({ x, y } = { x: 'Hi', y: 'Blue' }) => console.log(x, y);

上面兩種寫法都對函數參數設定了默認值,區別是寫法一函數參數的默認值是空對象,但是設置了對象解構賦值的默認值,寫法二函數參數的默認值是一個有具體屬性的對象,但是沒有設置對象解構賦值的默認值。

let fun = ({ x = 'Hi', y = 'Blue' } = {}) => console.log(x, y);

let fun1 = ({ x, y } = { x: 'Hi', y: 'Blue' }) => console.log(x, y);
//都沒有參數的情況
fun();  //Hi Blue
fun1(); //Hi Blue

//一樣參數的情況
fun({x: 'Hello', y: 'Crazy'});  //Hello Crazy
fun1({x: 'Hello', y: 'Crazy'}); //Hello Crazy

//缺失值得情況
fun({x: 'Hello'});  //Hello Blue
fun1({x: 'Hello'}); //Hello undefined

//都無值得情況
fun({});    //Hi Blue
fun1({});   //undefined undefined

看出區別了嗎?函數在傳入參數的時候,會替換原有=右邊的對象,如果我們將默認值放在=右邊的對象屬性中進行默認解構,那麼當函數傳入參數的時候這個對象就被替換掉了。

1.3 參數默認值的位置

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

let fun = (x, y = 'Blue', z) => console.log(x, y, z);

fun();  //undefined 'Blue' undefined

fun('Hi', 'Crazy', 'Nice')  //Hi Crazy Nice

f('Hi',, 'Nice') // 報錯

上面代碼中,有默認值的參數不是尾參數。這時,無法只省略該參數,而不省略它後面的參數,除非顯式輸入undefined。

let fun = (x, y = 'Blue', z) => console.log(x, y, z);

fun('Hi', undefined, 'Nice');   //Hi Blue Nice

上面代碼中y傳入undefined參數,根據解構賦值的規則會觸發默認值

1.4 函數的length屬性

函數的【length】屬性返回的是沒有默認值的參數個數,也就是說指定了默認值的參數就不會計入length屬性的計數中。

(x => x).length //1

((x='Blue') => x).length    //0

((x, y, z = 'Blue') => x).length    //2

上面代碼中第二段和第三段都設置了參數默認值,可以看出設置了默認值的參數不計入length計算。

((x = 'Blue', y, z) => x).length;   //0

((x, y = 'Blue', z) => x).length;   //1

((x, y, z = 'Blue') => x).length;   //2

上面代碼中參數默認值的位置不同導致了【length】屬性的不同,其實是,【如果設置了默認值的參數不是尾參數,那麼length屬性也不再計入後面的參數了。】

1.5 作用域

一旦設置了參數的默認值,函數進行聲明初始化時,參數會形成一個單獨的作用域(context)。等到初始化結束,這個作用域就會消失。這種語法行爲,在不設置參數默認值時,是不會出現的。

let x = 'Blue';

let fun = (x, y = x) => console.log(x, y);

fun('Crazy');   //Crazy Crazy

上面代碼中,參數y的默認值等於變量x。調用函數f時,參數形成一個單獨的作用域。在這個作用域裏面,默認值變量x指向第一個參數x,而不是全局變量x,所以輸出是Crazy Crazy。

let x = 'Blue';
let fun = (y = x) => {
    let x = 'Crazy';
    console.log(y);
}
fun();  //Blue

上面代碼中,函數調用時,參數y = x形成一個單獨的作用域。這個作用域裏面,變量x本身沒有定義,所以指向外層的全局變量x。函數調用時,由於塊級作用域,所以函數體內部的局部變量x影響不到默認值變量x。

上面例子中因爲默認值賦值的X變量是指向外層的全局x,所以如果全局沒有x則會報錯

let fun = (y = x) => {
    let x = 'Crazy';
    console.log(y);
}
fun();  // 'ReferenceError: x is not defined'

下面這樣子寫也會報錯

var x = 1;

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

foo() // ReferenceError: x is not defined

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

1.6 默認值應用

利用參數默認值,可以指定某一個參數不得省略,如果省略就拋出一個錯誤。

function throwIfMissing() {
  throw new Error('Missing parameter');
}

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

foo()
// Error: Missing parameter

上面代碼中參數默認值設置爲一個函數,由於是【惰性求值】,所以foo在運行時,如果沒有傳入參數則執行函數拋出錯誤。

另外,可以將參數默認值設爲undefined,表明這個參數是可以省略的

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

2.rest參數

什麼事rest參數,形式爲(…變量名)這樣子的函數參數我們稱之爲rest參數,rest參數將多餘的組合成一個數組。

function fun(...values) {
    console.log(values);
}
fun(1, 2, 3, 4)
//[ 1, 2, 3, 4 ]

上面代碼中rest參數將所有傳入的參數都放入了values數組中,這樣就完全取代了arguments對象,並且擁有更多的屬性。

【注意】rest 參數之後不能再有其他參數(即只能是最後一個參數),否則會報錯。

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

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

(function(a) {}).length  // 1

(function(...a) {}).length  // 0

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

3.嚴格模式

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

// 報錯
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
  }
};

4.name屬性

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

function foo() {}

foo.name // "foo"

這個屬性是在ES5中就有的,但是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 "

5.箭頭函數

5.1 基本用法

以前的函數定義就是function,ES6使得這一切更加簡單清晰

let f = x => x;

上面代碼翻譯跟ES5就是下面這樣子

var f = function f(x) {
  return x;
};

和if語句一樣,代碼庫多餘一條語句,就要使用大括號將它們括起來,並且要返回值時使用return語句

let f = x => {
    if (typeof x === undefined) {
        x = 'Blue';
    }
    return x;
};

由於大括號被解釋爲代碼塊,所以如果箭頭函數直接返回一個對象,必須在對象外面加上括號。

var getTempItem = id => ({ id: id, name: "Temp" });

既然箭頭函數也是函數,那麼參數同樣可以與變量解構結合使用

let fun = ({ x, y }) => console.log(x + y);

fun({ x: 1, y: 2 });    //3

上面代碼爲對象的解構賦值和箭頭函數的應用

let fun = ([x, y]) => console.log(x + y);

fun([1, 2]);    //3

上面代碼爲數組的解構賦值和箭頭函數的應用

如果參數列表只有一個參數,則可以省略參數列表的括號

[1, 2, 3, 4].map(x => x * 10);
//[ 10, 20, 30, 40 ]

如果箭頭函數不需要參數或者需要多個參數,則必須使用圓括號代表安琥是列表


let fun = () => 'Blue';

let fun = (x, y, z) => x + y + z;

既然正常函數可以與rest參數聯合使用,那麼箭頭函數也可以

const mkArr = (...arr) => arr;
mkArr(1, 2, 3, 4);  //[1, 2, 3, 4]

const mkArr = (first,...arr) => [first,arr];
mkArr(1, 2, 3, 4);
//[1, [2, 3, 4]]

5.2 使用箭頭函數的注意點

  1. 函數體內的this對象,就是【定義時所在的對象】,而不是使用時所在的對象。
  2. 【不可以當作構造函數】,也就是說,不可以使用new命令,否則會拋出一個錯誤。
  3. 【不可以使用arguments對象】,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。
  4. 【不可以使用yield命令】,因此箭頭函數不能用作 Generator 函數。
function fun() {
    setTimeout(() => {
        console.log('name:', this.name);
    }, 100);
}

var name = 'Crazy';

fun.call({ name: 'Blue' }); //name: Blue

上面代碼中,setTimeout的參數是一個箭頭函數,這個箭頭函數的定義生效是在fun函數生成時,而它的真正執行要等到100毫秒後。如果是普通函數,執行時this應該指向全局對象window,這時應該輸出Crazy。但是,箭頭函數導致this總是指向函數定義生效時所在的對象(本例是{ name: ‘Blue’ }),所以輸出的是Blue。

箭頭函數可以讓setTimeout裏面的this,綁定定義時所在的作用域,而不是指向運行時所在的作用域。下面是另一個例子。

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭頭函數
  setInterval(() => this.s1++, 1000);

  // 普通函數
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

上面代碼中,Timer函數內部設置了兩個定時器,分別使用了箭頭函數和普通函數。前者的this綁定定義時所在的作用域(即Timer函數),後者的this指向運行時所在的作用域(即全局對象)。所以,3100毫秒之後,timer.s1被更新了3次,而timer.s2一次都沒更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章