JavaScript中的this陷阱的最全收集

JavaScript來自一門健全的語言,所以你可能覺得JavaScript中的this和其他面向對象的語言如java的this一樣,是指存儲在實例屬性中的值。事實並非如此,在JavaScript中,最好把this當成哈利波特中的博格特的揹包,有着深不可測的魔力。 
下面的部分是我希望我的同事在使用JavaScript的this的時候應當知道的。內容很多,是我學習好幾年總結出來的。

JavaScript中很多時候會用到this,下面詳細介紹每一種情況。在這裏我想首先介紹一下宿主環境這個概念。一門語言在運行的時候,需要一個環境,叫做宿主環境。對於JavaScript,宿主環境最常見的是web瀏覽器,瀏覽器提供了一個JavaScript運行的環境,這個環境裏面,需要提供一些接口,好讓JavaScript引擎能夠和宿主環境對接。JavaScript引擎纔是真正執行JavaScript代碼的地方,常見的引擎有V8(目前最快JavaScript引擎、Google生產)、JavaScript core。JavaScript引擎主要做了下面幾件事情:

  • 一套與宿主環境相聯繫的規則;

  • JavaScript引擎內核(基本語法規範、邏輯、命令和算法);

  • 一組內置對象和API;

  • 其他約定。

但是環境不是唯一的,也就是JavaScript不僅僅能夠在瀏覽器裏面跑,也能在其他提供了宿主環境的程序裏面跑,最常見的就是nodejs。同樣作爲一個宿主環境,nodejs也有自己的JavaScript引擎–V8。根據官方的定義: 
Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications

global this

  • 在瀏覽器裏,在全局範圍內,this等價於window對象。

<script type="text/javascript">

 console.log(this === window); //true

</script>

  • 在瀏覽器裏,在全局範圍內,用var聲明一個變量和給this或者window添加屬性是等價的。

<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(Read-Eval-Print Loop,簡稱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不是等價的。

test.js腳本內容:

console.log(this);

console.log(this === global);

REPL運行腳本:

$ node test.js

{}

false

  • 在node環境裏,在全局範圍內,如果你用REPL執行一個腳本文件,用var聲明一個變量並不會和在瀏覽器裏面一樣將這個變量添加給this。

test.js:

var foo = "bar";

console.log(this.foo);

$ node test.js

undefined

  • 但是如果你不是用REPL執行腳本文件,而是直接執行代碼,結果和在瀏覽器裏面是一樣的(神坑)

> var foo = "bar";

> this.foo

bar

> global.foo

bar

  • 在node環境裏,用REPL運行腳本文件的時候,如果在聲明變量的時候沒有使用var或者let,這個變量會自動添加到global對象,但是不會自動添加給this對象。如果是直接執行代碼,則會同時添加給global和this

test.js

foo = "bar";

console.log(this.foo);

console.log(global.foo);

$ node test.js

undefined

bar

上面的八種情況可能大家已經繞暈了,總結起來就是:在瀏覽器裏面this是老大,它等價於window對象,如果你聲明一些全局變量(不管在任何地方),這些變量都會作爲this的屬性。在node裏面,有兩種執行JavaScript代碼的方式,一種是直接執行寫好的JavaScript文件,另外一種是直接在裏面執行一行行代碼。對於直接運行一行行JavaScript代碼的方式,global纔是老大,this和它是等價的。在這種情況下,和瀏覽器比較相似,也就是聲明一些全局變量會自動添加給老大global,順帶也會添加給this。但是在node裏面直接腳本文件就不一樣了,你聲明的全局變量不會自動添加到this,但是會添加到global對象。所以相同點是,在全局範圍內,全局變量終究是屬於老大的。 
function this 
無論是在瀏覽器環境還是node環境, 除了在DOM事件處理程序裏或者給出了thisArg(接下來會講到)外,如果不是用new調用,在函數裏面使用this都是指代全局範圍的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>

test.js

 

foo = "bar";

function testThis () {

 this.foo = "foo";

}

console.log(global.foo);

testThis();

console.log(global.foo);

$ node test.js

bar

foo

  • 除非你使用嚴格模式,這時候this就會變成undefined。

1 <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就會變成一個新的值,和global的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其實相對比較好理解,如果我們在一個函數裏面使用this,需要注意的就是我們調用函數的方式,如果是正常的方式調用函數,this指代全局的this,如果我們加一個new,這個函數就變成了一個構造函數,我們就創建了一個實例,this指代這個實例,這個和其他面向對象的語言很像。另外,寫JavaScript很常做的一件事就是綁定事件處理程序,也就是諸如button.addEventListener(‘click’, fn, false)之類的,如果在fn裏面需要使用this,this指代事件處理程序對應的對象,也就是button。

prototype this

  • 你創建的每一個函數都是函數對象。它們會自動獲得一個特殊的屬性prototype,你可以給這個屬性賦值。當你用new的方式調用一個函數的時候,你就能通過this訪問你給prototype賦的值了。

function Thing() {

 console.log(this.foo);

}

Thing.prototype.foo = "bar";

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

console.log(thing.foo);  //logs "bar"

  • 當你使用new爲你的函數創建多個實例的時候,這些實例會共享你給prototype設定的值。對於下面的例子,當你調用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想成一種獲取prototype的值的一種方式。當你在一個實例裏面直接給this添加屬性的時候,會隱藏prototype中與之同名的屬性。如果你想訪問prototype中的這個屬性值而不是你自己設定的屬性值,你可以通過在實例裏面刪除你自己添加的屬性的方式來實現。

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";

  • 或者你也能直接通過引用函數對象的prototype 來獲得你需要的值。

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";

  • 通過一個函數創建的實例會共享這個函數的prototype屬性的值,如果你給這個函數的prototype賦值一個Array,那麼所有的實例都會共享這個Array,除非你在實例裏面重寫了這個Array,這種情況下,函數的prototype的Array就會被隱藏掉。

function Thing() {

}

Thing.prototype.things = [];

var thing1 = new Thing();

var thing2 = new Thing();

thing1.things.push("foo");

console.log(thing2.things); //logs ["foo"]

  • 給一個函數的prototype賦值一個Array通常是一個錯誤的做法。如果你想每一個實例有他們專屬的Array,你應該在函數裏面創建而不是在prototype裏面創建。

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 []

  • 實際上你可以通過把多個函數的prototype鏈接起來的從而形成一個原型鏈,因此this就會魔法般地沿着這條原型鏈往上查找直到找你你需要引用的值。

function Thing1() {

}

Thing1.prototype.foo = "bar";

function Thing2() {

}

Thing2.prototype = new Thing1();

var thing = new Thing2();

console.log(thing.foo); //logs "bar"

  • 一些人利用原型鏈的特性來在JavaScript模仿經典的面向對象的繼承方式。任何給用於構建原型鏈的函數的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"

  • 我喜歡把被賦值給prototype的函數叫做方法。在上面的例子中,我已經使用過方法了,如logFoo。這些方法有着相同的prototype,即創建這些實力的原始函數。我通常把這些原始函數叫做構造函數。在prototype裏面定義的方法裏面使用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是global對象或者在嚴格模式下面是undefined。這是造成很多不熟悉JavaScript的人深陷 this陷阱的根源。在這種情況下事情變得非常糟糕,就像你把一個實例的方法當作一個值,把這個值當作函數參數傳遞給另外一個函數但是卻不把這個實例傳遞給這個函數一樣。在這種情況下,一個方法裏面的環境變成了全局範圍,或者在嚴格模式下面的undefined。

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,來避免上面這種情況的發生。

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

  • 你同樣可以使用apply和call來在新的上下文中調用方法或函數。

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來代替任何一個函數或者方法的this,即便它沒有賦值給實例的初始prototype。

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();

object 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);

     }

   }

};

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

  • 你可以直接通過對象引用你需要的屬性

var obj = {

   foo: "bar",

   deeper: {

       logFoo: function () {

           console.log(obj.foo);

       }

   }

};

 

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

DOM event this

  • 在一個HTML DOM事件處理程序裏面,this始終指向這個處理程序被所綁定到的HTML 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節點的屬性裏面,你可以放置JavaScript代碼,this指向了這個元素

<div id="foo" onclick="console.log(this);"></div>

<script type="text/javascript">

document.getElementById("foo").click(); //logs <div id="foo"...

</script>

override 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的屬性的時候不需要通過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都指向HTML元素節點。這在事件處理程序和一些方便的方法中都是管用的,比如$.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>

thisArg this 
如果你用過underscore.js 或者 lo-dash 你可能知道許多類庫的方法可以通過一個叫做thisArg 的函數參數來傳遞實例,這個函數參數會作爲this的上下文。舉個例子,這適用於_.each。原生的JavaScript在ECMAScript 5的時候也允許函數傳遞一個thisArg參數了,比如forEach。事實上,之前闡述的bind,apply和call的使用已經給你創造了傳遞thisArg參數給函數的機會。這個參數將this綁定爲你所傳遞的對象。

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暫存的使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章