ES6函數擴展

前面的話

  函數是全部編程語言的重要組成部分,在ES6出現前,JS的函數語法一直沒有太大的變化,從而遺留了不少問題,致使實現一些基本的功能常常要編寫不少代碼。ES6大力度地更新了函數特性,在ES5的基礎上進行了許多改進,使用JS編程能夠更少出錯,同時也更加靈活。本文將詳細介紹ES6函數擴展html

 

形參默認值

  Javascript函數有一個特別的地方,不管在函數定義中聲明瞭多少形參,均可以傳入任意數量的參數,也能夠在定義函數時添加針對參數數量的處理邏輯,當已定義的形參無對應的傳入參數時爲其指定一個默認值編程

【ES5模擬】數組

  在ES5中,通常地,經過下列方式建立函數併爲參數設置默認值瀏覽器

function makeRequest(url, timeout, callback) {
    timeout = timeout || 2000;
    callback = callback || function() {};
    // 函數的剩餘部分
}

  在這個示例中,timeout和callback爲可選參數,若是不傳入相應的參數系統會給它們賦予一個默認值。在含有邏輯或操做符的表達式中,前一個操做數的值爲false時,總會返回後一個值。對於函數的命名參數,若是不顯式傳值,則其值默認爲undefined安全

  所以咱們常用邏輯或操做符來爲缺失的參數提供默認值閉包

  然而這個方法也有缺陷,若是咱們想給makeRequest函數的第二個形參timeout傳入值0,即便這個值是合法的,也會被視爲一個false值,並最終將timeout賦值爲2000app

  在這種狀況下,更安全的選擇是經過typeof檢查參數類型,以下所示編程語言

function makeRequest(url, timeout, callback) {
    timeout = (typeof timeout !== "undefined") ? timeout : 2000;
    callback = (typeof callback !== "undefined") ? callback : function() {};
    // 函數的剩餘部分
}

  雖然這種方法更安全,但依然爲實現一個基本需求而書寫了額外的代碼。它表明了一種常見的模式,而流行的 JS 庫中都充斥着相似的模式進行默認補全函數式編程

【ES6默認參數】函數

  ES6簡化了爲形參提供默認值的過程,若是沒爲參數傳入值則爲其提供一個初始值

function makeRequest(url, timeout = 2000, callback = function() {}) {
    // 函數的剩餘部分
}

  在這個函數中,只有第一個參數被認爲老是要爲其傳入值的,其餘兩個參數都有默認值,並且不須要添加任何校驗值是否缺失的代碼,因此函數代碼比較簡潔

  若是調用make Request()方法時傳入3個參數,則不使用默認值

// 使用默認的 timeout 與 callback
makeRequest("/foo");
// 使用默認的 callback
makeRequest("/foo", 500);
// 不使用默認值
makeRequest("/foo", 500, function(body) {
    doSomething(body);
});

【觸發默認值】

  聲明函數時,能夠爲任意參數指定默認值,在已指定默認值的參數後能夠繼續聲明無默認值參數

function makeRequest(url, timeout = 2000, callback) {
    console.log(url);
    console.log(timeout);
    console.log(callback);
}

  在這種狀況下,只有當不爲第二個參數傳入值或主動爲第二個參數傳入undefined時纔會使用timeout的默認值

  [注意]若是傳入undefined,將觸發該參數等於默認值,null則沒有這個效果

function makeRequest(url, timeout = 2000, callback) {
    console.log(timeout);
}
makeRequest("/foo");//2000
makeRequest("/foo", undefined);//2000
makeRequest("/foo", null);//null
makeRequest("/foo", 100);//100

  上面代碼中,timeout參數對應undefined,結果觸發了默認值,y參數等於null,就沒有觸發默認值

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

// SyntaxError: Duplicate parameter name not allowed in this context
function foo(x, x, y = 1) {
  // ...
}

  另外,一個容易忽略的地方是,參數默認值不是傳值的,而是每次都從新計算默認值表達式的值。也就是說,參數默認值是惰性求值的

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

【length屬性】

  指定了默認值之後,函數的length屬性,將返回沒有指定默認值的參數個數。也就是說,指定了默認值後,length屬性將失真

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

  這是由於length屬性的含義是,該函數預期傳入的參數個數。某個參數指定默認值之後,預期傳入的參數個數就不包括這個參數了。同理,rest 參數也不會計入length屬性

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

  若是設置了默認值的參數不是尾參數,那麼length屬性也再也不計入後面的參數了

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

【arguments】

  當使用默認參數值時,arguments對象的行爲與以往不一樣。在ES5非嚴格模式下,函數命名參數的變化會體如今arguments對象中

function mixArgs(first, second) {
    console.log(first === arguments[0]);//true
    console.log(second === arguments[1]);//true
    first = "c";
    second = "d";
    console.log(first === arguments[0]);//true
    console.log(second === arguments[1]);//true
}
mixArgs("a", "b");

   在非嚴格模式下,命名參數的變化會同步更新到arguments對象中,因此當first和second被賦予新值時,arguments[0]和arguments[1]相應更新,最終全部===全等比較的結果爲true  

  然而,在ES5的嚴格模式下,取消了arguments對象的這個使人感到困惑的行爲,不管參數如何變化,arguments對象再也不隨之改變

function mixArgs(first, second) {
    "use strict";
    console.log(first === arguments[0]);//true
    console.log(second === arguments[1]);//true
    first = "c";
    second = "d"
    console.log(first === arguments[0]);//false
    console.log(second === arguments[1]);//false
}
mixArgs("a", "b");

  這一次更改 first 與 second 就不會再影響 arguments 對象,所以輸出結果符合一般的指望

  在ES6中,若是一個函數使用了默認參數值,則不管是否顯式定義了嚴格模式,arguments對象的行爲都將與ES5嚴格模式下保持一致。默認參數值的存在使得arguments對象保持與命名參數分離,這個微妙的細節將影響使用arguments對象的方式

// 非嚴格模式
function mixArgs(first, second = "b") {
    console.log(first);//a
    console.log(second);//b
    console.log(arguments.length);//1
    console.log(arguments[0]);//a
    console.log(arguments[1]);//undefined
    first = 'aa';
    arguments[1] = 'b';
    console.log(first);//aa
    console.log(second);//b
    console.log(arguments.length);//1
    console.log(arguments[0]);//a
    console.log(arguments[1]);//b
}
mixArgs("a");

  在這個示例中,只給mixArgs()方法傳入一個參數,arguments. Iength 的值爲 1, arguments[1] 的值爲 undefined, first與arguments[0]全等,改變first和second並不會影響arguments對象

【默認參數表達式】

  關於默認參數值,最有趣的特性多是非原始值傳參了。能夠經過函數執行來獲得默認參數的值

function getValue() {
    return 5;
}
function add(first, second = getValue()) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6

  在這段代碼中,若是不傳入最後一個參數,就會調用getvalue()函數來獲得正確的默認值。切記,初次解析函數聲明時不會調用getvalue()方法,只有當調用add()函數且不傳入第二個參數時纔會調用

let value = 5;
function getValue() {
    return value++;
}
function add(first, second = getValue()) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
console.log(add(1)); // 7

  在此示例中,變量value的初始值爲5,每次調用getvalue()時加1。第一次調用add(1)返回6,第二次調用add(1)返回7,由於變量value已經被加了1。由於只要調用add()函數就有可能求second的默認值,因此任什麼時候候均可以改變那個值

  正由於默認參數是在函數調用時求值,因此可使用先定義的參數做爲後定義參數的默認值

function add(first, second = first) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 2

  在上面這段代碼中,參數second的默認值爲參數first的值,若是隻傳入一個參數,則兩個參數的值相同,從而add(1,1)返回2,add(1)也返回2

function getValue(value) {
    return value + 5;
}
function add(first, second = getValue(first)) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7

  在上面這個示例中,聲明second=getvalue(first),因此儘管add(1,1)仍然返回2,可是add(1)返回的是(1+6)也就是7

  在引用參數默認值的時候,只容許引用前面參數的值,即先定義的參數不能訪問後定義的參數

function add(first = second, second) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // 拋出錯誤

  調用add(undefined,1)會拋出錯誤,由於second比first晚定義,所以其不能做爲first的默認值

【臨時死區】

  在介紹塊級做用域時提到過臨時死區TDZ,其實默認參數也有一樣的臨時死區,在這裏的參數不可訪問。與let聲明相似,定義參數時會爲每一個參數建立一個新的標識符綁定,該綁定在初始化以前不可被引用,若是試圖訪問會致使程序拋出錯誤。當調用函數時,會經過傳入的值或參數的默認值初始化該參數

function getValue(value) {
    return value + 5;
}
function add(first, second = getValue(first)) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7

  調用add(1,1)和add(1)時實際上至關於執行如下代碼來建立first和second參數值

// JS 調用 add(1, 1) 可表示爲
let first = 1;
let second = 1;
// JS 調用 add(1) 可表示爲
let first = 1;
let second = getValue(first);

  當初次執行函數add()時,first和second被添加到一個專屬於函數參數的臨時死區(與let的行爲相似)。因爲初始化second時first已經被初始化,因此它能夠訪問first的值,可是反過來就錯了

function add(first = second, second) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // 拋出錯誤

  在這個示例中,調用add(1,1)和add(undefined,1)至關於在引擎的背後作了以下事情

// JS 調用 add(1, 1) 可表示爲
let first = 1;
let second = 1;
// JS 調用 add(1) 可表示爲
let first = second;
let second = 1;

  在這個示例中,調用add(undefined,1)函數,由於當first初始化時second還沒有初始化,因此會致使程序拋出錯誤,此時second尚處於臨時死區中,全部引用臨時死區中綁定的行爲都會報錯

【形參與自由變量】

  下列代碼中,y是形參,須要考慮臨時死區的問題;而x是自由變量,不須要考慮。因此調用函數時,因爲未傳入參數,執行y=x,x是自由變量,經過做用域鏈,在全局做用域找到x=1,並賦值給y,因而y取值1

let x = 1;
function f(y = x) {}
f() // 1

  下列代碼中,x和y是形參,須要考慮臨時死區的問題。由於沒有自由變量,因此不考慮做用域鏈尋值的問題。調用函數時,因爲未傳入參數,執行y=x,因爲x正處於臨時死區內,全部引用臨時死區中綁定的行爲都會報錯

let x = 1;
function f(y = x,x) {}
f()// ReferenceError: x is not defined

  相似地,下列代碼也報錯

let x = 1;
function foo(x = x) {}
foo() // ReferenceError: x is not defined

 

不定參數

  不管函數已定義的命名參數有多少,都不限制調用時傳入的實際參數數量,調用時老是能夠傳入任意數量的參數。當傳入更少數量的參數時,默認參數值的特性能夠有效簡化函數聲明的代碼;當傳入更多數量的參數時,ES6一樣也提供了更好的方案。

【ES5】

   早先,Javascript提供arguments對象來檢查函數的全部參數,從而沒必要定義每個要用的參數。儘管arguments對象檢査在大多數狀況下運行良好,可是實際使用起來卻有些笨重

function pick(object) {
    let result = Object.create(null);
    // 從第二個參數開始處理
    for (let i = 1, len = arguments.length; i < len; i++) {
        result[arguments[i]] = object[arguments[i]];
    }
    return result;
}
let book = {
    title: "ES6",
    author: "huochai",
    year: 2017
};
let bookData = pick(book, "author", "year");
console.log(bookData.author); // "huochai"
console.log(bookData.year); // 2017

  這個函數模仿了Underscore.js庫中的pick()方法,返回一個給定對象的副本,包含原始對象屬性的特定子集。在這個示例中只定義了一個參數,第一個參數傳入的是被複制屬性的源對象,其餘參數爲被複制屬性的名稱

  關於pick()函數應該注意這樣幾件事情:首先,並不容易發現這個函數能夠接受任意數量的參數,固然,能夠定義更多的參數,可是怎麼也達不到要求;其次,由於第一個參數爲命名參數且已被使用,要查找須要拷貝的屬性名稱時,不得不從索引1而不是索引0開始遍歷arguments對象

【ES6】

  在ES6中,經過引入不定參數(rest parameters)的特性能夠解決這些問題,不定參數也稱爲剩餘參數或rest參數

  在函數的命名參數前添加三個點(...)就代表這是一個不定參數,該參數爲一個數組,包含着自它以後傳入的全部參數,經過這個數組名便可逐一訪問裏面的參數

function pick(object, ...keys) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}

  在這個函數中,不定參數keys包含的是object以後傳入的全部參數,而arguments對象包含的則是全部傳入的參數,包括object。這樣一來,就能夠放心地遍歷keys對象了。這種方法還有另外一個好處,只需看一眼函數就能夠知道該函數能夠處理的參數數量

【使用限制】

  不定參數有兩條使用限制

  一、每一個函數最多隻能聲明一個不定參數,並且必定要放在全部參數的末尾

// 語法錯誤:不能在剩餘參數後使用具名參數
function pick(object, ...keys, last) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}

  二、不定參數不能在對象字面量的 setter 屬性中使用

let object = {
    // 語法錯誤:不能在 setter 中使用剩餘參數
    set name(...value) {
        // 一些操做
    }
};

  之因此存在這條限制,是由於對象字面量setter的參數有且只能有一個。而在不定參數的定義中,參數的數量能夠無限多,因此在當前上下文中不容許使用不定參數

【arguments】

  不定參數的設計初衷是代替JS的arguments對象。起初,在ES4草案中,arguments對象被移除並添加了不定參數的特性,從而能夠傳入不限數量的參數。可是ES4從未被標準化,這個想法被擱置下來,直到從新引入了ES6標準,惟一的區別是arguments對象依然存在

function checkArgs(n,...args) {
    console.log(args.length);//2
    console.log(arguments.length);//3
    console.log(args);//['b','c']
    console.log(arguments);//['a','b','c']
}
checkArgs("a", "b", "c");

【應用】

  不定參數中的變量表明一個數組,因此數組特有的方法均可以用於這個變量

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

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

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

 

展開運算符

  在全部的新功能中,與不定參數最類似的是展開運算符。不定參數能夠指定多個各自獨立的參數,並經過整合後的數組來訪問;而展開運算符能夠指定一個數組,將它們打散後做爲各自獨立的參數傳入函數。JS內建的Math.max()方法能夠接受任意數量的參數並返回值最大的那一個

let value1 = 25,
value2 = 50;
console.log(Math.max(value1, value2)); // 50

  如上例所示,若是隻處理兩個值,那麼Math.max()很是簡單易用。傳入兩個值後返回更大的那一個。可是若是想從一個數組中挑選出最大的那個值應該怎麼作呢?Math.max()方法不容許傳入數組,因此在ES5中,可能須要手動實現從數組中遍歷取值,或者使用apply()方法

let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values)); // 100

  這個解決方案確實可行,但卻讓人很難看懂代碼的真正意圖

  使用ES6中的展開運算符能夠簡化上述示例,向Math.max()方法傳入一個數組,再在數組前添加不定參數中使用的...符號,就無須再調用apply()方法了。JS引擎讀取這段程序後會將參數數組分割爲各自獨立的參數並依次傳入

let values = [25, 50, 75, 100]
// 等價於 console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100

  使用apply()方法須要手動指定this的綁定,若是使用展開運算符可使這種簡單的數學運算看起來更加簡潔

  能夠將展開運算符與其餘正常傳入的參數混合使用。假設限定Math.max()返回的最小值爲0,能夠單獨傳入限定值,其餘的參數仍然使用展開運算符獲得

let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0)); // 0

  在這個示例中,Math.max()函數先用展開運算符傳入數組中的值,又傳入了參數0

  展開運算符能夠簡化使用數組給函數傳參的編碼過程,在大多數使用apply()方法的狀況下展開運算符多是一個更合適的方案

 

嚴格模式

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

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

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

// 報錯
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表示八進制,因此應該報錯。可是實際上,JS引擎會先成功執行value = 070,而後進入函數體內部,發現須要用嚴格模式執行,這時纔會報錯

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

  兩種方法能夠規避這種限制:

  一、設定全局性的嚴格模式

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

  二、把函數包在一個無參數的當即執行函數裏面

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

 

構造函數

  Function構造函數是JS語法中不多被用到的一部分,一般咱們用它來動態建立新的函數。這種構造函數接受字符串形式的參數,分別爲函數參數及函數體

var add = new Function("first", "second", "return first + second");
console.log(add(1, 1)); // 2

  ES6加強了Function構造函數的功能,支持在建立函數時定義默認參數和不定參數。惟一須要作的是在參數名後添加一個等號及一個默認值

var add = new Function("first", "second = first","return first + second"); console.log(add(1, 1)); // 2 console.log(add(1)); // 2

  在這個示例中,調用add(1)時只傳入一個參數,參數second被賦值爲first的值。這種語法與不使用Function聲明函數很像

  定義不定參數,只需在最後一個參數前添加...

var pickFirst = new Function("...args", "return args[0]");
console.log(pickFirst(1, 2)); // 1

  在這段建立函數的代碼中,只定義了一個不定參數,函數返回傳入的第一個參數。對於Function構造函數,新增的默認參數和不定參數這兩個特性使其具有了與聲明式建立函數相同的能力

  

參數尾逗號

  ES8容許函數的最後一個參數有尾逗號(trailing comma)。

  此前,函數定義和調用時,都不容許最後一個參數後面出現逗號

function clownsEverywhere(
  param1,
  param2
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar'
);

  上面代碼中,若是在param2bar後面加一個逗號,就會報錯。

  若是像上面這樣,將參數寫成多行(即每一個參數佔據一行),之後修改代碼的時候,想爲函數clownsEverywhere添加第三個參數,或者調整參數的次序,就勢必要在原來最後一個參數後面添加一個逗號。這對於版本管理系統來講,就會顯示添加逗號的那一行也發生了變更。這看上去有點冗餘,所以新的語法容許定義和調用時,尾部直接有一個逗號

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);

  這樣的規定使得函數參數與數組和對象的尾逗號規則保持一致了

 

name屬性

  因爲在JS中有多種定義函數的方式,於是辨別函數就是一項具備挑戰性的任務。此外,匿名函數表達式的普遍使用更是加大了調試的難度,開發者們常常要追蹤難以解讀的棧記錄。爲了解決這些問題,ES6爲全部函數新增了name屬性

  ES6中全部的函數的name屬性都有一個合適的值 

function doSomething() {
    // ...
}
var doAnotherThing = function() {
    // ...
};
console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"

  在這段代碼中,dosomething()函數的name屬性值爲"dosomething",對應着聲明時的函數名稱;匿名函數表達式doAnotherThing()的name屬性值爲"doAnotherThing",對應着被賦值爲該匿名函數的變量的名稱

【特殊狀況】

  儘管肯定函數聲明和函數表達式的名稱很容易,ES6仍是作了更多的改進來確保全部函數都有合適的名稱

var doSomething = function doSomethingElse() {
    // ...
};
var person = {
    get firstName() {
        return "huochai"
    },
    sayName: function() {
        console.log(this.name);
    }
}
console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
var descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor.get.name); // "get firstName"

  在這個示例中,dosomething.name的值爲"dosomethingElse",是因爲函數表達式有一個名字,這個名字比函數自己被賦值的變量的權重高

  person.sayName()的name屬性的值爲"sayName",由於其值取自對象字面量。與之相似,person.firstName其實是一個getter函數,因此它的名稱爲"get firstName",setter函數的名稱中固然也有前綴"set"

  還有另外兩個有關函數名稱的特例:經過bind()函數建立的函數,其名稱將帶有"bound"前綴;經過Function構造函數建立的函數,其名稱將帶有前綴"anonymous"

var doSomething = function() {
    // ...
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"

  綁定函數的name屬性老是由被綁定函數的name屬性及字符串前綴"bound"組成,因此綁定函數dosomething()的name屬性值爲"bound dosomething"

  [注意]函數name屬性的值不必定引用同名變量,它只是協助調試用的額外信息,因此不能使用name屬性的值來獲取對於函數的引用

 

判斷調用

  ES5中的函數結合new使用,函數內的this值將指向一個新對象,函數最終會返回這個新對象

function Person(name) {
    this.name = name;
}
var person = new Person("huochai");
var notAPerson = Person("huochai");
console.log(person); // "[Object object]"
console.log(notAPerson); // "undefined"

  給notAperson變量賦值時,沒有經過new關鍵字來調用person(),最終返回undefined(若是在非嚴格模式下,還會在全局對象中設置一個name屬性)。只有經過new關鍵字調用person()時才能體現其能力,就像常見的JS程序中顯示的那樣

  而在ES6中,函數混亂的雙重身份終於將有一些改變

  JS函數有兩個不一樣的內部方法:[[Call]]和[[Construct]]

  當經過new關鍵字調用函數時,執行的是[[construct]]函數,它負責建立一個一般被稱做實例的新對象,而後再執行函數體,將this綁定到實例上

  若是不經過new關鍵字調用函數,則執行[[call]]函數,從而直接執行代碼中的函數體

  具備[[construct]]方法的函數被統稱爲構造函數

  [注意]不是全部函數都有[[construct]]方法,所以不是全部函數均可以經過new來調用

【ES5判斷函數被調用】

  在ES5中,若是想肯定一個函數是否經過new關鍵字被調用,或者說,判斷該函數是否做爲構造函數被調用,最經常使用的方式是使用instanceof操做符

function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("huochai");
var notAPerson = Person("huochai"); // 拋出錯誤

  在這段代碼中,首先檢查this的值,看它是否爲構造函數的實例,若是是,則繼續正常執行。若是不是,則拋出錯誤。因爲[[construct]]方法會建立一個person的新實例,並將this綁定到新實例上,一般來說這樣作是正確的

  但這個方法也不徹底可靠,由於有一種不依賴new關鍵字的方法也能夠將this綁定到person的實例上

function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("huochai");
var notAPerson = Person.call(person, "huochai"); // 不報錯

  調用person.call()時將變量person傳入做爲第一個參數,至關於在person函數裏將this設爲了person實例。對於函數自己,沒法區分是經過person.call()(或者是person.apply())仍是new關鍵字調用獲得的person的實例

【元屬性new.target】

  爲了解決判斷函數是否經過new關鍵字調用的問題,ES6引入了new.target這個元屬性。元屬性是指非對象的屬性,其能夠提供非對象目標的補充信息(例如new)。當調用函數的[[construct]]方法時,new.target被賦值爲new操做符的目標,一般是新建立對象實例,也就是函數體內this的構造函數;若是調用[[call]]方法,則new.target的值爲undefined

  有了這個元屬性,能夠經過檢查new.target是否被定義過,檢測一個函數是不是經過new關鍵字調用的

function Person(name) {
    if (typeof new.target !== "undefined") {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("huochai");
var notAPerson = Person.call(person, "match"); // 出錯!

  也能夠檢查new.target是否被某個特定構造函數所調用

function Person(name) {
    if (new.target === Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
function AnotherPerson(name) {
    Person.call(this, name);
}
var person = new Person("huochai");
var anotherPerson = new AnotherPerson("huochai"); // 出錯!

  在這段代碼中,若是要讓程序正確運行,new.target必定是person。當調用 new Anotherperson("huochai") 時, 真正的調用Person. call(this,name)沒有使用new關鍵字,所以new.target的值爲undefined會拋出錯誤

  [注意]在函數外使用new.target是一個語法錯誤

 

塊級函數

  在ES3中,在代碼塊中聲明一個函數(即塊級函數)嚴格來講應當是一個語法錯誤, 但全部的瀏覽器都支持該語法。不幸的是,每一個瀏覽器對這個特性的支持都稍有不一樣,因此最好不要在代碼塊中聲明函數,更好的選擇是使用函數表達式

   爲了遏制這種不兼容行爲, ES5的嚴格模式爲代碼塊內部的函數聲明引入了一個錯誤

"use strict";
if (true) {
    // 在 ES5 會拋出語法錯誤, ES6 則不會
    function doSomething() {
        // ...
    }
}

  在ES5中,代碼會拋出語法錯誤。而在ES6中,會將dosomething()函數視爲一個塊級聲明,從而能夠在定義該函數的代碼塊內訪問和調用它

"use strict";
if (true) {
    console.log(typeof doSomething); // "function"
    function doSomething() {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething); // "undefined"

  在定義函數的代碼塊內,塊級函數會被提高至頂部,因此typeof dosomething的值爲"function",這也佐證了,即便在函數定義的位置前調用它,仍是能返回正確結果。可是一旦if語句代碼塊結束執行,dosomething()函數將再也不存在

【使用場景】

  塊級函數與let函數表達式相似,一旦執行過程流出了代碼塊,函數定義當即被移除。兩者的區別是,在該代碼塊中,塊級函數會被提高至塊的頂部,而用let定義的函數表達式不會被提高

"use strict";
if (true) {
    console.log(typeof doSomething); // 拋出錯誤
    let doSomething = function () {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething);

  在這段代碼中,當執行到typeof dosomething時,因爲此時還沒有執行let聲明語句,dosomething()還在當前塊做用域的臨時死區中,所以程序被迫中斷執行

  所以,若是須要函數提高至代碼塊頂部,則選擇塊級函數;若是不須要,則選擇let表達式

【非嚴格模式】

  在ES6中,即便處於非嚴格模式下,也能夠聲明塊級函數,但其行爲與嚴格模式下稍有不一樣。這些函數再也不提高到代碼塊的頂部,而是提高到外圍函數或全局做用域的頂部

// ES6 behavior
if (true) {
    console.log(typeof doSomething); // "function"
    function doSomething() {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething); // "function"

  在這個示例中,dosomething()函數被提高至全局做用域,因此在if代碼塊外也能夠訪問到。ES6將這個行爲標準化了,移除了以前存在於各瀏覽器間不兼容的行爲,因此全部ES6的運行時環境都將執行這一標準

 

箭頭函數

  在ES6中,箭頭函數是其中最有趣的新增特性。顧名思義,箭頭函數是一種使用箭頭(=>)定義函數的新語法,可是它與傳統的JS函數有些許不一樣,主要集中在如下方面 

  一、沒有this、super、arguments和new.target

  綁定箭頭函數中的this、super、arguments和new.target這些值由外圍最近一層非箭頭函數決定

  二、不能經過new關鍵字調用

  箭頭函數沒有[[construct]]方法,不能被用做構造函數,若是經過new關鍵字調用箭頭函數,程序拋出錯誤

  三、沒有原型

  因爲不能夠經過new關鍵字調用箭頭函數,於是沒有構建原型的需求,因此箭頭函數不存在prototype這個屬性

  四、不能夠改變this綁定

  函數內部的this值不可被改變,在函數的生命週期內始終保持一致

  五、不支持arguments對象

  箭頭函數沒有arguments綁定,必須經過命名參數和不定參數這兩種形式訪問函數的參數

  六、不支持重複的命名參數

  不管在嚴格仍是非嚴格模式下,箭頭函數都不支持重複的命名參數;而在傳統函數的規定中,只有在嚴格模式下才不能有重複的命名參數

  在箭頭函數內,其他的差別主要是減小錯誤以及理清模糊不清的地方。這樣一來,JS引擎就能夠更好地優化箭頭函數的執行過程

  這些差別的產生有以下幾個緣由

  一、最重要的是,this綁定是JS程序中一個常見的錯誤來源,在函數內很容易對this的值失去控制,其常常致使程序出現意想不到的行爲,箭頭函數消除了這方面的煩惱

  二、若是限制箭頭函數的this值,簡化代碼執行的過程,則JS引擎能夠更輕鬆地優化這些操做,而常規函數每每同時會做爲構造函數使用或者以其餘方式對其進行修改

  [注意]箭頭函數一樣也有一個name屬性,這與其餘函數的規則相同

【語法】

  箭頭函數的語法多變,根據實際的使用場景有多種形式。全部變種都由函數參數、箭頭、函數體組成,根據使用的需求,參數和函數體能夠分別採起多種不一樣的形式

var reflect = value => value;
// 有效等價於:
var reflect = function(value) {
    return value;
};

  當箭頭函數只有一個參數時,能夠直接寫參數名,箭頭緊隨其後,箭頭右側的表達式被求值後便當即返回。即便沒有顯式的返回語句,這個箭頭函數也能夠返回傳入的第一個參數

  若是要傳入兩個或兩個以上的參數,要在參數的兩側添加一對小括號

var sum = (num1, num2) => num1 + num2;
// 有效等價於:
var sum = function(num1, num2) {
    return num1 + num2;
};

  這裏的sum()函數接受兩個參數,將它們簡單相加後返回最終結果,它與reflect()函數惟一的不一樣是,它的參數被包裹在小括號中,而且用逗號進行分隔(相似傳統函數)

  若是函數沒有參數,也要在聲明的時候寫一組沒有內容的小括號

var getName = () => "huochai";
// 有效等價於:
var getName = function() {
    return "huochai";
};

  若是但願爲函數編寫由多個表達式組成的更傳統的函數體,那麼須要用花括號包裹函數體,並顯式地定義一個返回值

var sum = (num1, num2) => {
    return num1 + num2;
};
// 有效等價於:
var sum = function(num1, num2) {
    return num1 + num2;
};

  除了arguments對象不可用之外,某種程度上均可以將花括號裏的代碼視做傳統的函數體定義

  若是想建立一個空函數,須要寫一對沒有內容的花括號

var doNothing = () => {};
// 有效等價於:
var doNothing = function() {};

  花括號表明函數體的部分,可是若是想在箭頭函數外返回一個對象字面量,則須要將該字面量包裹在小括號裏

var getTempItem = id => ({ id: id, name: "Temp" });
// 有效等價於:
var getTempItem = function(id) {
    return {
        id: id,
        name: "Temp"
    };
};

  將對象字面量包裹在小括號中是爲了將其與函數體區分開來

【IIFE】

  JS函數的一個流行的使用方式是建立當即執行函數表達式(IIFE),能夠定義一個匿名函數並當即調用,自始至終不保存對該函數的引用。當建立一個與其餘程序隔離的做用域時,這種模式很是方便

let person = function(name) {
    return {
        getName: function() {
            return name;
        }
    };
}("huochai");
console.log(person.getName()); // "huochai"

  在這段代碼中,IIFE經過getName()方法建立了一個新對象,將參數name做爲該對象的一個私有成員返回給函數的調用者

  只要將箭頭函數包裹在小括號裏,就能夠用它實現相同的功能

let person = ((name) => {
    return {
        getName: function() {
            return name;
        }
    };
})("huochai");
console.log(person.getName()); // "huochai"

  [注意]小括號只包裹箭頭函數定義,沒有包含("huochai"),這一點與正常函數有所不一樣,由正常函數定義的當即執行函數表達式既能夠用小括號包裹函數體,也能夠額外包裹函數調用的部分

【this】

  函數內的this綁定是JS中最常出現錯誤的因素,函數內的this值能夠根據函數調用的上下文而改變,這有可能錯誤地影響其餘對象

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click", function(event) {
            this.doSomething(event.type); // 錯誤
        }, false);
    },
    doSomething: function(type) {
        console.log("Handling " + type + " for " + this.id);
    }
};

  在這段代碼中,對象pageHandler的設計初衷是用來處理頁面上的交互,經過調用init()方法設置交互,依次分配事件處理程序來調用this.dosomething()。然而,這段代碼並無如預期的正常運行

  實際上,由於this綁定的是事件目標對象的引用(在這段代碼中引用的是document),而沒有綁定pageHandler,且因爲this.dosonething()在目標document中不存在,因此沒法正常執行,嘗試運行這段代碼只會使程序在觸發事件處理程序時拋出錯誤

  可使用bind()方法顯式地將this綁定到pageHandler函數上來修正這個問題

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click", (function(event) {
            this.doSomething(event.type); // 錯誤
        }).bind(this), false);
    },
    doSomething: function(type) {
        console.log("Handling " + type + " for " + this.id);
    }
};

  如今代碼如預期的運行,但可能看起來仍然有點奇怪。調用bind(this)後,事實上建立了一個新函數,它的this被綁定到當前的this,也就是page Handler

  能夠經過一個更好的方式來修正這段代碼:使用箭頭函數

  箭頭函數中沒有this綁定,必須經過查找做用城鏈來決定其值。若是箭頭函數被非箭頭函數包含,則this綁定的是最近一層非箭頭函數的this;不然,this的值會被設置爲undefined

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click",
            event => this.doSomething(event.type), false);
        },
    doSomething: function(type) {
        console.log("Handling " + type + " for " + this.id);
    }
};

  這個示例中的事件處理程序是一個調用了this.doSomething()的箭頭函數,此處的this與init()函數裏的this一致,因此此版本代碼的運行結果與使用bind(this)一致。雖然dosomething()方法不返回值,可是它還是函數體內惟一的一條執行語句,因此沒必要用花括號將它包裹起來

  箭頭函數缺乏正常函數所擁有的prototype屬性,它的設計初衷是即用即棄,因此不能用它來定義新的類型。若是嘗試經過new關鍵字調用一個箭頭函數,會致使程序拋出錯誤

var MyType = () => {},
object = new MyType(); // 錯誤:不能對箭頭函數使用 'new'

  在這段代碼中,MyType是一個沒有[[Construct]]方法的箭頭函數,因此不能正常執行new MyType()。也正由於箭頭函數不能與new關鍵字混用,因此JS引擎能夠進一步優化它們的行爲。一樣,箭頭函數中的this值取決於該函數外部非箭頭函數的this值,且不能經過call()、apply()或bind()方法來改變this的值

【數組】 

  箭頭函數的語法簡潔,很是適用於數組處理。若是想給數組排序,一般須要寫一個自定義的比較器

var result = values.sort(function(a, b) {
    return a - b;
});

  只想實現一個簡單功能,但這些代碼實在太多了。用箭頭函數簡化以下

var result = values.sort((a, b) => a - b);

  諸如sort()、map()及reduce()這些能夠接受回調函數的數組方法,均可以經過箭頭函數語法簡化編碼過程並減小編碼量

// 正常函數寫法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭頭函數寫法
[1,2,3].map(x => x * x);

【arguments】

  箭頭函數沒有本身的arguments對象,且將來不管函數在哪一個上下文中執行,箭頭函數始終能夠訪問外圍函數的arguments對象

function createArrowFunctionReturningFirstArg() {
    return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction()); // 5

  在createArrowFunctionReturningFirstArg()中,箭頭函數引用了外圍函數傳入的第一個參數arguments[0],也就是後續執行過程當中傳入的數字5。即便函數箭頭此時已再也不處於建立它的函數的做用域中,卻依然能夠訪問當時的arguments對象,這是arguments標識符的做用域鏈解決方案所規定的

【辨識方法】

  儘管箭頭函數與傳統函數的語法不一樣,但它一樣能夠被識別出來

var comparator = (a, b) => a - b;
console.log(typeof comparator); // "function"
console.log(comparator instanceof Function); // true

  一樣地,仍然能夠在箭頭函數上調用call()、apply()及bind()方法,但與其餘函數不一樣的是,箭頭函數的this值不會受這些方法的影響

var sum = (num1, num2) => num1 + num2;
console.log(sum.call(null, 1, 2)); // 3
console.log(sum.apply(null, [1, 2])); // 3
var boundSum = sum.bind(null, 1, 2);
console.log(boundSum()); // 3

  包括回調函數在內全部使用匿名函數表達式的地方都適合用箭頭函數來改寫

【函數柯里化】

  柯里化是一種把接受多個參數的函數變換成接受一個單一參數的函數,而且返回(接受餘下的參數並且返回結果的)新函數的技術

  若是使用ES5的語法來寫,以下所示

function add(x){
  return function(y){
    return y + x;
  };
}
 
var addTwo = add(2);
addTwo(3);          // => 5
add(10)(11);        // => 21

  使用ES6的語法來寫,以下所示

var add = (x) => (y) => x+y

  通常來講,出現連續地箭頭函數調用的狀況,就是在使用函數柯里化的技術

 

尾調用優化

  ES6關於函數最有趣的變化多是尾調用系統的引擎優化。尾調用指的是函數做爲另外一個函數的最後一條語句被調用

function doSomething() {
    return doSomethingElse(); // 尾調用
}

  尾調用之因此與其餘調用不一樣,就在於它的特殊的調用位置

  咱們知道,函數調用會在內存造成一個「調用記錄」,又稱「調用幀」(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用幀上方,還會造成一個B的調用幀。等到B運行結束,將結果返回到AB的調用幀纔會消失。若是函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。全部的調用幀,就造成一個「調用棧」(call stack)

  尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用幀,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就能夠了

  尾調用優化(Tail call optimization),即只保留內層函數的調用幀。若是全部函數都是尾調用,那麼徹底能夠作到每次執行時,調用幀只有一項,這將大大節省內存

  ES6縮減了嚴格模式下尾調用棧的大小(非嚴格模式下不受影響),若是知足如下條件,尾調用再也不建立新的棧幀,而是清除並重用當前棧幀

  一、尾調用不訪問當前棧幀的變量(也就是說函數不是一個閉包)

  二、在函數內部,尾調用是最後一條語句

  三、尾調用的結果做爲函數值返回

  如下這段示例代碼知足上述的三個條件,能夠被JS引擎自動優化

"use strict";
function doSomething() {
    // 被優化
    return doSomethingElse();
}

  在這個函數中,尾調用doSomethingElse()的結果當即返回,不調用任何局部做用域變量。若是作一個小改動,不返回最終結果,那麼引擎就沒法優化當前函數

"use strict";
function doSomething() {
    // 未被優化:缺乏 return
    doSomethingElse();
}

  一樣地,若是定義了一個函數,在尾調用返回後執行其餘操做,則函數也沒法獲得優化

"use strict";
function doSomething() {
    // 未被優化:在返回以後還要執行加法
    return 1 + doSomethingElse();
}

  若是把函數調用的結果存儲在一個變量裏,最後再返回這個變量,則可能致使引擎沒法優化

"use strict";
function doSomething() {
    // 未被優化:調用並不在尾部
    var result = doSomethingElse();
    return result;
}

  可能最難避免的狀況是閉包的使用,它能夠訪問做用域中全部變量,於是致使尾調用優化失效

"use strict";
function doSomething() {
    var num = 1,
    func = () => num;
    // 未被優化:此函數是閉包
    return func();
}

  在示例中,閉包func()能夠訪問局部變量num,即便調用func()後當即返回結果,也沒法對代碼進行優化

【應用】

  實際上,尾調用的優化發生在引擎背後,除非嘗試優化一個函數,不然無須思考此類問題。遞歸函數是其最主要的應用場景,此時尾調用優化的效果最顯著

function factorial(n) {
    if (n <= 1) {
        return 1;
    } else {
        // 未被優化:在返回以後還要執行乘法
        return n * factorial(n - 1);
    }
}

  因爲在遞歸調用前執行了乘法操做,於是當前版本的階乘函數沒法被引擎優化。若是n是一個很是大的數,則調用棧的尺寸就會不斷增加並存在最終致使棧溢出的潛在風險

  優化這個函數,首先要確保乘法不會在函數調用後執行,能夠經過默認參數來將乘法操做移出return語句,結果函數能夠攜帶着臨時結果進入到下一個迭代中

function factorial(n, p = 1) {
    if (n <= 1) {
        return 1 * p;
    } else {
        let result = n * p;
        // 被優化
        return factorial(n - 1, result);
    }
}

  在這個重寫後的factorial()函數中,第一個參數p的默認值爲1,用它來保存乘法結果,下一次迭代中能夠取出它用於計算,再也不須要額外的函數調用。當n大於1時,先執行一輪乘法計算,而後將結果傳給第二次factorial()調用的參數。如今,ES6引擎就能夠優化遞歸調用了

  寫遞歸函數時,最好得用尾遞歸優化的特性,若是遞歸函數的計算量足夠大,則尾遞歸優化能夠大幅提高程序的性能

  另外一個常見的事例是Fibonacci數列

function Fibonacci (n) {
  if ( n <= 1 ) {return 1};
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 堆棧溢出
Fibonacci(500) // 堆棧溢出

  尾遞歸優化過的 Fibonacci 數列實現以下

function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};
  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

  因而可知,「尾調用優化」對遞歸操做意義重大,因此一些函數式編程語言將其寫入了語言規格。ES6 是如此,第一次明確規定,全部 ECMAScript 的實現,都必須部署「尾調用優化」。這就是說,ES6 中只要使用尾遞歸,就不會發生棧溢出,相對節省內存

相關文章
相關標籤/搜索