深刻淺出JavaScript的this機制

本文發於ONE中無一,轉載請註明出處,謝謝。javascript

「this 是在函數被調用時發生的綁定,它指向什麼徹底取決於函數在哪裏被調用。」java

說到this,可能要涉及到一點JavaScript基礎:數組

「詞法做用域」與「動態做用域」

一般來講,做用域一共有兩種主要的工做模型。瀏覽器

  • 詞法做用域
  • 動態做用域

詞法做用域:定義在詞法階段的做用域,也就是說詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏決定的緩存

動態做用域:動態做用域並不關心函數和做用域是如何聲明以及在任何處聲明的,只關心它們從何處調用。換句話說,做用域鏈是基於調用棧的,而不是代碼中的做用域嵌套閉包

JavaScript採用的是詞法做用域,大多數時候,對做用域產生混亂的主要緣由是分不清楚應該按照函數位置的嵌套順序,仍是按照函數的調用順序進行變量查找。再加上this機制的干擾,使得變量查找極易出錯。app


下面咱們就深刻了解一下JavaScript的this機制函數

this 的四種綁定規則

在JavaScript中,this綁定規則有四種:oop

  • 默認綁定ui

  • 隱式綁定

  • 顯式綁定

  • new 綁定

默認綁定

默認綁定是JavaScript中this綁定最基本、最直接的一種綁定方式,簡而言之,就是直接調用函數

function foo(){
    console.log(this.a); // 輸出: 2 直接調用 this指向全局對象window
}

var a = 2;
foo(); // 等價於 window.foo(); foo()由 window直接調用,故this指向 window 
複製代碼

注:在瀏覽器中全局對象是BOM的window對象,Node中是global對象

Node環境下,沒有window對象,只有全局的global對象
複製代碼

嚴格模式下this指向會有一些誤差,請注意區分

一、嚴格模式下,獨立調用 的函數的this爲undefined

二、非嚴格模式下,使用call、apply時,null、undefined會被轉換爲全局對象(window/global),嚴格模式下,this始終是指定的值

// demo1
function Bar() {
 "use strict";
	console.log( this.a );
}
var a = 2;
Bar(); // TypeError: `this` is `undefined` 

// demo2
'use strict';
function test() {
  console.log(this);
};
test();// undefined 

//demo3
function foo() {
	console.log( this.a );
}
var a = 2;

(function(){
 "use strict";	// 調用點在嚴格模式下而不是函數內容在嚴格模式下,this指向window
	foo(); // 2
})();

// demo4
var color = 'red';
function displayColor(){
    console.log(this.color);
}
displayColor.call(null);//red

var color = 'red';
function displayColor(){
 'use strict';
    console.log(this.color);
}
displayColor.call(null);// TypeError: Cannot read property 'color' of null
複製代碼

在默認綁定中存在幾種容易混淆的狀況:

  • 嵌套函數獨立調用(this默認綁定到window)
  • IIFE當即執行函數
  • 閉包

嵌套函數獨立調用

// Code nested
var a = 0;
var obj = {
    a : 2,
    foo:function(){
        function test(){
        //雖然test()函數被嵌套在obj.foo()函數中,但test()函數是獨立調用,而不是方法調用。因此this默認綁定到window
        console.log(this.a);
        }
        test(); 
    }
}
obj.foo();//0
複製代碼

IIFE(Imdiately Invoked Function Expression)當即執行函數

IIFE有一點的特殊性,但IIFE函數實際上就是函數聲明後直接調用執行,這樣this綁定就比較清楚了

// 上個例子 Code nested 等價於這個IIFE版本
var a = 0;
function foo(){
    (function test(){
        console.log(this.a);
    })()
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo();//0
複製代碼

閉包

var a = 0;
function foo(){
    function test(){
        console.log(this.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();// 0 閉包中返回的函數test在這裏是獨立調用,而不是方法調用,因此this指向window
複製代碼

閉包中的this默認指向window對象,有時候咱們須要在閉包中訪問嵌套函數的this,因此咱們也經常使用臨時變量 var that = this 緩存外層的this,而後在閉包中使用緩存變量that去訪問外層的this


隱式綁定

在隱式綁定中,this的指向可能受「上下文對象」影響,對於函數來說也就是方法調用。

形如obj.fn,調用點用obj對象來引用函數,this指向obj。看些例子:

function foo() {
	console.log( this.a );
}

var obj1 = {
	a: 2,
	foo: foo 
};
var obj2 = {
	a: 222,
    foo: foo
};

obj1.foo(); // 2
obj2.foo(); // 222

// 只有對象屬性引用鏈最後一層影響調用點
// 結果是42而不是2
var obj3 = {
	a: 42,
	foo: foo
};

var obj4 = {
	a: 2,
	obj3: obj3
};

obj4.obj3.foo(); // 42 誰直接調用,this就指向誰
複製代碼

注:當調用一個函數時,若是該函數具備上下文對象,this會被綁定到該上下文對象 當調用obj1.foo();this 指向 obj1

首先去判斷誰是最直接調用,誰直接調用this就指向誰,若是沒有,this指向window,這種方法適用於大部分狀況

特殊狀況:隱式丟失

隱式丟失是指被隱式綁定的函數丟失綁定對象,從而默認綁定到window。這是一種常見的問題,須要注意判別

隱式丟失通常有如下幾種狀況:

  • 【函數別名】
  • 【參數傳遞】
  • 【內置函數】
  • 【間接引用】
  • 【其餘狀況】

【函數別名】

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
// 把obj.foo賦予別名bar,形成了隱式丟失
// 由於只是把foo()函數賦給了bar,而bar與obj對象則毫無關係,故this指向window
var bar = obj.foo;
bar();//0

// 上面的例子等價於
var a = 0;
var bar = function foo(){
    console.log(this.a);
}
bar();// 0
複製代碼

【參數傳遞】

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);
});
複製代碼

函數做爲參數傳遞,若是想函數執行時保留this指向,使用硬綁定。在上例中,foo.bind(obj)代替obj.foo

【內置函數】

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
複製代碼

【間接引用】

函數的"間接引用"很容易在無心間建立,最容易在賦值時發生,會形成隱式丟失

// demo1
function foo() {
   console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
// 將o.foo函數賦值給p.foo函數,而後當即執行。
// 至關於僅僅是foo()函數的當即執行
(p.foo = o.foo)(); // 2

// demo2 對比 demo1
function bar () {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, bar: bar };
var p = { a: 4 };
o.bar(); // 3
// 將o.foo函數賦值給p.foo函數,以後p.foo函數再執行,是屬於p對象的foo函數的執行
p.bar = o.bar;
p.bar();// 4

複製代碼

【其餘狀況】

var a = 0;
var obj = {
    a : 2,
    foo:foo
};
function foo() {
    console.log( this.a );
};

(obj.foo = obj.foo)(); // 0

(false || obj.foo)(); // 0

(1, obj.foo)(); // 0 知識點:逗號操做符 對它的每一個操做數求值(從左到右),並返回最後一個操做數的值。
複製代碼

判斷函數時直接調用,仍是經過對象的方法調用就能夠判斷出this的指向


顯式綁定

顯式綁定是藉助 call(),apply(),bind() 去顯式的改變this指向。對於被調用的函數來講,叫作間接調用。

apply、call 的區別

在 JavaScript 中,call 和 apply 都是爲了改變某個**函數運行時的上下文(context)**而存在的,簡而言之,就是爲了改變函數體內部 this 的指向。兩者做用徹底同樣,都是當即執行函數,只是接受的參數形式不太同樣:

var func = function(arg1, arg2) {
};
func.call(this, arg1, arg2, ...)
func.apply(this, [arg1, arg2, ...])
複製代碼

其中 this 是你想指定的上下文,能夠是任何一個 JavaScript 對象(JavaScript 中一切皆對象)

call 須要把參數按順序傳遞進去。明確知道參數數量時使用

apply 則是把參數放在數組裏。不肯定參數數量時使用,而後把參數 push 進數組傳遞進去。當參數數量不肯定時,函數內部也能夠經過 arguments 這個數組來遍歷全部的參數【arguments 已經不推薦使用】。

說到JavaScript中上下文(context),又涉及到幾個經常使用的概念:「定義時上下文」和「運行時上下文」以及「上下文是能夠改變的」,這裏不深刻。

bind

MDN的解釋是: bind() 方法會建立一個新函數,稱爲綁定函數,當調用這個綁定函數時,綁定函數會以建立它時傳入bind()方法的第一個參數做爲 this,傳入bind()方法的第二個以及之後的參數加上綁定函數運行時自己的參數按照順序做爲原函數的參數來調用原函數。

bind() 則是建立一個新的包裝函數,而且返回該包裝函數,便於稍後調用,而不是馬上執行。

fun.bind(thisArg[, arg1[, arg2[, ...]]])
複製代碼

注:將null或者undefined做爲applycall或者bindthis指定值,回到默認綁定的規則。

因爲回到默認綁定規則考慮到污染全局對象,傳一個空對象替換null或`undefined,以下:

function foo(a,b){...}

// 空對象,也被稱爲DMZ空對象
var ø = Object.create( null );
                  
foo.apply(ø,[2,3])
複製代碼

顯式綁定中有個特例:硬綁定 在硬綁定中,this的指向不能再被修改

var a = 0;
function foo(){
    console.log(this.a);
}
var obj = {
    a:2
};
var bar= function(){
    foo.call(obj);
}
// 在bar函數內部手動調用foo.call(obj)。
// 所以,不管以後如何調用函數bar,它總會手動在obj上調用foo
bar(); // 2
setTimeout(bar,100); // 2
bar.call(window); // 2
複製代碼

上面提到到this的硬綁定 和 bind 方法,但硬綁定一旦綁定就沒法修改this指向,有一種軟綁定的實現方案提供了一種更加靈活的綁定方式

//softBind方法
if (!Function.prototype.softBind) {
	Function.prototype.softBind = function(obj) {
		var fn = this,
			curried = [].slice.call( arguments, 1 ),
			bound = function bound() {
				return fn.apply(
					(!this ||
						(typeof window !== "undefined" &&
							this === window) ||
						(typeof global !== "undefined" &&
							this === global)
					) ? obj : this,
					[].concat.call( curried, arguments )
				);
			};
		bound.prototype = Object.create( fn.prototype );
		return bound;
	};
}


//使用
function foo() {
   console.log("name: " + this.name);
}

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

var fooOBJ = foo.softBind( obj );

fooOBJ(); // name: obj

obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!

fooOBJ.call( obj3 ); // name: obj3 <---- 看!

setTimeout( obj2.foo, 10 ); // name: obj <---- 退回到軟綁定
複製代碼

此外JavaScript還有不少內置函數,默認具備顯示綁定的功能:如:map()、forEach()、filter()、some()、every()

var id = 'oops, global';
function foo(el){
    console.log(el,this.id);
}
var obj = {
    id: 'fn'
};
[1,2,3].forEach(foo);// 1 "oops, global" 2 "oops, global" 3 "oops, global"
[1,2,3].forEach(foo,obj);// 1 "fn" 2 "fn" 3 "fn"
複製代碼

new 綁定

new 運算符建立一個用戶定義的對象類型的實例或具備構造函數的內置對象的實例。

當使用new調用函數時,會執行如下操做(四個步驟):

  • 建立一個全新對象 :obj
  • 對建立的新對象執行 [[Prototype]] 鏈接:obj.__proto__ = foo.prototype
  • 綁定this到新建立的新對象上:foo函數內this指向obj
  • 由構造函數返回的對象就是 new 表達式的結果。若是函數沒有顯式的返回一個對象,那麼 new 表達式中的函數調用會自動返回這個建立的新對象(通常狀況下,構造函數不返回值,可是用戶能夠選擇主動返回對象,來覆蓋正常的對象建立步驟)
// this new綁定demo
function foo(a) {
	this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2
複製代碼

綜上:this的四種綁定規則:默認綁定、隱式綁定、顯式綁定和new綁定,分別對應函數的四種調用方式:獨立調用(直接調用)、方法調用、間接調用和構造函數調用。


優先級

「new 綁定」>「顯式綁定」> 「隱式綁定」> 「默認綁定」

// Demo1 
function foo(){
    console.log(this.a);
}
var obj = {
    a:222,
    foo,
};
obj.foo(); // 222;
obj.foo.call({a:1}); // 1 優先級: 顯式綁定 > 隱式綁定 

// Demo2
function func1(a){
    this.a = a;
}
var obj1 = {};
var bar = func1.bind(obj1);
bar(2);
console.log(obj1) // {a:2}

var obj2 = new bar(3);
console.log(obj1) // {a:2}
console.log(obj2) // {a:3} 優先級: new綁定 > 顯式綁定 
複製代碼

ES6 箭頭函數 =>

箭頭函數沒有本身的this, 它的this是繼承而來;ES6箭頭語法會保存函數建立時的this值,而不是調用時的值。因此this默認指向在定義它時所處的對象(宿主對象),而不是執行時的對象, 定義它的時候,可能環境是window; 箭頭函數能夠方便地讓咱們在 setTimeout ,setInterval中方便的使用this。

function foo() {
  // 返回一個箭頭函數
	return (a) => {
    // 這裏的 `this` 是詞法上從 `foo()` 採用的
		console.log( this.a );
	};
}

var obj1 = {
	a: 2
};

var obj2 = {
	a: 3
};

var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3!
複製代碼

箭頭函數的本質是詞法做用域(和調用點決定的機制不同)

相關文章
相關標籤/搜索