前面的話
函數(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è)計初衷是代替
JS
的arguments
對象症革。起初,在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é)
- ES6-數(shù)字擴展
- ES6-字符串拓展
- ES6-模板字面量
- ES6-關(guān)于Unicode的相關(guān)擴展
- ES6-正則表達式擴展
- ES6-函數(shù)擴展
- ES6-對象擴展
- ES6-Symbol
- ES6-Set和Map集合
- ES6-數(shù)組擴展
- ES6-定型數(shù)組
- ES6-塊級作用域
- ES6-解構(gòu)賦值
- ES6-類
- ES6-代理(Proxy)和反射(Reflection)
- ES6-ES6中的模塊
- ES6-ES2017中的修飾器Decorator
- ES6-迭代器(Iterator)和生成器(Generator)
- ES6-Promise和異步編程
- ES6-ES2017中的async