Javascript 當中的 this 與其他語言是完全不同的機制,很有可能會讓一些編寫其他語言的工程師迷惑。
1. 誤以為 this 指向函數自身
根據 this 的英語語法,很容易將函數中出現的 this 理解為函數自身。在 javascript 當中函數作為一等公民,確實可以在調用的時候將屬性值存儲起來。但是如果使用方法不對,就會發生與實際預期不一致的情況。具體情況,請看下面代碼
function fn(num){ this.count++; } fn.count = 0; for(var i=0;i<3;i++){ fn(i); } console.log(fn.count); // 0
如果 fn 函數裡面的 this 指向自身函數,那麼 count 屬性的屬性值就應該產生變化,但實際上卻是紋絲不動。對於這個問題,有些人會利用作用域來解決,比如這麼寫
var data = { count:0 }; function fn(num){ data.count++; } for(var i=0;i<3;i++){ fn(i); } console.log(data.count); //3
又或者更直接的這麼寫
function fn(num){ fn.count++; } fn.count = 0; for(var i=0;i<3;i++){ fn(i); } console.log(fn.count);//3
雖然這兩種方式都輸出了正確的結果,但是卻避開了 this 到底綁定在哪裡的問題。如果對一個事物的工作原理不清晰,就往往會產生頭痛治頭,腳痛治腳的問題,從而導致代碼變得的丑陋,而且維護性也會變得很差。
2. this神奇的綁定規則
2.1 默認綁定規則
第一種是最常見的 this 的綁定,看一下下面的代碼
function fn(){ console.log(window === this); //浏覽器環境 } fn(); //true
函數fn 是直接在全局作用域下調用的,沒有帶其他任何修飾,這種情況下,函數調用的時候使用了 this 的默認綁定,指向了全局對象。
這樣就清楚了第一個例子中的 this 指向, fn 函數中的 this 指向了全局變量,所以 this.count++ 相當於 window.count++(浏覽器環境下),當然不會對 fn 函數的count屬性產生影響。
有一點要說明的是,上面種情況只能在非嚴格模式(strict mode)下才能發生,在嚴格模式下,會將 this 默認綁定為 undefined。以避免全局變量的污染。
2.2 隱式綁定規則
如果函數在以對象為上下文進行調用,那麼 this 的綁定就會產生變化。this 會綁定到調用這個函數的對象,查看下面代碼:
var obj = { a:1, fn:function(){ console.log(this.a); } } obj.fn(); //1
即使函數聲明不在對象當中,this 指向仍會產生變化
function fn(){ console.log(this.a); } var obj = { a:1, fn:fn } obj.fn(); //1
由此可見,this 的綁定,不與函數定義的位置有關,而是與調用者和調用方式有關。
在隱式的綁定規則下,有一些特殊的地方,需要注意。
2.2.1 多層對象調用 this 的指向
function fn(){ console.log(this.a); } var obj = { a:1, obj2:obj2 } var obj2 = { a:2, obj3:obj3 } var obj3 = { a:3, fn:fn } obj.obj2.obj3.fn(); //3
在多層對象引用下,this 指向的是調用的函數的那個對象。
2.2.2 隱式賦值可能存在丟失現象
查看下面代碼
function fn(){ console.log(this); } var obj = { fn:fn } var fun = obj.fn; fun(); //window
雖然 fn 引用了 obj.fun ,但是函數的調用方式,仍是不帶任何修飾的,所以 this 還是綁定在了 window 上。
還有一種情況,容易讓大家忽略,那就是傳參的時候,其實會進行隱式賦值。
function fn(){ console.log(this); } function doFn(fn){ fn(); } var obj = { fn:fn } doFn(obj.fn); //window
隱式綁定 this 不是一種很推薦的方式,因為很有可能就發生丟失的情況,如果業務當中對 this 的綁定有要求,建議還是使用顯示綁定的方式。
2.3 顯式綁定規則
顯示綁定就是利用函數原型上的 apply 與 call 方法來對 this 進行綁定。用法就是把想要綁定的對象作為第一個參數傳進去。
function fn(){ console.log(this); } var obj = {}; fn.call(obj); //{}
有些時候會想將函數的 this 綁定在某個對象上,但是不需要立即調用,這樣的話,直接利用 call 或者 apply 是無法做的。
function fn(){ console.log(this); } function bind(fn){ fn(); } var obj = { fn:fn } bind.call(obj,fn); //window
上面這個例子,看似好像可以,但實際上是 bind 函數的 this 綁定到了 obj 這個對象,但是 fn 仍然是沒有任何修飾的調用,所以 fn 仍然是默認的綁定方式。
function fn(){ console.log(this); } function bind(fn,obj){ return function(){ fn.apply(obj,arguments); } } var obj = { fn:fn } var fun = bind(fn,obj); fun(); //obj
這樣調用,就可以將靈活多變的 this ,牢牢的控制住了,因為 fn 的調用方式為 apply 調用。所以,this 就被綁定在傳入的 obj 對象上,在 ES5 當中,函數的原型方法上多了一個 bind。效果與上面的函數基本一致,具體用法限於篇幅就不多說了。
2.4 new 綁定
new 是一個被很多人誤解的一個關鍵字,但實際上 javascript 的 new 與傳統面向對象的語言完全不同。
個人把 new 理解為一種特殊的函數調用,當使用 new 關鍵字來調用函數的時候,會執行下面操作,
function fn(a){ this.a = a; } fn.prototype.hi = function(){ console.log('hi') } var obj = new fn(2); console.log(obj); function fn(a){ this.a = a; return {}; } var obj = new fn(2); console.log(obj); //{}
2.5 特殊的傳參
null 和 undefined 也是可以作為 this 的綁定對象的,但是實際上應用的是默認的綁定。
但是這種傳參的實際效用是什麼呢?
常見的用法是將一個數組展開,作為參數傳入參數。比如
function fn(a,b){ console.log('a:',a,'b:',b); } fn.apply(null,[1,2]); // a: 1 b: 2
但是這種用法會有一個坑,那就是如果函數存在了 this ,那麼就會應用默認的綁定規則,將 this 綁定在全局對象上,發生於預期不一致的情況。為了代碼更加穩健,可以使創建一個比空對象更空的對象。
var obj = Object.create(null); console.log(obj.__proto__); //undefined var obj2 = {} console.log(obj2.__proto__); //Object {}
Object原型上有一個 create 方法,這個方法會創建一個對象,然後將對象的原型指向傳入的參數,所以傳入 null 的話,產生一個沒有 prototype 的對象,所以會比空對象更加"空"。
所以傳入這個對象,會比傳入 null 更加安全。
var obj = Object.create(null); fn.apply(obj,[1,2]);
2.6 根據作用域來決定 this 的綁定
在 ES6 當中,出現了一個新的函數類型,箭頭函數。
如果使用箭頭函數,那麼就不會使用上面提到的四種 this 綁定方式,而是根據作用域來決定
比較常見的是用於事件函數和定時器的情況。
下面是比較常見的傳統 this 寫法
function fn(){ var _this = this; setTimeout(function(){ console.log(_this.a); },100) } var obj = { a:2 } fn.call(obj); //2
如果使用箭頭函數則可以這麼寫
function fn(){ setTimeout(()=>{ //this 來源於 fn 函數的作用域 console.log(this.a); },100) } var obj = { a:2 } fn.call(obj); //2
2.7 事件函數當中 this 的綁定機制
如果是在事件函數當中,this 的綁定是指向觸發事件的 DOM 元素的,
$('body')[0].addEventListener('click',function(){ console.log(this); },false);
點擊 body 元素之後,控制台則會顯示 body 元素
3. 小結
如果想判斷一個函數的 this 綁定在哪裡,首先是找到函數的調用位置,之後是按照規則來判斷。
以上就是關於Javascript中神奇的this的相關介紹,希望對大家的學習有所幫助。