JavaScript中this的繫結規則
前言
我們知道瀏覽器執行環境下在全域性作用域下的this是指向window的,但是開發中卻很少在全域性作用域下去使用this,通常都是在函式中進行使用,而函式使用不同方式進行呼叫,其this的指向是不一樣的。JavaScript中函式在被呼叫時,會先建立一個函式執行上下文(FEC),而這個上下文中記錄著函式的呼叫棧、活動物件(AO)以及this等等。那麼this到底指向什麼呢?下面一起來看看this的四個繫結規則。
1.this四個繫結規則
1.1.預設繫結
在進行獨立函式呼叫時,this就會使用預設繫結,而預設繫結的值就是全域性的window。獨立函式呼叫可以簡單認為函式沒有被繫結到某個物件上進行呼叫。
滿足預設繫結的函式呼叫情況:
-
全域性定義的函式,直接在全域性進行呼叫;
function foo() { console.log(this) } foo() // window
-
多個函式進行巢狀呼叫,雖然以下函式巢狀了一層又一層,原則上還是獨立函式呼叫;
function foo1() { console.log(this) // window } function foo2() { console.log(this) // window foo1() } function foo3() { console.log(this) // window foo2() } foo3()
-
函式的返回值為一個函式,通過呼叫該函式將返回值賦值給某個變數,最後呼叫這個變數(最後fn的呼叫也是獨立函式呼叫,與函式的定義位置無關);
function foo() { return function() { console.log(this) } } const fn = foo() fn() // window
-
將物件中的方法賦值給別人,別人再進行呼叫,雖然是物件中的方法再次賦值,最後被賦值的物件都是進行獨立函式呼叫的;
-
obj中的bar方法作為引數傳遞到foo函式中,然後進行呼叫(最後被傳遞的引數進行了獨立呼叫)
function foo(func) { func() } const obj = { bar: function() { console.log(this) } } foo(obj.bar) // window
-
obj中的bar方法賦值給fn變數,然後進行呼叫(最後被賦值的fn進行了獨立呼叫)
const obj = { bar: function() { console.log(this) } } const fn = obj.bar fn() // window
-
1.2.隱式繫結
隱式繫結是開發中比較常見的,通過某個物件對函式進行呼叫,也就是說函式的呼叫是通過物件發起的,常見為物件中方法的呼叫(這個物件會被js引擎繫結到函式中的this裡面)。
滿足隱式繫結的函式呼叫情況:
-
直接呼叫物件中的方法;
const obj = { foo: function() { console.log(this) } } obj.foo() // obj物件
-
將函式定義在全域性,然後再賦值給物件屬性,再通過物件呼叫;
function bar() { console.log(this) } const obj = { foo: bar } obj.foo() // obj物件
-
呼叫物件中屬性值為物件中的方法(最後foo函式的呼叫發起者為obj2);
const obj1 = { name: 'obj1', obj2: { name: 'obj2', foo: function() { console.log(this) } } } obj1.obj2.foo() // obj2物件
1.3.顯示繫結
如果我們不希望使用隱式繫結的物件,而想在呼叫函式時給this繫結上自己想繫結的東西,那麼這樣的行為就稱為顯示繫結,顯示繫結主要藉助於三個方法,分別為call、apply和bind。
1.3.1.簡介
- call 方法:使用一個指定的this值和單獨給出的一個或多個引數來呼叫一個函式。
- apply方法:呼叫一個具有給定this值的函式,以及以一個陣列(或類陣列物件)的形式提供的引數。
- bind方法:建立一個新的函式,在 bind 被呼叫時,這個新函式的this被指定為bind的第一個引數,而其餘引數將作為新函式的引數,供呼叫時使用。
1.3.2.call、apply繫結this
JS中所有的函式都有call和apply方法屬性(主要是其原型鏈上實現了這兩個方法),可以說這兩個函式就是給this準備的。簡單介紹一下這兩個函式的區別:在進行函式呼叫時,都可以使用call和apply進行呼叫,其傳入的第一個引數都是一樣的,也就是我們希望給this繫結的值,後面的引數apply對應為包含多個引數的陣列,call對應為引數列表。
-
foo函式不僅可以直接進行呼叫,還可以通過call和apply進行呼叫,並且在執行foo函式時,可以明確繫結this;
function foo() { console.log(this) } const obj = { name: 'curry', age: 30 } foo() // window foo.call(obj) // obj物件 foo.apply(obj) // obj物件
-
當被呼叫函式有引數需要傳入時,就可以在call和apply後面傳入對應的引數;
function sum(x, y) { console.log(this, x + y) } const obj = { name: 'curry', age: 30 } sum(10, 20) // window 30 sum.call(obj, 10, 20) // obj物件 30 sum.apply(obj, [10, 20]) // obj物件 30
-
上面的示例都是給this繫結一個物件,其實還可以給this繫結不同資料型別;
function foo() { console.log(this) } // 1.繫結字串 foo.call('aaa') foo.apply('bbb') // 2.繫結字串 foo.call(111) foo.apply(222) // 3.繫結陣列 foo.call([1, 2, 3]) foo.apply(['a', 'b', 'c'])
1.3.3.bind繫結this
call和apply可以幫助我們呼叫函式明確this的繫結物件,而bind與這兩種的用法不太一樣。如果希望一個函式的this可以一直繫結到某個物件上,那麼bind就可以派出用場了。
-
上面提到了使用bind對this進行繫結時,會給我們返回一個新函式,而這個新函式的this就繫結為我們指定的物件;
function foo() { console.log(this) } const obj = { name: 'curry', age: 30 } const newFoo = foo.bind(obj) newFoo() // obj物件
-
使用bind繫結的this是不會被我們上面提到的繫結規則所改變的,比如,使用bind繫結的物件為obj1,不管是使用call、apply還是物件方法呼叫(隱式繫結),都不會改變this的指向了;
function foo() { console.log(this) } const obj1 = { name: 'curry', age: 30 } const obj2 = { name: 'kobe', age: 24 } const newFoo = foo.bind(obj1) newFoo() // obj1物件 newFoo.call(obj2) // obj1物件 newFoo.apply(obj2) // obj1物件 const obj3 = { bar: newFoo } obj3.bar() // obj1物件
1.4.new繫結
JavaScript中的函式不僅可以通過上面的方法進行呼叫,還可以使用new關鍵字來呼叫函式,當使用new來呼叫的函式,一般稱為建構函式(ES6中的類),以這樣的方式來呼叫的函式this所繫結的值,就叫做new繫結。
建構函式一般用於建立物件,通過new呼叫建構函式可以返回一個例項物件,而this繫結就是這個例項物件。那麼使用new呼叫時,函式內部會進行哪些操作呢?如下:
-
1.函式內部建立一個全新的物件(空物件);
-
2.這個新物件內部的[[prototype]]屬性(也就是物件原型)會被賦值為該建構函式的prototype屬性;
-
3.建構函式內部的this,會指向這個建立出來的新物件;
-
4.執行建構函式中的程式碼(函式體程式碼);
-
5.如果該建構函式沒有返回其它物件,則返回建立出來的新物件;
function Student(sno, name, age) { this.sno = sno this.name = name this.age = age console.log('this: ', this) } const stu1 = new Student(1, 'curry', 30) // this: {sno: 1, name: "curry", age: 30} console.log(stu1) // Student {sno: 1, name: "curry", age: 30} const stu2 = new Student(2, 'kobe', 24) // this: {sno: 2, name: "kobe", age: 24} console.log(stu2) // Student {sno: 2, name: "kobe", age: 24} console.log(stu1.__proto__ === Student.prototype) // true console.log(stu2.__proto__ === Student.prototype) // true
2.JS內建函式的this繫結
-
setTimeout:第一個引數在內部是進行獨立函式呼叫的;
setTimeout(function() { console.log(this) // window }, 1000) // ------相當於------ function mySetTimeout(callback, delay) { callback() // 獨立函式呼叫 }
-
DOM事件監聽:事件觸發後的回撥函式中的this是指向當前DOM物件的;
const boxDiv = document.querySelector('.box') boxDiv.onclick = function() { console.log(this) // boxDiv(DOM物件) } boxDiv.addEventListener('click', function() { console.log(this) // boxDiv(DOM物件) })
-
陣列內建方法:陣列中的forEach、map、filter、find等這些方法都可以傳入第二個引數,這個引數即為內部函式的this指向。
const arr = [1, 2, 3, 4, 5] const obj = { name: 'curry', age: 30 } arr.forEach(function() { console.log(this) // obj物件 }, obj) arr.map(function() { console.log(this) // obj物件 }, obj) arr.filter(function() { console.log(this) // obj物件 }, obj)
3.this繫結規則優先順序
上面提到了this的四種繫結規則,如果在函式呼叫時,使用到了多種繫結規則,最終函式的this指向什麼呢?那麼這裡就涉及到this繫結的優先順序,優先順序高的規則決定this的最終繫結。
this四種繫結規則優先順序如下:
-
預設繫結優先順序最低:函式呼叫存在其它規則時,就會遵循其它規則來繫結其this;
-
顯示繫結優先順序高於隱式繫結:
const obj1 = { name: 'obj1', foo: function() { console.log(this) } } const obj2 = { name: 'obj2' } obj1.foo.call(obj2) // obj2物件 obj1.foo.apply(obj2) // obj2物件 const newFoo = obj1.foo.bind(obj2) newFoo() // obj2物件
-
顯示繫結中bind高於call和apply:
function foo() { console.log(this) } const obj1 = { name: 'curry', age: 30 } const obj2 = { name: 'kobe', age: 24 } const newFoo = foo.bind(obj1) newFoo() // obj1物件 newFoo.call(obj2) // obj1物件 newFoo.apply(obj2) // obj1物件
-
new繫結優先順序高於隱式繫結和顯示繫結:
// 1.new繫結高於隱式繫結 const obj1 = { foo: function() { console.log(this) } } const f = new obj1.foo() // foo函式物件 // 2.new繫結高於顯示繫結 function foo(name) { console.log(this) } const obj2 = { name: 'obj2' } const newFoo = foo.bind(obj2) const nf = new newFoo(123) // foo函式物件
總結:
- new繫結 > 顯示繫結(apply/call/bind) > 隱式繫結 > 預設繫結;
- 注意new關鍵字不能和apply、call一起使用,所以不太好進行比較,預設為new繫結是優先順序最高的;
4.特殊情況下的this繫結
在特殊情況下,this的繫結不一定滿足上面的繫結規則,主要有以下特殊情況:
-
顯示繫結的忽略:在顯示繫結中,如果給call、apply和bind第一個引數傳入null或者undefined,那麼這樣的顯示繫結會被忽略,最終使用預設繫結,也就是全域性的window;
function foo() { console.log(this) } foo.call(null) // window foo.call(undefined) // window foo.apply(null) // window foo.apply(undefined) // window const newFoo1 = foo.bind(null) const newFoo2 = foo.bind(undefined) newFoo1() // window newFoo2() // window
-
間接函式的引用:建立一個函式的間接引用,該情況也使用預設繫結。如下程式碼中給obj2建立一個bar屬性並賦值為obj1中foo函式時直接進行呼叫;
const obj1 = { name: 'obj1', foo: function() { console.log(this) } } const obj2 = { name: 'obj2' } // 被認為是獨立函式呼叫 ;(obj2.bar = obj1.foo)() // window
-
ES6中的箭頭函式:箭頭函式不會使用上面的四種繫結規則,也就是說不繫結this,箭頭函式的this是根據它外層作用域中的this繫結來決定的;
-
陣列內建方法中回撥使用箭頭函式:
const names = ['curry', 'kobe', 'klay'] const obj = { name: 'obj' } names.map(() => { console.log(this) // window }, obj)
-
多層物件中的方法屬性使用箭頭函式:
const obj1 = { name: 'obj1', obj2: { name: 'obj2', foo: () => { console.log(this) } } } obj1.obj2.foo() // window
-
函式的返回值為箭頭函式:
function foo() { return () => { console.log(this) } } const obj = { name: 'curry', age: 30 } const bar = foo.call() bar() // obj物件
-
5.node環境下全域性this的指向
以上提到的內容都是在瀏覽器環境中進行測試的,在瀏覽器環境下的this是指向全域性window的,那麼在node環境中全域性this指向什麼呢?
-
在node環境下列印一下全域性this:
-
為什麼全域性this列印為一個空物件?
-
當我們js檔案被node執行時,該js檔案會被視為一個模組;
-
node載入編譯這個模組,將模組中的所有程式碼放入到一個函式中;
-
然後會執行這個函式,在執行這個函式時會通過apply繫結一個this,而繫結的this就為
{}
;
-
-
那為什麼node中還是可以使用想window中的全域性方法呢?像setTimeout、setInterval等。
-
因為node環境中也是存在全域性物件的,通過列印
globalThis
就可以進行檢視;
-