ES6(六):函數(shù)擴展

前面的話


函數(shù)是所有編程語言的重要組成部分,在ES6出現(xiàn)前辙浑,JS的函數(shù)語法一直沒有太大的變化激涤,從而遺留了很多問題,導(dǎo)致實現(xiàn)一些基本的功能經(jīng)常要編寫很多代碼判呕。ES6大力度地更新了函數(shù)特性倦踢,在ES5的基礎(chǔ)上進行了許多改進,使用JS編程可以更少出錯侠草,同時也更加靈活辱挥。本文將詳細介紹ES6函數(shù)擴展

形參默認值

Javascript函數(shù)有一個特別的地方,無論在函數(shù)定義中聲明了多少形參边涕,都可以傳入任意數(shù)量的參數(shù)晤碘,也可以在定義函數(shù)時添加針對參數(shù)數(shù)量的處理邏輯褂微,當已定義的形參無對應(yīng)的傳入?yún)?shù)時為其指定一個默認值

【ES5模擬】

ES5中,一般地宠蚂,通過下列方式創(chuàng)建函數(shù)并為參數(shù)設(shè)置默認值

functionmakeRequest(url, timeout, callback) {
  timeout = timeout || 2000;
  callback = callback ||function() {};    // 函數(shù)的剩余部分
}
  • 在這個示例中童社,timeout和callback為可選參數(shù)呀癣,如果不傳入相應(yīng)的參數(shù)系統(tǒng)會給它們賦予一個默認值。在含有邏輯或操作符的表達式中弦赖,前一個操作數(shù)的值為false時项栏,總會返回后一個值。對于函數(shù)的命名參數(shù)蹬竖,如果不顯式傳值忘嫉,則其值默認為undefined
  • 因此我們經(jīng)常使用邏輯或操作符來為缺失的參數(shù)提供默認值
  • 然而這個方法也有缺陷,如果我們想給makeRequest函數(shù)的第二個形參timeout傳入值0案腺,即使這個值是合法的,也會被視為一個false值康吵,并最終將timeout賦值為2000

在這種情況下劈榨,更安全的選擇是通過typeof檢查參數(shù)類型,如下所示

functionmakeRequest(url, timeout, callback) {
  timeout = (typeoftimeout !== "undefined") ? timeout : 2000;
  callback = (typeof callback !== "undefined") ? callback : function() {};    // 函數(shù)的剩余部分
}

雖然這種方法更安全晦嵌,但依然為實現(xiàn)一個基本需求而書寫了額外的代碼同辣。它代表了一種常見的模式,而流行的JS 庫中都充斥著類似的模式進行默認補全

【ES6默認參數(shù)】

ES6簡化了為形參提供默認值的過程惭载,如果沒為參數(shù)傳入值則為其提供一個初始值

function makeRequest(url, timeout = 2000, callback = function() {}) {    
  // 函數(shù)的剩余部分
}
  • 在這個函數(shù)中旱函,只有第一個參數(shù)被認為總是要為其傳入值的,其他兩個參數(shù)都有默認值描滔,而且不需要添加任何校驗值是否缺失的代碼棒妨,所以函數(shù)代碼比較簡潔
  • 如果調(diào)用make Request()方法時傳入3個參數(shù),則不使用默認值
// 使用默認的 timeout 與 callbackmakeRequest("/foo");
// 使用默認的callbackmakeRequest("/foo", 500);
// 不使用默認值makeRequest("/foo", 500, function(body) {doSomething(body);});

【觸發(fā)默認值】

聲明函數(shù)時含长,可以為任意參數(shù)指定默認值券腔,在已指定默認值的參數(shù)后可以繼續(xù)聲明無默認值參數(shù)

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

在這種情況下,只有當不為第二個參數(shù)傳入值或主動為第二個參數(shù)傳入undefined時才會使用timeout的默認值

[注意]如果傳入undefined拘泞,將觸發(fā)該參數(shù)等于默認值纷纫,null則沒有這個效果

functionmakeRequest(url, timeout = 2000, callback) {
  console.log(timeout);
}
makeRequest("/foo");//2000
makeRequest("/foo", undefined);//2000
makeRequest("/foo", null);//null
makeRequest("/foo", 100);//100
  • 上面代碼中,timeout參數(shù)對應(yīng)undefined陪腌,結(jié)果觸發(fā)了默認值辱魁,y參數(shù)等于null烟瞧,就沒有觸發(fā)默認值

使用參數(shù)默認值時,函數(shù)不能有同名參數(shù)

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

另外染簇,一個容易忽略的地方是参滴,參數(shù)默認值不是傳值的,而是每次都重新計算默認值表達式的值剖笙。也就是說卵洗,參數(shù)默認值是惰性求值的

let x = 99;functionfoo(p = x + 1) {
  console.log(p);
}
foo() // 100x = 100;
foo() // 101

上面代碼中,參數(shù)p的默認值是x+1弥咪。這時过蹂,每次調(diào)用函數(shù)foo,都會重新計算x+1聚至,而不是默認p等于100

【length屬性】

指定了默認值以后酷勺,函數(shù)的length屬性,將返回沒有指定默認值的參數(shù)個數(shù)扳躬。也就是說脆诉,指定了默認值后,length屬性將失真

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

這是因為length屬性的含義是贷币,該函數(shù)預(yù)期傳入的參數(shù)個數(shù)击胜。某個參數(shù)指定默認值以后,預(yù)期傳入的參數(shù)個數(shù)就不包括這個參數(shù)了役纹。同理偶摔,rest參數(shù)也不會計入length屬性

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

如果設(shè)置了默認值的參數(shù)不是尾參數(shù),那么length屬性也不再計入后面的參數(shù)了

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

【arguments】

當使用默認參數(shù)值時促脉,arguments對象的行為與以往不同辰斋。在ES5非嚴格模式下,函數(shù)命名參數(shù)的變化會體現(xiàn)在arguments對象中

functionmixArgs(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");
  • 在非嚴格模式下瘸味,命名參數(shù)的變化會同步更新到arguments對象中宫仗,所以當first和second被賦予新值時,arguments[0]和arguments[1]相應(yīng)更新旁仿,最終所有===全等比較的結(jié)果為true
  • 然而藕夫,在ES5的嚴格模式下,取消了arguments對象的這個令人感到困惑的行為丁逝,無論參數(shù)如何變化汁胆,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 對象,因此輸出結(jié)果符合通常的期望
  • 在ES6中霜幼,如果一個函數(shù)使用了默認參數(shù)值嫩码,則無論是否顯式定義了嚴格模式,arguments對象的行為都將與ES5嚴格模式下保持一致罪既。默認參數(shù)值的存在使得arguments對象保持與命名參數(shù)分離铸题,這個微妙的細節(jié)將影響使用arguments對象的方式
// 非嚴格模式
functionmixArgs(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()方法傳入一個參數(shù),arguments.Iength 的值為1, arguments[1] 的值為 undefined, first與arguments[0]全等丢间,改變first和second并不會影響arguments對象

【默認參數(shù)表達式】

關(guān)于默認參數(shù)值探熔,最有趣的特性可能是非原始值傳參了『娲欤可以通過函數(shù)執(zhí)行來得到默認參數(shù)的值

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

在這段代碼中诀艰,如果不傳入最后一個參數(shù),就會調(diào)用getvalue()函數(shù)來得到正確的默認值饮六。切記其垄,初次解析函數(shù)聲明時不會調(diào)用getvalue()方法,只有當調(diào)用add()函數(shù)且不傳入第二個參數(shù)時才會調(diào)用

let value = 5;function getValue() {    
  return value++;
}
functionadd(first, second = getValue()) {      
  return first +second;
}
console.log(add(1, 1));// 2
console.log(add(1)); // 6
console.log(add(1)); // 7
  • 在此示例中卤橄,變量value的初始值為5绿满,每次調(diào)用getvalue()時加1。第一次調(diào)用add(1)返回6窟扑,第二次調(diào)用add(1)返回7喇颁,因為變量value已經(jīng)被加了1。因為只要調(diào)用add()函數(shù)就有可能求second的默認值嚎货,所以任何時候都可以改變那個值

正因為默認參數(shù)是在函數(shù)調(diào)用時求值橘霎,所以可以使用先定義的參數(shù)作為后定義參數(shù)的默認值

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

在上面這段代碼中,參數(shù)second的默認值為參數(shù)first的值殖属,如果只傳入一個參數(shù)茎毁,則兩個參數(shù)的值相同,從而add(1,1)返回2忱辅,add(1)也返回2

function getValue(value) {    
  return value + 5;
}
functionadd(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

在引用參數(shù)默認值的時候,只允許引用前面參數(shù)的值扮念,即先定義的參數(shù)不能訪問后定義的參數(shù)

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

調(diào)用add(undefined,1)會拋出錯誤损搬,因為second比first晚定義,因此其不能作為first的默認值

【臨時死區(qū)】

在介紹塊級作用域時提到過臨時死區(qū)TDZ柜与,其實默認參數(shù)也有同樣的臨時死區(qū)巧勤,在這里的參數(shù)不可訪問。與let聲明類似弄匕,定義參數(shù)時會為每個參數(shù)創(chuàng)建一個新的標識符綁定颅悉,該綁定在初始化之前不可被引用,如果試圖訪問會導(dǎo)致程序拋出錯誤迁匠。當調(diào)用函數(shù)時剩瓶,會通過傳入的值或參數(shù)的默認值初始化該參數(shù)

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

調(diào)用add(1,1)和add(1)時實際上相當于執(zhí)行以下代碼來創(chuàng)建first和second參數(shù)值

// JS 調(diào)用 add(1, 1) 可表示為
let first = 1;
let second = 1;
// JS 調(diào)用 add(1) 可表示為
let first = 1;
let second = getValue(first);

當初次執(zhí)行函數(shù)add()時驹溃,first和second被添加到一個專屬于函數(shù)參數(shù)的臨時死區(qū)(與let的行為類似)。由于初始化second時first已經(jīng)被初始化延曙,所以它可以訪問first的值豌鹤,但是反過來就錯了

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

在這個示例中,調(diào)用add(1,1)和add(undefined,1)相當于在引擎的背后做了如下事情

// JS 調(diào)用 add(1, 1) 可表示為
let first = 1;
let second = 1;
// JS 調(diào)用 add(1) 可表示為
let first =second;
let second = 1;

在這個示例中枝缔,調(diào)用add(undefined,1)函數(shù)布疙,因為當first初始化時second尚未初始化,所以會導(dǎo)致程序拋出錯誤愿卸,此時second尚處于臨時死區(qū)中灵临,所有引用臨時死區(qū)中綁定的行為都會報錯

【形參與自由變量】

下列代碼中,y是形參擦酌,需要考慮臨時死區(qū)的問題俱诸;而x是自由變量,不需要考慮赊舶。所以調(diào)用函數(shù)時睁搭,由于未傳入?yún)?shù),執(zhí)行y=x笼平,x是自由變量园骆,通過作用域鏈,在全局作用域找到x=1寓调,并賦值給y锌唾,于是y取值1

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

下列代碼中,x和y是形參夺英,需要考慮臨時死區(qū)的問題晌涕。因為沒有自由變量,所以不考慮作用域鏈尋值的問題痛悯。調(diào)用函數(shù)時余黎,由于未傳入?yún)?shù),執(zhí)行y=x载萌,由于x正處于臨時死區(qū)內(nèi)惧财,所有引用臨時死區(qū)中綁定的行為都會報錯

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

類似地,下列代碼也報錯

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

不定參數(shù)

無論函數(shù)已定義的命名參數(shù)有多少扭仁,都不限制調(diào)用時傳入的實際參數(shù)數(shù)量垮衷,調(diào)用時總是可以傳入任意數(shù)量的參數(shù)。當傳入更少數(shù)量的參數(shù)時乖坠,默認參數(shù)值的特性可以有效簡化函數(shù)聲明的代碼搀突;當傳入更多數(shù)量的參數(shù)時,ES6同樣也提供了更好的方案熊泵。

【ES5】

早先描姚,Javascript提供arguments對象來檢查函數(shù)的所有參數(shù)涩赢,從而不必定義每一個要用的參數(shù)。盡管arguments對象檢査在大多數(shù)情況下運行良好轩勘,但是實際使用起來卻有些笨重

functionpick(object) {
  let result = Object.create(null);    // 從第二個參數(shù)開始處理
  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
  • 這個函數(shù)模仿了Underscore.js庫中的pick()方法筒扒,返回一個給定對象的副本,包含原始對象屬性的特定子集绊寻。在這個示例中只定義了一個參數(shù)花墩,第一個參數(shù)傳入的是被復(fù)制屬性的源對象,其他參數(shù)為被復(fù)制屬性的名稱
  • 關(guān)于pick()函數(shù)應(yīng)該注意這樣幾件事情:首先澄步,并不容易發(fā)現(xiàn)這個函數(shù)可以接受任意數(shù)量的參數(shù)冰蘑,當然,可以定義更多的參數(shù)村缸,但是怎么也達不到要求祠肥;其次,因為第一個參數(shù)為命名參數(shù)且已被使用梯皿,要查找需要拷貝的屬性名稱時仇箱,不得不從索引1而不是索引0開始遍歷arguments對象

【ES6】

ES6中,通過引入不定參數(shù)(rest parameters)的特性可以解決這些問題东羹,不定參數(shù)也稱為剩余參數(shù)或rest參數(shù)
在函數(shù)的命名參數(shù)前添加三個點(...)就表明這是一個不定參數(shù)剂桥,該參數(shù)為一個數(shù)組,包含著自它之后傳入的所有參數(shù)属提,通過這個數(shù)組名即可逐一訪問里面的參數(shù)

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

在這個函數(shù)中权逗,不定參數(shù)keys包含的是object之后傳入的所有參數(shù),而arguments對象包含的則是所有傳入的參數(shù)冤议,包括object斟薇。這樣一來,就可以放心地遍歷keys對象了恕酸。這種方法還有另一個好處奔垦,只需看一眼函數(shù)就可以知道該函數(shù)可以處理的參數(shù)數(shù)量

【使用限制】

不定參數(shù)有兩條使用限制

  • 1、每個函數(shù)最多只能聲明一個不定參數(shù)尸疆,而且一定要放在所有參數(shù)的末尾
// 語法錯誤:不能在剩余參數(shù)后使用具名參數(shù)
functionpick(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;
}
  • 2、不定參數(shù)不能在對象字面量的setter 屬性中使用
let object = {    
  // 語法錯誤:不能在 setter 中使用剩余參數(shù)    
  set name(...value) {        
    // 一些操作
  }
};

之所以存在這條限制惶岭,是因為對象字面量setter的參數(shù)有且只能有一個寿弱。而在不定參數(shù)的定義中,參數(shù)的數(shù)量可以無限多按灶,所以在當前上下文中不允許使用不定參數(shù)

【arguments】

不定參數(shù)的設(shè)計初衷是代替JSarguments對象症革。起初,在ES4草案中鸯旁,arguments對象被移除并添加了不定參數(shù)的特性噪矛,從而可以傳入不限數(shù)量的參數(shù)量蕊。但是ES4從未被標準化,這個想法被擱置下來艇挨,直到重新引入了ES6標準残炮,唯一的區(qū)別是arguments對象依然存在

functioncheckArgs(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");

【應(yīng)用】

不定參數(shù)中的變量代表一個數(shù)組,所以數(shù)組特有的方法都可以用于這個變量

// arguments變量的寫法
function sortNumbers() {  
  return Array.prototype.slice.call(arguments).sort();
}
// 不定參數(shù)的寫法
const sortNumbers = (...numbers) => numbers.sort();

上面代碼的兩種寫法缩滨,比較后可以發(fā)現(xiàn)势就,不定參數(shù)的寫法更自然也更簡潔

展開運算符

在所有的新功能中,與不定參數(shù)最相似的是展開運算符脉漏。不定參數(shù)可以指定多個各自獨立的參數(shù)苞冯,并通過整合后的數(shù)組來訪問;而展開運算符可以指定一個數(shù)組侧巨,將它們打散后作為各自獨立的參數(shù)傳入函數(shù)舅锄。JS內(nèi)建的Math.max()方法可以接受任意數(shù)量的參數(shù)并返回值最大的那一個

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

如上例所示,如果只處理兩個值司忱,那么Math.max()非常簡單易用皇忿。傳入兩個值后返回更大的那一個。但是如果想從一個數(shù)組中挑選出最大的那個值應(yīng)該怎么做呢?Math.max()方法不允許傳入數(shù)組烘贴,所以在ES5中禁添,可能需要手動實現(xiàn)從數(shù)組中遍歷取值,或者使用apply()方法

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

  • 這個解決方案確實可行桨踪,但卻讓人很難看懂代碼的真正意圖
  • 使用ES6中的展開運算符可以簡化上述示例老翘,向Math.max()方法傳入一個數(shù)組,再在數(shù)組前添加不定參數(shù)中使用的...符號锻离,就無須再調(diào)用apply()方法了铺峭。JS引擎讀取這段程序后會將參數(shù)數(shù)組分割為各自獨立的參數(shù)并依次傳入
let values = [25, 50, 75, 100]
// 等價于 console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100

使用apply()方法需要手動指定this的綁定,如果使用展開運算符可以使這種簡單的數(shù)學(xué)運算看起來更加簡潔可以將展開運算符與其他正常傳入的參數(shù)混合使用汽纠。假設(shè)限定Math.max()返回的最小值為0卫键,可以單獨傳入限定值,其他的參數(shù)仍然使用展開運算符得到

let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0));// 0
  • 在這個示例中虱朵,Math.max()函數(shù)先用展開運算符傳入數(shù)組中的值莉炉,又傳入了參數(shù)0
  • 展開運算符可以簡化使用數(shù)組給函數(shù)傳參的編碼過程,在大多數(shù)使用apply()方法的情況下展開運算符可能是一個更合適的方案

嚴格模式

ES5 開始碴犬,函數(shù)內(nèi)部可以設(shè)定為嚴格模式

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

ES7做了一點修改絮宁,規(guī)定只要函數(shù)參數(shù)使用了默認值、解構(gòu)賦值服协、或者擴展運算符绍昂,那么函數(shù)內(nèi)部就不能顯式設(shè)定為嚴格模式,否則會報錯

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

這樣規(guī)定的原因是,函數(shù)內(nèi)部的嚴格模式窘游,同時適用于函數(shù)體和函數(shù)參數(shù)唠椭。但是,函數(shù)執(zhí)行的時候忍饰,先執(zhí)行函數(shù)參數(shù)贪嫂,然后再執(zhí)行函數(shù)體。這樣就有一個不合理的地方喘批,只有從函數(shù)體之中撩荣,才能知道參數(shù)是否應(yīng)該以嚴格模式執(zhí)行,但是參數(shù)卻應(yīng)該先于函數(shù)體執(zhí)行

// 報錯
functiondoSomething(value = 070) {  
  'use strict';  returnvalue;
}
  • 上面代碼中饶深,參數(shù)value的默認值是八進制數(shù)070餐曹,但是嚴格模式下不能用前綴0表示八進制,所以應(yīng)該報錯敌厘。但是實際上台猴,JS引擎會先成功執(zhí)行value = 070,然后進入函數(shù)體內(nèi)部俱两,發(fā)現(xiàn)需要用嚴格模式執(zhí)行饱狂,這時才會報錯
  • 雖然可以先解析函數(shù)體代碼,再執(zhí)行參數(shù)代碼宪彩,但是這樣無疑就增加了復(fù)雜性休讳。因此举瑰,標準索性禁止了這種用法科盛,只要參數(shù)使用了默認值摹恰、解構(gòu)賦值失暂、或者擴展運算符,就不能顯式指定嚴格模式棵癣。

兩種方法可以規(guī)避這種限制:

  • 1过椎、設(shè)定全局性的嚴格模式
'use strict';
functiondoSomething(a, b = a) {  
  // code
}
  • 2橡庞、把函數(shù)包在一個無參數(shù)的立即執(zhí)行函數(shù)里面
const doSomething = (function () {  
  'use strict';  
  return function(value = 42) {    
    returnvalue;
  };
}());

構(gòu)造函數(shù)

Function構(gòu)造函數(shù)是JS語法中很少被用到的一部分白指,通常我們用它來動態(tài)創(chuàng)建新的函數(shù)留晚。這種構(gòu)造函數(shù)接受字符串形式的參數(shù),分別為函數(shù)參數(shù)及函數(shù)體

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

ES6增強了Function構(gòu)造函數(shù)的功能告嘲,支持在創(chuàng)建函數(shù)時定義默認參數(shù)和不定參數(shù)错维。唯一需要做的是在參數(shù)名后添加一個等號及一個默認值

var add = new Function("first", "second = first","return first + second");
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
  • 在這個示例中,調(diào)用add(1)時只傳入一個參數(shù)橄唬,參數(shù)second被賦值為first的值赋焕。這種語法與不使用Function聲明函數(shù)很像

定義不定參數(shù),只需在最后一個參數(shù)前添加...

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

在這段創(chuàng)建函數(shù)的代碼中轧坎,只定義了一個不定參數(shù),函數(shù)返回傳入的第一個參數(shù)泽示。對于Function構(gòu)造函數(shù)缸血,新增的默認參數(shù)和不定參數(shù)這兩個特性使其具備了與聲明式創(chuàng)建函數(shù)相同的能力

參數(shù)尾逗號

ES8允許函數(shù)的最后一個參數(shù)有尾逗號(trailing comma)此前蜜氨,函數(shù)定義和調(diào)用時,都不允許最后一個參數(shù)后面出現(xiàn)逗號

functionclownsEverywhere(
  param1,
  param2
) { 
  /* ... */
}
clownsEverywhere(  'foo',  'bar');
  • 上面代碼中捎泻,如果在param2或bar后面加一個逗號飒炎,就會報錯。
  • 如果像上面這樣笆豁,將參數(shù)寫成多行(即每個參數(shù)占據(jù)一行)郎汪,以后修改代碼的時候,想為函數(shù)clownsEverywhere添加第三個參數(shù)闯狱,或者調(diào)整參數(shù)的次序煞赢,就勢必要在原來最后一個參數(shù)后面添加一個逗號。這對于版本管理系統(tǒng)來說哄孤,就會顯示添加逗號的那一行也發(fā)生了變動照筑。這看上去有點冗余,因此新的語法允許定義和調(diào)用時瘦陈,尾部直接有一個逗號
functionclownsEverywhere(
  param1,
  param2,
) { 
  /* ... */
}
clownsEverywhere(  'foo',  'bar',);

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

name屬性

由于在JS中有多種定義函數(shù)的方式凝危,因而辨別函數(shù)就是一項具有挑戰(zhàn)性的任務(wù)。此外晨逝,匿名函數(shù)表達式的廣泛使用更是加大了調(diào)試的難度蛾默,開發(fā)者們經(jīng)常要追蹤難以解讀的棧記錄。為了解決這些問題捉貌,ES6為所有函數(shù)新增了name屬性

ES6中所有的函數(shù)的name屬性都有一個合適的值

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

在這段代碼中支鸡,dosomething()函數(shù)的name屬性值為"dosomething",對應(yīng)著聲明時的函數(shù)名稱昏翰;匿名函數(shù)表達式doAnotherThing()的name屬性值為"doAnotherThing"苍匆,對應(yīng)著被賦值為該匿名函數(shù)的變量的名稱

【特殊情況】

盡管確定函數(shù)聲明和函數(shù)表達式的名稱很容易,ES6還是做了更多的改進來確保所有函數(shù)都有合適的名稱

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"浸踩,是由于函數(shù)表達式有一個名字,這個名字比函數(shù)本身被賦值的變量的權(quán)重高
  • person.sayName()的name屬性的值為"sayName"统求,因為其值取自對象字面量检碗。與之類似,person.firstName實際上是一個getter函數(shù)码邻,所以它的名稱為"get firstName"折剃,setter函數(shù)的名稱中當然也有前綴"set"

還有另外兩個有關(guān)函數(shù)名稱的特例:通過bind()函數(shù)創(chuàng)建的函數(shù),其名稱將帶有"bound"前綴像屋;通過Function構(gòu)造函數(shù)創(chuàng)建的函數(shù)怕犁,其名稱將帶有前綴"anonymous"

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

綁定函數(shù)的name屬性總是由被綁定函數(shù)的name屬性及字符串前綴"bound"組成,所以綁定函數(shù)dosomething()的name屬性值為"bound dosomething"

[注意]函數(shù)name屬性的值不一定引用同名變量,它只是協(xié)助調(diào)試用的額外信息奏甫,所以不能使用name屬性的值來獲取對于函數(shù)的引用

判斷調(diào)用

ES5中的函數(shù)結(jié)合new使用戈轿,函數(shù)內(nèi)的this值將指向一個新對象,函數(shù)最終會返回這個新對象

function Person(name) {    
  this.name =name;
}
var person = newPerson("huochai");
var notAPerson = Person("huochai");
console.log(person); // "[Object object]"
console.log(notAPerson); // "undefined"
  • 給notAperson變量賦值時阵子,沒有通過new關(guān)鍵字來調(diào)用person()思杯,最終返回undefined(如果在非嚴格模式下,還會在全局對象中設(shè)置一個name屬性)挠进。只有通過new關(guān)鍵字調(diào)用person()時才能體現(xiàn)其能力色乾,就像常見的JS程序中顯示的那樣
  • 而在ES6中,函數(shù)混亂的雙重身份終于將有一些改變
  • JS函數(shù)有兩個不同的內(nèi)部方法:[[Call]]和[[Construct]]
  • 當通過new關(guān)鍵字調(diào)用函數(shù)時领突,執(zhí)行的是[[construct]]函數(shù)暖璧,它負責創(chuàng)建一個通常被稱作實例的新對象,然后再執(zhí)行函數(shù)體攘须,將this綁定到實例上
  • 如果不通過new關(guān)鍵字調(diào)用函數(shù)漆撞,則執(zhí)行[[call]]函數(shù),從而直接執(zhí)行代碼中的函數(shù)體
  • 具有[[construct]]方法的函數(shù)被統(tǒng)稱為構(gòu)造函數(shù)

[注意]不是所有函數(shù)都有[[construct]]方法于宙,因此不是所有函數(shù)都可以通過new來調(diào)用

【ES5判斷函數(shù)被調(diào)用】

ES5中浮驳,如果想確定一個函數(shù)是否通過new關(guān)鍵字被調(diào)用,或者說捞魁,判斷該函數(shù)是否作為構(gòu)造函數(shù)被調(diào)用至会,最常用的方式是使用instanceof操作符

function Person(name) {    
  if (this instanceof Person) {        
    this.name = name; // 使用 new
  }else {        
  throw newError("You must use new with Person.")
}
}
var person = newPerson("huochai");
var notAPerson = Person("huochai"); // 拋出錯誤
  • 在這段代碼中,首先檢查this的值谱俭,看它是否為構(gòu)造函數(shù)的實例奉件,如果是,則繼續(xù)正常執(zhí)行昆著。如果不是县貌,則拋出錯誤。由于[[construct]]方法會創(chuàng)建一個person的新實例凑懂,并將this綁定到新實例上煤痕,通常來講這樣做是正確的
  • 但這個方法也不完全可靠,因為有一種不依賴new關(guān)鍵字的方法也可以將this綁定到person的實例上
function Person(name) {    
  if (this instanceof Person) {        
    this.name = name; // 使用 new
  }else {        
    throw newError("You must use new with Person.")
  }
}
var person = newPerson("huochai");
var notAPerson = Person.call(person, "huochai"); // 不報錯

調(diào)用person.call()時將變量person傳入作為第一個參數(shù)接谨,相當于在person函數(shù)里將this設(shè)為了person實例摆碉。對于函數(shù)本身,無法區(qū)分是通過person.call()(或者是person.apply())還是new關(guān)鍵字調(diào)用得到的person的實例

【元屬性new.target】

為了解決判斷函數(shù)是否通過new關(guān)鍵字調(diào)用的問題脓豪,ES6引入了new.target這個元屬性巷帝。元屬性是指非對象的屬性,其可以提供非對象目標的補充信息(例如new)扫夜。當調(diào)用函數(shù)的[[construct]]方法時楞泼,new.target被賦值為new操作符的目標驰徊,通常是新創(chuàng)建對象實例,也就是函數(shù)體內(nèi)this的構(gòu)造函數(shù)堕阔;如果調(diào)用[[call]]方法辣垒,則new.target的值為undefined

有了這個元屬性,可以通過檢查new.target是否被定義過印蔬,檢測一個函數(shù)是否是通過new關(guān)鍵字調(diào)用的

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

也可以檢查new.target是否被某個特定構(gòu)造函數(shù)所調(diào)用

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

在這段代碼中侥猬,如果要讓程序正確運行,new.target一定是person捐韩。當調(diào)用new Anotherperson("huochai") 時, 真正的調(diào)用Person.call(this,name)沒有使用new關(guān)鍵字退唠,因此new.target的值為undefined會拋出錯誤

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

塊級函數(shù)

ES3中,在代碼塊中聲明一個函數(shù)(即塊級函數(shù))嚴格來說應(yīng)當是一個語法錯誤荤胁,但所有的瀏覽器都支持該語法瞧预。不幸的是,每個瀏覽器對這個特性的支持都稍有不同仅政,所以最好不要在代碼塊中聲明函數(shù)垢油,更好的選擇是使用函數(shù)表達式

為了遏制這種不兼容行為,ES5的嚴格模式為代碼塊內(nèi)部的函數(shù)聲明引入了一個錯誤

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

在ES5中滩愁,代碼會拋出語法錯誤。而在ES6中辫封,會將dosomething()函數(shù)視為一個塊級聲明硝枉,從而可以在定義該函數(shù)的代碼塊內(nèi)訪問和調(diào)用它

"use strict";
if (true) {
  console.log(typeof doSomething); // "function"
  function doSomething() {        
    // ...
  }
  doSomething();
}
console.log(typeof doSomething); // "undefined"
  • 在定義函數(shù)的代碼塊內(nèi),塊級函數(shù)會被提升至頂部倦微,所以typeof
  • dosomething的值為"function"妻味,這也佐證了,即使在函數(shù)定義的位置前調(diào)用它欣福,還是能返回正確結(jié)果责球。但是一旦if語句代碼塊結(jié)束執(zhí)行,dosomething()函數(shù)將不再存在

【使用場景】

塊級函數(shù)與let函數(shù)表達式類似劣欢,一旦執(zhí)行過程流出了代碼塊棕诵,函數(shù)定義立即被移除。二者的區(qū)別是凿将,在該代碼塊中校套,塊級函數(shù)會被提升至塊的頂部,而用let定義的函數(shù)表達式不會被提升

"use strict";
if (true) {
  console.log(typeof doSomething); // 拋出錯誤
  let doSomething =function () {        
    // ...
  }
  doSomething();
}
console.log(typeofdoSomething);
  • 在這段代碼中牧抵,當執(zhí)行到typeof dosomething時笛匙,由于此時尚未執(zhí)行l(wèi)et聲明語句侨把,dosomething()還在當前塊作用域的臨時死區(qū)中,因此程序被迫中斷執(zhí)行
  • 因此妹孙,如果需要函數(shù)提升至代碼塊頂部秋柄,則選擇塊級函數(shù);如果不需要蠢正,則選擇let表達式

【非嚴格模式】

ES6中骇笔,即使處于非嚴格模式下,也可以聲明塊級函數(shù)嚣崭,但其行為與嚴格模式下稍有不同笨触。這些函數(shù)不再提升到代碼塊的頂部,而是提升到外圍函數(shù)或全局作用域的頂部

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

在這個示例中雹舀,dosomething()函數(shù)被提升至全局作用域芦劣,所以在if代碼塊外也可以訪問到。ES6將這個行為標準化了说榆,移除了之前存在于各瀏覽器間不兼容的行為虚吟,所以所有ES6的運行時環(huán)境都將執(zhí)行這一標準

箭頭函數(shù)

ES6中,箭頭函數(shù)是其中最有趣的新增特性签财。顧名思義串慰,箭頭函數(shù)是一種使用箭頭(=>)定義函數(shù)的新語法,但是它與傳統(tǒng)的JS函數(shù)有些許不同

主要集中在以下方面
1唱蒸、沒有this模庐、super、arguments和new.target
綁定箭頭函數(shù)中的this油宜、super掂碱、arguments和new.target這些值由外圍最近一層非箭頭函數(shù)決定

2、不能通過new關(guān)鍵字調(diào)用
箭頭函數(shù)沒有[[construct]]方法慎冤,不能被用作構(gòu)造函數(shù)疼燥,如果通過new關(guān)鍵字調(diào)用箭頭函數(shù),程序拋出錯誤

3蚁堤、沒有原型
由于不可以通過new關(guān)鍵字調(diào)用箭頭函數(shù)醉者,因而沒有構(gòu)建原型的需求,所以箭頭函數(shù)不存在prototype這個屬性

4披诗、不可以改變this綁定
函數(shù)內(nèi)部的this值不可被改變撬即,在函數(shù)的生命周期內(nèi)始終保持一致

5、不支持arguments對象
箭頭函數(shù)沒有arguments綁定呈队,必須通過命名參數(shù)和不定參數(shù)這兩種形式訪問函數(shù)的參數(shù)

6剥槐、不支持重復(fù)的命名參數(shù)
無論在嚴格還是非嚴格模式下,箭頭函數(shù)都不支持重復(fù)的命名參數(shù)宪摧;而在傳統(tǒng)函數(shù)的規(guī)定中粒竖,只有在嚴格模式下才不能有重復(fù)的命名參數(shù)

在箭頭函數(shù)內(nèi)颅崩,其余的差異主要是減少錯誤以及理清模糊不清的地方。這樣一來蕊苗,JS引擎就可以更好地優(yōu)化箭頭函數(shù)的執(zhí)行過程

這些差異的產(chǎn)生有如下幾個原因
1沿后、最重要的是,this綁定是JS程序中一個常見的錯誤來源朽砰,在函數(shù)內(nèi)很容易對this的值失去控制尖滚,其經(jīng)常導(dǎo)致程序出現(xiàn)意想不到的行為,箭頭函數(shù)消除了這方面的煩惱

2瞧柔、如果限制箭頭函數(shù)的this值熔掺,簡化代碼執(zhí)行的過程,則JS引擎可以更輕松地優(yōu)化這些操作非剃,而常規(guī)函數(shù)往往同時會作為構(gòu)造函數(shù)使用或者以其他方式對其進行修改

[注意]箭頭函數(shù)同樣也有一個name屬性,這與其他函數(shù)的規(guī)則相同

【語法】

箭頭函數(shù)的語法多變推沸,根據(jù)實際的使用場景有多種形式备绽。所有變種都由函數(shù)參數(shù)、箭頭鬓催、函數(shù)體組成肺素,根據(jù)使用的需求,參數(shù)和函數(shù)體可以分別采取多種不同的形式

var reflect = value => value;
// 有效等價于:
var reflect = function(value) {    
  return value;
};
  • 當箭頭函數(shù)只有一個參數(shù)時宇驾,可以直接寫參數(shù)名倍靡,箭頭緊隨其后,箭頭右側(cè)的表達式被求值后便立即返回课舍。即使沒有顯式的返回語句塌西,這個箭頭函數(shù)也可以返回傳入的第一個參數(shù)
  • 如果要傳入兩個或兩個以上的參數(shù),要在參數(shù)的兩側(cè)添加一對小括號

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

  • 這里的sum()函數(shù)接受兩個參數(shù)筝尾,將它們簡單相加后返回最終結(jié)果捡需,它與reflect()函數(shù)唯一的不同是,它的參數(shù)被包裹在小括號中筹淫,并且用逗號進行分隔(類似傳統(tǒng)函數(shù))
  • 如果函數(shù)沒有參數(shù)站辉,也要在聲明的時候?qū)懸唤M沒有內(nèi)容的小括號
var getName = () => "huochai";
// 有效等價于:
var getName = function() {    
  return "huochai";
};

如果希望為函數(shù)編寫由多個表達式組成的更傳統(tǒng)的函數(shù)體,那么需要用花括號包裹函數(shù)體损姜,并顯式地定義一個返回值

var sum = (num1, num2) => {    
  return num1 +num2;
};
// 有效等價于:
var sum = function(num1, num2) {    
  return num1 +num2;
};
  • 除了arguments對象不可用以外饰剥,某種程度上都可以將花括號里的代碼視作傳統(tǒng)的函數(shù)體定義

如果想創(chuàng)建一個空函數(shù),需要寫一對沒有內(nèi)容的花括號

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

花括號代表函數(shù)體的部分摧阅,但是如果想在箭頭函數(shù)外返回一個對象字面量汰蓉,則需要將該字面量包裹在小括號里

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

將對象字面量包裹在小括號中是為了將其與函數(shù)體區(qū)分開來

【IIFE】

JS函數(shù)的一個流行的使用方式是創(chuàng)建立即執(zhí)行函數(shù)表達式(IIFE),可以定義一個匿名函數(shù)并立即調(diào)用棒卷,自始至終不保存對該函數(shù)的引用古沥。當創(chuàng)建一個與其他程序隔離的作用域時瘸右,這種模式非常方便

let person = function(name) {    
  return{
    getName: function() {            
      return name;
    }
  };
}("huochai");
console.log(person.getName()); // "huochai"
  • 在這段代碼中,IIFE通過getName()方法創(chuàng)建了一個新對象岩齿,將參數(shù)name作為該對象的一個私有成員返回給函數(shù)的調(diào)用者

只要將箭頭函數(shù)包裹在小括號里太颤,就可以用它實現(xiàn)相同的功能

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

[注意]小括號只包裹箭頭函數(shù)定義,沒有包含("huochai")盹沈,這一點與正常函數(shù)有所不同龄章,由正常函數(shù)定義的立即執(zhí)行函數(shù)表達式既可以用小括號包裹函數(shù)體,也可以額外包裹函數(shù)調(diào)用的部分

【this】

函數(shù)內(nèi)的this綁定是JS中最常出現(xiàn)錯誤的因素乞封,函數(shù)內(nèi)的this值可以根據(jù)函數(shù)調(diào)用的上下文而改變做裙,這有可能錯誤地影響其他對象

varPageHandler ={
  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的設(shè)計初衷是用來處理頁面上的交互肃晚,通過調(diào)用init()方法設(shè)置交互锚贱,依次分配事件處理程序來調(diào)用this.dosomething()。然而关串,這段代碼并沒有如預(yù)期的正常運行
  • 實際上拧廊,因為this綁定的是事件目標對象的引用(在這段代碼中引用的是document),而沒有綁定pageHandler晋修,且由于this.dosonething()在目標document中不存在吧碾,所以無法正常執(zhí)行,嘗試運行這段代碼只會使程序在觸發(fā)事件處理程序時拋出錯誤

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

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);
  }
};
  • 現(xiàn)在代碼如預(yù)期的運行墓卦,但可能看起來仍然有點奇怪倦春。調(diào)用bind(this)后,事實上創(chuàng)建了一個新函數(shù)落剪,它的this被綁定到當前的this睁本,也就是pageHandler
  • 可以通過一個更好的方式來修正這段代碼:使用箭頭函數(shù)
  • 箭頭函數(shù)中沒有this綁定,必須通過查找作用城鏈來決定其值忠怖。如果箭頭函數(shù)被非箭頭函數(shù)包含添履,則this綁定的是最近一層非箭頭函數(shù)的this;否則脑又,this的值會被設(shè)置為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);
  }
};
  • 這個示例中的事件處理程序是一個調(diào)用了this.doSomething()的箭頭函數(shù)暮胧,此處的this與init()函數(shù)里的this一致,所以此版本代碼的運行結(jié)果與使用bind(this)一致问麸。雖然dosomething()方法不返回值往衷,但是它仍是函數(shù)體內(nèi)唯一的一條執(zhí)行語句,所以不必用花括號將它包裹起來
  • 箭頭函數(shù)缺少正常函數(shù)所擁有的prototype屬性严卖,它的設(shè)計初衷是即用即棄席舍,所以不能用它來定義新的類型。如果嘗試通過new關(guān)鍵字調(diào)用一個箭頭函數(shù)哮笆,會導(dǎo)致程序拋出錯誤
var MyType = () =>{},
object =new MyType(); // 錯誤:不能對箭頭函數(shù)使用 'new'

在這段代碼中来颤,MyType是一個沒有[[Construct]]方法的箭頭函數(shù)汰扭,所以不能正常執(zhí)行new
MyType()。也正因為箭頭函數(shù)不能與new關(guān)鍵字混用福铅,所以JS引擎可以進一步優(yōu)化它們的行為萝毛。同樣,箭頭函數(shù)中的this值取決于該函數(shù)外部非箭頭函數(shù)的this值滑黔,且不能通過call()笆包、apply()或bind()方法來改變this的值

【數(shù)組】

箭頭函數(shù)的語法簡潔,非常適用于數(shù)組處理略荡。如果想給數(shù)組排序庵佣,通常需要寫一個自定義的比較器

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

只想實現(xiàn)一個簡單功能,但這些代碼實在太多了汛兜。用箭頭函數(shù)簡化如下

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

諸如sort()巴粪、map()及reduce()這些可以接受回調(diào)函數(shù)的數(shù)組方法,都可以通過箭頭函數(shù)語法簡化編碼過程并減少編碼量

// 正常函數(shù)寫法
[1,2,3].map(function (x) {  
  returnx *x;
});
// 箭頭函數(shù)寫法
[1,2,3].map(x => x * x);

【arguments】

箭頭函數(shù)沒有自己的arguments對象粥谬,且未來無論函數(shù)在哪個上下文中執(zhí)行肛根,箭頭函數(shù)始終可以訪問外圍函數(shù)的arguments對象

function createArrowFunctionReturningFirstArg() {    
  return() => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction()); // 5
  • 在createArrowFunctionReturningFirstArg()中,箭頭函數(shù)引用了外圍函數(shù)傳入的第一個參數(shù)arguments[0]帝嗡,也就是后續(xù)執(zhí)行過程中傳入的數(shù)字5。即使函數(shù)箭頭此時已不再處于創(chuàng)建它的函數(shù)的作用域中璃氢,卻依然可以訪問當時的arguments對象哟玷,這是arguments標識符的作用域鏈解決方案所規(guī)定的

【辨識方法】

盡管箭頭函數(shù)與傳統(tǒng)函數(shù)的語法不同,但它同樣可以被識別出來

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

同樣地一也,仍然可以在箭頭函數(shù)上調(diào)用call()巢寡、apply()及bind()方法,但與其他函數(shù)不同的是椰苟,箭頭函數(shù)的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

包括回調(diào)函數(shù)在內(nèi)所有使用匿名函數(shù)表達式的地方都適合用箭頭函數(shù)來改寫

【函數(shù)柯里化】

柯里化是一種把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)的函數(shù)抑月,并且返回(接受余下的參數(shù)而且返回結(jié)果的)新函數(shù)的技術(shù)

如果使用ES5的語法來寫,如下所示

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

使用ES6的語法來寫舆蝴,如下所示

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

一般來說谦絮,出現(xiàn)連續(xù)地箭頭函數(shù)調(diào)用的情況,就是在使用函數(shù)柯里化的技術(shù)

尾調(diào)用優(yōu)化

ES6關(guān)于函數(shù)最有趣的變化可能是尾調(diào)用系統(tǒng)的引擎優(yōu)化洁仗。尾調(diào)用指的是函數(shù)作為另一個函數(shù)的最后一條語句被調(diào)用

function doSomething() {    
  return doSomethingElse(); // 尾調(diào)用
}
  • 尾調(diào)用之所以與其他調(diào)用不同层皱,就在于它的特殊的調(diào)用位置
  • 我們知道,函數(shù)調(diào)用會在內(nèi)存形成一個“調(diào)用記錄”赠潦,又稱“調(diào)用幀”(call frame)叫胖,保存調(diào)用位置和內(nèi)部變量等信息。如果在函數(shù)A的內(nèi)部調(diào)用函數(shù)B她奥,那么在A的調(diào)用幀上方瓮增,還會形成一個B的調(diào)用幀怎棱。等到B運行結(jié)束,將結(jié)果返回到A绷跑,B的調(diào)用幀才會消失拳恋。如果函數(shù)B內(nèi)部還調(diào)用函數(shù)C,那就還有一個C的調(diào)用幀你踩,以此類推诅岩。所有的調(diào)用幀,就形成一個“調(diào)用棿ぃ”(call stack)
  • 尾調(diào)用由于是函數(shù)的最后一步操作吩谦,所以不需要保留外層函數(shù)的調(diào)用幀,因為調(diào)用位置膝藕、內(nèi)部變量等信息都不會再用到了式廷,只要直接用內(nèi)層函數(shù)的調(diào)用幀,取代外層函數(shù)的調(diào)用幀就可以了
  • 尾調(diào)用優(yōu)化(Tail call optimization)芭挽,即只保留內(nèi)層函數(shù)的調(diào)用幀滑废。如果所有函數(shù)都是尾調(diào)用,那么完全可以做到每次執(zhí)行時袜爪,調(diào)用幀只有一項蠕趁,這將大大節(jié)省內(nèi)存

ES6縮減了嚴格模式下尾調(diào)用棧的大小(非嚴格模式下不受影響),如果滿足以下條件辛馆,尾調(diào)用不再創(chuàng)建新的棧幀俺陋,而是清除并重用當前棧幀
  1、尾調(diào)用不訪問當前棧幀的變量(也就是說函數(shù)不是一個閉包)
  2昙篙、在函數(shù)內(nèi)部腊状,尾調(diào)用是最后一條語句
  3、尾調(diào)用的結(jié)果作為函數(shù)值返回

以下這段示例代碼滿足上述的三個條件苔可,可以被JS引擎自動優(yōu)化

"use strict";
function doSomething() {    
  // 被優(yōu)化
  returndoSomethingElse();
}

在這個函數(shù)中缴挖,尾調(diào)用doSomethingElse()的結(jié)果立即返回,不調(diào)用任何局部作用域變量焚辅。如果做一個小改動映屋,不返回最終結(jié)果,那么引擎就無法優(yōu)化當前函數(shù)

"use strict";
function doSomething() {    
  // 未被優(yōu)化:缺少 
  return doSomethingElse();
}

同樣地同蜻,如果定義了一個函數(shù)秧荆,在尾調(diào)用返回后執(zhí)行其他操作,則函數(shù)也無法得到優(yōu)化

"use strict";
function doSomething() {    
  // 未被優(yōu)化:在返回之后還要執(zhí)行加法
  return1 +doSomethingElse();
}

如果把函數(shù)調(diào)用的結(jié)果存儲在一個變量里埃仪,最后再返回這個變量乙濒,則可能導(dǎo)致引擎無法優(yōu)化

"use strict";
function doSomething() {    
  // 未被優(yōu)化:調(diào)用并不在尾部
  var result = doSomethingElse();    
  return result;
}

可能最難避免的情況是閉包的使用,它可以訪問作用域中所有變量,因而導(dǎo)致尾調(diào)用優(yōu)化失效

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

在示例中颁股,閉包func()可以訪問局部變量num么库,即使調(diào)用func()后立即返回結(jié)果,也無法對代碼進行優(yōu)化

【應(yīng)用】

實際上甘有,尾調(diào)用的優(yōu)化發(fā)生在引擎背后诉儒,除非嘗試優(yōu)化一個函數(shù),否則無須思考此類問題亏掀。遞歸函數(shù)是其最主要的應(yīng)用場景忱反,此時尾調(diào)用優(yōu)化的效果最顯著

function factorial(n) {    
  if(n <= 1) {        
    return1;
  } else {        
    // 未被優(yōu)化:在返回之后還要執(zhí)行乘法``
    returnn * factorial(n - 1);
  }
}
  • 由于在遞歸調(diào)用前執(zhí)行了乘法操作,因而當前版本的階乘函數(shù)無法被引擎優(yōu)化滤愕。如果n是一個非常大的數(shù)温算,則調(diào)用棧的尺寸就會不斷增長并存在最終導(dǎo)致棧溢出的潛在風險
  • 優(yōu)化這個函數(shù),首先要確保乘法不會在函數(shù)調(diào)用后執(zhí)行间影,可以通過默認參數(shù)來將乘法操作移出return語句注竿,結(jié)果函數(shù)可以攜帶著臨時結(jié)果進入到下一個迭代中
function factorial(n, p = 1) {    
  if(n <= 1) {        
    return1 *p;
  } else{
    let result = n * p;        // 被優(yōu)化
    returnfactorial(n - 1, result);
  }
}
  • 在這個重寫后的factorial()函數(shù)中,第一個參數(shù)p的默認值為1魂贬,用它來保存乘法結(jié)果巩割,下一次迭代中可以取出它用于計算,不再需要額外的函數(shù)調(diào)用付燥。當n大于1時宣谈,先執(zhí)行一輪乘法計算,然后將結(jié)果傳給第二次factorial()調(diào)用的參數(shù)〖疲現(xiàn)在闻丑,ES6引擎就可以優(yōu)化遞歸調(diào)用了
  • 寫遞歸函數(shù)時,最好得用尾遞歸優(yōu)化的特性萝嘁,如果遞歸函數(shù)的計算量足夠大梆掸,則尾遞歸優(yōu)化可以大幅提升程序的性能

另一個常見的事例是Fibonacci數(shù)列

function Fibonacci (n) {  
  if ( n <= 1 ) {return 1};  
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10)// 89
Fibonacci(100) // 堆棧溢出
Fibonacci(500) // 堆棧溢出

尾遞歸優(yōu)化過的Fibonacci 數(shù)列實現(xiàn)如下

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

由此可見扬卷,“尾調(diào)用優(yōu)化”對遞歸操作意義重大牙言,所以一些函數(shù)式編程語言將其寫入了語言規(guī)格。ES6是如此怪得,第一次明確規(guī)定咱枉,所有ECMAScript 的實現(xiàn),都必須部署“尾調(diào)用優(yōu)化”徒恋。這就是說蚕断,ES6中只要使用尾遞歸,就不會發(fā)生棧溢出远豺,相對節(jié)省內(nèi)存霹购。

其他章節(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末泡徙,一起剝皮案震驚了整個濱河市该肴,隨后出現(xiàn)的幾起案子葛假,更是在濱河造成了極大的恐慌障陶,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件聊训,死亡現(xiàn)場離奇詭異抱究,居然都是意外死亡,警方通過查閱死者的電腦和手機带斑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門鼓寺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人勋磕,你說我怎么就攤上這事妈候。” “怎么了朋凉?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵州丹,是天一觀的道長。 經(jīng)常有香客問我杂彭,道長墓毒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任亲怠,我火速辦了婚禮所计,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘团秽。我一直安慰自己主胧,他們只是感情好,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布习勤。 她就那樣靜靜地躺著踪栋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪图毕。 梳的紋絲不亂的頭發(fā)上夷都,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機與錄音予颤,去河邊找鬼囤官。 笑死,一個胖子當著我的面吹牛蛤虐,可吹牛的內(nèi)容都是我干的党饮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼驳庭,長吁一口氣:“原來是場噩夢啊……” “哼刑顺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤蹲堂,失蹤者是張志新(化名)和其女友劉穎荞驴,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贯城,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡熊楼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了能犯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲫骗。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖踩晶,靈堂內(nèi)的尸體忽然破棺而出执泰,到底是詐尸還是另有隱情,我是刑警寧澤渡蜻,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布术吝,位于F島的核電站,受9級特大地震影響茸苇,放射性物質(zhì)發(fā)生泄漏排苍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一学密、第九天 我趴在偏房一處隱蔽的房頂上張望淘衙。 院中可真熱鬧,春花似錦腻暮、人聲如沸彤守。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽具垫。三九已至,卻和暖如春试幽,著一層夾襖步出監(jiān)牢的瞬間筝蚕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工抡草, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留饰及,地道東北人蔗坯。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓康震,卻偏偏與公主長得像,于是被迫代替她去往敵國和親宾濒。 傳聞我的和親對象是個殘疾皇子腿短,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 第2章 基本語法 2.1 概述 基本句法和變量 語句 JavaScript程序的執(zhí)行單位為行(line),也就是一...
    悟名先生閱讀 4,118評論 0 13
  • 1.函數(shù)參數(shù)的默認值 (1).基本用法 在ES6之前,不能直接為函數(shù)的參數(shù)指定默認值橘忱,只能采用變通的方法赴魁。
    趙然228閱讀 683評論 0 0
  • 1.函數(shù)參數(shù)的默認值 基本用法 ES6 之前,不能直接為函數(shù)的參數(shù)指定默認值钝诚,只能采用變通的方法颖御。 functio...
    Masami_9e88閱讀 490評論 0 0
  • 這個周末,小編看了《耐撕偵探》凝颇,想由此引申談?wù)勥@些年的菜鳥特工片潘拱。隨著特工電影的蓬勃發(fā)展,人們已經(jīng)不滿足于占士邦拧略、...
    朱誠逸閱讀 139評論 0 0
  • 米撲博客有一篇博客非常贊,分享出來《## 看山是山袱饭,看山不是山川无,看山還是山》 宋代禪宗大師青原行思提出參禪的三重境...
    米撲閱讀 1,608評論 1 2