JavaScript中的this陷阱的最全收集

當有人問起你JavaScript有什麼特色的時候,你可能立馬就想到了單線程、事件驅動、面向對象等一堆詞語,可是若是真的讓你解釋一下這些概念,可能真解釋不清楚。有句話這麼說:若是你不能向一個6歲小孩解釋清楚一個東西,那麼你本身也不懂這個東西。這句話或許有點誇張,可是極其有道理。我的以爲,若是須要掌握一門語言,掌握它的API只是學了皮毛,理解這門語言的精髓纔是重點。說起JavaScript的精髓,this、閉包、做用域鏈、函數是當之無愧的。這門語言正式由於這幾個東西而變得魅力無窮。
博客的標題是《JavaScript中的this陷阱的最全收集--沒有之一》,很顯然這篇博客闡述的是this。相信作過JavaScript開發的人都遇到過很多this的陷阱,我本身自己也遇到過很多坑,可是若是非要給出一個系統的總結的話,尚未足夠的底蘊。很是幸運的是,今天早上起來看《Hacker News》的時候,恰巧看到了一篇有關於JavaScript this的解析:all 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。
<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);
        }
    }
};
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暫存的使用。
做者:yuanzm
文章源自:http://segmentfault.com/a/1190000002640298
相關文章
相關標籤/搜索