JavaScript筆記(三)
函數(shù)
理解函數(shù)
Javascript函數(shù)的參數(shù)與大多數(shù)其他語言中的函數(shù)的參數(shù)不同。Javascript函數(shù)不介意傳遞進來多少個參數(shù)艺玲,也不介意傳遞進來的參數(shù)是什么數(shù)據(jù)類型括蝠。
之所以會這樣,原因是Javascript中的參數(shù)在內(nèi)部是用一個數(shù)組來表示的饭聚。函數(shù)接收到的始終是這個數(shù)組忌警,而不關(guān)心這個數(shù)組包含多少參數(shù)。
實際上秒梳,在函數(shù)體內(nèi)可以通過arguments對象來訪問這個參數(shù)數(shù)組法绵,從而獲取傳遞給函數(shù)的每一個參數(shù)。
其實arguments對象只是與數(shù)組類似(它并不是Array的實例)酪碘,它可以使用方括號來訪問它的每一個元素(第一個元素是arguments[0],第二個元素是arguments[1],以此類推)朋譬,使用length屬性來確定傳進來多少個參數(shù)。
可以利用這一點讓函數(shù)能夠接受任意個參數(shù)并分別實現(xiàn)適當?shù)墓δ堋?/p>
所以兴垦,可以得到另一個結(jié)論:js的函數(shù)沒有重載徙赢。
函數(shù)聲明與函數(shù)表達式
有兩種寫法:
函數(shù)聲明:
function add(a, b) {
return a + b;
}
函數(shù)表達式:
var add = function(a, b) {
return a + b;
}
實際上,解析器在向執(zhí)行環(huán)境中加載數(shù)據(jù)時探越,對函數(shù)聲明和函數(shù)表達式并非一視同仁狡赐。解析器會率先讀取函數(shù)聲明,并使其在執(zhí)行任何代碼之前可用(可以訪問),這稱為一種函數(shù)聲明提前的過程钦幔;至于函數(shù)表達式枕屉,則必須等到解析器執(zhí)行到它所在的代碼行,才會真正被解釋執(zhí)行鲤氢。
函數(shù)作參數(shù)傳遞
function也是一種object搀庶。所以可以作為普通對象進行傳遞拐纱。
要訪問函數(shù)的指針而不執(zhí)行函數(shù)的話,必須去掉函數(shù)后面那對圓括號哥倔。
也可以作為返回值,return一個function即可揍庄。
function add(num){
return num + 10;
}
function greeting(f,num){
console.log(f(num))
}
greeting(add,10);
20
arguments
在函數(shù)內(nèi)部有兩個特殊對象分別是arguments和this咆蒿。
arguments前面已經(jīng)提到過,它是一個類數(shù)組的對象蚂子,包含著傳入函數(shù)中的所有參數(shù)沃测。雖然arguments的主要用途是保存函數(shù)參數(shù),但是這個對象還有一個名叫callee的屬性食茎,該屬性是一個指針蒂破,指向擁有這個arguments對象的函數(shù),請看下面非常經(jīng)典的階乘函數(shù)别渔。
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
定義階乘函數(shù)一般都要用到遞歸算法附迷;如上面代碼所示,在函數(shù)有名字哎媚,而且名字以后也不會變的情況下喇伯,這樣定義沒有問題。但是問題是這個函數(shù)的執(zhí)行與函數(shù)名factorial緊緊耦合在了一起拨与。為了消除這種緊密耦合的現(xiàn)象稻据,可以像下面這樣使用arguments.callee。
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
在這個重寫后的factorial()函數(shù)體內(nèi)买喧,沒有再引用函數(shù)名factorial捻悯。這樣,無論引用函數(shù)時使用的是什么名字淤毛,都可以保證正常完成遞歸調(diào)用今缚。例如:
var trueFactorial = factorial;
factorial = function() {
return 0;
}
alert(trueFactorial(5)); // 120
alert(factorial(5)); // 0
在此,變量trueFactorial獲得了factorial的值钱床,實際上是在另一個位置上保存了一個函數(shù)的指針荚斯。然后,我們又將一個返回了0的函數(shù)賦值給factorial變量查牌。如果將原來的factorial()那樣不使用arguments.callee事期,調(diào)用trueFactorial會返回0.可是,在解除了函數(shù)體內(nèi)的代碼與函數(shù)名的耦合狀態(tài)后纸颜,trueFactorial()仍能正常地計算階乘兽泣;至于factorial(),它現(xiàn)在只是個返回0的函數(shù)胁孙。
this
在JavaScript中唠倦,函數(shù)的this關(guān)鍵字的行為與其他語言相比有很多不同称鳞。在絕大多數(shù)情況下,函數(shù)的調(diào)用方式?jīng)Q定了this的值稠鼻。this不能在執(zhí)行期間被賦值冈止,在每次函數(shù)被調(diào)用時this的值也可能會不同。ES5引入了bind方法來設(shè)置函數(shù)的this值候齿,而不用考慮函數(shù)如何被調(diào)用的熙暴。
-
全局上下文
在全局運行上下文中(在任何函數(shù)體外部),this 指代全局對象console.log(this.document === document); // true // 在瀏覽器中慌盯,全局對象為 window 對象: console.log(this === window); // true this.a = 37; console.log(window.a); // 37
-
函數(shù)上下文
在函數(shù)內(nèi)部周霉,this的值取決于函數(shù)是如何調(diào)用的。
直接調(diào)用function f1(){ return this; } f1() === window; // true
在上面的例子中亚皂,this的值不是由函數(shù)調(diào)用設(shè)定俱箱。this 的值總是一個對象且默認為全局對象。
-
對象方法中的 this
當以對象里的方法的方式調(diào)用函數(shù)時灭必,它們的 this 是調(diào)用該函數(shù)的對象. 下面的例子中狞谱,當 o.f() 被調(diào)用時,函數(shù)內(nèi)的this將綁定到o對象厂财。var o = { prop: 37, f: function() { return this.prop; } }; console.log(o.f()); // logs 37
注意芋簿,在何處或者如何定義調(diào)用函數(shù)完全不會影響到this的行為。在上一個例子中璃饱,我們在定義o的時候為其成員f定義了一個匿名函數(shù)与斤。但是,我們也可以首先定義函數(shù)然后再將其附屬到o.f荚恶。這樣做this的行為也一致:
```
var o = {prop: 37};
function independent() {
return this.prop;
}
o.f = independent;
console.log(o.f()); // logs 37
```
這說明this的值只與函數(shù) f 作為 o 的成員被調(diào)用有關(guān)系撩穿。
類似的,this 的綁定只受最靠近的成員引用的影響谒撼。在下面的這個例子中食寡,我們把一個方法g當作對象o.b的函數(shù)調(diào)用。在這次執(zhí)行期間廓潜,函數(shù)中的this將指向o.b抵皱。事實上,這與對象本身的成員沒有多大關(guān)系辩蛋,最靠近的引用才是最重要的呻畸。
```
o.b = {
g: independent,
prop: 42
};
console.log(o.b.g()); // logs 42
```
- 原型鏈中的 this
相同的概念在定義在原型鏈中的方法也是一致的。如果該方法存在于一個對象的原型鏈上悼院,那么this指向的是調(diào)用這個方法的對象伤为,表現(xiàn)得好像是這個方法就存在于這個對象上一樣。
var o = {
f : function(){
return this.a + this.b;
}
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
在這個例子中据途,對象p沒有屬于它自己的f屬性绞愚,它的f屬性繼承自它的原型叙甸。但是這對于最終在o中找到f屬性的查找過程來說沒有關(guān)系;查找過程首先從p.f的引用開始位衩,所以函數(shù)中的this指向p裆蒸。也就是說,因為f是作為p的方法調(diào)用的蚂四,所以它的this指向了p光戈。這是JavaScript的原型繼承中的一個有趣的特性。
- getter 與 setter 中的 this
再次遂赠,相同的概念也適用函數(shù)作為一個 getter 或者 一個setter調(diào)用。作為getter或setter函數(shù)都會綁定 this 到從設(shè)置屬性或得到屬性的那個對象晌杰。
function modulus(){
return Math.sqrt(this.re * this.re + this.im * this.im);
}
var o = {
re: 1,
im: -1,
get phase(){
return Math.atan2(this.im, this.re);
}
};
Object.defineProperty(o, 'modulus', {
get: modulus, enumerable:true, configurable:true});
console.log(o.phase, o.modulus); // logs -0.78 1.4142
-
構(gòu)造函數(shù)中的 this
當一個函數(shù)被作為一個構(gòu)造函數(shù)來使用(使用new關(guān)鍵字)跷睦,它的this與即將被創(chuàng)建的新對象綁定。
注意:當構(gòu)造器返回的默認值是一個this引用的對象時肋演,可以手動設(shè)置返回其他的對象抑诸,如果返回值不是一個對象,返回this爹殊。function C(){ this.a = 37; } var o = new C(); console.log(o.a); // logs 37 function C2(){ this.a = 37; return {a:38}; } o = new C2(); console.log(o.a); // logs 38
在最后的例子中(C2)蜕乡,因為在調(diào)用構(gòu)造函數(shù)的過程中,手動的設(shè)置了返回對象梗夸,與this綁定的默認對象被取消(本質(zhì)上這使得語句“this.a = 37;”成了“僵尸”代碼层玲,實際上并不是真正的“僵尸”,這條語句執(zhí)行了但是對于外部沒有任何影響反症,因此完全可以忽略它)辛块。
- call 和 apply
當一個函數(shù)的內(nèi)部使用了this關(guān)鍵字時,通過從Function對象的原型中繼承的call()方法和apply()方法調(diào)用這個函數(shù)時铅碍,this的值可以綁定到一個指定的對象上润绵。
function add(c, d){
return this.a + this.b + c + d;
}
var o = {a:1, b:3};
// The first parameter is the object to use as 'this', subsequent parameters are passed as
// arguments in the function call
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
// The first parameter is the object to use as 'this', the second is an array whose
// members are used as the arguments in the function call
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
使用 call 和 apply 函數(shù)的時候要注意,如果傳遞的 this 值不是一個對象胞谈,JavaScript 將會嘗試使用內(nèi)部 ToObject 操作將其轉(zhuǎn)換為對象尘盼。因此,如果傳遞的值是一個原始值比如 7 或 'foo' 烦绳,那么就會使用相關(guān)構(gòu)造函數(shù)將它轉(zhuǎn)換為對象卿捎,所以原始值 7 通過new Number(7)被轉(zhuǎn)換為對象,而字符串'foo'使用 new String('foo') 轉(zhuǎn)化為對象爵嗅,例如:
unction bar() {
console.log(Object.prototype.toString.call(this));
}
bar.call(7); // [object Number]
- bind 方法
ECMAScript 5 引入了 Function.prototype.bind娇澎。調(diào)用f.bind(someObject)會創(chuàng)建一個與f具有相同函數(shù)體和作用域的函數(shù),但是在這個新函數(shù)中睹晒,this將永久地被綁定到了bind的第一個參數(shù)趟庄,無論這個函數(shù)是如何被調(diào)用的括细。
function f(){
return this.a;
}
var g = f.bind({a:"azerty"});
console.log(g()); // azerty
var o = {a:37, f:f, g:g};
console.log(o.f(), o.g()); // 37, azerty
- DOM事件處理函數(shù)中的 this
當函數(shù)被用作事件處理函數(shù)時(以addEventListener綁定事件),它的this指向觸發(fā)事件的元素戚啥。
// 被調(diào)用時奋单,將關(guān)聯(lián)的元素變成藍色
function bluify(e){
console.log(this === e.currentTarget); // 總是 true
// 當 currentTarget 和 target 是同一個對象是為 true
console.log(this === e.target);
this.style.backgroundColor = '#A5D9F3';
}
// 獲取文檔中的所有元素的列表
var elements = document.getElementsByTagName('*');
// 將bluify作為元素的點擊監(jiān)聽函數(shù),當元素被點擊時猫十,就會變成藍色
for(var i=0 ; i<elements.length ; i++){
elements[i].addEventListener('click', bluify, false);
}
-
內(nèi)聯(lián)事件處理函數(shù)中的 this
當代碼被內(nèi)聯(lián)處理函數(shù)調(diào)用時览濒,它的this指向監(jiān)聽器所在的DOM元素:<button onclick="alert(this.tagName.toLowerCase());"> Show this </button>
上面的alert會顯示button。
作用域
函數(shù)內(nèi)部拖云,未聲明(即沒有var)而直接賦值的變量贷笛,是全局作用域。
-
作用域鏈
因為全局變量總是存在于運行期上下文作用域鏈的最末端宙项,因此在標識符解析的時候乏苦,查找全局變量是最慢的。所以尤筐,在編寫代碼的時候應(yīng)盡量少使用全局變量,盡可能使用局部變量盆繁。一個好的經(jīng)驗法則是:如果一個跨作用域的對象被引用了一次以上掀淘,則先把它存儲到局部變量里再使用。例如下面的代碼:function changeColor(){ document.getElementById("btnChange").onclick=function(){ document.getElementById("targetCanvas").style.backgroundColor="red"; }; }
這個函數(shù)引用了兩次全局變量document油昂,查找該變量必須遍歷整個作用域鏈革娄,直到最后在全局對象中才能找到。這段代碼可以重寫如下:
function changeColor(){
var doc=document;
doc.getElementById("btnChange").onclick=function(){
doc.getElementById("targetCanvas").style.backgroundColor="red";
};
}
這段代碼比較簡單秕狰,重寫后不會顯示出巨大的性能提升稠腊,但是如果程序中有大量的全局變量被從反復(fù)訪問,那么重寫后的代碼性能會有顯著改善鸣哀。
閉包
- 原理
閉包是一種特殊的對象架忌。它由兩部分構(gòu)成:函數(shù),以及創(chuàng)建該函數(shù)的環(huán)境我衬。環(huán)境由閉包創(chuàng)建時在作用域中的任何局部變量組成叹放。
function makeFunc()
{
var name = 'yunsheng';
function theFunc(){
console.log(name);
}
return theFunc;
}
makeFunc就是一個閉包,它由函數(shù)theFunc和函數(shù)的環(huán)境--局部變量name組成挠羔。
function mySum(x){
return function(y){
return x + y;
}
}
var add5 = mySum(5);
var add10 = mySum(10);
add5(2); // 7
add10(2); // 12
這個例子更明顯井仰,add5 和 add10 都是閉包。它們共享相同的函數(shù)定義破加,但是保存了不同的環(huán)境俱恶。在 add5 的環(huán)境中,x 為 5。而在 add10 中合是,x 則為 10了罪。
- 閉包的實際使用案例。
- 在 Web 中聪全,您可能想這樣做的情形非常普遍泊藕。大部分我們所寫的 Web JavaScript 代碼都是事件驅(qū)動的 — 定義某種行為,然后將其添加到用戶觸發(fā)的事件之上(比如點擊或者按鍵)难礼。我們的代碼通常添加為回調(diào):響應(yīng)事件而執(zhí)行的函數(shù)娃圆。
以下是一個實際的示例:假設(shè)我們想在頁面上添加一些可以調(diào)整字號的按鈕。一種方法是以像素為單位指定 body 元素的 font-size蛾茉,然后通過相對的 em 單位設(shè)置頁面中其它元素(例如頁眉)的字號:
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
我們的交互式的文本尺寸按鈕可以修改 body 元素的 font-size 屬性讼呢,而由于我們使用相對的單位,頁面中的其它元素也會相應(yīng)地調(diào)整谦炬。
以下是 JavaScript:
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
size12吝岭,size14 和 size16 為將 body 文本相應(yīng)地調(diào)整為 12,14吧寺,16 像素的函數(shù)。我們可以將它們分別添加到按鈕上(這里是鏈接)散劫。如下所示:
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
- 用閉包模擬私有方法
諸如 Java 在內(nèi)的一些語言支持將方法聲明為私有的稚机,即它們只能被同一個類中的其它方法所調(diào)用。
對此获搏,JavaScript 并不提供原生的支持赖条,但是可以使用閉包模擬私有方法。私有方法不僅僅有利于限制對代碼的訪問:還提供了管理全局命名空間的強大能力常熙,避免非核心的方法弄亂了代碼的公共接口部分纬乍。
下面的示例展現(xiàn)了如何使用閉包來定義公共函數(shù),且其可以訪問私有函數(shù)和變量裸卫。這個方式也稱為 模塊模式(module pattern):
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
這里有很多細節(jié)仿贬。在以往的示例中,每個閉包都有它自己的環(huán)境墓贿;而這次我們只創(chuàng)建了一個環(huán)境茧泪,為三個函數(shù)所共享:Counter.increment聋袋,Counter.decrement 和 Counter.value队伟。
該共享環(huán)境創(chuàng)建于一個匿名函數(shù)體內(nèi),該函數(shù)一經(jīng)定義立刻執(zhí)行幽勒。環(huán)境中包含兩個私有項:名為 privateCounter 的變量和名為 changeBy 的函數(shù)嗜侮。 這兩項都無法在匿名函數(shù)外部直接訪問。必須通過匿名包裝器返回的三個公共函數(shù)訪問。
這三個公共函數(shù)是共享同一個環(huán)境的閉包锈颗。多虧 JavaScript 的詞法范圍的作用域顷霹,它們都可以訪問 privateCounter 變量和 changeBy 函數(shù)。
您應(yīng)該注意到了宜猜,我們定義了一個匿名函數(shù)用于創(chuàng)建計數(shù)器泼返,然后直接調(diào)用該函數(shù),并將返回值賦給 Counter 變量姨拥。也可以將這個函數(shù)保存到另一個變量中绅喉,以便創(chuàng)建多個計數(shù)器。
```
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
```
請注意兩個計數(shù)器是如何維護它們各自的獨立性的叫乌。每次調(diào)用 makeCounter() 函數(shù)期間柴罐,其環(huán)境是不同的。每次調(diào)用中憨奸, privateCounter 中含有不同的實例革屠。
這種形式的閉包提供了許多通常由面向?qū)ο缶幊趟碛械囊嫣帲绕涫菙?shù)據(jù)隱藏和封裝排宰。
- 如果不是因為某些特殊任務(wù)而需要閉包似芝,在沒有必要的情況下,在其它函數(shù)中創(chuàng)建函數(shù)是不明智的板甘,因為閉包對腳本性能具有負面影響党瓮,包括處理速度和內(nèi)存消耗。