標籤:style blog http color io 使用 ar java for
最近發現了JavaScript Garden這個JS黑魔法收集處,不過裡面有一些東西並沒有說得很透徹,於是邊看邊查文檔or做實驗,寫了一些筆記,順手放在部落格。等看完了You don‘t know JS講this和prototype的部分,說不定又會再寫一點。
函數名字是可選的
通常用匿名函數的地方,匿名函數也是可以帶名字的(ES3開始)。便於debug時提供點額外資訊/遞迴。
foo(function bar(){ ... });
但這時候bar只能在bar裡訪問,不能在外面訪問(not defined)。同樣地:
var foo = function bar() { bar(); // Works}bar(); // ReferenceError
這跟
function bar() { ... }
的區別在於後者被賦給了window(或其他global object),相當於
bar = function() { ... }
前者的引用轉給了foo(第一段的引用則在其他地方都無法訪問)。賦給了global object當然都可以訪問。由於JS的name resolution,函數名可以在函數自己內訪問。
追記:IE8-會leak掉這個bar到外面去=__=!!
this的五種綁定
在全域下直接用this,指的是global object,瀏覽器中
console.log(this === window); // true
在以function foo()形式聲明的函數裡指的也是global object(注意甚至函式宣告內嵌在方法裡都是這樣,後面會講到)
function foo() { console.log(this === window); // true};foo();
在以形如a.foo()調用的時候,指的是調用的對象,點前面的東西(注意一定要出現括弧才是以方法形式調用,否則調用時不是方法,依然是普通函數,看後文)
var a = {};a.foo = function() { console.log(this === a); // true};a.foo();
在建構函式裡指的是新new出來的對象。注意這裡不能直接用this == b檢查,因為建構函式調用完之前和之後這個新構造的對象本身是有區別的,不過如果延遲一下再判斷,等構造完之後就可以看出this指向的是被返回的那個新對象了。(用that儲存而不是直接用this是因為setTimeout調用函數時用的是global object,看後文)
function foo() { var that = this; setTimeout(function(){console.log(that === b);}, 1000); // true}var b = new foo();
用apply,call,bind是指哪打哪,這裡不贅述
內嵌函數的this綁定
var foo = {};foo.method = function() { function test() { console.log(this === window); // true } test();}foo.method();
如果在方法裡聲明一個函數,這函數裡的this又變成了global object,裡面和外面的this不一樣。事實上這麼看就明白怎麼回事了:
var foo = {};foo.method = function() { test = function() { // test belongs to the global object console.log(this === window); // true } test();}foo.method();
最主要的是要理解,直接用function test() {}這樣的聲明,無論在哪裡都會綁到global object去的。要放在本地,必須用function expression,用var test = function test() {}
一般的workaround有:
常見的用that
var foo = {};foo.method = function() { var that = this; var test = function test() { // store the outer this console.log(that == foo); // true } test();}foo.method();來讓裡面的函數也能用到外面的this。(注意that不是特殊名字,可以隨便用)通常和閉包一起用,來將this傳來傳去。注意var test = function test()這一句是關鍵,如果還是用function test(),其實換湯不換藥,因為test依然綁到了global object去,只不過加了個閉包而已,而且這樣test就泄露到了全域去,你就可以直接在全域調用這個看上去似乎只屬於method的test函數……2333
將內嵌函數綁在this上
var foo = {};foo.method = function() { this._test = function() { console.log(this == foo); // true } this._test();}foo.method();不過這樣一來foo就額外帶上+暴露了一個外面不需要的函數
用bind
var foo = {};foo.method = function() { var test = (function test() { console.log(this == foo); // true }).bind(this); test()}foo.method();
this的延遲綁定
var bar = {}bar.baz = function() { console.log(this === bar); // false console.log(this === window); // true}var foo = bar.baz;foo();
在foo裡this又指回了foo所屬對象——global object。所以this是在函數執行時而不是聲明時綁定的,這也是prototypal inheritance的基礎
function Foo() {}Foo.prototype.method = function() { console.log(this === b); // b would be available when executed after b is declared!};function Bar() {}Bar.prototype = Foo.prototype;var b = new Bar();b.method();
在b.method()裡this指的就是b了(Bar的執行個體),不然指的應該是一個Foo的執行個體……呵呵那就是implemation inheritance了。注意由於函數執行的時候已經有b,所以在foo裡引用b的時候不會報錯。JS的函數裡的引用都延遲到執行時去找,聲明時是不檢查的。
setTimeout裡的this
function Foo() { this.value = 42; this.method = function() { // this refers to the global object console.log(this.value); // undefined console.log(this === window); // true }; setTimeout(this.method, 500);}new Foo();
setTimeout會脫離當前上下文,用global object調用第一個參數。事實上會以為setTimeout(this.method, 500);,無非是腦補成了會調用this.method(),但事實上傳進去的不過是一個沒有bind過的函數引用,可以理解為:
method = this.method; // method belongs to the global objectsetTimeout(method, 500);
簡單來說,只要記得只有實際在代碼裡看到形如this.method()(注意括弧)的調用,才能認為函數執行時的this指向點前面的部分。沒有看到括弧,就不能這樣想當然。
如果想要讓this是直覺上的那個對象,可以用that+閉包來保證傳進去的函數裡的this是你想要的值
function Foo() { this.value = 42; var that = this; this.method = function() { // this refers to the new instance console.log(that.value); // 42 console.log(that === b); // true }; setTimeout(this.method, 500);}var b = new Foo();
或者用bind:
function Foo() { this.value = 42; this.method = (function method() { console.log(this.value); // 42 console.log(this === b); // true }).bind(this); setTimeout(this.method, 500);}var b = new Foo();setTimeout v.s. setInterval
setInterval只管調用函數,不管函數執行,所以如果被調用的函數阻塞了,而且阻塞的時間大於調用間隔,那麼當這個函數執行完之後,可能會有一大波被調用還沒開始執行的函數擠上來,像這樣:
function foo(){ // something that blocks for 1 second}setInterval(foo, 100);
解決方案是
function foo(){ // something that blocks for 1 second setTimeout(foo, 1000);}foo();
這樣會等到函數執行完之後,再等待間隔,再進行下一次調用。注意用setTimeout+傳函數的方式遞迴的時候是不會stackoverflow的,因為setTimeout+傳函數只是做標記要調用而不是真的要調用。傳進去的函數在執行完之後會立刻返回(setTimeout不會阻塞,所以不需要等待他返回),不會在棧上等著,自然也就不會stackoverflow了。
如何清除所有的timeout
setTimeout屬於DOM的一部分(而且是DOM 0),所以在ECMAScript標準裡沒有說明,但是在各大瀏覽器中,setTimeout的ID事實上是越後的越大,所以可以立刻setTimeout一下,得到當前的最大ID,然後逐個清除
// clear "all" timeoutsvar biggestTimeoutId = window.setTimeout(function(){}, 1),i;for(i = 1; i <= biggestTimeoutId; i++) { clearTimeout(i);}
但是因為標準裡沒有說,所以這個方法在未來不一定靠譜。HTML5對setTimeout做了規範,但是目前對這個返回的ID的規範是“a user-agent-defined integer that is greater than zero that will identify the timeout to be set by this call in the list of active timers.”也就是說只要是唯一的正整數就可以了,至於怎麼變就是user-agent-defined,依然不靠譜啊噗
arguments不是數組
arguments.callee
最佳化殺手,盡量不要用。
arguments.callee通常用來reference函數本身,但是除非在用apply、call否則完全可以用函數名代替。arguments.callee.caller(同Function.caller)通常用於reference調用這個函數的函數,但是這種用法顯然破壞封裝(函數的行為居然要依賴被調用的上下文)。使用了arguments.callee或者Function.caller之後解譯器很難確定函數的行為,導致無法進行inline最佳化。
在ES5 strict mode下使用arguments.callee會報錯。
JS黑魔法之this,setTimeout/setInterval,arguments