深入理解javascript函數(shù)參數(shù)與閉包
最近在學(xué)習(xí)javascript的函數(shù),函數(shù)是javascript的一等對象,想要學(xué)好javascript,就必須深刻理解函數(shù)。本人把學(xué)習(xí)的過程整理成文章,一是為了加深自己函數(shù)的理解,二是給讀者提供學(xué)習(xí)的途徑,避免走彎路。內(nèi)容有些多,但都是筆者對于函數(shù)的總結(jié)。
1.函數(shù)參數(shù)
1.1:參數(shù)是什么
1.2:參數(shù)的省略
1.3:參數(shù)默認值
1.4:參數(shù)傳遞方式
1.5:同名參數(shù)
1.6:arguments對象
2.閉包
2.1:閉包定義
2.2:立即調(diào)用的函數(shù)表達式(IIFE, Immediately invoked function expression)
1.函數(shù)參數(shù)
1.1:參數(shù)是什么
在定義一個函數(shù)時,有時候需要為函數(shù)傳遞額外的數(shù)據(jù),不同的外部數(shù)據(jù)會得到不同的結(jié)果,這種外部數(shù)據(jù)就叫做參數(shù)。
function keith(a){ return a+a; } console.log(keith(3)); //6
上面代碼中,給keith函數(shù)傳遞了參數(shù)a,并且返回了a+a表達式。
1.2:參數(shù)的省略
函數(shù)參數(shù)不是必須的,javascript規(guī)范允許省略調(diào)用時傳遞的實際參數(shù)。
function keith(a, b, c) { return a; } console.log(keith(1, 2, 3)); //1 console.log(keith(1)); //1 console.log(keith()); // 'undefined'
上面代碼中,keith函數(shù)定義了三個參數(shù),但是在調(diào)用時無論傳遞了多少個參數(shù),javascript都不會報錯。被省略的參數(shù)的默認值就變?yōu)閡ndefined。了解函數(shù)定義與函數(shù)作用域 的都知道,函數(shù)的length屬性會返回參數(shù)個數(shù)。需要注意的是,length屬性與實際參數(shù)的個數(shù)無關(guān),只是返回形式參數(shù)的個數(shù)。
(實際參數(shù):調(diào)用時傳遞的參數(shù)。 形式參數(shù):定義時傳遞的參數(shù)。)
但是沒有辦法省略只靠前的元素,而保留靠后的元素。如果一定要省略靠前的元素,只有顯示傳入undefined。
function keith(a, b) { return a; } console.log(keith(, 1)); //SyntaxError: expected expression, got ',' console.log(keith(undefined, 2)); //'undefined'
上面代碼中,如果省略了第一個參數(shù),瀏覽器就會報錯。如果給第一個參數(shù)傳遞undefined,則不會報錯。
1.3:默認值
在JavaScript中,函數(shù)參數(shù)的默認值是undefined。然而,在某些情況下設(shè)置不同的默認值是有用的。一般策略是在函數(shù)的主體測試參數(shù)值是否為undefined,如果是則賦予一個值,如果不是,則返回實際參數(shù)傳遞的值。
function keith(a, b) { (typeof b !== 'undefined') ? b = b: b = 1; return a * b; } console.log(keith(15)); //15 console.log(keith(15, 2)) //30
上面代碼中,做了個判斷。當在調(diào)用時沒有傳入b參數(shù),則默認為1。
從ECMAScript 6開始,定義了默認參數(shù)(default parameters)。使用默認參數(shù),在函數(shù)體的檢查就不再需要了。
function keith(a, b = 1) { return a * b; } console.log(keith(15)); //15 console.log(keith(15, 2)) //30
1.4:參數(shù)傳遞方式
函數(shù)參數(shù)的傳遞方式有兩種,一個是傳值傳遞,一個是傳址傳遞。
當函數(shù)參數(shù)是原始數(shù)據(jù)類型時(字符串,數(shù)值,布爾值),參數(shù)的傳遞方式為傳值傳遞。也就是說,在函數(shù)體內(nèi)修改參數(shù)值,不會影響到函數(shù)外部。
var a = 1; function keith(num) { num = 5; } keith(a); console.log(a); //1
上面代碼中,全局變量a是一個原始類型的值,傳入函數(shù)keith的方式是傳值傳遞。因此,在函數(shù)內(nèi)部,a的值是原始值的拷貝,無論怎么修改,都不會影響到原始值。
但是,如果函數(shù)參數(shù)是復(fù)合類型的值(數(shù)組、對象、其他函數(shù)),傳遞方式是傳址傳遞(pass by reference)。也就是說,傳入函數(shù)的是原始值的地址,因此在函數(shù)內(nèi)部修改參數(shù),將會影響到原始值。
var arr = [2, 5]; function keith(Arr) { Arr[0] = 3; } keith(arr); console.log(arr[0]); //3
上面代碼中,傳入函數(shù)keith的是參數(shù)對象arr的地址。因此,在函數(shù)內(nèi)部修改arr第一個值,會影響到原始值。
注意,如果函數(shù)內(nèi)部修改的,不是參數(shù)對象的某個屬性,而是替換掉整個參數(shù),這時不會影響到原始值。
var arr = [2, 3, 5]; function keith(Arr) { Arr = [1, 2, 3]; } keith(arr); console.log(arr); // [2,3,5]
上面代碼中,在函數(shù)keith內(nèi)部,參數(shù)對象arr被整個替換成另一個值。這時不會影響到原始值。這是因為,形式參數(shù)(Arr)與實際參數(shù)arr存在一個賦值關(guān)系。
1.5:同名參數(shù)
如果有同名參數(shù),則取最后面出現(xiàn)的那個值,如果未提供最后一個參數(shù)的值,則取值變成undefined。
function keith(a, a) { return a; } console.log(keith(1, 3)); //3 console.log(keith(1)); //undefined
如果想訪問同名參數(shù)中的第一個參數(shù),則使用arguments對象。
function keith(a, a) { return arguments[0]; } console.log(keith(2)); //2
1.6 arguments對象
JavaScript 中每個函數(shù)內(nèi)都能訪問一個特別變量 arguments。這個變量維護著所有傳遞到這個函數(shù)中的參數(shù)列表。
arguments 對象包含了函數(shù)運行時的所有參數(shù),arguments[0]就是第一個參數(shù),arguments[1]就是第二個參數(shù),以此類推。這個對象只有在函數(shù)體內(nèi)部,才可以使用。
可以訪問arguments對象的length屬性,判斷函數(shù)調(diào)用時到底帶幾個參數(shù)。
function keith(a, b, c) { console.log(arguments[0]); //1 console.log(arguments[2]); //3 console.log(arguments.length); //4 } keith(1, 2, 3, 4);
arguments對象與數(shù)組的關(guān)系
arguments 對象不是一個數(shù)組(Array)。 盡管在語法上它有數(shù)組相關(guān)的屬性 length,但它不從 Array.prototype 繼承,實際上它是一個類數(shù)組對象。因此,無法對 arguments 變量使用標準的數(shù)組方法,比如 push, pop 或者 slice。但是可以使用數(shù)組中的length屬性。
通常使用如下方法把arguments對象轉(zhuǎn)換為數(shù)組。
var arr = Array.prototype.slice.call(arguments);
2.閉包
2.1:閉包定義
要理解閉包,需要先理解全局作用域和局部作用域的區(qū)別。函數(shù)內(nèi)部可以訪問全局作用域下定義的全局變量,而函數(shù)外部卻無法訪問到函數(shù)內(nèi)部定義(局部作用域)的局部變量。
var a = 1; function keith() { return a; var b = 2; } console.log(keith()); //1 console.log(b); //ReferenceError: b is not defined
上面代碼中,全局變量a可以在函數(shù)keith內(nèi)部訪問??墒蔷植孔兞縝卻無法在函數(shù)外部訪問。
如果需要得到函數(shù)內(nèi)部的局部變量,只有通過在函數(shù)的內(nèi)部,再定義一個函數(shù)。
function keith(){ var a=1; function rascal(){ return a; } return rascal; } var result=keith(); console.log(result()); //1 function keith(){ var a=1; return function(){ return a; }; } var result=keith(); console.log(result()) //1
上面代碼中,兩種寫法相同,唯一的區(qū)別是內(nèi)部函數(shù)是否是匿名函數(shù)。函數(shù)rascal就在函數(shù)keith內(nèi)部,這時keith內(nèi)部的所有局部變量,對rascal都是可見的。但是反過來就不行,rascal內(nèi)部的局部變量,對keith就是不可見的。這就是JavaScript語言特有的”鏈式作用域”結(jié)構(gòu)(chain scope),子對象會一級一級地向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。函數(shù)keith的返回值就是函數(shù)rascal,由于rascal可以讀取keith的內(nèi)部變量,所以就可以在外部獲得keith的內(nèi)部變量了。
閉包就是函數(shù)rascal,即能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。由于在JavaScript語言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取內(nèi)部變量,因此可以把閉包簡單理解成“定義在一個函數(shù)內(nèi)部的函數(shù)”。閉包最大的特點,就是它可以“記住”誕生的環(huán)境,比如rascal記住了它誕生的環(huán)境keith,所以從rascal可以得到keith的內(nèi)部變量。
閉包可以使得它誕生環(huán)境一直存在??聪旅嬉粋€例子,閉包使得內(nèi)部變量記住上一次調(diào)用時的運算結(jié)果。
function keith(num) { return function() { return num++; }; } var result = keith(2); console.log(result()) //2 console.log(result()) //3 console.log(result()) //4
上面代碼中,參數(shù)num其實就相當于函數(shù)keith內(nèi)部定義的局部變量。通過閉包,num的狀態(tài)被保留了,每一次調(diào)用都是在上一次調(diào)用的基礎(chǔ)上進行計算。從中可以看到,閉包result使得函數(shù)keith的內(nèi)部環(huán)境,一直存在。
通過以上的例子,總結(jié)一下閉包的特點:
1:在一個函數(shù)內(nèi)部定義另外一個函數(shù),并且返回內(nèi)部函數(shù)或者立即執(zhí)行內(nèi)部函數(shù)。
2:內(nèi)部函數(shù)可以讀取外部函數(shù)定義的局部變量
3:讓局部變量始終保存在內(nèi)存中。也就是說,閉包可以使得它誕生環(huán)境一直存在。
閉包的另一個用處,是封裝對象的私有屬性和私有方法。
function Keith(name) { var age; function setAge(n) { age = n; } function getAge() { return age; } return { name: name, setAge: setAge, getAge: getAge }; } var person = Keith('keith'); person.setAge(21); console.log(person.name); // 'keith' console.log(person.getAge()); //21
2.2:立即調(diào)用的函數(shù)表達式(IIFE)
通常情況下,只對匿名函數(shù)使用這種“立即執(zhí)行的函數(shù)表達式”。它的目的有兩個:一是不必為函數(shù)命名,避免了污染全局變量;二是IIFE內(nèi)部形成了一個單獨的作用域,可以封裝一些外部無法讀取的私有變量。
循環(huán)中的閉包
一個常見的錯誤出現(xiàn)在循環(huán)中使用閉包,假設(shè)我們需要在每次循環(huán)中調(diào)用循環(huán)序號
for(var i=0;i<10;i++){ setTimeout(function(){ console.log(i); //10 }, 1000) }
上面代碼中,不會符合我們的預(yù)期,輸出數(shù)字0-9。而是會輸出數(shù)字10十次。
當匿名函數(shù)被調(diào)用的時候,匿名函數(shù)保持著對全局變量 i 的引用,也就是說會記住i循環(huán)時執(zhí)行的結(jié)果。此時for循環(huán)結(jié)束,i 的值被修改成了10。
為了得到想要的效果,避免引用錯誤,我們應(yīng)該使用IIFE來在每次循環(huán)中創(chuàng)建全局變量 i 的拷貝。
for(var i = 0; i < 10; i++) { (function(e) { setTimeout(function() { console.log(e); //1,2,3,....,10 }, 1000); })(i); }
外部的匿名函數(shù)會立即執(zhí)行,并把 i 作為它的參數(shù),此時函數(shù)內(nèi) e 變量就擁有了 i 的一個拷貝。當傳遞給 setTimeout 的匿名函數(shù)執(zhí)行時,它就擁有了對 e 的引用,而這個值是不會被循環(huán)改變的。
以上就是本文的全部內(nèi)容,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,同時也希望多多支持腳本之家!
相關(guān)文章
D3.js中data(), enter() 和 exit()的問題詳解
相信大多數(shù)人對D3.js并不陌生。這是一個由紐約時報可視化編輯 Mike Bostock與他斯坦福的教授和同學(xué)合作開發(fā)的數(shù)據(jù)文件處理的JavaScript Library,全稱叫做Data-Driven Documents,在d3.js中data(), enter() 和 exit()比較常見,下面給大家就這方面的知識給大家詳解2015-08-08