本文繼續(xù)對JavaScript高級程序設(shè)計第四版 第十章 函數(shù) 進(jìn)行學(xué)習(xí)
一、創(chuàng)建函數(shù)
函數(shù)是ECMAScript中最有意思的部分之一规肴,這主要是因?yàn)楹瘮?shù)實(shí)際上是對象。每個函數(shù)都是Function類型的實(shí)例夜畴,而 Function 也有屬性和方法拖刃,跟其他引用類型一樣。因?yàn)楹瘮?shù)是對象贪绘,所以函數(shù)名就是指向函數(shù)對象的指針兑牡,而且不一定與函數(shù)本身緊密綁定。
1.以函數(shù)聲明的方式定義
function sum (num1, num2) {
return num1 + num2;
}
注意函數(shù)定義最后沒有加分號税灌。
function sum(num1, num2) {
return num1 + num2;
}
console.log(sum(10, 10)); // 20
let anotherSum = sum;
console.log(anotherSum(10, 10)); // 20
sum = null;
console.log(anotherSum(10, 10)); // 20
以上代碼定義了一個名為 sum()的函數(shù)均函,用于求兩個數(shù)之和亿虽。然后又聲明了一個變量 anotherSum,并將它的值設(shè)置為等于 sum边酒。注意经柴,使用不帶括號的函數(shù)名會訪問函數(shù)指針狸窘,而不會執(zhí)行函數(shù)墩朦。此時,anotherSum 和 sum 都指向同一個函數(shù)翻擒。調(diào)用 anotherSum()也可以返回結(jié)果氓涣。把 sum 設(shè)置為 null之后,就切斷了它與函數(shù)之間的關(guān)聯(lián)陋气。而 anotherSum()還是可以照常調(diào)用劳吠,沒有問題。
2.函數(shù)表達(dá)式
let sum = function(num1, num2) {
return num1 + num2;
};
這里巩趁,代碼定義了一個變量 sum 并將其初始化為一個函數(shù)痒玩。注意 function 關(guān)鍵字后面沒有名稱,因?yàn)椴恍枰槲俊_@個函數(shù)可以通過變量 sum 來引用蠢古。注意這里的函數(shù)末尾是有分號的,與任何變量初始化語句一樣别凹。
3.箭頭函數(shù)
let sum = (num1, num2) => {
return num1 + num2;
};
任何可以使用函數(shù)表達(dá)式的地方草讶,都可以使用箭頭函數(shù)。如果只有一個參數(shù)炉菲,那也可以不用括號堕战。只有沒有參數(shù),或者多個參數(shù)的情況下拍霜,才需要使用括號:
// 以下兩種寫法都有效
let double = (x) => { return 2 * x; };
let triple = x => { return 3 * x; };
// 沒有參數(shù)需要括號
let getRandom = () => { return Math.random(); };
// 多個參數(shù)需要括號
let sum = (a, b) => { return a + b; };
// 無效的寫法:
let multiply = a, b => { return a * b; };
箭頭函數(shù)雖然語法簡潔嘱丢,但也有很多場合不適用。箭頭函數(shù)不能使用 arguments祠饺、super 和new.target越驻,也不能用作構(gòu)造函數(shù)。此外吠裆,箭頭函數(shù)也沒有 prototype 屬性伐谈。
4.使用 Function 構(gòu)造函數(shù)
這個構(gòu)造函數(shù)接收任意多個字符串參數(shù),最后一個參數(shù)始終會被當(dāng)成函數(shù)體试疙,而之前的參數(shù)都是新函數(shù)的參數(shù)诵棵。來看下面的例子:
let sum = new Function("num1", "num2", "return num1 + num2"); // 不推薦
我們不推薦使用這種語法來定義函數(shù),因?yàn)檫@段代碼會被解釋兩次:第一次是將它當(dāng)作常規(guī)ECMAScript 代碼祝旷,第二次是解釋傳給構(gòu)造函數(shù)的字符串履澳。這顯然會影響性能嘶窄。不過,把函數(shù)想象為對象距贷,把函數(shù)名想象為指針是很重要的柄冲。而上面這種語法很好地詮釋了這些概念。
二忠蝗、理解參數(shù)
ECMAScript 函數(shù)的參數(shù)跟大多數(shù)其他語言不同现横。ECMAScript 函數(shù)既不關(guān)心傳入的參數(shù)個數(shù),也不關(guān)心這些參數(shù)的數(shù)據(jù)類型阁最。定義函數(shù)時要接收兩個參數(shù)戒祠,并不意味著調(diào)用時就傳兩個參數(shù)。你可以傳一個速种、三個姜盈,甚至一個也不傳,解釋器都不會報錯配阵。
之所以會這樣馏颂,主要是因?yàn)?ECMAScript 函數(shù)的參數(shù)在內(nèi)部表現(xiàn)為一個數(shù)組。函數(shù)被調(diào)用時總會接收一個數(shù)組棋傍,但函數(shù)并不關(guān)心這個數(shù)組中包含什么救拉。如果數(shù)組中什么也沒有,那沒問題舍沙;如果數(shù)組的元素超出了要求近上,那也沒問題。事實(shí)上拂铡,在使用 function 關(guān)鍵字定義(非箭頭)函數(shù)時壹无,可以在函數(shù)內(nèi)部訪問 arguments 對象,從中取得傳進(jìn)來的每個參數(shù)值感帅。
arguments 對象是一個類數(shù)組對象(但不是 Array 的實(shí)例)斗锭,因此可以使用中括號語法訪問其中的元素(第一個參數(shù)是 arguments[0],第二個參數(shù)是 arguments[1])失球。而要確定傳進(jìn)來多少個參數(shù)岖是,可以訪問 arguments.length 屬性。
function doAdd() {
if (arguments.length === 1) {
console.log(arguments[0] + 10);
} else if (arguments.length === 2) {
console.log(arguments[0] + arguments[1]);
}
}
doAdd(10); // 20
doAdd(30, 20); // 50
雖然不像真正的函數(shù)重載那么明確实苞,但這已經(jīng)足以彌補(bǔ) ECMAScript 在這方面的缺失了豺撑。
ECMAScript 函數(shù)不能像傳統(tǒng)編程那樣重載。在其他語言比如 Java 中黔牵,一個函數(shù)可以有兩個定義聪轿,只要簽名(接收參數(shù)的類型和數(shù)量)不同就行。如前所述猾浦,ECMAScript 函數(shù)沒有簽名陆错,因?yàn)閰?shù)是由包含零個或多個值的數(shù)組表示的灯抛。沒有函數(shù)簽名,自然也就沒有重載音瓷。如果在 ECMAScript 中定義了兩個同名函數(shù)对嚼,則后定義的會覆蓋先定義的。
還有一個必須理解的重要方面绳慎,那就是 arguments 對象可以跟命名參數(shù)一起使用纵竖,比如:
function doAdd(num1, num2) {
if (arguments.length === 1) {
console.log(num1 + 10);
} else if (arguments.length === 2) {
console.log(arguments[0] + num2);
}
}
在這個 doAdd()函數(shù)中,同時使用了兩個命名參數(shù)和 arguments 對象偷线。命名參數(shù) num1 保存著與arugments[0]一樣的值磨确,因此使用誰都無所謂沽甥。(同樣声邦,num2 也保存著跟 arguments[1]一樣的值。)
如果函數(shù)是使用箭頭語法定義的摆舟,那么傳給函數(shù)的參數(shù)將不能使用 arguments 關(guān)鍵字訪問亥曹,而只能通過定義的命名參數(shù)訪問。
function foo() {
console.log(arguments[0]);
}
foo(5); // 5
let bar = () => {
console.log(arguments[0]);
};
bar(5); // ReferenceError: arguments is not defined
雖然箭頭函數(shù)中沒有 arguments 對象恨诱,但可以在包裝函數(shù)中把它提供給箭頭函數(shù):
function foo() {
let bar = () => {
console.log(arguments[0]); // 5
};
bar();
}
foo(5);
注意 ECMAScript 中的所有參數(shù)都按值傳遞的媳瞪。不可能按引用傳遞參數(shù)。如果把對象作為參數(shù)傳遞照宝,那么傳遞的值就是這個對象的引用蛇受。
三、默認(rèn)參數(shù)
在 ECMAScript5.1 及以前厕鹃,實(shí)現(xiàn)默認(rèn)參數(shù)的一種常用方式就是檢測某個參數(shù)是否等于 undefined兢仰,如果是則意味著沒有傳這個參數(shù),那就給它賦一個值:
function makeKing(name) {
name = (typeof name !== 'undefined') ? name : 'Henry';
return `King ${name} VIII`;
}
console.log(makeKing()); // 'King Henry VIII'
console.log(makeKing('Louis')); // 'King Louis VIII'
ECMAScript 6 之后就不用這么麻煩了剂碴,因?yàn)樗С诛@式定義默認(rèn)參數(shù)了把将。下面就是與前面代碼等價的 ES6 寫法,只要在函數(shù)定義中的參數(shù)后面用=就可以為參數(shù)賦一個默認(rèn)值:
function makeKing(name = 'Henry') {
return `King ${name} VIII`;
}
console.log(makeKing('Louis')); // 'King Louis VIII'
console.log(makeKing()); // 'King Henry VIII'
給參數(shù)傳 undefined 相當(dāng)于沒有傳值忆矛,不過這樣可以利用多個獨(dú)立的默認(rèn)值:
function makeKing(name = 'Henry', numerals = 'VIII') {
return `King ${name} ${numerals}`;
}
console.log(makeKing()); // 'King Henry VIII'
console.log(makeKing('Louis')); // 'King Louis VIII'
console.log(makeKing(undefined, 'VI')); // 'King Henry VI'
在使用默認(rèn)參數(shù)時察蹲,arguments 對象的值不反映參數(shù)的默認(rèn)值,只反映傳給函數(shù)的參數(shù)催训。當(dāng)然洽议,跟 ES5 嚴(yán)格模式一樣,修改命名參數(shù)也不會影響 arguments 對象漫拭,它始終以調(diào)用函數(shù)時傳入的值為準(zhǔn):
function makeKing(name = 'Henry') {
name = 'Louis';
return `King ${arguments[0]}`;
}
console.log(makeKing()); // 'King undefined'
console.log(makeKing('Louis')); // 'King Louis'
默認(rèn)參數(shù)值并不限于原始值或?qū)ο箢愋脱切郑部梢允褂谜{(diào)用函數(shù)返回的值:
let romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI'];
let ordinality = 0;
function getNumerals() {
// 每次調(diào)用后遞增
return romanNumerals[ordinality++];
}
function makeKing(name = 'Henry', numerals = getNumerals()) {
return `King ${name} ${numerals}`;
}
console.log(makeKing()); // 'King Henry I'
console.log(makeKing('Louis', 'XVI')); // 'King Louis XVI'
console.log(makeKing()); // 'King Henry II'
console.log(makeKing()); // 'King Henry III'
函數(shù)的默認(rèn)參數(shù)只有在函數(shù)被調(diào)用時才會求值,不會在函數(shù)定義時求值嫂侍。而且儿捧,計算默認(rèn)值的函數(shù)只有在調(diào)用函數(shù)但未傳相應(yīng)參數(shù)時才會被調(diào)用荚坞。
箭頭函數(shù)同樣也可以這樣使用默認(rèn)參數(shù),只不過在只有一個參數(shù)時菲盾,就必須使用括號而不能省略了:
let makeKing = (name = 'Henry') => `King ${name}`;
console.log(makeKing()); // King Henry
四颓影、參數(shù)擴(kuò)展與收集
1.擴(kuò)展參數(shù)
在給函數(shù)傳參時,有時候可能不需要傳一個數(shù)組懒鉴,而是要分別傳入數(shù)組的元素诡挂。假設(shè)有如下函數(shù)定義,它會將所有傳入的參數(shù)累加起來:
let values = [1, 2, 3, 4];
function getSum() {
let sum = 0;
for (let i = 0; i < arguments.length; ++i) {
sum += arguments[i];
}
return sum;
}
這個函數(shù)希望將所有加數(shù)逐個傳進(jìn)來临谱,然后通過迭代 arguments 對象來實(shí)現(xiàn)累加璃俗。如果不使用擴(kuò)展操作符,想把定義在這個函數(shù)這面的數(shù)組拆分悉默,那么就得求助于 apply()方法:
console.log(getSum.apply(null, values)); // 10
但在 ECMAScript 6 中城豁,可以通過擴(kuò)展操作符極為簡潔地實(shí)現(xiàn)這種操作。對可迭代對象應(yīng)用擴(kuò)展操作符抄课,并將其作為一個參數(shù)傳入唱星,可以將可迭代對象拆分,并將迭代返回的每個值單獨(dú)傳入跟磨。比如间聊,使用擴(kuò)展操作符可以將前面例子中的數(shù)組像這樣直接傳給函數(shù):
console.log(getSum(...values)); // 10
因?yàn)閿?shù)組的長度已知,所以在使用擴(kuò)展操作符傳參的時候抵拘,并不妨礙在其前面或后面再傳其他的值哎榴,包括使用擴(kuò)展操作符傳其他參數(shù):
console.log(getSum(-1, ...values)); // 9
console.log(getSum(...values, 5)); // 15
console.log(getSum(-1, ...values, 5)); // 14
console.log(getSum(...values, ...[5,6,7])); // 28
2.收集參數(shù)
在構(gòu)思函數(shù)定義時,可以使用擴(kuò)展操作符把不同長度的獨(dú)立參數(shù)組合為一個數(shù)組僵蛛。這有點(diǎn)類似arguments 對象的構(gòu)造機(jī)制尚蝌,只不過收集參數(shù)的結(jié)果會得到一個 Array 實(shí)例。
function getSum(...values) {
// 順序累加 values 中的所有值
// 初始值的總和為 0
return values.reduce((x, y) => x + y, 0);
}
console.log(getSum(1,2,3)); // 6
收集參數(shù)的前面如果還有命名參數(shù)墩瞳,則只會收集其余的參數(shù)驼壶;如果沒有則會得到空數(shù)組。因?yàn)槭占瘏?shù)的結(jié)果可變喉酌,所以只能把它作為最后一個參數(shù):
// 不可以
function getProduct(...values, lastValue) {}
// 可以
function ignoreFirst(firstValue, ...values) {
console.log(values);
}
ignoreFirst(); // []
ignoreFirst(1); // []
ignoreFirst(1,2); // [2]
ignoreFirst(1,2,3); // [2, 3]
箭頭函數(shù)雖然不支持 arguments 對象热凹,但支持收集參數(shù)的定義方式,因此也可以實(shí)現(xiàn)與使用arguments 一樣的邏輯:
let getSum = (...values) => {
return values.reduce((x, y) => x + y, 0);
}
console.log(getSum(1,2,3)); // 6
另外泪电,使用收集參數(shù)并不影響 arguments 對象般妙,它仍然反映調(diào)用時傳給函數(shù)的參數(shù):
function getSum(...values) {
console.log(arguments.length); // 3
console.log(arguments); // [1, 2, 3]
console.log(values); // [1, 2, 3]
}
console.log(getSum(1,2,3));
五、函數(shù)聲明與函數(shù)表達(dá)式
本章到現(xiàn)在一直沒有把函數(shù)聲明和函數(shù)表達(dá)式區(qū)分得很清楚相速。事實(shí)上碟渺,JavaScript 引擎在加載數(shù)據(jù)時對它們是區(qū)別對待的。JavaScript 引擎在任何代碼執(zhí)行之前突诬,會先讀取函數(shù)聲明苫拍,并在執(zhí)行上下文中生成函數(shù)定義芜繁。而函數(shù)表達(dá)式必須等到代碼執(zhí)行到它那一行,才會在執(zhí)行上下文中生成函數(shù)定義绒极。來看下面的例子:
// 沒問題
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
以上代碼可以正常運(yùn)行骏令,因?yàn)楹瘮?shù)聲明會在任何代碼執(zhí)行之前先被讀取并添加到執(zhí)行上下文。這個過程叫作函數(shù)聲明提升(function declaration hoisting)垄提。在執(zhí)行代碼時榔袋,JavaScript 引擎會先執(zhí)行一遍掃描,把發(fā)現(xiàn)的函數(shù)聲明提升到源代碼樹的頂部铡俐。因此即使函數(shù)定義出現(xiàn)在調(diào)用它們的代碼之后凰兑,引擎也會把函數(shù)聲明提升到頂部。如果把前面代碼中的函數(shù)聲明改為等價的函數(shù)表達(dá)式审丘,那么執(zhí)行的時候就會出錯:
// 會出錯
console.log(sum(10, 10));
let sum = function(num1, num2) {
return num1 + num2;
};
除了函數(shù)什么時候真正有定義這個區(qū)別之外吏够,這兩種語法是等價的。
六备恤、函數(shù)作為值
因?yàn)楹瘮?shù)名在 ECMAScript 中就是變量稿饰,所以函數(shù)可以用在任何可以使用變量的地方。這意味著不僅可以把函數(shù)作為參數(shù)傳給另一個函數(shù)露泊,而且還可以在一個函數(shù)中返回另一個函數(shù)。來看下面的例子:
function callSomeFunction(someFunction, someArgument) {
return someFunction(someArgument);
}
這個函數(shù)接收兩個參數(shù)旅择。第一個參數(shù)應(yīng)該是一個函數(shù)惭笑,第二個參數(shù)應(yīng)該是要傳給這個函數(shù)的值。任何函數(shù)都可以像下面這樣作為參數(shù)傳遞:
function add10(num) {
return num + 10;
}
let result1 = callSomeFunction(add10, 10);
console.log(result1); // 20
function getGreeting(name) {
return "Hello, " + name;
}
let result2 = callSomeFunction(getGreeting, "Nicholas");
console.log(result2); // "Hello, Nicholas"
從一個函數(shù)中返回另一個函數(shù)也是可以的生真,而且非常有用沉噩。例如,假設(shè)有一個包含對象的數(shù)組弧圆,而我們想按照任意對象屬性對數(shù)組進(jìn)行排序茬缩。為此著角,可以定義一個 sort()方法需要的比較函數(shù),它接收兩個參數(shù)畜眨,即要比較的值。但這個比較函數(shù)還需要想辦法確定根據(jù)哪個屬性來排序术瓮。這個問題可以通過定義一個根據(jù)屬性名來創(chuàng)建比較函數(shù)的函數(shù)來解決康聂。比如:
function createComparisonFunction(propertyName) {
return function(object1, object2) {
let value1 = object1[propertyName];
let value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
這個函數(shù)的語法乍一看比較復(fù)雜,但實(shí)際上就是在一個函數(shù)中返回另一個函數(shù)胞四,注意那個 return操作符恬汁。內(nèi)部函數(shù)可以訪問 propertyName 參數(shù),并通過中括號語法取得要比較的對象的相應(yīng)屬性值辜伟。取得屬性值以后氓侧,再按照 sort()方法的需要返回比較值就行了脊另。這個函數(shù)可以像下面這樣使用:
let data = [
{name: "Zachary", age: 28},
{name: "Nicholas", age: 29}
];
data.sort(createComparisonFunction("name"));
console.log(data[0].name); // Nicholas
data.sort(createComparisonFunction("age"));
console.log(data[0].name); // Zachary
七、函數(shù)內(nèi)部
在 ECMAScript 5 中约巷,函數(shù)內(nèi)部存在兩個特殊的對象:arguments 和 this尝蠕。ECMAScript 6 又新增了 new.target 屬性。
1.arguments.callee
arguments 對象前面討論過多次了载庭,它是一個類數(shù)組對象看彼,包含調(diào)用函數(shù)時傳入的所有參數(shù)。這個對象只有以 function 關(guān)鍵字定義函數(shù)(相對于使用箭頭語法創(chuàng)建函數(shù))時才會有囚聚。雖然主要用于包含函數(shù)參數(shù)靖榕,但 arguments 對象其實(shí)還有一個 callee 屬性,是一個指向 arguments 對象所在函數(shù)的指針顽铸。來看下面這個經(jīng)典的階乘函數(shù):
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
階乘函數(shù)一般定義成遞歸調(diào)用的茁计,就像上面這個例子一樣。只要給函數(shù)一個名稱谓松,而且這個名稱不會變星压,這樣定義就沒有問題。但是鬼譬,這個函數(shù)要正確執(zhí)行就必須保證函數(shù)名是 factorial娜膘,從而導(dǎo)致了緊密耦合。使用 arguments.callee 就可以讓函數(shù)邏輯與函數(shù)名解耦:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
這個重寫之后的 factorial()函數(shù)已經(jīng)用 arguments.callee 代替了之前硬編碼的 factorial优质。這意味著無論函數(shù)叫什么名稱竣贪,都可以引用正確的函數(shù)。
2.this
另一個特殊的對象是 this巩螃,它在標(biāo)準(zhǔn)函數(shù)和箭頭函數(shù)中有不同的行為演怎。在標(biāo)準(zhǔn)函數(shù)中,this 引用的是把函數(shù)當(dāng)成方法調(diào)用的上下文對象避乏,這時候通常稱其為 this 值(在網(wǎng)頁的全局上下文中調(diào)用函數(shù)時爷耀,this 指向 windows)。來看下面的例子:
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'blue'
定義在全局上下文中的函數(shù) sayColor()引用了 this 對象拍皮。這個 this 到底引用哪個對象必須到函數(shù)被調(diào)用時才能確定歹叮。因此這個值在代碼執(zhí)行的過程中可能會變。如果在全局上下文中調(diào)用sayColor()春缕,這結(jié)果會輸出"red"盗胀,因?yàn)?this 指向 window,而 this.color 相當(dāng)于 window.color锄贼。而在把 sayColor()賦值給 o 之后再調(diào)用 o.sayColor()票灰,this 會指向 o,即 this.color 相當(dāng)于o.color,所以會顯示"blue"屑迂。
在箭頭函數(shù)中浸策,this引用的是定義箭頭函數(shù)的上下文。下面的例子演示了這一點(diǎn)惹盼。在對sayColor()的兩次調(diào)用中庸汗,this 引用的都是 window 對象,因?yàn)檫@個箭頭函數(shù)是在 window 上下文中定義的:
window.color = 'red';
let o = {
color: 'blue'
};
let sayColor = () => console.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red'
有讀者知道手报,在事件回調(diào)或定時回調(diào)中調(diào)用某個函數(shù)時蚯舱,this 值指向的并非想要的對象。此時將回調(diào)函數(shù)寫成箭頭函數(shù)就可以解決問題掩蛤。這是因?yàn)榧^函數(shù)中的 this 會保留定義該函數(shù)時的上下文枉昏。
更多參考js es6 => arrow function箭頭函數(shù)
3.caller
function outer() {
inner();
}
function inner() {
console.log(arguments.callee.caller);
}
outer();
以上代碼會顯示 outer()函數(shù)的源代碼
? outer() {
inner();
}
4.new.target
ECMAScript 中的函數(shù)始終可以作為構(gòu)造函數(shù)實(shí)例化一個新對象,也可以作為普通函數(shù)被調(diào)用揍鸟。ECMAScript 6 新增了檢測函數(shù)是否使用 new 關(guān)鍵字調(diào)用的 new.target 屬性兄裂。如果函數(shù)是正常調(diào)用的,則 new.target 的值是 undefined阳藻;如果是使用 new 關(guān)鍵字調(diào)用的晰奖,則 new.target 將引用被調(diào)用的構(gòu)造函數(shù)。
function King() {
if (!new.target) {
throw 'King must be instantiated using "new"'
}
console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new"
八腥泥、函數(shù)屬性與方法
1.length和 prototype
length 屬性保存函數(shù)定義的命名參數(shù)的個數(shù)匾南,如下例所示:
function sayName(name) {
console.log(name);
}
function sum(num1, num2) {
return num1 + num2;
}
function sayHi() {
console.log("hi");
}
console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0
以上代碼定義了 3 個函數(shù),每個函數(shù)的命名參數(shù)個數(shù)都不一樣道川。sayName()函數(shù)有 1 個命名參數(shù)午衰,所以其 length 屬性為 1。類似地冒萄,sum()函數(shù)有兩個命名參數(shù),所以其 length 屬性是 2橙数。而 sayHi()沒有命名參數(shù)尊流,其 length 屬性為 0。
prototype 屬性也許是 ECMAScript 核心中最有趣的部分灯帮。prototype 是保存引用類型所有實(shí)例方法的地方崖技,這意味著 toString()、valueOf()等方法實(shí)際上都保存在 prototype 上钟哥,進(jìn)而由所有實(shí)例共享迎献。這個屬性在自定義類型時特別重要。(相關(guān)內(nèi)容已經(jīng)在第 8 章詳細(xì)介紹過了腻贰。)在 ECMAScript 5中吁恍,prototype 屬性是不可枚舉的,因此使用 for-in 循環(huán)不會返回這個屬性。
2.apply()和 call()
這兩個方法都會以指定的 this 值來調(diào)用函數(shù)冀瓦,即會設(shè)置調(diào)用函數(shù)時函數(shù)體內(nèi) this 對象的值伴奥。apply()方法接收兩個參數(shù):函數(shù)內(nèi) this 的值和一個參數(shù)數(shù)組。第二個參數(shù)可以是 Array 的實(shí)例翼闽,但也可以是 arguments 對象拾徙。來看下面的例子:
function sum(num1, num2) {
return num1 + num2;
}
function callSum1(num1, num2) {
return sum.apply(this, arguments); // 傳入 arguments 對象
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]); // 傳入數(shù)組
}
console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20
call()方法與 apply()的作用一樣,只是傳參的形式不同感局。第一個參數(shù)跟 apply()一樣尼啡,也是 this值,而剩下的要傳給被調(diào)用函數(shù)的參數(shù)則是逐個傳遞的询微。換句話說崖瞭,通過 call()向函數(shù)傳參時,必須將參數(shù)一個一個地列出來拓提,比如:
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2);
}
console.log(callSum(10, 10)); // 20
這里的 callSum()函數(shù)必須逐個地把參數(shù)傳給 call()方法读恃。結(jié)果跟 apply()的例子一樣。到底是使用 apply()還是 call()代态,完全取決于怎么給要調(diào)用的函數(shù)傳參更方便寺惫。如果想直接傳 arguments對象或者一個數(shù)組,那就用 apply()蹦疑;否則西雀,就用 call()。當(dāng)然歉摧,如果不用給被調(diào)用的函數(shù)傳參艇肴,則使用哪個方法都一樣。
apply()和 call()真正強(qiáng)大的地方并不是給函數(shù)傳參叁温,而是控制函數(shù)調(diào)用上下文即函數(shù)體內(nèi) this值的能力再悼。考慮下面的例子:
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
這個例子是在之前那個關(guān)于 this 對象的例子基礎(chǔ)上修改而成的膝但。同樣冲九,sayColor()是一個全局函數(shù),如果在全局作用域中調(diào)用它跟束,那么會顯示"red"莺奸。這是因?yàn)?this.color 會求值為 window.color。如果在全局作用域中顯式調(diào)用 sayColor.call(this)或者 sayColor.call(window)冀宴,則同樣都會顯示"red"灭贷。而在使用 sayColor.call(o)把函數(shù)的執(zhí)行上下文即 this 切換為對象 o 之后,結(jié)果就變成了顯示"blue"了略贮。
使用 call()或 apply()的好處是可以將任意對象設(shè)置為任意函數(shù)的作用域甚疟,這樣對象可以不用關(guān)心方法仗岖。在前面例子最初的版本中,為切換上下文需要先把 sayColor()直接賦值為 o 的屬性古拴,然后再調(diào)用箩帚。而在這個修改后的版本中,就不需要這一步操作了黄痪。
ECMAScript 5 出于同樣的目的定義了一個新方法:bind()紧帕。bind()方法會創(chuàng)建一個新的函數(shù)實(shí)例,其 this 值會被綁定到傳給 bind()的對象桅打。比如:
window.color = 'red';
var o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
let objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
這里是嗜,在 sayColor()上調(diào)用 bind()并傳入對象 o 創(chuàng)建了一個新函數(shù) objectSayColor()。objectSayColor()中的 this 值被設(shè)置為 o挺尾,因此直接調(diào)用這個函數(shù)鹅搪,即使是在全局作用域中調(diào)用,也會返回字符串"blue"遭铺。
九丽柿、閉包
匿名函數(shù)經(jīng)常被人誤認(rèn)為是閉包(closure)。閉包指的是那些引用了另一個函數(shù)作用域中變量的函數(shù)魂挂,通常是在嵌套函數(shù)中實(shí)現(xiàn)的甫题。比如,下面是之前展示的 createComparisonFunction()函數(shù)涂召,注意其中加粗的代碼:
function createComparisonFunction(propertyName) {
return function(object1, object2) {
let value1 = object1[propertyName];
let value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
這里加粗的代碼位于內(nèi)部函數(shù)(匿名函數(shù))中坠非,其中引用了外部函數(shù)的變量 propertyName。在這個內(nèi)部函數(shù)被返回并在其他地方被使用后果正,它仍然引用著那個變量炎码。這是因?yàn)閮?nèi)部函數(shù)的作用域鏈包含createComparisonFunction()函數(shù)的作用域。要理解為什么會這樣秋泳,可以想想第一次調(diào)用這個函數(shù)時會發(fā)生什么潦闲。
本書在第 4 章曾介紹過作用域鏈的概念。理解作用域鏈創(chuàng)建和使用的細(xì)節(jié)對理解閉包非常重要迫皱。在調(diào)用一個函數(shù)時矫钓,會為這個函數(shù)調(diào)用創(chuàng)建一個執(zhí)行上下文,并創(chuàng)建一個作用域鏈舍杜。然后用 arguments和其他命名參數(shù)來初始化這個函數(shù)的活動對象。外部函數(shù)的活動對象是內(nèi)部函數(shù)作用域鏈上的第二個對象赵辕。這個作用域鏈一直向外串起了所有包含函數(shù)的活動對象既绩,直到全局執(zhí)行上下文才終止。在函數(shù)執(zhí)行時还惠,要從作用域鏈中查找變量饲握,以便讀、寫值。來看下面的代碼:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let result = compare(5, 10);
這里定義的 compare()函數(shù)是在全局上下文中調(diào)用的救欧。第一次調(diào)用 compare()時衰粹,會為它創(chuàng)建一個包含 arguments、value1 和 value2 的活動對象笆怠,這個對象是其作用域鏈上的第一個對象铝耻。而全局上下文的變量對象則是 compare()作用域鏈上的第二個對象,其中包含 this蹬刷、result 和 compare瓢捉。 圖 10-1 展示了以上關(guān)系。
函數(shù)執(zhí)行時办成,每個執(zhí)行上下文中都會有一個包含其中變量的對象泡态。全局上下文中的叫變量對象,它會在代碼執(zhí)行期間始終存在迂卢。而函數(shù)局部上下文中的叫活動對象某弦,只在函數(shù)執(zhí)行期間存在。在定義compare()函數(shù)時而克,就會為它創(chuàng)建作用域鏈靶壮,預(yù)裝載全局變量對象,并保存在內(nèi)部的[[Scope]]中拍摇。在調(diào)用這個函數(shù)時亮钦,會創(chuàng)建相應(yīng)的執(zhí)行上下文,然后通過復(fù)制函數(shù)的[[Scope]]來創(chuàng)建其作用域鏈充活。接著會創(chuàng)建函數(shù)的活動對象(用作變量對象)并將其推入作用域鏈的前端蜂莉。在這個例子中,這意味著 compare()函數(shù)執(zhí)行上下文的作用域鏈中有兩個變量對象:局部變量對象和全局變量對象混卵。作用域鏈其實(shí)是一個包含指針的列表映穗,每個指針分別指向一個變量對象,但物理上并不會包含相應(yīng)的對象幕随。
函數(shù)內(nèi)部的代碼在訪問變量時蚁滋,就會使用給定的名稱從作用域鏈中查找變量。函數(shù)執(zhí)行完畢后赘淮,局部活動對象會被銷毀辕录,內(nèi)存中就只剩下全局作用域。不過梢卸,閉包就不一樣了走诞。
在一個函數(shù)內(nèi)部定義的函數(shù)會把其包含函數(shù)的活動對象添加到自己的作用域鏈中。因此蛤高,在createComparisonFunction()函數(shù)中蚣旱,匿名函數(shù)的作用域鏈中實(shí)際上包含 createComparisonFunction()的活動對象碑幅。圖 10-2 展示了以下代碼執(zhí)行后的結(jié)果。
let compare = createComparisonFunction('name');
let result = compare({ name: 'Nicholas' }, { name: 'Matt' });
在 createComparisonFunction()返回匿名函數(shù)后塞绿,它的作用域鏈被初始化為包含 createComparisonFunction()的活動對象和全局變量對象沟涨。這樣,匿名函數(shù)就可以訪問到 createComparisonFunction()可以訪問的所有變量异吻。
另一個有意思的副作用就是裹赴,createComparisonFunction()的活動對象并不能在它執(zhí)行完畢后銷毀,因?yàn)槟涿瘮?shù)的作用域鏈中仍然有對它的引用涧黄。在 createComparisonFunction()執(zhí)行完畢后篮昧,其執(zhí)行上下文的作用域鏈會銷毀,但它的活動對象仍然會保留在內(nèi)存中笋妥,直到匿名函數(shù)被銷毀后才會被銷毀:
// 創(chuàng)建比較函數(shù)
let compareNames = createComparisonFunction('name');
// 調(diào)用函數(shù)
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
// 解除對函數(shù)的引用懊昨,這樣就可以釋放內(nèi)存了
compareNames = null;
這里,創(chuàng)建的比較函數(shù)被保存在變量 compareNames 中春宣。把 compareNames 設(shè)置為等于 null 會解除對函數(shù)的引用酵颁,從而讓垃圾回收程序可以將內(nèi)存釋放掉。作用域鏈也會被銷毀月帝,其他作用域(除全局作用域之外)也可以銷毀躏惋。圖 10-2 展示了調(diào)用 compareNames()之后作用域鏈之間的關(guān)系。
注意 因?yàn)殚]包會保留它們包含函數(shù)的作用域嚷辅,所以比其他函數(shù)更占用內(nèi)存簿姨。過度使用閉包可能導(dǎo)致內(nèi)存過度占用,因此建議僅在十分必要時使用簸搞。V8 等優(yōu)化的 JavaScript 引擎會努力回收被閉包困住的內(nèi)存扁位,不過我們還是建議在使用閉包時要謹(jǐn)慎。
1.this 對象
在閉包中使用 this 會讓代碼變復(fù)雜趁俊。如果內(nèi)部函數(shù)沒有使用箭頭函數(shù)定義域仇,則 this 對象會在運(yùn)行時綁定到執(zhí)行函數(shù)的上下文。如果在全局函數(shù)中調(diào)用寺擂,則 this 在非嚴(yán)格模式下等于 window暇务,在嚴(yán)格模式下等于 undefined。如果作為某個對象的方法調(diào)用怔软,則 this 等于這個對象垦细。匿名函數(shù)在這種情況下不會綁定到某個對象,這就意味著 this 會指向 window挡逼,除非在嚴(yán)格模式下 this 是 undefined蝠检。
不過,由于閉包的寫法所致挚瘟,這個事實(shí)有時候沒有那么容易看出來叹谁。來看下面的例子:
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentityFunc() {
return function() {
return this.identity;
};
}
};
console.log(object.getIdentityFunc()()); // 'The Window'
這里先創(chuàng)建了一個全局變量 identity,之后又創(chuàng)建一個包含 identity 屬性的對象乘盖。這個對象還包含一個 getIdentityFunc()方法焰檩,返回一個匿名函數(shù)。這個匿名函數(shù)返回 this.identity订框。因?yàn)間etIdentityFunc()返回函數(shù)析苫,所以 object.getIdentityFunc()()會立即調(diào)用這個返回的函數(shù),從而得到一個字符串穿扳●媒模可是,此時返回的字符串是"The Winodw"矛物,即全局變量 identity 的值茫死。為什么匿名函數(shù)沒有使用其包含作用域(getIdentityFunc())的 this 對象呢?
前面介紹過履羞,每個函數(shù)在被調(diào)用時都會自動創(chuàng)建兩個特殊變量:this 和 arguments峦萎。內(nèi)部函數(shù)永遠(yuǎn)不可能直接訪問外部函數(shù)的這兩個變量。但是忆首,如果把 this 保存到閉包可以訪問的另一個變量中爱榔,則是行得通的。比如:
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentityFunc() {
let that = this;
return function() {
return that.identity;
};
}
};
console.log(object.getIdentityFunc()()); // 'My Object'
這里加粗的代碼展示了與前面那個例子的區(qū)別糙及。在定義匿名函數(shù)之前详幽,先把外部函數(shù)的 this 保存到變量 that 中。然后在定義閉包時浸锨,就可以讓它訪問 that唇聘,因?yàn)檫@是包含函數(shù)中名稱沒有任何沖突的一個變量。即使在外部函數(shù)返回之后揣钦,that 仍然指向 object雳灾,所以調(diào)用 object.getIdentityFunc()()就會返回"My Object"。
注意 this 和 arguments 都是不能直接在內(nèi)部函數(shù)中訪問的冯凹。如果想訪問包含作用域中的 arguments 對象谎亩,則同樣需要將其引用先保存到閉包能訪問的另一個變量中。在一些特殊情況下宇姚,this 值可能并不是我們所期待的值匈庭。比如下面這個修改后的例子:
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentity () {
return this.identity;
}
};
getIdentity()方法就是返回 this.identity 的值。以下是幾種調(diào)用 object.getIdentity()的方式及返回值:
object.getIdentity(); // 'My Object'
(object.getIdentity)(); // 'My Object'
(object.getIdentity = object.getIdentity)(); // 'The Window'
第一行調(diào)用 object.getIdentity()是正常調(diào)用浑劳,會返回"My Object",因?yàn)?this.identity就是 object.identity魔熏。第二行在調(diào)用時把 object.getIdentity 放在了括號里衷咽。雖然加了括號之后看起來是對一個函數(shù)的引用,但 this 值并沒有變镶骗。這是因?yàn)榘凑找?guī)范,object.getIdentity 和(object.getIdentity)是相等的。第三行執(zhí)行了一次賦值慰于,然后再調(diào)用賦值后的結(jié)果婆赠。因?yàn)橘x值表達(dá)式的值是函數(shù)本身,this 值不再與任何對象綁定战授,所以返回的是"The Window"植兰。
一般情況下份帐,不大可能像第二行和第三行這樣調(diào)用對象上的方法。但通過這個例子楣导,我們可以知道废境,即使語法稍有不同,也可能影響 this 的值筒繁。
2.內(nèi)存泄漏
由于 IE 在 IE9 之前對 JScript 對象和 COM 對象使用了不同的垃圾回收機(jī)制(第 4 章討論過)噩凹,所以閉包在這些舊版本 IE 中可能會導(dǎo)致問題。在這些版本的 IE 中毡咏,把 HTML 元素保存在某個閉包的作用域中驮宴,就相當(dāng)于宣布該元素不能被銷毀。來看下面的例子:
function assignHandler() {
let element = document.getElementById('someElement');
element.onclick = () => console.log(element.id);
}
以上代碼創(chuàng)建了一個閉包呕缭,即 element 元素的事件處理程序(事件處理程序?qū)⒃诘?13 章討論)堵泽。而這個處理程序又創(chuàng)建了一個循環(huán)引用。匿名函數(shù)引用著 assignHandler()的活動對象恢总,阻止了對element 的引用計數(shù)歸零迎罗。只要這個匿名函數(shù)存在,element 的引用計數(shù)就至少等于 1片仿。也就是說纹安,內(nèi)存不會被回收。其實(shí)只要這個例子稍加修改,就可以避免這種情況厢岂,比如:
function assignHandler() {
let element = document.getElementById('someElement');
let id = element.id;
element.onclick = () => console.log(id);
element = null;
}
在這個修改后的版本中光督,閉包改為引用一個保存著 element.id 的變量 id,從而消除了循環(huán)引用咪笑。不過可帽,光有這一步還不足以解決內(nèi)存問題。因?yàn)殚]包還是會引用包含函數(shù)的活動對象窗怒,而其中包含element。即使閉包沒有直接引用 element蓄拣,包含函數(shù)的活動對象上還是保存著對它的引用扬虚。因此,必須再把 element 設(shè)置為 null球恤。這樣就解除了對這個 COM 對象的引用辜昵,其引用計數(shù)也會減少,從而確保其內(nèi)存可以在適當(dāng)?shù)臅r候被回收咽斧。
十堪置、立即調(diào)用的函數(shù)表達(dá)式(IIFE,Immediately Invoked Function Expression)
立即調(diào)用的匿名函數(shù)又被稱作立即調(diào)用的函數(shù)表達(dá)式(IIFE张惹,Immediately Invoked Function Expression)舀锨。它類似于函數(shù)聲明,但由于被包含在括號中宛逗,所以會被解釋為函數(shù)表達(dá)式坎匿。緊跟在第一組括號后面的第二組括號會立即調(diào)用前面的函數(shù)表達(dá)式。下面是一個簡單的例子:
(function() {
// 塊級作用域
})();
使用 IIFE 可以模擬塊級作用域雷激,即在一個函數(shù)表達(dá)式內(nèi)部聲明變量替蔬,然后立即調(diào)用這個函數(shù)。這樣位于函數(shù)體作用域的變量就像是在塊級作用域中一樣屎暇。ECMAScript 5 尚未支持塊級作用域承桥,使用 IIFE模擬塊級作用域是相當(dāng)普遍的。比如下面的例子:
// IIFE
(function () {
for (var i = 0; i < count; i++) {
console.log(i);
}
})();
console.log(i); // 拋出錯誤
前面的代碼在執(zhí)行到 IIFE 外部的 console.log()時會出錯根悼,因?yàn)樗L問的變量是在 IIFE 內(nèi)部定義的凶异,在外部訪問不到。在 ECMAScript 5.1 及以前番挺,為了防止變量定義外泄唠帝,IIFE 是個非常有效的方式。這樣也不會導(dǎo)致閉包相關(guān)的內(nèi)存問題玄柏,因?yàn)椴淮嬖趯@個匿名函數(shù)的引用襟衰。為此,只要函數(shù)執(zhí)行完畢粪摘,其作用域鏈就可以被銷毀瀑晒。
在 ECMAScript 6 以后绍坝,IIFE 就沒有那么必要了,因?yàn)閴K級作用域中的變量無須 IIFE 就可以實(shí)現(xiàn)同樣的隔離苔悦。下面展示了兩種不同的塊級作用域形式:
// 內(nèi)嵌塊級作用域
{
let i;
for (i = 0; i < count; i++) {
console.log(i);
}
}
console.log(i); // 拋出錯誤
// 循環(huán)的塊級作用域
for (let i = 0; i < count; i++) {
console.log(i);
}
console.log(i); // 拋出錯誤