JavaScript中的this例題實戰總結詳析
前言
是否能夠深刻理解 this,是前端 JavaScript 進階的重要一環。
面向對象語言中 this 表示當前對象的一個引用,但在 JavaScript 中 this 不是固定不變的,它會隨著執行環境的改變而改變。
有一種廣為流傳的說法是:“誰調用它,this 就指向誰”。也就是說普通函數在定義的時候無法確定 this 引用取值,因為函數沒有被調用,也就沒有運行的上下文環境,因此在函數中 this 的引用取值,是在函數被調用的時候確定的。
函數在不同的情況下被調用,就會產生多種不同的運行上下文環境,所以 this 的引用取值也就是隨著執 行環境的改變而改變瞭。
還有一句經典的話:“匿名函數的 this 永遠指向 window”。
還有通過 call、apply、bind 和 new 操作符改變的 this 它們誰的優先級更高呢?還有它們的實現原理是怎麼樣的呢?最後我們手寫實現 call、apply、bind 和 new 操作符來進一步理解 this。
下面先根據具體環境來逐一分析。
普通函數中的 this
我們來看例題:請給出下面代碼的運行結果。
例題1
function f1 () { console.log(this) } f1() // window
普通函數在非嚴格的全局環境下調用時,其中的 this 指向的是 window。
例題2
"use strict" function f1 () { console.log(this) } f1() // undefined
用瞭嚴格模式 "use strict",嚴格模式下無法再意外創建全局變量,所以 this 不為 window 而為 undefined。
註意:babel 轉成 ES6 的,babel 會自動給 js 文件上加上嚴格模式。
箭頭函數中的 this
在箭頭函數中,this 的指向是由外層(函數或全局)作用域來決定的。
例題3
const Animal = { getName: function() { setTimeout(function() { console.log(this) }) } } Animal.getName() // window
此時 this 指向 window。這裡也印證瞭那句經典的話:“匿名函數的 this 永遠指向 window”。
如果要讓 this 指向 Animal 這個對象,則可以巧用箭頭函數來解決。
例題4
const Animal = { getName: function() { setTimeout(() => { console.log(this) }) } } Animal.getName() // {getName: ƒ}
嚴格模式對箭頭函數沒有效果
例題5
"use strict"; const f1 = () => { console.log(this) } f1() // window
我們都知道箭頭函數體內的 this 對象,就是定義時所在的對象,而不是使用時所在的對象。普通函數使用瞭嚴格模式 this 會指向 undefined 但箭頭函數依然指向瞭 window。
函數作為對象的方法中的 this
例題6
const obj = { name: "coboy", age: 18, add: function() { console.log(this, this.name, this.age) } }; obj.add(); // {name: "coboy", age: 18, add: ƒ} "coboy" 18
在對象方法中,作為對象的一個方法被調用時,this 指向調用它所在方法的對象。也就是開頭我們所說的那句:“誰調用瞭它,它就指向誰”,在這裡很明顯是 obj 調用瞭它。
例題7
const obj = { name: "coboy", age: 18, add: function() { console.log(this, this.name, this.age) } }; const fn = obj.add fn() // window
這個時候 this 則仍然指向 window。obj 對象方法 add 賦值給 fn 之後,fn 仍然在 window 的全局環境中執行,所以 this 仍然指向 window。
例題8
const obj = { name: "coboy", age: 18, add: function() { function fn() { console.log(this) } fn() } }; obj.add() // window
如果在對象方法內部聲明一個函數,這個函數的 this 在對象方法執行的時候指向就不是這個對象瞭,而是指向 window 瞭。
同樣想要 this 指向 obj 可以通過箭頭函數來實現:
const obj = { name: "coboy", age: 18, add: function() { const fn = () => { console.log(this) } fn() } }; obj.add() // obj
再次說明箭頭函數的 this 是由外層函數作用域或者全局作用域決定的。
上下文對象調用中的 this
例題9
const obj = { name: "coboy", age: 18, add: function() { return this } }; console.log(obj.add() === obj) // true
參考上文我們很容易知道 this 就是指向 obj 對象本身,所以返回 true。
例題10
const animal = { name: "coboy", age: 18, dog: { name: 'cobyte', getName: function() { console.log(this.name) } } }; animal.dog.getName() // 'cobyte'
如果函數中的 this 是被上一級的對象所調用的,那麼 this 指向的就是上一級的對象,也就是開頭所說的:“誰調用瞭它,它就指向誰”。
例題11
const obj1 = { txt: 'coboy1', getName: function() { return this.txt } } const obj2 = { txt: 'coboy2', getName: function() { return obj1.getName() } } const obj3 = { txt: 'coboy3', getName: function() { return obj2.getName() } } console.log(obj1.getName()) console.log(obj2.getName()) console.log(obj3.getName())
三個最終都打印瞭coboy1。
執行 obj3.getName 裡面返回的是 obj2.getName 裡面返回的結果,obj2.getName 裡面返回的是 obj1.getName 的結果,obj1.getName 返回的結果就是 'coboy1'。
如果上面的題改一下:
例題12
const obj1 = { txt: 'coboy1', getName: function() { return this.txt } } const obj2 = { txt: 'coboy2', getName: function() { return obj1.getName() } } const obj3 = { txt: 'coboy3', getName: function() { const fn = obj1.getName return fn() } } console.log(obj1.getName()) console.log(obj2.getName()) console.log(obj3.getName())
這個時候輸出瞭 coboy1, coboy1, undefined。
最後一個其實在上面例題5中已經有說明瞭。通過 const fn = obj1.getName 的賦值進行瞭“裸奔”調用,因此這裡的 this 指向瞭 window,運行結果當然是 undefined。
例題13
上述的例題10中的 obj2.getName() 如果要它輸出‘coboy2’,如果不使用 bind、call、apply 方法該怎麼做?
const obj1 = { txt: 'coboy1', getName: function() { return this.txt } } const obj2 = { txt: 'coboy2', getName: obj1.getName } console.log(obj1.getName()) console.log(obj2.getName())
上述方法同樣說明瞭那個重要的結論:this 指向最後調用它的對象。
我們將函數 obj1 的 getName 函數掛載到瞭 obj2 的對象上,getName 最終作為 obj2 對象的方法被調用。
在構造函數中的 this
通過 new 操作符來構建一個構造函數的實例對象,這個構造函數中的 this 就指向這個新的實例對象。同時構造函數 prototype 屬性下面方法中的 this 也指向這個新的實例對象。
例題14
function Animal(){ console.log(this) // Animal {} } const a1 = new Animal(); console.log(a1) // Animal {}
function Animal(){ this.txt = 'coboy'; this.age = 100; } Animal.prototype.getNum = function(){ return this.txt; } const a1 = new Animal(); console.log(a1.age) // 100 console.log(a1.getNum()) // 'coboy'
在構造函數中出現顯式 return 的情況。
例題15
function Animal(){ this.txt = 'coboy' const obj = {txt: 'cobyte'} return obj } const a1 = new Animal(); console.log(a1.txt) // cobyte
此時 a1 返回的是空對象 obj。
例題16
function Animal(){ this.txt = 'coboy' return 1 } const a1 = new Animal(); console.log(a1.txt) // 'coboy'
由此可以看出,如果構造函數中顯式返回一個值,且返回的是一個對象,那麼 this 就指向返回的對象,如果返回的不是一個對象,而是基本類型,那麼 this 仍然指向實例。
call,apply,bind 顯式修改 this 指向
call方法
例題17
const obj = { txt: "coboy", age: 18, getName: function() { console.log(this, this.txt) } }; const obj1 = { txt: 'cobyte' } obj.getName(); // this指向obj obj.getName.call(obj1); // this指向obj1 obj.getName.call(); // this指向window
apply方法
例題18
const obj = { txt: "coboy", age: 18, getName: function() { console.log(this, this.txt) } }; const obj1 = { txt: 'cobyte' } obj.getName.apply(obj1) // this指向obj1 obj.getName.apply() // this指向window
call 方法和 apply 方法的區別
例題19
const obj = { txt: "coboy", age: 18, getName: function(name1, name2) { console.log(this, name1, name2) } }; const obj1 = { txt: 'cobyte' } obj.getName.call(obj1, 'coboy1', 'coboy2') obj.getName.apply(obj1, ['coboy1', 'coboy2'])
可見 call 和 apply 主要區別是在傳參上。apply 方法與 call 方法用法基本相同,不同點主要是 call() 方法的第二個參數和之後的參數可以是任意數據類型,而 apply 的第二個參數是數組類型或者 arguments 參數集合。
bind 方法
例題20
const obj = { txt: "coboy", age: 18, getName: function() { console.log(this.txt) } }; const obj2 = { txt: "cobyte" } const newGetName = obj.getName.bind(obj2) newGetName() // this指向obj2 obj.getName() // this仍然指向obj
bind() 方法也能修改 this 指向,不過調用 bind() 方法不會執行 getName()函數,也不會改變 getName() 函數本身,隻會返回一個已經修改瞭 this 指向的新函數,這個新函數可以賦值給一個變量,調用這個變量新函數才能執行 getName()。
call() 方法和 bind() 方法的區別在於
- bind 的返回值是函數,並且不會自動調用執行。
- 兩者後面的參數的使用也不同。call 是 把第二個及以後的參數作為原函數的實參傳進去, 而 bind 實參在其傳入參數的基礎上往後獲取參數執行。
例題21
function fn(a, b, c){ console.log(a, b, c); } const fn1 = fn.bind({abc : 123},600); fn(100,200,300) // 100,200,300 fn1(100,200,300) // 600,100,200 fn1(200,300) // 600,200,300 fn.call({abc : 123},600) // 600,undefined,undefined fn.call({abc : 123},600,100,200) // 600,100,200
this 優先級
我們通常把通過 call、apply、bind、new 對 this 進行綁定的情況稱為顯式綁定,而把根據調用關系確定 this 指向的情況稱為隱式綁定。那麼顯示綁定和隱式綁定誰的優先級更高呢?
例題22
function getName() { console.log(this.txt) } const obj1 = { txt: 'coboy1', getName: getName } const obj2 = { txt: 'coboy2', getName: getName } obj1.getName.call(obj2) // 'coboy2' obj2.getName.apply(obj1) // 'coboy1'
可以看出 call、apply 的顯示綁定比隱式綁定優先級更高些。
例題23
function getName(name) { this.txt = name } const obj1 = {} const newGetName = getName.bind(obj1) newGetName('coboy') console.log(obj1) // {txt: "coboy"}
當再使用 newGetName 作為構造函數時。
const obj2 = new newGetName('cobyte') console.log(obj2.txt) // 'cobyte'
這個時候新對象中的 txt 屬性值為 'cobyte'。
newGetName 函數本身是通過 bind 方法構造的函數,其內部已經將this綁定為 obj1,當它再次作為構造函數通過 new 被調用時,返回的實例就已經和 obj1 解綁瞭。也就是說,new 綁定修改瞭 bind 綁定中的 this 指向,所以 new 綁定的優先級比顯式 bind 綁定的更高。
例題24
function getName() { return name => { return this.txt } } const obj1 = { txt: 'coboy1' } const obj2 = { txt: 'coboy2' } const newGetName = getName.call(obj1) console.log(newGetName.call(obj2)) // 'coboy1'
由於 getName 中的 this 綁定到瞭 obj1 上,所以 newGetName(引用箭頭函數) 中的 this 也會綁到 obj1 上,箭頭函數的綁定無法被修改。
例題25
var txt = 'good boy' const getName = () => name => { return this.txt } const obj1 = { txt: 'coboy1' } const obj2 = { txt: 'coboy2' } const newGetName = getName.call(obj1) console.log(newGetName.call(obj2)) // 'good boy'
例題26
const txt = 'good boy' const getName = () => name => { return this.txt } const obj1 = { txt: 'coboy1' } const obj2 = { txt: 'coboy2' } const newGetName = getName.call(obj1) console.log(newGetName.call(obj2)) // undefined
const 聲明的變量不會掛到 window 全局對象上,所以 this 指向 window 時,自然也找不到 txt 變量瞭。
箭頭函數的 this 綁定無法修改
例題27
function Fn() { return txt => { return this.txt } } const obj1 = { txt: 'coboy' } const obj2 = { txt: 'cobyte' } const f = Fn.call(obj1) console.log(f.call(obj2)) // 'coboy'
由於 Fn 中的 this 綁定到瞭 obj1 上,所以 f 中的 this 也會綁定到 obj1 上, 箭頭函數的綁定無法被修改。
例題28
var txt = '意外不' const Fn = () => txt => { return this.txt } const obj1 = { txt: 'coboy' } const obj2 = { txt: 'cobyte' } const f = Fn.call(obj1) console.log(f.call(obj2)) // '意外不'
如果將 var 聲明方式改成 const 或 let 則最後輸出為 undefined,原因是使用 const 或 let 聲明的變量不會掛載到 window 全局對象上。因此,this 指向 window 時,自然也找不到 txt 變量瞭。
從手寫 new 操作符中去理解 this
有一道經典的面試題,JS 的 new 操作符到底做瞭什麼?
- 創建一個新的空對象
- 把這個新的空對象的隱式原型(
__proto__
)指向構造函數的原型對象(prototype
) - 把構造函數中的 this 指向新創建的空對象並且執行構造函數返回執行結果
- 判斷返回的執行結果是否是引用類型,如果是引用類型則返回執行結果,new 操作失敗,否則返回創建的新對象
/* create函數要接受不定量的參數,第一個參數是構造函數(也就是new操作符的目標函數),其餘參數被構造函數使用。 new Create() 是一種js語法糖。我們可以用函數調用的方式模擬實現 */ function create(Fn,...args){ // 1、創建一個空的對象 let obj = {}; // let obj = Object.create({}); // 2、將空對象的原型prototype指向構造函數的原型 Object.setPrototypeOf(obj,Fn.prototype); // obj.__proto__ = Fn.prototype // 以上 1、2步還可以通過 const obj = Object.create(Fn.prototype) 實現 // 3、改變構造函數的上下文(this),並將參數傳入 let result = Fn.apply(obj,args); // 4、如果構造函數執行後,返回的結果是對象類型,則直接將該結果返回,否則返回 obj 對象 return result instanceof Object ? result : obj; // return typeof result === 'object' && result != null ? result : obj }
一般情況下構造函數沒有返回值,但是作為函數,是可以有返回值的,這就解析瞭上面例題15和例題16的原因瞭。 在 new 的時候,會對構造函數的返回值做一些判斷:如果返回值是基礎數據類型,則忽略返回值,如果返回值是引用數據類型,則使用 return 的返回,也就是 new 操作符無效。
從手寫 call、apply、bind 中去理解 this
手寫 call 的實現
Function.prototype.myCall = function (context, ...args) { context = context || window // 創建唯一的屬性防止污染 const key = Symbol() // this 就是綁定的那個函數 context[key] = this const result = context[key](...args) delete context[key] return result }
- myCall 中的 this 指向誰?
myCall 已經設置在 Function 構造函數的原型對象(prototype)上瞭,所以每個函數都可以調用 myCall 方法,比如函數 Fn.myCall(),根據 this 的確定規律:“誰調用它,this 就指向誰”,所以myCall方法內的 this 就指向瞭調用的函數,也可以說是要綁定的那個函數。
- Fn.myCall(obj) 本質就是把函數 Fn 賦值到 對象 obj 上,然後通過對象 obj.Fn() 來執行函數 Fn,那麼最終又回到那個 this 的確定規律:“誰調用它,this 就指向誰”,因為對象 obj 調用瞭 Fn 所以 Fn 內部的 this 就指向瞭對象 obj。
手寫 apply 的實現
apply 的實現跟 call 的實現基本是一樣的,因為他們的使用方式也基本一樣,隻是傳參的方式不一樣。apply 的參數必須以數組的形式傳參。
Function.prototype.myApply = function (context, args) { if(!Array.isArray(args)) { new Error('參數必須是數組') } context = context || window // 創建唯一的屬性防止污染 const key = Symbol() // this 就是綁定的那個函數 context[key] = this const result = context[key](...args) delete context[key] return result }
手寫 bind 的實現
bind 和 call、apply 方法的區別是它不會立即執行,它是返回一個改變瞭 this 指向的函數,在綁定的時候可以傳參,之後執行的時候,還可以繼續傳參數數。這個就是一個典型的閉包行為瞭,是不是。
我們先來實現一個簡單版的:
Function.prototype.myBind = function (ctx, ...args) { // 根據上文我們可以知道 this 就是調用的那個函數 const self = this return function bound(...newArgs) { // 在再次執行的的時候去改變 this 的指向 return self.apply(ctx, [...args, ...newArgs]) } }
但是,就如之前在 this 優先級分析那裡所展示的規則:bind 返回的函數如果作為構造函數通過 new 操作符進行實例化操作的話,綁定的 this 就會實效。
為瞭實現這樣的規則,我們就需要區分這兩種情況的調用方式,那麼怎麼區分呢?首先返回出去的是 bound 函數,那麼 new 操作符實例化的就是 bound 函數。通過上文 “從手寫 new 操作符中去理解 this” 中我們可以知道當函數被 new 進行實例化的時候, 構造函數內部的 this 就是指向實例化的對象,那麼判斷一個函數是否是一個實例化的對象的構造函數時可以通過 intanceof 操作符判斷。
知識點: instanceof 運算符用於檢測構造函數的 prototype
屬性是否出現在某個實例對象的原型鏈上。
Function.prototype.myBind = function (ctx, ...args) { // 根據上文我們可以知道 this 就是調用的那個函數 const self = this return function bound(...newArgs) { // new Fn 的時候, this 是 Fn 的實例對象 if(this instanceof Fn) { return new self(...args, ...newArgs) } // 在再次執行的的時候去改變 this 的指向 return self.apply(ctx, [...args, ...newArgs]) } }
另外我們也可以通過上文的實現 new 操作符的代碼來實現 bind 裡面的 new 操作。
完整的復雜版:
Function.prototype.myBind = function (ctx) { const self = this if (!Object.prototype.toString.call(self) === '[object Function]') { throw TypeError('myBind must be called on a function'); } // 對 context 進行深拷貝,防止 bind 執行後返回函數未執行期間,context 被修改 ctx = JSON.parse(JSON.stringify(ctx)) || window; const args = Array.prototype.slice.call(arguments, 1); /** * 構造函數生成對象實例 * @returns {Object|*} */ const create = function (conFn) { const obj = {}; /* 設置原型指向,確定繼承關系 */ obj.__proto__ = conFn.prototype; /** * 1、執行目標函數,綁定函數內部的屬性 * 2、如果目標函數有對象類型的返回值則取返回值,符合js new關鍵字的規范 */ const res = conFn.apply(obj, Array.prototype.slice.call(arguments,1)); return typeof res === 'object' && res != null ? res : obj; }; const bound = function () { // new 操作符操作的時候 if (this instanceof bound) { return create(self, args.concat(Array.prototype.slice.call(arguments))); } return self.apply(ctx, args.concat(Array.prototype.slice.call(arguments))); }; return bound; };
為什麼顯式綁定的 this 要比隱式綁定的 this 優先級要高
通過上面的實現原理,我們就可以理解為什麼上面的 this 優先級中通過 call、apply、bind 和 new 操作符的顯式綁定的 this 要比隱式綁定的 this 優先級要高瞭。例如上面的 obj1.getName.call(obj2) 中的 getName 方法本來是通過 obj1 來調用的,但通過 call 方法之後,實際 getName 方法變成瞭 obj2.getName() 來執行瞭。
總結
通過本篇內容的學習,我們看到 this 的用法紛繁多樣,確實不容易掌握。但總的來說可以總結為以下幾條規則:
- 在函數體中,非顯式或隱式地簡單調用函數時,在嚴格模式下,函數內的 this 會綁定到 undefined 上,在非嚴格模式下則會被綁定到全局對象 window/global 上。
- 一般使用 new 方法調用構造函數時,構造函數內的 this 會被綁定到新創建的對象上,且優先級要比 bind 的高。
- 一般通過 call、apply、bind 方法顯式調用函數時,函數體內的 this 會被綁定到指定參數的對象上,顯式綁定的 this 要比隱式綁定的 this 優先級要高。
- 一般通過上下文對象調用函數時,函數體內的 this 會被綁定到該對象上。
- 在箭頭函數中,this 的指向是由外層(函數或全局)作用域來決定的。
最後推薦一個學習 vue3 源碼的庫,它是基於崔效瑞老師的開源庫 mini-vue 而來,在 mini-vue 的基礎上實現更多的 vue3 核心功能,用於深入學習 vue3, 讓你更輕松地理解 vue3 的核心邏輯。
Github 地址:mini-vue3-plus
到此這篇關於JavaScript中this例題實戰的文章就介紹到這瞭,更多相關js this例題內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- JS 函數的 call、apply 及 bind 超詳細方法
- JavaScript函數之call、apply以及bind方法案例詳解
- JavaScript中call,apply,bind的區別與實現
- Vue3中Provide / Inject的實現原理分享
- 原生js如何實現call,apply以及bind