摘要:提及的精髓,閉包作用域鏈函數是當之無愧的。博客的標題是中的陷阱的最全收集沒有之一,很顯然這篇博客闡述的是。這是造成很多不熟悉的人深陷陷阱的根源。你應該避免在構造函數里面返回任何東西,因為這可能代
當有人問起你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;
其他約定。
global this但是環境不是唯一的,也就是JavaScript不僅僅能夠在瀏覽器里面跑,也能在其他提供了宿主環境的程序里面跑,最常見的就是nodejs。同樣作為一個宿主環境,nodejs也有自己的JavaScript引擎--V8。根據官方的定義:
Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications
在瀏覽器里,在全局范圍內,this等價于window對象。
1
在瀏覽器里,在全局范圍內,用var聲明一個變量和給this或者window添加屬性是等價的。
1
如果你在聲明一個變量的時候沒有使用var或者let(ECMAScript 6),你就是在給全局的this添加或者改變屬性值。
1
在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不是等價的。
1 test.js腳本內容: 2 3 console.log(this); 4 console.log(this === global); 5 6 REPL運行腳本: 7 8 $ node test.js 9 {} 10 false
在node環境里,在全局范圍內,如果你用REPL執行一個腳本文件,用var聲明一個變量并不會和在瀏覽器里面一樣將這個變量添加給this。
1 test.js: 2 3 var foo = "bar"; 4 console.log(this.foo); 5 6 $ node test.js 7 undefined
但是如果你不是用REPL執行腳本文件,而是直接執行代碼,結果和在瀏覽器里面是一樣的(神坑)
1 > var foo = "bar"; 2 > this.foo 3 bar 4 > global.foo 5 bar
在node環境里,用REPL運行腳本文件的時候,如果在聲明變量的時候沒有使用var或者let,這個變量會自動添加到global對象,但是不會自動添加給this對象。如果是直接執行代碼,則會同時添加給global和this
1 test.js 2 3 foo = "bar"; 4 console.log(this.foo); 5 console.log(global.foo); 6 7 $ node test.js 8 undefined 9 bar
function this上面的八種情況可能大家已經繞暈了,總結起來就是:在瀏覽器里面this是老大,它等價于window對象,如果你聲明一些全局變量(不管在任何地方),這些變量都會作為this的屬性。在node里面,有兩種執行JavaScript代碼的方式,一種是直接執行寫好的JavaScript文件,另外一種是直接在里面執行一行行代碼。對于直接運行一行行JavaScript代碼的方式,global才是老大,this和它是等價的。在這種情況下,和瀏覽器比較相似,也就是聲明一些全局變量會自動添加給老大global,順帶也會添加給this。但是在node里面直接腳本文件就不一樣了,你聲明的全局變量不會自動添加到this,但是會添加到global對象。所以相同點是,在全局范圍內,全局變量終究是屬于老大的。
無論是在瀏覽器環境還是node環境, 除了在DOM事件處理程序里或者給出了thisArg(接下來會講到)外,如果不是用new調用,在函數里面使用this都是指代全局范圍的this。
1
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
如果你在調用函數的時候在前面使用了new,this就會變成一個新的值,和global的this脫離干系。
1
我更喜歡把新的值稱作一個實例。
prototype this函數里面的this其實相對比較好理解,如果我們在一個函數里面使用this,需要注意的就是我們調用函數的方式,如果是正常的方式調用函數,this指代全局的this,如果我們加一個new,這個函數就變成了一個構造函數,我們就創建了一個實例,this指代這個實例,這個和其他面向對象的語言很像。另外,寫JavaScript很常做的一件事就是綁定事件處理程序,也就是諸如button.addEventListener(‘click’, fn, false)之類的,如果在fn里面需要使用this,this指代事件處理程序對應的對象,也就是button。
你創建的每一個函數都是函數對象。它們會自動獲得一個特殊的屬性prototype,你可以給這個屬性賦值。當你用new的方式調用一個函數的時候,你就能通過this訪問你給prototype賦的值了。
1 function Thing() { 2 console.log(this.foo); 3 } 4 5 Thing.prototype.foo = "bar"; 6 7 var thing = new Thing(); //logs "bar" 8 console.log(thing.foo); //logs "bar"
當你使用new為你的函數創建多個實例的時候,這些實例會共享你給prototype設定的值。對于下面的例子,當你調用this.foo的時候,都會返回相同的值,除非你在某個實例里面重寫了自己的this.foo
復制代碼
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 Thing.prototype.logFoo = function () { 5 console.log(this.foo); 6 } 7 Thing.prototype.setFoo = function (newFoo) { 8 this.foo = newFoo; 9 } 10 11 var thing1 = new Thing(); 12 var thing2 = new Thing(); 13 14 thing1.logFoo(); //logs "bar" 15 thing2.logFoo(); //logs "bar" 16 17 thing1.setFoo("foo"); 18 thing1.logFoo(); //logs "foo"; 19 thing2.logFoo(); //logs "bar"; 20 21 thing2.foo = "foobar"; 22 thing1.logFoo(); //logs "foo"; 23 thing2.logFoo(); //logs "foobar";
實例里面的this是一個特殊的對象。你可以把this想成一種獲取prototype的值的一種方式。當你在一個實例里面直接給this添加屬性的時候,會隱藏prototype中與之同名的屬性。如果你想訪問prototype中的這個屬性值而不是你自己設定的屬性值,你可以通過在實例里面刪除你自己添加的屬性的方式來實現。
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 Thing.prototype.logFoo = function () { 5 console.log(this.foo); 6 } 7 Thing.prototype.setFoo = function (newFoo) { 8 this.foo = newFoo; 9 } 10 Thing.prototype.deleteFoo = function () { 11 delete this.foo; 12 } 13 var thing = new Thing(); 14 thing.setFoo("foo"); 15 thing.logFoo(); //logs "foo"; 16 thing.deleteFoo(); 17 thing.logFoo(); //logs "bar"; 18 thing.foo = "foobar"; 19 thing.logFoo(); //logs "foobar"; 20 delete thing.foo; 21 thing.logFoo(); //logs "bar";
或者你也能直接通過引用函數對象的prototype 來獲得你需要的值。
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 Thing.prototype.logFoo = function () { 5 console.log(this.foo, Thing.prototype.foo); 6 } 7 8 var thing = new Thing(); 9 thing.foo = "foo"; 10 thing.logFoo(); //logs "foo bar";
通過一個函數創建的實例會共享這個函數的prototype屬性的值,如果你給這個函數的prototype賦值一個Array,那么所有的實例都會共享這個Array,除非你在實例里面重寫了這個Array,這種情況下,函數的prototype的Array就會被隱藏掉。
1 function Thing() { 2 } 3 Thing.prototype.things = []; 4 5 6 var thing1 = new Thing(); 7 var thing2 = new Thing(); 8 thing1.things.push("foo"); 9 console.log(thing2.things); //logs ["foo"]
給一個函數的prototype賦值一個Array通常是一個錯誤的做法。如果你想每一個實例有他們專屬的Array,你應該在函數里面創建而不是在prototype里面創建。
1 function Thing() { 2 this.things = []; 3 } 4 5 6 var thing1 = new Thing(); 7 var thing2 = new Thing(); 8 thing1.things.push("foo"); 9 console.log(thing1.things); //logs ["foo"] 10 console.log(thing2.things); //logs []
實際上你可以通過把多個函數的prototype鏈接起來的從而形成一個原型鏈,因此this就會魔法般地沿著這條原型鏈往上查找直到找你你需要引用的值。
1 function Thing1() { 2 } 3 Thing1.prototype.foo = "bar"; 4 5 function Thing2() { 6 } 7 Thing2.prototype = new Thing1(); 8 9 10 var thing = new Thing2(); 11 console.log(thing.foo); //logs "bar"
一些人利用原型鏈的特性來在JavaScript模仿經典的面向對象的繼承方式。任何給用于構建原型鏈的函數的this的賦值的語句都會隱藏原型鏈上游的相同的屬性。
1 function Thing1() { 2 } 3 Thing1.prototype.foo = "bar"; 4 5 function Thing2() { 6 this.foo = "foo"; 7 } 8 Thing2.prototype = new Thing1(); 9 10 function Thing3() { 11 } 12 Thing3.prototype = new Thing2(); 13 14 15 var thing = new Thing3(); 16 console.log(thing.foo); //logs "foo"
我喜歡把被賦值給prototype的函數叫做方法。在上面的例子中,我已經使用過方法了,如logFoo。這些方法有著相同的prototype,即創建這些實力的原始函數。我通常把這些原始函數叫做構造函數。在prototype里面定義的方法里面使用this會影響到當前實例的原型鏈的上游的this。這意味著你直接給this賦值的時候,隱藏了原型鏈上游的相同的屬性值。這個實例的任何方法都會使用這個最新的值而不是原型里面定義的這個相同的值。
1 function Thing1() { 2 } 3 Thing1.prototype.foo = "bar"; 4 Thing1.prototype.logFoo = function () { 5 console.log(this.foo); 6 } 7 8 function Thing2() { 9 this.foo = "foo"; 10 } 11 Thing2.prototype = new Thing1(); 12 13 14 var thing = new Thing2(); 15 thing.logFoo(); //logs "foo";
在JavaScript里面你可以嵌套函數,也就是你可以在函數里面定義函數。嵌套函數可以通過閉包捕獲父函數的變量,但是這個函數沒有繼承this
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 Thing.prototype.logFoo = function () { 5 var info = "attempting to log this.foo:"; 6 function doIt() { 7 console.log(info, this.foo); 8 } 9 doIt(); 10 } 11 12 13 var thing = new Thing(); 14 thing.logFoo(); //logs "attempting to log this.foo: undefined"
在doIt里面的this是global對象或者在嚴格模式下面是undefined。這是造成很多不熟悉JavaScript的人深陷 this陷阱的根源。在這種情況下事情變得非常糟糕,就像你把一個實例的方法當作一個值,把這個值當作函數參數傳遞給另外一個函數但是卻不把這個實例傳遞給這個函數一樣。在這種情況下,一個方法里面的環境變成了全局范圍,或者在嚴格模式下面的undefined。
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 Thing.prototype.logFoo = function () { 5 console.log(this.foo); 6 } 7 8 function doIt(method) { 9 method(); 10 } 11 12 13 var thing = new Thing(); 14 thing.logFoo(); //logs "bar" 15 doIt(thing.logFoo); //logs undefined
一些人喜歡先把this捕獲到一個變量里面,通常這個變量叫做self,來避免上面這種情況的發生。
博主非常喜歡用這種方式
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 Thing.prototype.logFoo = function () { 5 var self = this; 6 var info = "attempting to log this.foo:"; 7 function doIt() { 8 console.log(info, self.foo); 9 } 10 doIt(); 11 } 12 13 14 var thing = new Thing(); 15 thing.logFoo(); //logs "attempting to log this.foo: bar"
但是當你需要把一個方法作為一個值傳遞給一個函數的時候并不管用。
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 Thing.prototype.logFoo = function () { 5 var self = this; 6 function doIt() { 7 console.log(self.foo); 8 } 9 doIt(); 10 } 11 12 function doItIndirectly(method) { 13 method(); 14 } 15 16 17 var thing = new Thing(); 18 thing.logFoo(); //logs "bar" 19 doItIndirectly(thing.logFoo); //logs undefined
你可以通過bind將實例和方法一切傳遞給函數來解決這個問題,bind是一個函數定義在所有函數和方法的函數對象上面
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 Thing.prototype.logFoo = function () { 5 console.log(this.foo); 6 } 7 8 function doIt(method) { 9 method(); 10 } 11 12 13 var thing = new Thing(); 14 doIt(thing.logFoo.bind(thing)); //logs bar
你同樣可以使用apply和call來在新的上下文中調用方法或函數。
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 Thing.prototype.logFoo = function () { 5 function doIt() { 6 console.log(this.foo); 7 } 8 doIt.apply(this); 9 } 10 11 function doItIndirectly(method) { 12 method(); 13 } 14 15 16 var thing = new Thing(); 17 doItIndirectly(thing.logFoo.bind(thing)); //logs bar
你可以用bind來代替任何一個函數或者方法的this,即便它沒有賦值給實例的初始prototype。
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 5 6 function logFoo(aStr) { 7 console.log(aStr, this.foo); 8 } 9 10 11 var thing = new Thing(); 12 logFoo.bind(thing)("using bind"); //logs "using bind bar" 13 logFoo.apply(thing, ["using apply"]); //logs "using apply bar" 14 logFoo.call(thing, "using call"); //logs "using call bar" 15 logFoo("using nothing"); //logs "using nothing undefined"
你應該避免在構造函數里面返回任何東西,因為這可能代替本來應該返回的實例。
1 function Thing() { 2 return {}; 3 } 4 Thing.prototype.foo = "bar"; 5 6 7 Thing.prototype.logFoo = function () { 8 console.log(this.foo); 9 } 10 11 12 var thing = new Thing(); 13 thing.logFoo(); //Uncaught TypeError: undefined is not a function
奇怪的是,如果你在構造函數里面返回了一個原始值,上面所述的情況并不會發生并且返回語句被忽略了。最好不要在你將通過new調用的構造函數里面返回任何類型的數據,即便你知道自己正在做什么。如果你想創建一個工廠模式,通過一個函數來創建一個實例,這個時候不要使用new來調用函數。當然這個建議是可選的。
你可以通過使用Object.create來避免使用new,這樣同樣能夠創建一個實例。
1 function Thing() { 2 } 3 Thing.prototype.foo = "bar"; 4 5 6 Thing.prototype.logFoo = function () { 7 console.log(this.foo); 8 } 9 10 11 var thing = Object.create(Thing.prototype); 12 thing.logFoo(); //logs "bar"
在這種情況下并不會調用構造函數
1 function Thing() { 2 this.foo = "foo"; 3 } 4 Thing.prototype.foo = "bar"; 5 6 7 Thing.prototype.logFoo = function () { 8 console.log(this.foo); 9 } 10 11 12 var thing = Object.create(Thing.prototype); 13 thing.logFoo(); //logs "bar"
因為Object.create不會調用構造函數的特性在你繼承模式下你想通過原型鏈重寫構造函數的時候非常有用。
1 function Thing1() { 2 this.foo = "foo"; 3 } 4 Thing1.prototype.foo = "bar"; 5 6 function Thing2() { 7 this.logFoo(); //logs "bar" 8 Thing1.apply(this); 9 this.logFoo(); //logs "foo" 10 } 11 Thing2.prototype = Object.create(Thing1.prototype); 12 Thing2.prototype.logFoo = function () { 13 console.log(this.foo); 14 } 15 16 var thing = new Thing2();object this
在一個對象的一個函數里,你可以通過this來引用這個對象的其他屬性。這個用new來新建一個實例是不一樣的。
1 var obj = { 2 foo: "bar", 3 logFoo: function () { 4 console.log(this.foo); 5 } 6 }; 7 8 obj.logFoo(); //logs "bar"
注意,沒有使用new,沒有使用Object.create,也沒有使用函數調用創建一個對象。你也可以將對象當作一個實例將函數綁定到上面。
1 var obj = { 2 foo: "bar" 3 }; 4 5 function logFoo() { 6 console.log(this.foo); 7 } 8 9 logFoo.apply(obj); //logs "bar"
當你用這種方式使用this的時候,并不會越出當前的對象。只有有相同直接父元素的屬性才能通過this共享變量
1 var obj = { 2 foo: "bar", 3 deeper: { 4 logFoo: function () { 5 console.log(this.foo); 6 } 7 } 8 }; 9 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節點
1 function Listener() { 2 document.getElementById("foo").addEventListener("click", 3 this.handleClick); 4 } 5 Listener.prototype.handleClick = function (event) { 6 console.log(this); //logs "" 7 } 8 9 var listener = new Listener(); 10 document.getElementById("foo").click();
除非你自己通過bind切換了上下文
1 function Listener() { 2 document.getElementById("foo").addEventListener("click", 3 this.handleClick.bind(this)); 4 } 5 Listener.prototype.handleClick = function (event) { 6 console.log(this); //logs Listener {handleClick: function} 7 } 8 9 var listener = new Listener(); 10 document.getElementById("foo").click();HTML this
在HTML節點的屬性里面,你可以放置JavaScript代碼,this指向了這個元素
1 2override this
你不能重寫this,因為它是保留字。
1 function test () { 2 var this = {}; // Uncaught SyntaxError: Unexpected token this 3 } 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
1 function Thing () { 2 } 3 Thing.prototype.foo = "bar"; 4 Thing.prototype.logFoo = function () { 5 with (this) { 6 console.log(foo); 7 foo = "foo"; 8 } 9 } 10 11 var thing = new Thing(); 12 thing.logFoo(); // logs "bar" 13 console.log(thing.foo); // logs "foo"
許多人認為這樣使用是不好的因為with本身就飽受爭議。
jQuery this和HTML DOM元素節點的事件處理程序一樣,在許多情況下JQuery的this都指向HTML元素節點。這在事件處理程序和一些方便的方法中都是管用的,比如$.each
1 2 3thisArg this
如果你用過underscore.js 或者 lo-dash 你可能知道許多類庫的方法可以通過一個叫做thisArg 的函數參數來傳遞實例,這個函數參數會作為this的上下文。舉個例子,這適用于_.each。原生的JavaScript在ECMAScript 5的時候也允許函數傳遞一個thisArg參數了,比如forEach。事實上,之前闡述的bind,apply和call的使用已經給你創造了傳遞thisArg參數給函數的機會。這個參數將this綁定為你所傳遞的對象。
1 function Thing(type) { 2 this.type = type; 3 } 4 Thing.prototype.log = function (thing) { 5 console.log(this.type, thing); 6 } 7 Thing.prototype.logThings = function (arr) { 8 arr.forEach(this.log, this); // logs "fruit apples..." 9 _.each(arr, this.log, this); //logs "fruit apples..." 10 } 11 12 var thing = new Thing("fruit"); 13 thing.logThings(["apples", "oranges", "strawberries", "bananas"]);
這使得代碼變得更加簡介,因為避免了一大堆bind語句、函數嵌套和this暫存的使用。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/85604.html
摘要:理解的函數基礎要搞好深入淺出原型使用原型模型,雖然這經常被當作缺點提及,但是只要善于運用,其實基于原型的繼承模型比傳統的類繼承還要強大。中文指南基本操作指南二繼續熟悉的幾對方法,包括,,。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。 怎樣使用 this 因為本人屬于偽前端,因此文中只看懂了 8 成左右,希望能夠給大家帶來幫助....(據說是阿里的前端妹子寫的) this 的值到底...
閱讀 545·2021-08-31 09:45
閱讀 1652·2021-08-11 11:19
閱讀 889·2019-08-30 15:55
閱讀 828·2019-08-30 10:52
閱讀 2851·2019-08-29 13:11
閱讀 2932·2019-08-23 17:08
閱讀 2838·2019-08-23 15:11
閱讀 3071·2019-08-23 14:33