Javascript - 淺談this與Closure

網友kink問了一個問題:

$.fn.wait = function(time, type) {
       time = time || 1000;
       type = type || "fx";
       return this.queue(type, function() {
           var bb = this;
           setTimeout(function() {
               $(bb).dequeue();
           }, time);
       });
   };

在以上這段程式setTimeout呼叫的函數裡,為什麼不能直接用$(this).dequeue(),而要先var bb = this; 再$(bb).dequeue()呢?

在Javascript,this是一個奧妙的關鍵字(至少對我來說很玄,我在學習jQuery後才慢慢搞清楚它的用法)。我們先避談this在自訂物件時的應用(那又要講一大串)。簡單來說,this代表函數的擁有者(Owner),例如在事件裡:
$("input:button").click(function() {  alert($(this).attr("id")); });

this會指向觸發click事件的元素,原因是我們背地裡等同於將這個函數宣告成為元素的Method,元素成為函數的擁有者。setTimeout其實是window.setTimeout()函數,並不是某個物件觸發的事件,在這種情況下,this就會指向window,因此你可以試試:
setTimeout(function() { this.alert(this.document.body.innerHTML); }, 100);

又可以this.alert,又有this.document可用,可以證明this在當時指向的是window。

歸納一下,你可以想像成當函數被掛到某個物件上時,我們去呼叫它,this就會指向它所被掛載的對象(Owner),若前述這一點不成立,this則指向window。用個例子來說明:

function someFunc() { alert(this.document); }
var obj = new Object();
obj.doIt = someFunc;
obj.document = "Object's property";
someFunc();
obj.doIt();

執行這段程式,你會發現someFunc()會傳回[object](在FF中更明確,會是[object HTMLDocument]),而obj.doIt()則傳回"Object's property"。由此驗證,當函數變成了物件的一個Method時,this會指向該物件;直接呼叫函數本身,this指向window。

以上的實驗可以解釋不能在setTimeout中使用this的原因。既然不能直接用,初學Javascript的人(或像我一樣寫了七八年仍只用到皮毛的人)直覺想到的應該是宣告一個全域變數,然後在setTimeout中引用它。不過,用全域變數就要留意多次呼叫時變數名稱打架的問題。Javascript中,有一個更方便的解決方法--Closure。

Closure是程式語言上的一種概念(並不限Javascript),要徹底從學理上說清楚講明白並不是件容易的事,在此只對它的應用做點示範。(當然,真正的理由是我的能力範圍只到這個程度,XD)

var x = 1;
function say() { alert(x); }
x = 3;
say();

在這段程式裡,我們會得到3。但如果我們在宣告函數時x=1,就希望函數中的x不要受外界影響,怎麼辦?

function makeFunc(c) {
    var x = c;
    return function() { alert(x); }
}
x = 3;
var say1 = makeFunc(1);
say1();
var say2 = makeFunc(2);
say2();

在這個程式裡,我們宣告了makeFunc函數。函數裡設法產生一個Closure的環境,裡面宣告的var x接下參數c,跟當下建立的匿名函數綁在一起被傳回來。我們可以想像成,say1 = makeFunc(1)時,當場產生了一個匿名函數,並綁了值為1的x變數;say2 = makeFunc(2)時,現場產生了第二個匿名函數,並綁了值為2的x變數。這樣就滿足了每個函數需要擁有自已專屬變數的需求,會比用全域變數為每個函數保存一份乾淨俐落許多。

但可以想見這種寫法會耗用較多的記憶體,也有可能會導致瀏覽器發生記憶體洩漏(Memory Leaking)的狀況,IE6過去在這議題上惡名昭彰,但隨著不斷Update/Fix,差不多都更新改善過,我用過一些測試工具都已測不到Memory Leaking,應該不用太過擔心,在應用時有點警覺即可。

【延伸閱讀】

歡迎推文分享:
Published 11 March 2009 08:06 AM 由 Jeffrey
Filed under:
Views: 65,976



意見

# Ammon said on 10 March, 2009 09:48 PM

既然提到了this

就不能不提 call 和 apply 啊 :p

# kink said on 10 March, 2009 10:14 PM

感謝J大又讓小弟獲益良多;另外J大提的Closure,範例看的多反而越難懂他耶><

是不是像private的意思呢....這樣想是不是另一種誤解0.0

# Jeffrey said on 11 March, 2009 03:43 AM

to Ammon大師,才剛在門前揮了兩下就被您發現,不要沒收我的斧頭呀~~~ call與apply對初階JS Developer來說,用的機會更少一些(我還是把目標設定成可以活用jQuery就好),暫且就排除在"淺談"的範圍之外了,省得講太多太深看得人眼睛愈花。

(謎之聲要我跟大家自首,真正的原因是目前了解的程度還不足支持我出來胡說八道,這部分留待未來再談)

to kink, Closure真的不好懂,.NET/VB裡也沒有直接對應的觀念,很難一比喻大家就懂。我也看過很多講Closure的文章回應裡跑出一堆人說你這裡寫錯了,那裡的觀念不正確,足見這真的是一個不好理解的觀念。我自己較傾向能掌握它的特性,知道怎麼應用就好了。

我沒把它比擬成private的理由是因為它在函數的宣告範圍大括號外,卻也不能算是object property。我都想像成有人用膠帶把一坨變數黏在function的外皮上,偷渡了一個體制外的變數 XD

# 路人 said on 11 March, 2009 07:40 AM

memory leak 是因為 DOM 物件 和 JavaScript 物件糾纏不清的所導致的,可參考大師的文章。

javascript.crockford.com/.../leak.html

# Billy said on 11 March, 2009 11:21 AM

這裡有幾個 blog post 對於Javascript 學習非常有用,大家不妨一看,有Jeffrey 提及的Closure 和 Ammon 提及的 call 和 apply:

What ASP.NET Developers Should Know About JavaScript

odetocode.com/.../473.aspx

Closure On JavaScript Closures

odetocode.com/.../11077.aspx

Function.apply and Function.call in JavaScript

odetocode.com/.../11067.aspx

# Jeffrey said on 11 March, 2009 03:15 PM

謝謝大家,補充的資料很珍貴,真的是團結力量大呢! 順便徵求一下經驗,前陣子我也研究了一些JS Memory Leak的文章,裡面甚至提供了範例程式用力地在DOM與Javascript間互相參照,刻意要驗證Memory Leak的現象,但不知是否IE6/IE7不斷Update早已修復還是怎樣,我始終沒法在機器上"實作出Memory Leak"的效果,有人成功過嗎?

# Ark said on 12 March, 2009 10:39 PM

我有個jQuery的瓶頸....

body  內3個BUTTON    <input id="Button1" type="button" value="button1" />

   <input id="Button2" type="button" value="button2" />

   <input id="Button3" type="button" value="button3" />

疑惑如下:

$(':button').each(function(i) { $(this).click(function() { alert("xxx") }) });  //==>OK

$(':button').each(function(i) { $(this).live("click", function() { alert("xxx") }) });//==>NO

$(':button').each(function(i) { $('#' + this.id).live("click", function() { alert("xxx") }) });==>OK

為何live無法用 $(<dom element object>) 的方式綁定~而click可以,痾~重點不是each

       $(document.getElementById("Button1")).live("click", function() { alert("xxx") });//==>NO

       $(document.getElementById("Button1")).click(function() { alert("xxx") });//==>OK

這樣表述比較明白~雖然$(<dom element object>) 不在jQuery Selectors 的標準內但是click卻可以綁過去

# Jeffrey said on 13 March, 2009 04:25 AM

to Ark, $(...)裡的東西可以是Selector, DOM元素或HTML語法,$(element)也是標準寫法之一。live綁事件的原理跟bind不太相同,是將Event掛在document上。由於網頁上的元素被觸發時,事件也會浮(Bubble Up)到document上,live所掛的事件再透過Selector去判別原始觸發的元素是否符合。live著眼的目標多半是要把未來才會建立的物件也包含進去,在這個前題下,傳入單一元素似乎不符合它的使命及函數規範,我想這是造成它失效的原因吧!

# 路人 said on 14 March, 2009 02:25 AM

先安裝 Memory Leak Detector:

blogs.msdn.com/.../javascript-memory-leak-detector.aspx

開啟 Script 偵錯,開啟 Detector Panel,再連到 Douglas Crockford 所提供的各個範例,按 F5 或離開該範例後,即可看到 leaks.

# Wolke said on 08 November, 2009 07:49 PM

大大有沒有什麼js的好書,可以提供一下

我之前用的那本js私房書,已經不太夠用

中英文皆可,

but中文為佳,

thx~

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<March 2009>
SunMonTueWedThuFriSat
22232425262728
1234567
891011121314
15161718192021
22232425262728
2930311234
 
RSS
創用 CC 授權條款
【廣告】
twMVC

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication