前端面試必殺技:this是個啥?

木頭房子發表於2019-04-26

講下函式呼叫

  • 函式呼叫三種方法(ES5):
    • func(p1, p2)
    • obj.child.method(p1, p2)
    • func.call(context, p1, p2) // 先不講 apply
  • 第三中呼叫形式才是正確的呼叫形式,其他兩種都是語法糖,可以等價的變換:
// 稱此程式碼為「轉換程式碼」
func(p1, p2) 等價於
func.call(undefined, p1, p2)
 
obj.child.method(p1, p2) 等價於
obj.child.method.call(obj.child, p1, p2)
// 第二個等價舉例
var obj = {
  foo: function(){
    console.log(this)
  }
}
obj.foo()
// 等價於
obj.foo.call(obj)
複製程式碼
  • 所以,this 就是你 call 一個函式時,傳入的 context。
  • 如果你的函式呼叫形式不是 call 形式,請按照「轉換程式碼」將其轉換為 call 形式。
  • 但是因為本文章是後來更新的緣故,只用「轉換程式碼」來講解其中的[]呼叫,其他的呼叫還是按照文章初創時的思路;

獨立呼叫

  • 預設繫結規則:this繫結給window;
  • 在嚴格模式下,預設繫結規則會把this繫結undefined上;
function foo() {
	console.log( this.a );
}
var a = 2;
(function(){
	"use strict";
	foo(); //2
})();
複製程式碼
  • 這裡有一個微妙但是非常重要的細節,雖然 this 的繫結規則完全取決於呼叫位置,但是隻有 foo()執行在非 嚴格模式下時,預設繫結才能繫結到全域性物件; 嚴格模式下呼叫foo()不會影響預設繫結規則;
function foo() {
	"use strict";
	console.log( this.a );
}
var a = 2;
foo(); //undefined
複製程式碼
  • 無論函式是在哪個作用域中被呼叫,只要是獨立呼叫則就會按預設繫結規則被繫結到全域性物件或者undefined上。

隱式呼叫:(使用物件的屬性呼叫)

隱式繫結

  • 隱式繫結的規則:

    • this給離函式最近的那個物件;
    • 判斷函式呼叫位置是否有上下文物件,或者說是否被某個物件擁有或者包含;
    //隱式繫結的規則是呼叫位置是否有上下文物件,或者說是否被某個物件擁有或者包含
    //當函式引用有上下文物件時,隱式繫結規則會把函式呼叫中的 this 繫結到這個上下文物件
    function foo() {
    	console.log( this.a );//2
    }
    var obj = {
    	a: 2,
    	foo: foo
    };
    obj.foo(); 
    複製程式碼
    • 當函式引用有上下文物件時,隱式繫結規則會把函式呼叫中的 this 繫結到這個上下文物件;
    • 物件屬性引用鏈中只有最頂層或者說最後一層會影響呼叫位置;
    //物件屬性引用鏈中只有最頂層或者說最後一層會影響呼叫位置
    function foo() {
    	console.log( this.a );
    }
    var obj2 = {
    	a: 42,
    	foo: foo
    };
    var obj1 = {
    	a: 2,
    	obj2: obj2
    };
    obj1.obj2.foo(); //42
    複製程式碼

隱式丟失

  • 將函式通過隱式呼叫的形式賦值給一個變數;
注意:經典面試題,這是一個隱式丟失:
function foo() {
	console.log( this.a );//oops, global
}
var a = "oops, global"; 
var obj = {

	a: 2,
	foo: foo
};
var bar = obj.foo; //把obj.foo賦予別名bar,造成了隱式丟失,因為只是把foo()函式賦給了bar,而bar與obj物件則毫無關係
bar(); 

//等價於
var a = "oops, global"; 
var bar = function foo(){
    console.log( this.a );
}
bar();//oops, global
複製程式碼
  • 將函式通過隱式呼叫的形式進行傳參;
var a = 0;
function foo(){
    console.log(this.a);
};
function bar(fn){
    fn();
}
var obj = {
    a : 2,
    foo:foo
}
//把obj.foo當作引數傳遞給bar函式時,有隱式的函式賦值fn=obj.foo。與上例類似,只是把foo函式賦給了fn,而fn與obj物件則毫無關係。
bar(obj.foo);//0

//等價於
var a = 0;
function bar(fn){
    fn();
}
bar(function foo(){
    console.log(this.a);
});
複製程式碼
  • 內建函式:內建函式與上例類似,也會造成隱式丟失
var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(obj.foo,100);//0

//等價於
var a = 0;
setTimeout(function foo(){
    console.log(this.a);
},100);//0
複製程式碼

顯式繫結:call()、apply()、bind()

  • 通過call()、apply()、bind()方法把物件繫結到this上,叫做顯式繫結。
//普通物件的屬性查詢 
function foo(a,b) {
	console.log( this.a,a,b );
}
var obj = {
	a:2
};
foo.call( obj,"a","b"); //2 a b
foo.apply(obj,["a","b"])//2 a b
複製程式碼
  • 顯式繫結規則:call,apply和bind指定的物件(第一個引數);
  • 硬繫結:硬繫結是顯式繫結的一個變種,使this不能再被修改。它有一個包裹函式,有一個目標函式的顯示呼叫(bind,返回只是一個函式);可以用來解決隱式丟失。
//	我們來看看這個顯式繫結變種到底是怎樣工作的。我們建立了函式 bar() ,並在它的內部手動呼叫了 foo.call(obj) ,因此強制把 foo 的 this 繫結到了 obj 。無論之後如何呼叫函式 bar ,它總會手動在 obj 上呼叫 foo 。這種繫結是一種顯式的強制繫結,因此我們稱之為硬繫結。
function foo() {
	console.log( this.a );
}
var a =1;
var obj = {a:2};
var obj_test = {a:"test"};
var bar = function() {
	console.log( this.a );
	foo.call( obj );};
bar(); // 1 2
setTimeout( bar, 1000 ); // 1 2
bar.call( obj_test ); //test  2   
//硬繫結的bar不可能再修改它的this(指的是foo中的this)

//硬繫結的典型應用場景就是建立一個包裹函式,傳入所有的引數並返回接收到的所有值
	function foo(arg1,arg2) {
		console.log( this.a,arg1,arg2);
		return this.a + arg1;
	}
	var obj = {a:2};
	var bar = function() {
		return foo.apply( obj, arguments);
	};
	var b = bar(3,2); // 2 3 2
	console.log( b ); // 5
複製程式碼

new繫結

//3. 這個新物件會繫結到函式呼叫的 this 。
function foo(a) {
	this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2		
//使用 new 來呼叫 foo(..) 時,我們會構造一個新物件並把它繫結到 foo(..) 呼叫中的 this 上。 new 是最
//後一種可以影響函式呼叫時 this 繫結行為的方法,我們稱之為 new 繫結。	
複製程式碼

繫結例外

箭頭函式

  • 箭頭函式:this的繫結和作用域有關。如果在當前的箭頭函式作用域中找不到變數,就向上一級作用域裡去找。
  • 箭頭函式內部的 this 是詞法作用域,由上下文確定,此作用域稱作 Lexical this ,在程式碼執行前就可以確定。沒有其他大佬可以覆蓋。
  • 這樣的好處就是方便讓回撥函式的this使用當前的作用域,不怕引起混淆。所以對於箭頭函式,只要看它在哪裡建立的就行。
function foo() {
	 setTimeout(() => {
	    console.log('id:', this.id); //id: 42
	  }, 100);
}
var id = 21;
foo.call({ id: 42 })

// 再舉個栗子
var returnThis = () => this
returnThis() // window
new returnThis() // TypeError
var boss1 = {
  name: 'boss1',
  returnThis () {
    var func = () => this
    return func()
  }
}
returnThis.call(boss1) // still window
var boss1returnThis = returnThis.bind(boss1)
boss1returnThis() // still window
boss1.returnThis() // boss1
var boss2 = {
  name: 'boss2',
  returnThis: boss1.returnThis
}
boss2.returnThis() // boss2
複製程式碼

被忽略的this:null\undefined

  • 當被繫結的是null 或者 undefined,則this是預設的 context(嚴格模式下預設 context 是 undefined);
//	如果你把 null 或者 undefined 作為 this 的繫結物件傳入 call 、 apply 或者 bind ,這些值在呼叫時會被忽略,實際應用的是預設繫結規則;
function foo() {
	console.log( this.a );
}
var a = 2222;
foo.call( null ); // 2
複製程式碼

柯里化

function foo(a,b) {	
	console.log( "a:" + a + ", b:" + b );
}
// 把陣列“展開”成引數
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進行柯里化
var bar = foo.bind( null, [2] );
bar( 3 ); // a:2, b:3

//升級版:不會汙染全域性	
function foo(a,b) {
	this
	console.log( "a:" + a + ", b:" + b );
}
// 我們的DMZ空物件,“DMZ”(demilitarized zone,非軍事區)
var ø = Object.create( null );//{}
// 把陣列展開成引數
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用bind(..)進行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
複製程式碼

[]呼叫

  • 很特殊的一個呼叫,有點出乎意料的結果,特此記錄;
function fn (){ console.log(this) }
var arr = [fn, fn2]
arr[0]() // 這裡面的 this 又是什麼呢?
複製程式碼
  • 這個可以用文章最前面講的函式呼叫方法來想:
// 等價於
arr[0]()
假想為    arr.0()
然後轉換為 arr.0.call(arr)
那麼裡面的 this 就是 arr 了
複製程式碼

總結

  • this是函式執行的上下文物件;
  • 根據函式呼叫的方式不同this的值也不同:
    • 1.以函式的形式直接呼叫,this是window
    • 2.以物件方法的形式呼叫,this是呼叫方法的物件
    • 3.以建構函式的形式呼叫,this是新建立的那個物件
    • 4.使用call、apply和bind呼叫的函式,第一個引數就是this
    • 5.在建構函式 prototype 屬性中被呼叫,this仍然指的是例項物件;
    • 6.在事件處理函式中:this是指觸發當前事件的HTML DOM節點元素;
    • 7.箭頭函式中,this指的是建立時的詞法作用域;
  • 注意:
    • 在函式中 this 到底取何值,是在函式真正被呼叫執行的時候確定下來的,函式定義的時候確定不了。
    • this本身不具備任何含義。
<script>
    // TODO 一、以全域性&呼叫普通的函式的形式呼叫,this是window.
    function fn(){
        console.log(this);
    }
    fn();


    //二、建構函式
    //如果函式作為建構函式使用,那麼其中的this就代表即將new出來的物件
    function Objfn(){
        this.a = 10;
        console.log(this);//此時輸出的是物件 Objfn {a: 10}
    }
    var objfn = new Objfn();
    console.log('objfn.a='+objfn.a);//objfn.a=10
    
    //但是如果直接呼叫Objfn1()函式,而不是new Objfn1(),那麼情況就變成了Objfn()是普通函式
    function Objfn1(){
        this.a = 10;
        console.log(this);
    }
    var objfn1 = Objfn1();//此時輸出的是物件 Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
    //console.log('objfn.a='+objfn1.a);//錯誤,Cannot read property 'a' of undefined

    //三、物件方法
    //如果函式作為物件的方法,方法中的this指向該物件
    var obj={
        a:10,
        foo:function () {
            console.log(this);//Object {a: 10, foo: function}
            console.log(this.a);//10
        }
    }
    obj.foo();
    //注意,要是此時在物件方法中定義函式,那麼情況就不同了
    //此時的函式fn雖然是在 obj1.foo1內部定義的,但它仍然屬於一個普通的函式,this仍然指向window.
    var obj1={
        a1:10,
        foo1:function () {
            function fn(){
                console.log(this);//Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
                console.log(this.a1);//undefined
            }
            fn();
        }
    }
    obj1.foo1();
    //另外,如果此時foo2不作為物件方法被呼叫,則
    var obj2 = {
        x: 10,
        foo2: function () {
            console.log(this);       //Window
            console.log(this.x);     //undefined
        }
    };
    var fn2 = obj2.foo2;
    //等價於fn2 = function () {
        //console.log(this);       //Window
        //console.log(this.x);     //undefined
    //}
    fn2();//此時又是在全域性裡執行的普通函式。


    //四、建構函式的prototype屬性
    //在 Foof.prototype.getX函式中,this 指向的 Foof物件。不僅僅如此,即便是在整個原型鏈中,this 代表的也是當前Foof物件的值。
    function Foof(){
        this.x = 10;
    }
    Foof.prototype.getX = function () {
        console.log(this);        //Foof {x: 10}
        console.log(this.x);      //10
    }
    var foof = new Foof();
    foof.getX();


    //五、函式用call、apply或者bind呼叫
    var obja = {
        x: 10
    }
    function fooa(){
        console.log(this);     //Object {x: 10}
        console.log(this.x);   //10
    }
    fooa.call(obja);
    fooa.apply(obja);
    fooa.bind(obja)();
</script>
複製程式碼

相關文章