[nodejs 內功心法] this全面解析

轉載請註明出處

this

在理解 this 的綁定過程之前,首先要理解調用位置: 調用位置就是函數在代碼中被調用位置(而不是聲明的位置)。

調用棧和調用位置

	function foo() {
		// 當前調用棧是:foo
		// 當前調用位置是全局作用域
		console.log("foo")
		bar() // bar的調用位置
	}
	function bar() {
		// 當前調用棧是 foo -> bar
		// 當前調用位置在foo中
		console.log("bar")
	}

注意我們是如何分析出函數真正的調用位置,因爲它決定了this的綁定

this的綁定規則

1.默認綁定

獨立函數調用時會使用默認綁定, 默認綁定this會指向全局對象或undefined(取決於是否是嚴格模式).
當函數運行在非嚴格模式時,默認綁定才能綁定到全局對象。嚴格模式下this會綁定到undefined

function foo() {
	console.log(this.varible)
}

varible = 2
foo() // 2

上面這個例子 this 指向了全局變量,那麼我們怎麼知道這裏應用了默認綁定呢?可以通過分析調用位置來看看 foo() 是如何調用的。在代碼中,foo() 是直接使用不帶任何修飾的函數引用進行調用的,因此只能使用默認綁定,無法應用其他規則。

function foo() {
  'use strict'
  console.log(this.varible)
}

varible = 2
foo() 
// TypeError: Cannot read property 'varible' of undefined
function foo() {
	console.log(this.varible)
}
varible = 2;

(function(){
	'use strict'
	foo()
})() // 2

//這裏就說明了只有在函數運行在嚴格模式下默認綁定纔會綁定undefinded, 
//而不是函數的調用位置是嚴格模式。
2.隱式綁定

這條規則主要是考慮調用位置是否有上下文對象

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

obj = { a: 100, foo: foo }

obj.foo() // 100

上面的代碼中當 foo() 被調用時,它的落腳點確實指向 obj 對象。當函數引用有上下文對象時,隱式綁定規則會把函數調用中的 this 綁定到這個上下文對象。因爲調用 foo() 時 this 被綁定到 obj,因此 this.a 和 obj.a 是一樣的。

對象屬性引用鏈中只有最後一層會影響this的綁定(或者說離函數調用最近的那個對象會影響)

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

obj2 = {
	a: 200,
	foo: foo
}

obj1 = {
	a: 100,
	obj2: obj2
}

obj1.obj2.foo() //200

隱式丟失

一個最常見的 this 綁定問題就是被隱式綁定的函數會丟失綁定對象,也就是說它會應用默認綁定,從而把 this 綁定到全局對象或者 undefined 上,取決於是否是嚴格模式。

function demo() {
	console.log(this.a)
}

const obj = {
	a: 100,
	demo: demo
}

const bar = obj.demo
a = 200
bar() // 200

// 此時的 bar() 其實是一個不帶任何修飾的函數調用,
// 因此應用了默認綁定。
function demo() {
	console.log(this.a)
}

const obj = {
	a: 1,
	demo: demo
}

a = "i am global"

// 在瀏覽器中執行
setTimeout(obj.demo, 100) // i am global

// 在node.js環境中執行
setTimeout(obj.demo, 100) // undefined
// 這是因爲node.js 中 _onTimeout = obj.demo 是被timer._onTimeout()調用。
// 所以this爲timer對象
3.顯示綁定

在分析隱式綁定時,我們必須在一個對象內部包含一個指向函數的屬性,並通過這個屬性間接引用函數,從而把 this 間接(隱式)綁定到這個對象上。

call 和 apply

它們的第一個參數是一個對象,它們會把這個對象綁定到 this,接着在調用函數時指定這個 this。因爲可以直接指定 this 的綁定對象,因此我 們稱之爲顯式綁定。

function demo() {
	console.log(this.a)
}
const obj = { a: 1 }

demo.call(obj) // 1

如果你傳入了一個原始值(字符串類型、布爾類型或者數字類型)來當作 this 的綁定對象,這個原始值會被轉換成它的對象形式(也就是new String(…)、new Boolean(…)或者 new Number(…))。這通常被稱爲“裝箱”。

call 和 apply的區別:

它們的區別只是參數的區別。

  1. apply的第二個參數是數組,會結構成參數列表傳給方法。Function.apply(obj, args)
  2. call可以傳入一個參數列表 Function.call(obj,[params1[,params2]])
硬綁定

es5提供了內置的方法Function.prototype.bind, bind(…) 會返回一個硬編碼的新函數,它會把參數設置爲this的上下文並調用原始函數。

function demo(str) {
	console.log(this.a, str)
}

const obj = { a: 2 }

const bar = demo.bind(obj);

bar("heihei"); // 2 heihei

forEach的第二個參數就可以指定this上下文(context)

function demo(el) {
	console.log(el, this.id);
}
const obj = { id: 'test!!' };

[1,2,3].forEach(demo, obj)

// 1 'test!!'
// 2 'test!!'
// 3 'test!! 

這些函數實際上就是通過 call(…) 或者 apply(…) 實現了顯式綁定

4. new 綁定

在傳統的面向類的語言中,“構造函數”是類中的一些特殊方法,使用 new 初始化類時會調用類中的構造函數。通常的形式是這樣的:

     something = new MyClass(..);

JavaScript 也有一個 new 操作符,使用方法看起來也和那些面向類的語言一樣,絕大多數開發者都認爲 JavaScript 中 new 的機制也和那些語言一樣。然而,JavaScript 中 new 的機制實際上和麪向類的語言完全不同。

在 JavaScript 中,構造函數只是一些使用 new 操作符時被調用的函數。它們並不會屬於某個類,也不會實例化一個類。實際上, 它們甚至都不能說是一種特殊的函數類型,它們只是被 new 操作符調用的普通函數而已。

使用 new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操作。(這一部分比較重要,大家要重點學習一下)

  1. 創建或者說構造一個全新的對象
  2. 這個對象會被執行[[原型]]連接
  3. 這個新對象會綁定到函數的this
  4. 如果函數沒有返回其他對象,那麼new表達式中的函數調用會自動返回這個新對象
function demo(a) {
 this.a = a
}

const obj = new demo(2)
console.log(obj) // demo { a: 2 }

// 使用 new 來調用 demo(..) 時,我們會構造一個新對象並把它綁定到 demo(..) 調用中的 this 上。

new 是最後一種可以影響函數調用時 this 綁定行爲的方法,我們稱之爲 new 綁定。

優先級

這四種規則的優先級

1.隱式和顯示

function demo() {
	console.log(this.a)
}

const obj1 = { a: 1, demo: demo }
const obj2 = { a: 2, demo: demo }

obj1.demo() // 1
obj2.demo() // 2

obj1.demo.call(obj2) // 2
obj2.demo.call(obj1) // 1

可以看到顯示綁定的優先級更高

  1. new 和 隱式綁定
function demo(str) {
	this.a = str
}

const obj1 = { demo: demo }

obj1.demo(2);
console.log(obj1.a) // 2

obj1.demo.call(obj2={}, 3)
console.log(obj2.a) // 3

const obj3 = new obj1.demo(4)
console.log(obj1.a) // 2
console.log(obj3.a) // 4

const obj5 = obj1.demo.bind(obj4=Object.create(null))
obj5(5)
console.log(obj4.a) //5

const obj6 = new obj5(6)
console.log(obj4.a) // 5
console.log(obj6.a) // 6

可以看到new綁定的優先級比隱式綁定和bind硬綁定都要高, 這是因爲es5的bind方法的實現會判斷硬綁定函數是否是被 new 調用,如果是的話就會使用新創建 的 this 替換硬綁定的 this。(new綁定是否優先級高這是根據bind的實現決定的)

爲什麼要在 new 中使用硬綁定函數呢? 直接使用普通函數不是更簡單嗎?

之所以要在 new 中使用硬綁定函數,主要目的是預先設置函數的一些參數,這樣在使用 new 進行初始化時就可以只傳入其餘的參數。bind(…) 的功能之一就是可以把除了第一個 參數(第一個參數用於綁定 this)之外的其他參數都傳給下層的函數(這種技術稱爲“部 分應用”,是“柯里化”的一種)

function demo(arg1, arg2) {
	this.a = arg1 + arg2
}

const bar = demo.bind(null, "debug: ")
const foo = new bar("i am jim")

console.log(foo.a) // debug: i am jim

總結一下上面的內容

this 綁定優先級順序

  1. 函數是否在new中調用(new綁定)? 如果是的話this綁定的是新創建的對象 (ex: const bar = new foo())
  2. 函數是否通過call、apply(顯式綁定)或者硬綁定調用? 如果是的話,this綁定的是指定的對象。(ex: bar = foo.call(obj2))
  3. 函數是否在某個上下文對象中調用(隱式綁定)? 如果是的話,this 綁定的是那個上下文對象。(ex: bar = obj.foo())
  4. 如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到undefined,否則綁定到全局對象(window 或者 global)。(ex: var bar = foo())

對於正常的函數調用來說,理解了這些知識你就可以明白 this 的綁定原理了。不過還有一些例外的情況

1.如果你把 null 或者 undefined 作爲 this 的綁定對象傳入 call、apply 或者 bind,這些值在調用時會被忽略,實際應用的是默認綁定規則(即被綁定到全局對象(window 或者 global))
// 運行在非嚴格模式下,如果在嚴格模式下會報錯,因爲嚴格模式this是默認綁定undefined
function demo() {
	this.a = 100
}

demo.call(null)
// run in node.js
console.log(global.a) // 100

// run in browser
console.log(window.a) // 100	

使用null 和 undefined作爲this綁定的情況

一種非常常見的做法是使用 apply(…) 來“展開”一個數組,並當作參數傳入一個函數。類似地, bind(…) 可以對參數進行柯里化(預先設置一些參數),這種方法有時非常有用

function test(a, b) {
	console.log("a:" + a + ", b:" + b)
}

// 把數組展開成參數
test.apply(null, [2,3])
// 在 ES6 中,可以用 ... 操作符代替 apply(..) 來“展
// 開”數組, foo(...[1,2]) 和 foo(1,2) 是一樣的,這樣可以避免不必要的
// this 綁定。可惜,在 ES6 中沒有柯里化的相關語法,因此還是需要使用
// bind(..) 。

// 使用bind來進行柯里化
const curring = test.bind(null, 2)
curring(5) // a:2, b:5

然而,總是使用 null 來忽略 this 綁定可能產生一些副作用。如果某個函數確實使用了this (比如第三方庫中的一個函數),那默認綁定規則會把 this 綁定到全局對象(在瀏覽器中這個對象是 window ),這將導致不可預計的後果(比如修改全局對象)。顯而易見,這種方式可能會導致許多難以分析和追蹤的 bug。

更安全的this

一種“更安全”的做法是傳入一個特殊的對象,把 this 綁定到這個對象不會對你的程序產生任何副作用。我們可以創建一個“DMZ”(demilitarized
zone,非軍事區)對象, 它就是一個空的對象。
如果我們在忽略 this 綁定時總是傳入一個 DMZ 對象,那就什麼都不用擔心了,因爲任何對於 this 的使用都會被限制在這個空對象中,不會對全局對象產生任何影響。

在 JavaScript 中創建一個空對象最簡單的方法都是 Object.create(null)
Object.create(null) 和 {} 很 像, 但 是 並 不 會 創 建 Object.prototype 這個委託,所以它比 {} “更空”

function test(a,b) {
	console.log( "a:" + a + ", b:" + b );
}
// 我們的 DMZ 空對象
var ø = Object.create( null );

// 把數組展開成參數
test.apply( ø, [2, 3] ); // a:2, b:3

// 使用 bind(..) 進行柯里化
var bar = test.bind( ø, 2 );
bar( 3 ); // a:2, b:3
間接引用

另一個需要注意的是,你有可能(有意或者無意地)創建一個函數的“間接引用”,在這種情況下,調用這個函數會應用默認綁定規則。

function test() {
	console.log(this.a)
}

var a = 1
var foo = { a: 3, test }
var bar = { a: 4 }
foo.test() // 3
(bar.test = foo.test)(); // 1

賦值表達式 p.foo = o.foo 的返回值是目標函數的引用,因此調用位置是 foo() 而不是p.foo() 或者 o.foo() 。根據我們之前說過的,這裏會應用默認綁定。

注意:

對於默認綁定來說,決定 this 綁定對象的並不是調用位置是否處於嚴格模式,而是函數體是否處於嚴格模式。如果函數體處於嚴格模式, this 會被綁定到 undefined ,否則this 會被綁定到全局對象。

this詞法 (針對箭頭函數() => {})

我們之前介紹的四條規則已經可以包含所有正常的函數。但是 ES6 中介紹了一種無法使用這些規則的特殊函數類型:箭頭函數。
箭頭函數並不是使用 function 關鍵字定義的,而是使用被稱爲“胖箭頭”的操作符 => 定義的。
箭頭函數不使用 this 的四種標準規則,而是根據外層(函數或者全局)作用域來決定 this 。

function test() {
	return () => {
		// this 繼承自test
		console.log(this.a)
	}
}

const obj1 = { a: 1 }
const obj2 = { a: 2 }

const foo = test.call(obj1)
foo.call(obj2) // 1 
//並不是返回 2

foo() 內部創建的箭頭函數會捕獲調用時 foo() 的 this 。由於 foo() 的 this 綁定到 obj1 , bar (引用箭頭函數)的 this 也會綁定到 obj1 ,箭頭函數的綁定無法被修改。( new 也不行!)

箭頭函數最常用於回調函數中,例如事件處理器或者定時器:

function foo() {
  setTimeout(() => {
    // 這裏的 this 在此法上繼承自 foo()
    console.log( this.a );
  },100);
}

var obj = {
  a:2
};

foo.call( obj ); // 2

如果你經常編寫 this 風格的代碼,但是絕大部分時候都會使用 self = this 或者箭頭函數來否定 this 機制,那你或許應當:

  1. 只使用詞法作用域並完全拋棄錯誤 this 風格的代碼;
  2. 完全採用 this 風格,在必要時使用 bind(…) ,儘量避免使用 self = this 和箭頭函數。

當然,包含這兩種代碼風格的程序可以正常運行,但是在同一個函數或者同一個程序中混合使用這兩種風格通常會使代碼更難維護,並且可能也會更難編寫。

小結

如果要判斷一個運行中函數的 this 綁定,就需要找到這個函數的直接調用位置。找到之後就可以順序應用下面這四條規則來判斷 this 的綁定對象。

  1. 由 new 調用?綁定到新創建的對象。
  2. 由 call 或者 apply (或者 bind )調用?綁定到指定的對象。
  3. 由上下文對象調用?綁定到那個上下文對象。
  4. 默認:在嚴格模式下綁定到 undefined ,否則綁定到全局對象。

一定要注意,有些調用可能在無意中使用默認綁定規則。如果想“更安全”地忽略 this 綁定,你可以使用一個 DMZ 對象,比如 ø = Object.create(null) ,以保護全局對象。

ES6 中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法作用域來決定this ,具體來說,箭頭函數會繼承外層函數調用的 this 綁定(無論 this 綁定到什麼)。這其實和 ES6 之前代碼中的 self = this 機制一樣

排版可能有點亂,還需要多多改進,看官們多包涵,哈哈!

轉載請註明出處喲:https://blog.csdn.net/a675697174/article/details/103689501

發佈了35 篇原創文章 · 獲贊 2 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章