JS黑魔法之this, setTimeout/setInterval, arguments

最近發現了JavaScript Garden這個JS黑魔法收集處,不過裏面有一些東西並無說得很透徹,因而邊看邊查文檔or作實驗,寫了一些筆記,順手放在博客。等看完了You don't know JS講this和prototype的部分,說不定又會再寫一點。html

函數名字是可選的

一般用匿名函數的地方,匿名函數也是能夠帶名字的(ES3開始)。便於debug時提供點額外信息/遞歸。git

foo(function bar(){ ... });

但這時候bar只能在bar裏訪問,不能在外面訪問(not defined)。一樣地:github

var foo = function bar() {
    bar(); // Works
}
bar(); // ReferenceError

這跟web

function bar() { ... }

的區別在於後者被賦給了window(或其餘global object),至關於api

bar = function() { ... }

前者的引用轉給了foo(第一段的引用則在其餘地方都沒法訪問)。賦給了global object固然均可以訪問。因爲JS的name resolution,函數名能夠在函數本身內訪問。數組

追記:IE8-會leak掉這個bar到外面去=__=!!瀏覽器

this的五種綁定

  1. 在全局下直接用this,指的是global object,瀏覽器中閉包

    console.log(this === window); // true
  2. 在以function foo()形式聲明的函數裏指的也是global object(注意甚至函數聲明內嵌在方法裏都是這樣,後面會講到)app

    function foo() {
        console.log(this === window); // true
    };
    foo();
  3. 在以形如a.foo()調用的時候,指的是調用的對象,點前面的東西(注意必定要出現括號纔是以方法形式調用,不然調用時不是方法,依然是普通函數,看後文)webapp

    var a = {};
    a.foo = function() {
        console.log(this === a); // true
    };
    a.foo();
  4. 在構造函數裏指的是新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();
  5. applycallbind是指哪打哪,這裏不贅述

內嵌函數的this綁定

var foo = {};
foo.method = function() {
    function test() {
        console.log(this === window);  // true
    }
    test();
}

foo.method();

若是在方法裏聲明一個函數,這函數裏的this又變成了global object,由於this是不會隱式傳遞的。this的值取決於函數如何調用,不是函數如何聲明。所以若是test在調用的時候不是xx.test()的形式,那麼就默認this指的是global object。

通常的workaround有:

  1. 常見的用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傳來傳去

  2. 將內嵌函數綁在this上

    var foo = {};
    foo.method = function() {
        this._test = function() {
            console.log(this == foo);  // true
        }
        this._test();
    }
    
    foo.method();

    不過這樣一來foo就額外帶上+暴露了一個外面不須要的函數

  3. 用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,由於調用的時候又不是xx.foo(),因而默認this又成了global object。注意賦值到foo的僅僅是一個函數引用,this的值沒有跟過去——實際上閉包=函數引用+環境的「環境」也是不將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 object
setTimeout(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" timeouts
var 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不是數組

  • 因此不能用pushpopslice
  • 能夠用for-in
  • 轉換爲數組

    Array.prototype.slice.call(arguments);

    可是這種作法 1.速度慢 2.解釋器沒法優化 因此不必的時候不要用

  • ES5 strict mode下arguments沒法用[]來訪問or修改

arguments.callee

優化殺手,儘可能不要用

arguments.callee一般用來reference函數自己,可是除非在用applycall不然徹底能夠用函數名代替。arguments.callee.caller(同Function.caller)一般用於reference調用這個函數的函數,可是這種用法顯然破壞封裝(函數的行爲竟然要依賴被調用的上下文)。使用了arguments.callee或者Function.caller以後解釋器很難肯定函數的行爲,致使沒法進行inline優化。

在ES5 strict mode下使用arguments.callee會報錯。

相關文章
相關標籤/搜索