this全面解析

this 深度解析


最近,重溫曾探的《JavaScript設計模式與開發實踐》【下載相關學習資料 】 在各種案例中,關於this、call、apply 的使用及奇平凡,有些設計模式的案列相對複雜,各種來回複雜的調用,讓人有點丈二的和尚摸不着頭腦,於是又重溫一下this,這裏我會由淺入深的對自己再次的學習進行一些總結。

this 指向

引用《javascript高級程序設計第三版》【下載相關學習資料 】中的官方解析。this 總是指向一個對象,而具體指向哪個對象是在運行時基於函數的執行環境動態綁定的,而非函數被聲明時的環境。(簡單來說就是:誰調用就指向誰)

this 的指向大致可以分爲以下 4 種:

  • 作爲對象的方法調用。
  • 作爲普通函數調用。
  • 構造器調用。
  • Function.prototype.call 或 Function.prototype.apply 或 Function.prototype.bind 調用
  • 箭頭函數調用
  • DOM對象的處理函數
  • 原型鏈中的this

先上基礎案列

作爲對象的方法調用


當函數作爲對象的方法被調用時, this 指向該對象

var obj = {
    a: 1,
    getA: function(){
        alert ( this === obj ); // 輸出: true
        alert ( this.a ); // 輸出: 1
    }
};
obj.getA();

這裏的getA()被obj這個對象調用,所以getA()內部中的this指向obj

作爲普通函數調用


最爲普通函數方式,此時的 this 總是指向全局對象
方式1:下面我稱之爲fun1

window.name = 'window';
var getName = function(){
    return this.name;
};
console.log( getName() ); // 輸出: globalName

方式2:下面我稱之爲fun2

window.name = 'window';
var myObject = {
    name: 'sven',
    getName: function(){
        return this.name;
    }
};
var getName = myObject.getName;
console.log( getName() ); // globalName

觀察上面兩種方式,fun1 和 fun2 的區別,fun2和 作爲對象的方法調用 的唯一區別是將 myObject.getName 先賦值給變量 getName,然後再調用,這種寫法其實就是下面的代碼形式:

window.name = 'window';
var getName = function(){
        return this.name;
    };
console.log( getName() ); // globalName

和 fun1 一樣,所以就一目瞭然了

作爲構造器調用


當用 new 運算符調用函數時,該函數總會返回一個對象,通常情況下,如果構造器不顯式地返回任何數據,或者是返回一個非對象類型的數據,構造器裏的 this 就指向返回的這個對象,

var MyClass = function(){
    this.name = 'sven';
    return 'anne'; // 返回 string 類型
};
var obj = new MyClass();
console.log(obj) // {name: 'sven'}
alert ( obj.name ); // 輸出: sven

這裏順便講一下爲啥是這樣,或者說 new MyClass 到底做了什麼?

var MyClass = function(){
    this.name = 'sven';
};
// new 的四步分別做了什麼
var newFun = function() {
  var obj = new Object(); // 創建一個對象
  var fn  = Array.prototype.shift.call(arguments);
  obj.__proto__ = fn.prototype; // 將新對象的原型指向構造函數的原型
  fn.call(obj); // 改變 this 的作用域
  return obj // 返回新對象
}
newFun(MyClass)

但用 new 調用構造器時,還要注意一個問題,如果構造器顯式地返回了一個顯示的 object 類型的對象,那麼此次運算結果最終會返回這個對象,而不是我們之前期待的 this:

var MyClass = function(){
    this.name = 'sven';
    return { // 顯式地返回一個對象
        name: 'anne'
    }
};
var obj = new MyClass();
cosole.log( obj); // 輸出: {name: 'anne'}
  • 這裏很明顯不是我們想要的 {name: ‘sven’}, 這又是爲啥,又是怎麼實現的?
  • 在剛纔的代碼基礎上,我們再稍作改動
var MyClass = function(){
    this.name = 'sven';
     return { // 顯式地返回一個對象
        name: 'anne'
    }
};
// 內部返回對象是如何進行判斷的
var newFun = function() {
  var obj = new Object(); // 創建一個對象
  var fn  = Array.prototype.shift.call(arguments);
  obj.__proto__ = fn.prototype; // 將新對象的原型指向構造函數的原型
  /*********以下爲修改代碼**********/
  var innertObj = fn.call(obj); // 改變 this 的作用域
  // 判斷函數內部是否返回了一個對象
  if (innertObj && (innertObj instanceof Object)) { 
    return innertObj
  }
  /*********以上爲修改代碼**********/
  return obj // 返回新對象
}
newFun(MyClass)

call、apply、bind

call、apply 方法

call 和 apply 可以動態地改變傳入函數的 this:

var obj1 = {
    a: 1,
    b: 2,
    caculate: function(c,d){
        return this.a + this.b + c + d;
    }
};
var obj2 = {
    a: 10,
    b: 20,
};
console.log( obj1.caculate(3,4) ); // 輸出: 10
console.log( obj1.caculate.call( obj2, 3, 4 ) ); // 輸出: 37
console.log( obj1.caculate.apply( obj2, [30, 40] ) ); // 輸出: 100

call 和 apply 的用法唯一的不同就是傳參的方式不同,參見上面兩種調用方式

bind 方法

bind 也可以動態地改變傳入函數的 this,但是Ta是永久綁定,一旦綁定不會在改變:
🌰:還是用上面的例子

// 調用方法1
var caculate = obj1.caculate
var fun1 = caculate.bind({a: 44, b: 5})(3,4)
var fun2 = caculate.bind({a: 22, b: 55})(3,4)
console.log(fun1()) // 56
console.log(fun2()) // 84
// 調用方法2
var fun3 = caculate.bind({a: 44, b: 5})(3,4) // fun3 被綁定
var fun4 = fun3.bind({a: 22, b: 55})(3,4) // fun4 改變 fun3 的作用域
console.log(fun1()) // 56
console.log(fun2()) // 56

思考???

  • 方法 2 中的 fun4 再次改變 fun3 的作用域並沒有生效?
  • 作爲一個合格程序員,我會有疑問,那問啥上面 fun1 和 fun2 中的 caculate 沒有永久綁定到 {a: 44, b: 5} 中呢???

原因其實很簡單,那是因爲 caculate 是沒有使用bind來改變其作用域的,誰用bind綁定後,誰的作用域就永久生效,不會再被改變

爲了進了一步給小夥伴們證實一下,請看下面的🌰:
var obj1 = {
    a: 1,
    b: 2,
    caculate: (function(c,d){
        return this.a + this.b + c + d;
    }){a: 100, b: 200} // 手動強行綁定
};
var caculate = obj1.caculate
var fun1 = caculate.bind({a: 44, b: 5})
var fun2 = caculate.bind({a: 22, b: 55})
console.log(fun1(3,4)) // 307
console.log(fun2(3,4)) // 307

箭頭函數

官方是這麼解釋的: 在箭頭函數中,this與封閉詞法環境的this保持一致。在全局代碼中,它將被設置爲全局對象
表示每臺看懂,腫麼辦,擼起袖子就是幹啊,看🌰:

var a = 'window'
var obj1 = {
    a: 1,
    b: 2,
    caculate1:() => {
		console.log(this.a) // this 從 window 中引用
    },
  	caculate2: function() {
		return () => console.log(this.a) // caculate2 和 obj1 構成了封閉的詞法環境,此時的 鏈式調用,this 指向 obj1
    },
	bar: function() {
    	console.log(this.a) // 同上
  	}
};
obj1.caculate1() // window
obj1.caculate2()() // 1
obj1.bar() // 1

要弄明白爲什麼是這樣的打印結果,我們得追本溯源,箭頭函數自身是沒有 this 關鍵字的,而是從外部通過鏈式引用而來的 只要明白這一點就不難解釋上面的輸出結果了

DOM事件的處理函數

當函數被用作事件處理函數時,它的this指向目標元素

// 被調用時,將關聯的元素變成藍色
function bluify(e){
  console.log(this === e.currentTarget); // 總是 true
  // 當 currentTarget 和 target 是同一個對象時爲 true
  console.log(this === e.target);        
  this.style.backgroundColor = '#A5D9F3';
}
// 獲取文檔中的所有元素的列表
var elements = document.getElementsByTagName('*');
// 將bluify作爲元素的點擊監聽函數,當元素被點擊時,就會變成藍色
for(var i=0 ; i<elements.length ; i++){
  elements[i].addEventListener('click', bluify, false);
}

當代碼被內聯on-event 處理函數調用時,它的this指向監聽器所在的DOM元素:

<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>

上面的 alert 會顯示button。注意:只有外層代碼中的this是這樣設置的:

<button onclick="alert((function(){return this})());">
  Show inner this
</button>

原型鏈中this

var o = {
  f: function() { 
    return this.a + this.b; 
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5

不做過多解釋了,其實本質和對象調用是一樣的,只不過 f() 是繼承而來,僅此而已,我們只需要記住,誰調用就指向誰是沒毛病的

案例:

var a=3;
var myObject = {
  foo: "bar",
  func: function() {
    var self = this;
    console.log("outer func:  this.foo = " + this.foo);  // bar
    console.log("outer func:  self.foo = " + self.foo);  // bar
    (function() {
      console.log("inner func:  this.foo = " + this.a); // 3  這裏的this是當作普通函數調用的,所以指向全局
      console.log("inner func:  self.foo = " + self.foo);  // bar  self 其實就是 myObject, 你可以理解被綁定了
      console.log.bind(self, "inner func:  self.foo = " + self.foo);
    }());
  }
};
myObject.func();

思考一下?

var a=3;
var myObject = {
  foo: "bar",
  func: function() {
    var self = this;
    console.log("outer func:  this.foo = " + this.foo);  // bar
    console.log("outer func:  self.foo = " + self.foo);  // bar
    (function() {
      console.log("inner func:  this.foo = " + this.a); // 3  這裏的this是當作普通函數調用的,所以指向全局
      console.log("inner func:  self.foo = " + self.foo);  // bar  self 其實就是 myObject, 你可以理解被綁定了
      console.log.bind(self, "inner func:  self.foo = " + self.foo);
    }());
  }
};
var fun = myObject.func;
fun() // 又會輸出什麼

小結

關於this的使用場景以及相關的原理總結到這裏,後續會持續跟進一些前端必須掌握的知識

更多的乾貨請點擊這裏
react-native 實戰項目學習
歡迎各位看官的批評和指正,共同學習和成長
希望該文章對您有幫助,也希望得到您的鼓勵和支持

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