1. 程式人生 > >詳解this

詳解this

原文:https://www.cnblogs.com/Wayou/p/all-this.html

this 虐我千百遍,看完此文效立見!不得不說,這篇文章的總結很地道很全面,適合收藏之用。
原文:all this

習慣了高階語言的你或許覺得JavaScript中的this跟Java這些面嚮物件語言相似,儲存了實體屬性的一些值。其實不然。將它視作幻影魔神比較恰當,手提一個裝滿未知符文的靈龕

以下內容我希望廣大同行們能夠了解。全是掏箱底的乾貨,其中大部分佔用了我很多時間才掌握。

全域性this

瀏覽器宿主的全域性環境中,this指的是window物件。

<script type="text/javascript">
    console.log(this === window); //true
</script>

示例

瀏覽器中在全域性環境下,使用var宣告變數其實就是賦值給thiswindow

<script type="text/javascript">
    var foo = "bar";
    console.log(this.foo); //logs "bar"
    console.log(window.foo); //logs "bar"
</script>

示例

任何情況下,建立變數時沒有使用var或者let(ECMAScript 6),也是在操作全域性this

<script type="text/javascript">
    foo = "bar";

    function testThis() {
      foo = "foo";
    }

    console.log(this.foo); //logs "bar"
    testThis();
    console.log(this.foo); //logs "foo"
</script>

示例

Node命令列(REPL)中,this是全域性名稱空間。可以通過global來訪問。

> this
{ ArrayBuffer: [Function: ArrayBuffer],
  Int8Array: { [Function: Int8Array] BYTES_PER_ELEMENT: 1 },
  Uint8Array: { [Function: Uint8Array] BYTES_PER_ELEMENT: 1 },
  ...
> global === this
true

在Node環境裡執行的JS指令碼中,this其實是個空物件,有別於global

console.log(this);
console.log(this === global);
$ node test.js
{}
false

當嘗試在Node中執行JS指令碼時,指令碼中全域性作用域中的var並不會將變數賦值給全域性this,這與在瀏覽器中是不一樣的。

var foo = "bar";
console.log(this.foo);
$ node test.js
undefined

...但在命令列裡進行求值卻會賦值到this身上。

> var foo = "bar";
> this.foo
bar
> global.foo
bar

在Node裡執行的指令碼中,建立變數時沒帶varlet關鍵字,會賦值給全域性的global但不是this(譯註:上面已經提到thisglobal不是同一個物件,所以這裡就不奇怪了)。

foo = "bar";
console.log(this.foo);
console.log(global.foo);
$ node test.js
undefined
bar

但在Node命令列裡,就會賦值給兩者了。

譯註:簡單來說,Node指令碼中globalthis是區別對待的,而Node命令列中,兩者可等效為同一物件。

函式或方法裡的this

除了DOM的事件回撥或者提供了執行上下文(後面會提到)的情況,函式正常被呼叫(不帶new)時,裡面的this指向的是全域性作用域。

<script type="text/javascript">
    foo = "bar";

    function testThis() {
      this.foo = "foo";
    }

    console.log(this.foo); //logs "bar"
    testThis();
    console.log(this.foo); //logs "foo"
</script>

示例

foo = "bar";

function testThis () {
  this.foo = "foo";
}

console.log(global.foo);
testThis();
console.log(global.foo);
$ node test.js
bar
foo

還有個例外,就是使用了"use strict";。此時thisundefined

<script type="text/javascript">
    foo = "bar";

    function testThis() {
      "use strict";
      this.foo = "foo";
    }

    console.log(this.foo); //logs "bar"
    testThis();  //Uncaught TypeError: Cannot set property 'foo' of undefined 
</script>

示例

當用呼叫函式時使用了new關鍵字,此刻this指代一個新的上下文,不再指向全域性this

<script type="text/javascript">
    foo = "bar";

    function testThis() {
      this.foo = "foo";
    }

    console.log(this.foo); //logs "bar"
    new testThis();
    console.log(this.foo); //logs "bar"

    console.log(new testThis().foo); //logs "foo"
</script>

示例

通常我將這個新的上下文稱作例項。

原型中的this

函式建立後其實以一個函式物件的形式存在著。既然是物件,則自動獲得了一個叫做prototype的屬性,可以自由地對這個屬性進行賦值。當配合new關鍵字來呼叫一個函式建立例項後,此刻便能直接訪問到原型身上的值。

function Thing() {
    console.log(this.foo);
}

Thing.prototype.foo = "bar";

var thing = new Thing(); //logs "bar"
console.log(thing.foo);  //logs "bar"

示例

當通過new的方式建立了多個例項後,他們會共用一個原型。比如,每個例項的this.foo都返回相同的值,直到this.foo被重寫。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    console.log(this.foo);
}
Thing.prototype.setFoo = function (newFoo) {
    this.foo = newFoo;
}

var thing1 = new Thing();
var thing2 = new Thing();

thing1.logFoo(); //logs "bar"
thing2.logFoo(); //logs "bar"

thing1.setFoo("foo");
thing1.logFoo(); //logs "foo";
thing2.logFoo(); //logs "bar";

thing2.foo = "foobar";
thing1.logFoo(); //logs "foo";
thing2.logFoo(); //logs "foobar";

示例

在例項中,this是個特殊的物件,而this自身其實只是個關鍵字。你可以把this想象成在例項中獲取原型值的一種途徑,同時對this賦值又會覆蓋原型上的值。完全可以將新增的值從原型中刪除從而將原型還原為初始狀態。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    console.log(this.foo);
}
Thing.prototype.setFoo = function (newFoo) {
    this.foo = newFoo;
}
Thing.prototype.deleteFoo = function () {
    delete this.foo;
}

var thing = new Thing();
thing.setFoo("foo");
thing.logFoo(); //logs "foo";
thing.deleteFoo();
thing.logFoo(); //logs "bar";
thing.foo = "foobar";
thing.logFoo(); //logs "foobar";
delete thing.foo;
thing.logFoo(); //logs "bar";

示例

...或者不通過例項,直接操作函式的原型。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    console.log(this.foo, Thing.prototype.foo);
}

var thing = new Thing();
thing.foo = "foo";
thing.logFoo(); //logs "foo bar";

示例

同一函式建立的所有例項均共享一個原型。如果你給原型賦值了一個數組,那麼所有例項都能獲取到這個陣列。除非你在某個例項中對其進行了重寫,實事上是進行了覆蓋。

function Thing() {
}
Thing.prototype.things = [];


var thing1 = new Thing();
var thing2 = new Thing();
thing1.things.push("foo");
console.log(thing2.things); //logs ["foo"]

示例

通常上面的做法是不正確的(譯註:改變thing1的同時也影響了thing2)。如果你想每個例項互不影響,那麼請在函式裡建立這些值,而不是在原型上。

function Thing() {
    this.things = [];
}


var thing1 = new Thing();
var thing2 = new Thing();
thing1.things.push("foo");
console.log(thing1.things); //logs ["foo"]
console.log(thing2.things); //logs []

示例

多個函式可以形成原型鏈,這樣this便會在原型鏈上逐步往上找直到找到你想引用的值。

function Thing1() {
}
Thing1.prototype.foo = "bar";

function Thing2() {
}
Thing2.prototype = new Thing1();


var thing = new Thing2();
console.log(thing.foo); //logs "bar"

示例

很多人便是利用這個特性在JS中模擬經典的物件繼承。

注意原型鏈底層函式中對this的操作會覆蓋上層的值。

function Thing1() {
}
Thing1.prototype.foo = "bar";

function Thing2() {
    this.foo = "foo";
}
Thing2.prototype = new Thing1();

function Thing3() {
}
Thing3.prototype = new Thing2();


var thing = new Thing3();
console.log(thing.foo); //logs "foo"

示例

我習慣將賦值到原型上的函式稱作方法。上面某些地方便使用了方法這樣的字眼,比如logFoo方法。這些方法中的this同樣具有在原型鏈上查詢引用的魔力。通常將最初用來建立例項的函式稱作建構函式。

原型鏈方法中的this是從例項中的this開始住上查詢整個原型鏈的。也就是說,如果原型鏈中某個地方直接對this進行賦值覆蓋了某個變數,那麼我們拿到 的是覆蓋後的值。

function Thing1() {
}
Thing1.prototype.foo = "bar";
Thing1.prototype.logFoo = function () {
    console.log(this.foo);
}

function Thing2() {
    this.foo = "foo";
}
Thing2.prototype = new Thing1();


var thing = new Thing2();
thing.logFoo(); //logs "foo";

示例

在JavaScript中,函式可以巢狀函式,也就是你可以在函式裡面繼續定義函式。但內層函式是通過閉包獲取外層函式裡定義的變數值的,而不是直接繼承this

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    var info = "attempting to log this.foo:";
    function doIt() {
        console.log(info, this.foo);
    }
    doIt();
}


var thing = new Thing();
thing.logFoo();  //logs "attempting to log this.foo: undefined"

示例

上面示例中,doIt 函式中的this指代是全域性作用域或者是undefined如果使用了"use strict";宣告的話。對於很多新手來說,理解這點是非常頭疼的。

還有更奇葩的。把例項的方法作為引數傳遞時,例項是不會跟著過去的。也就是說,此時方法中的this在呼叫時指向的是全域性this或者是undefined在聲明瞭"use strict";時。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {  
    console.log(this.foo);   
}

function doIt(method) {
    method();
}


var thing = new Thing();
thing.logFoo(); //logs "bar"
doIt(thing.logFoo); //logs undefined

示例

所以很多人習慣將this快取起來,用個叫self或者其他什麼的變數來儲存,以將外層與內層的this區分開來。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    var self = this;
    var info = "attempting to log this.foo:";
    function doIt() {
        console.log(info, self.foo);
    }
    doIt();
}


var thing = new Thing();
thing.logFoo();  //logs "attempting to log this.foo: bar"

示例

...但上面的方式不是萬能的,在將方法做為引數傳遞時,就不起作用了。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () { 
    var self = this;
    function doIt() {
        console.log(self.foo);
    }
    doIt();
}

function doItIndirectly(method) {
    method();
}


var thing = new Thing();
thing.logFoo(); //logs "bar"
doItIndirectly(thing.logFoo); //logs undefined

示例

解決方法就是傳遞的時候使用bind方法顯示指明上下文,bind方法是所有函式或方法都具有的。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () { 
    console.log(this.foo);
}

function doIt(method) {
    method();
}


var thing = new Thing();
doIt(thing.logFoo.bind(thing)); //logs bar

示例

同時也可以使用applycall 來呼叫該方法或函式,讓它在一個新的上下文中執行。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () { 
    function doIt() {
        console.log(this.foo);
    }
    doIt.apply(this);
}

function doItIndirectly(method) {
    method();
}


var thing = new Thing();
doItIndirectly(thing.logFoo.bind(thing)); //logs bar

示例

使用bind可以任意改變函式或方法的執行上下文,即使它沒有被繫結到一個例項的原型上。

function Thing() {
}
Thing.prototype.foo = "bar";


function logFoo(aStr) {
    console.log(aStr, this.foo);
}


var thing = new Thing();
logFoo.bind(thing)("using bind"); //logs "using bind bar"
logFoo.apply(thing, ["using apply"]); //logs "using apply bar"
logFoo.call(thing, "using call"); //logs "using call bar"
logFoo("using nothing"); //logs "using nothing undefined"

示例

避免在建構函式中返回作何東西,因為返回的東西可能覆蓋本來該返回的例項。

function Thing() {
    return {};
}
Thing.prototype.foo = "bar";


Thing.prototype.logFoo = function () {
    console.log(this.foo);
}


var thing = new Thing();
thing.logFoo(); //Uncaught TypeError: undefined is not a function

示例

但,如果你在建構函式裡返回的是個原始值比如字串或者數字什麼的,上面的錯誤就不會發生了,返回語句將被忽略。所以最好別在一個將要通過new來呼叫的建構函式中返回作何東西,即使你是清醒的。如果你想實現工廠模式,那麼請用一個函式來建立例項,並且不通過new來呼叫。當然這只是個人建議。

誠然,你也可以使用Object.create從而避免使用new。這樣也能建立一個例項。

function Thing() {
}
Thing.prototype.foo = "bar";


Thing.prototype.logFoo = function () {
    console.log(this.foo);
}


var thing =  Object.create(Thing.prototype);
thing.logFoo(); //logs "bar"

示例

這種方式不會呼叫該建構函式。

function Thing() {
    this.foo = "foo";
}
Thing.prototype.foo = "bar";


Thing.prototype.logFoo = function () {
    console.log(this.foo);
}


var thing =  Object.create(Thing.prototype);
thing.logFoo(); //logs "bar"

示例

正因為Object.create沒有呼叫建構函式,這在當你想實現一個繼承時是非常有用的,隨後你可能想要重寫建構函式。

function Thing1() {
    this.foo = "foo";
}
Thing1.prototype.foo = "bar";

function Thing2() {
    this.logFoo(); //logs "bar"
    Thing1.apply(this);
    this.logFoo(); //logs "foo"
}
Thing2.prototype = Object.create(Thing1.prototype);
Thing2.prototype.logFoo = function () {
    console.log(this.foo);
}

var thing = new Thing2();

示例

物件中的this

可以在物件的任何方法中使用this來訪問該物件的屬性。這與用new得到的例項是不一樣的。

var obj = {
    foo: "bar",
    logFoo: function () {
        console.log(this.foo);
    }
};

obj.logFoo(); //logs "bar"

示例

注意這裡並沒有使用new,也沒有用Object.create,更沒有函式的呼叫來建立物件。也可以將函式繫結到物件,就好像這個物件是一個例項一樣。

var obj = {
    foo: "bar"
};

function logFoo() {
    console.log(this.foo);
}

logFoo.apply(obj); //logs "bar"

示例

此時使用this沒有向上查詢原型鏈的複雜工序。通過this所拿到的只是該物件身上的屬性而以。

var obj = {
    foo: "bar",
    deeper: {
        logFoo: function () {
            console.log(this.foo);
        }
    }
};

obj.deeper.logFoo(); //logs undefined

示例

也可以不通過this,直接訪問物件的屬性。

var obj = {
    foo: "bar",
    deeper: {
        logFoo: function () {
            console.log(obj.foo);
        }
    }
};

obj.deeper.logFoo(); //logs "bar"

示例

DOM 事件回撥中的this

在DOM事件的處理函式中,this指代的是被繫結該事件的DOM元素。

function Listener() {
    document.getElementById("foo").addEventListener("click",
       this.handleClick);
}
Listener.prototype.handleClick = function (event) {
    console.log(this); //logs "<div id="foo"></div>"
}

var listener = new Listener();
document.getElementById("foo").click();

示例

...除非你通過bind人為改變了事件處理器的執行上下文。

function Listener() {
    document.getElementById("foo").addEventListener("click", 
        this.handleClick.bind(this));
}
Listener.prototype.handleClick = function (event) {
    console.log(this); //logs Listener {handleClick: function}
}

var listener = new Listener();
document.getElementById("foo").click();

示例

HTML中的this

HTML標籤的屬性中是可能寫JS的,這種情況下this指代該HTML元素。

<div id="foo" onclick="console.log(this);"></div>
<script type="text/javascript">
document.getElementById("foo").click(); //logs <div id="foo"...
</script>

示例

重寫this

無法重寫this,因為它是一個關鍵字。

function test () {
    var this = {};  // Uncaught SyntaxError: Unexpected token this 
}

示例

eval中的this

eval 中也可以正確獲取當前的 this

function Thing () {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    eval("console.log(this.foo)"); //logs "bar"
}

var thing = new Thing();
thing.logFoo();

示例

這裡存在安全隱患。最好的辦法就是避免使用eval

使用Function關鍵字建立的函式也可以獲取this

function Thing () {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = new Function("console.log(this.foo);");

var thing = new Thing();
thing.logFoo(); //logs "bar"

示例

使用with時的this

使用with可以將this人為新增到當前執行環境中而不需要顯示地引用this

function Thing () {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    with (this) {
        console.log(foo);
        foo = "foo";
    }
}

var thing = new Thing();
thing.logFoo(); // logs "bar"
console.log(thing.foo); // logs "foo"

示例

正如很多人認為的那樣,使用with是不好的,因為會產生歧義。

jQuery中的this

一如HTML DOM元素的事件回撥,jQuery庫中大多地方的this也是指代的DOM元素。頁面上的事件回撥和一些便利的靜態方法比如$.each 都是這樣的。

<div class="foo bar1"></div>
<div class="foo bar2"></div>
<script type="text/javascript">
$(".foo").each(function () {
    console.log(this); //logs <div class="foo...
});
$(".foo").on("click", function () {
    console.log(this); //logs <div class="foo...
});
$(".foo").each(function () {
    this.click();
});
</script>

示例

傳遞 this

如果你用過underscore.js或者lo-dash你便知道,這兩個庫中很多方法你可以傳遞一個引數來顯示指定執行的上下文。比如_.each。自ECMAScript 5 標準後,一些原生的JS方法也允許傳遞上下文,比如forEach。事實上,上文提到的bindapply還有call 已經給我們手動指定函式執行上下文的能力了。

function Thing(type) {
    this.type = type;
}
Thing.prototype.log = function (thing) {
    console.log(this.type, thing);
}
Thing.prototype.logThings = function (arr) {
   arr.forEach(this.log, this); // logs "fruit apples..."
   _.each(arr, this.log, this); //logs "fruit apples..."
}

var thing = new Thing("fruit");
thing.logThings(["apples", "oranges", "strawberries", "bananas"]);

示例

這樣可以使得程式碼簡潔些,不用層層巢狀bind,也不用不斷地快取this

一些程式語言上手很簡單,比如Go語言手冊可以被快速讀完。然後你差不多就掌握這門語言了,只是在實戰時會有些小的問題或陷阱在等著你。

而JavaScript不是這樣的。手冊難讀。非常多缺陷在裡面,以至於人們抽離出了它好的部分The Good Parts)。最好的文件可能是MDN上的了。所以我建議你看看他上面關於this的介紹,並且始終在搜尋JS相關問題時加上"mdn" 來獲得最好的文件資料。靜態程式碼檢查也是個不錯的工具,比如jshint

歡迎勘誤及討論,我的推特@bjorntipling。