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 實戰項目學習
歡迎各位看官的批評和指正,共同學習和成長
希望該文章對您有幫助,也希望得到您的鼓勵和支持