你不知道的JS之 this 和對象原型(一)this 是什么


 原文:你不知道的js系列

 

JavaScript 的 this 機制並沒有那么復雜

為什么會有 this?

在如何使用 this 之前,我們要搞清楚一個問題,為什么要使用 this。

下面的代碼嘗試去說明 this 的使用動機:

function identify() {
    return this.name.toUpperCase();
}

function speak() {
    var greeting = "Hello, I'm " + identify.call( this );
    console.log( greeting );
}

var me = {
    name: "Kyle"
};

var you = {
    name: "Reader"
};

identify.call( me ); // KYLE
identify.call( you ); // READER

speak.call( me ); // Hello, I'm KYLE
speak.call( you ); // Hello, I'm READER

這段代碼使得函數 identify() 和 speak() 可以在多個上下文(me 和 you)對象中重用,不用給每個對象分別創建函數。

 

如果不用 this,你也可以將上下文對象直接傳入函數:

function identify(context) {
    return context.name.toUpperCase();
}

function speak(context) {
    var greeting = "Hello, I'm " + identify( context );
    console.log( greeting );
}

identify( you ); // READER
speak( me ); // Hello, I'm KYLE

然而 this 機制可以隱式地傳遞一個對象引用,使得 API 設計得更簡潔和更容易復用。

你的使用模式越復雜,你就能更加明白,顯式傳遞一個參數經常比傳遞 this 上下文還混亂。

 

困惑

在解釋 this 如何工作之前,必須要先摒棄錯誤的概念。開發者們總是太過依賴 this 的字面意思。

引用自身 Itself

一種普遍的錯誤是認為 this 指代這個函數自身。

為什么你會想從一個函數內部引用它自己呢,通常的原因是遞歸,或者事件回調函數在被調用之后解除綁定。

JS 新手會認為將函數作為對象引用可以在函數調用期間存儲狀態(屬性的值)。這確實是可以的但是用處有限,后面會介紹其它模式,除了函數對象本身還有更好的存儲狀態的地方。

下面的代碼會說明,this並不會像我們以為的那樣讓函數得到對自身的引用:

function foo(num) {
    console.log( "foo: " + num );

    // keep track of how many times `foo` is called
    this.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// how many times was `foo` called?
console.log( foo.count ); // 0 -- WTF?

foo.count 還是 0 ,循環確實執行了 4 次,console.log 也確實被調用了 4 次。

foo.count = 0 執行之后,實際上給函數對象 foo 添加了一個屬性 count。

但是在函數內部的 this.count 中,this 實際上並不指向這個函數對象,即使這個屬性名字是一樣的,但屬性所在的對象是不同的。

如果 foo 的屬性 count 的值沒有改變,那么我們改變的究竟是什么。實際上,如果你再深究一下,就會發現,這段代碼意外地創建了一個全局變量 count,而且當時會有一個值 NaN(具體看這個系列的第二節)。

很多開發者就會通過別的方式避免這個問題,比如創建另外一個對象儲存這個屬性 count:

function foo(num) {
    console.log( "foo: " + num );

    // keep track of how many times `foo` is called
    data.count++;
}

var data = {
    count: 0
};

var i;

for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// how many times was `foo` called?
console.log( data.count ); // 4

這確實解決了問題,但是很遺憾這忽略了真正的問題——不理解 this 的含義和用法,只是回到熟悉的詞法作用域機制。

 

如果想在一個函數對象內部引用自身,this 是不夠的,你需要一個標識符:

function foo() {
    foo.count = 4; // `foo` refers to itself
}

setTimeout( function(){
    // anonymous function (no name), cannot
    // refer to itself
}, 10 );

在第一個函數中,函數被命名為 foo,這個標識符 foo 就可以用來指代這個函數對象自身。

但在第二段中,回調函數沒有名字,所以沒辦法引用自己。

注:老派的已經被廢棄的 arguments.callee 在函數中可以用來指代正在執行的函數對象。這是在匿名函數內部訪問函數對象的唯一方式。

當然最好的方式還是避免匿名函數的使用。

 

另外一種解決辦法就是使用 foo 標識符,不使用 this:

function foo(num) {
    console.log( "foo: " + num );

    // keep track of how many times `foo` is called
    foo.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// how many times was `foo` called?
console.log( foo.count ); // 4

然而這種方法同樣回避了對 this 的理解。

 

另外一種解決這個問題的方式是,將 this 強制綁定到 foo 這個函數對象上:

function foo(num) {
    console.log( "foo: " + num );

    // keep track of how many times `foo` is called
    // Note: `this` IS actually `foo` now, based on
    // how `foo` is called (see below)
    this.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
    if (i > 5) {
        // using `call(..)`, we ensure the `this`
        // points at the function object (`foo`) itself
        foo.call( foo, i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// how many times was `foo` called?
console.log( foo.count ); // 4

 

作用域的引用 Its Scope

第二個常見的關於 this 的錯誤理解是,this 指向這個函數的作用域。這是一個有點狡猾的問題,因為在某種意義上這種說法是有些正確的,但在另一種意義上,這又是被誤導的。

首先,this 並沒有指向函數的詞法作用域。作用域確實就像是一個包含所有標識符屬性的對象,但是這個作用域 “對象” 是無法被代碼直接訪問的,這是引擎內部實現的。

所以下面的代碼是錯誤的:

function foo() {
    var a = 2;
    this.bar();
}

function bar() {
    console.log( this.a );
}

foo(); //undefined

你可能覺得這段代碼很做作,但這是摘自一些幫助論壇里的真實代碼。

首先,這段代碼試圖通過 this.bar() 引用函數 bar(),能運行起來也是巧合。調用 bar() 最自然的方式就是直接使用標識符引用,去掉前面的 this。

然而,寫這段代碼的開發者其實是想讓 bar() 訪問 foo() 內部的變量 a,但 this 不能被用來查詢詞法作用域的。

 

this 到底是什么

前面講到過,this 是在運行時綁定的,它的上下文環境取決於函數調用的條件。this 的綁定和函數聲明的位置沒有關系,和函數調用的位置有關。

當一個函數被調用時,一個執行上下文被創建。這個上下文記錄包含函數調用的位置,函數調用的方式以及傳入的參數這些信息。this 的引用就是在這個時候決定的。

在下一節中,會介紹根據一個函數的調用位置確定它執行過程中將如何綁定this。

 

小結:

  • this 既不指代函數本身,也不指代函數的詞法作用域。
  • this 是在函數調用的時候綁定的,它引用的內容完全取決於函數調用的位置。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM