close
點擊下方「前端技術優選」,選擇「設為星標」
第一時間關注技術乾貨!

this是一個比較迷惑人的東西,儘管你對this有很多的了解,但是面試題裡面考察this指向,總會讓你有種猜謎的感覺,知道一些,但是還是會出錯,或許你猜對了,但是又好像解釋不太清楚。

嗯,不只你一個人這樣,很多人都是這樣,包括我自己,本質上就是面試埋下的坑,讓你跳進去,你想跳過去,那還是不太容易,真正對知識的理解與應用,絕不只是停留在概念與理念,也不是為了完成一道面試題,答不對也沒關係,如果面試官給你耐心解釋了這道題,那也是一次不錯的學習機會。

正文開始...

在閱讀本文之前,主要會從以下幾點對this的思考

this是什麼時候產生的

迷惑的this在函數中的指向問題

箭頭函數中this

常用改變this的指向方案

this是什麼

全局this

為了了解this,我們先看下this,新建一個index.html與1.js


console.log(this, Object.getPrototypeOf(this));

index.html


<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>this</title></head><body> <div id="app"></div> <script src="./1.js"></script></body></html>

當我們在瀏覽器打開時,我們會發現this是一個window對象

如果我們在終端直接運行1.js呢


{} [Object: null prototype] {}

在node環境下,全局的this居然是一個{}對象

嚴格模式下函數內部的this

現在我們在js的最頂部使用use strict採用嚴格模式。

我們在函數內部寫一個this


"use strict"console.log(this, Object.getPrototypeOf(this));var publicName = "Maic";function hello() { console.log(this) // undefined console.log(this.publicName) // undefined}hello();

在嚴格模式下函數內部會是undefined,並且訪問publicName會直接報錯

為啥use strict嚴格模式下全局this無法訪問

於是查找資料尋得,嚴格模式主要有以下特徵

未提前申明的變量不能使用,會報錯

不能用delete刪除對象的屬性

定義的變量名不能重複申明

函數內部的this不再指向全局對象

還有其他的更多的參考js-script[1]

this的指向

在這之前我們很基礎的了解到在非嚴格模式下this指向的是window或者{}對象,在普通函數中this的指向是window全局對象

而你通常會看到this的指向並不都是指向全局對象,而是動態變化的,正因為它會變化,所以令人十分費腦殼

非嚴格模式普通函數this指向


function hello() { console.log(this) // window // console.log(this.publicName);}hello();

在普通函數內部this指向的是window對象

構造函數的this指向


...function Person() { this.age = 10; this.name = 'Web技術學苑'; console.log(this, '111')}const person = new Person();console.log(person, '222'); // Person { age: 10, name: 'Web技術學苑' }

至此你會發現,構造函數內部的this居然就是實例化的那個對象person

對象定義的內部函數


const userInfo = { publicName: 'Jack', getName: function () { console.log(this.name, '--useInfo') // Jack }}userInfo.getName();

不出意外打印都知道肯定publicName肯定是Jack,內部的this也是指向userInfo

箭頭函數的this

但是如果改成下面這種呢


var publicName = "Maic";const userInfo = { publicName: 'Jack', getName: () => { console.log(this.publicName, '---useInfo') }}userInfo.getName();

這是一個很迷惑的問題,箭頭函數不是沒有自己的this嗎,而且這裡是userInfo.getName()這不是一個隱式調用嗎?應也是userInfo這個對象才對,但是並不是,當改成箭頭函數後,內部的this居然變成了全局的window對象了

我們看下babel對上面一段代碼編譯成es5的代碼

es6代碼


var publicName = 'Maic';const userInfo = { publicName: 'Jack', getName: () => { console.log(this.publicName, '---useInfo') }}userInfo.getName();

編譯後的代碼,大概就是下面這樣的了


var _this = this;var publicName = "Maic";var userInfo = { publicName: "Jack", getName: function getName() { console.log(_this.publicName, "---useInfo"); }};userInfo.getName();

其實箭頭函數是非常迷惑人的,而且外面是一個被調用的是一個對象,所以時常會給人一種幻覺,我們常聽到一句this指向的是被調用的那個對象,那麼這裡箭頭函數的this指向的是window,而const定義的變量會被轉換成var

那怎麼能讓getName指向的是本身自己的useInfo呢


var publicName = 'Maic';const userInfo = { publicName: 'Jack', getName: function(){ console.log(this.publicName, '---useInfo') // Jack }}userInfo.getName();

你看當我把箭頭函數改成普通函數,這個普通函數內部的this就指向userInfo了

this指向被調用的那個對象貌似這句話後又在此時好像又是正確的

我們接下來看下下面一種情況


var publicName = 'Maic';const userInfo = { publicName: 'Jack', getName: function(){ console.log(this.publicName, '---useInfo') // Jack }}var user = userInfo.getName;user();

那麼此時getName內部的this又是誰呢?

此時你會發現打印的是Maic

此時會發現this指向的是window,也就是說指向的那個被調用者,那被調用者是誰?

上面那段代碼同等於下面,你仔細看


var publicName = 'Maic'; // var 定義,實際上等同於window.publicName = publicNamefunction getName () {console.log(this.publicName, '---useInfo') // Jack}const userInfo = { publicName: 'Jack', getName}// var user = userInfo.getName;// or 等價於// window.user = userInfo.getName;// or 進一步等價window.user = function getName () { console.log(this.publicName, '---useInfo') // Jack}// user();// or 等價於window.user();

所以你現在是不是很清晰明白this指向的也是被調用的那個對象window了

但是有一點必須申明,必須在非嚴格模式下,此時的this才會指向window。

迷失中的this指向

在這之前我們了解到非嚴格模式下

普通函數內部的this指向的是window對象

構造函數內的this指向的是實例化的那個對象

普通申明的對象,如果調用的方法是箭頭函數,那麼內部this指向的是全局對象,如果不是那麼指向的是被調用本身的那個對象

我們再來看下那些面試題中很迷惑的this


var user = { name: 'Maic', a: { name: 'Tom', b: function () { console.log(this.name) } }}console.log(user.a.b()) // Tom

沒錯,你看到的這個打印是Tom,這裡直接調用的是b這個方法,被調用的是user.a這個對象,所以在b這個方法內部的this指向了a對象

如果是箭頭函數呢


var name = "Maic";...var user = { name: 'Jack', a: { name: 'Tom', b: () => { console.log(this.name) } }}console.log(user.a.b()) // Maic

我們會發現通過babel轉換後會是這樣的


var _this = this;var user = { name: "Jack", a: { name: "Tom", b: function b() { console.log(_this.name); } }};

所以依然箭頭函數內部依然是個全局對象window

我們接下來看一道真實的面試題


var obj = { a: 1, b: function () { console.log(this.a) }, c: () => { console.log(this.a) }}var a = 2;var objb = obj.b;var objc = { a: 3}objc.b = obj.b;const t = objc.b;obj.b(); // 1obj.c(); // 2objb(); // 2objc.b(); // 3obj.b.call(null); // 2obj.b.call(objc); // 3t() // 2

我想信絕大大部分第一個obj.b()肯定是可以正確答出來,但是後面的貌似有些迷惑人,時常會讓你掉進坑裡

我們先看結論打印的依次肯定是


1223232

obj.b()的調用實際上在之前例子已經有講,b方法是一個普通方法,內部this指向的就是被調用的obj對象,所以此時內部訪問的a屬性就是對象obj

var objb = obj.b,當我們看到這樣的代碼時,其實這段代碼可以拆分以下


function b() { console.log(this.b)}window.objb = b;

本質上就是將對象obj的一個方法b賦值給了window.objb的一個屬性

所以objb()的調用也是window.objb(),objb方法內部this自然指向的就是window對象,而我們用var a = 2這個默認會綁定在window對象上

obj.c(),因為c是一個箭頭函數,所以內部的this就是指向的全局對象

obj.b.call(null)這個null是非常迷惑人,通常來說call不是改變函數內部this的指向嗎,但是這裡,如果call(null)實際上會默認指向window對象

objc.b()這打印的是3,其實與objb的賦值有異曲同工之筆


...var objc = { a: 3}objc.b = obj.b;

本質上就在objc動態的新增了一個屬性b,而這個屬性b賦值了一個方法,也就是下面這樣


objc.b = function() { console.log(this.a)}objc.b() // 3

如果是const t = objc.b,至此你會發現,當我們執行t()時,此時打印的卻是2那是因為const t定義的變量會編譯成var從而t變量變成一個全局的window對象下的屬性,本質上等價下面


...// const t = objc.bvar a = 2;/* 等價於下面var t = function() { console.log(this.a)}*/// 本質上就是window.t = function() { console.log(this.a)}

多層對象嵌套下的this


var nobj = { name: '1', a: { name: '2', b: { name: '3', c: function () { console.log(this.name) } } }}console.log(nobj.a.b.c()); //3

以上的結果是3,實際上我們從之前案例中明白,非嚴格模式下this指向被調用那個對象

所以你可以把上面那段代碼看成下面這樣


...console.log((nobj.a.b).c()); //3//or 相當於/** var n = nobj.a.b; n.c()*/
改變this對象的指向

這個相信很多小夥伴已經耳熟能祥了,call,apply,bind,能手撕call,apply,bind的文章已經不計其數

這裡就只講解如何使用,以及他們在業務中的一些具體使用場景

call

用一段偽代碼舉證以下


// index.vueimport configOption from './config'export default { name: 'index', computed: { optionsBtnGroup() { return configOption.call(this) } }, methods: { handleEdit(id) { console.log(id) }, handleDelete(id) { console.log(id) } }}

對應的template可能就是下面這樣幾個按鈕


<div> <a href="javascript:void(0)" v-for="(item, index) in optionsBtnGroup" :key="index" @click="item.handle(item.id)">{{item.text}}</a></div>

我們再來看下config.js


export default () => { const options = [ { text: '編輯', id: 123, handle: (id) => { this.handleEdit(id) } }, { text: '刪除', id: 234, handle: (id) => { this.handleDelete(id) } } ]}

正因為在計算屬性中用了call所以在config.js中才能訪問外部methods的方法,有些人看到這樣的代碼肯定會說,兩個按鈕這麼搞配置,代碼反而多了這麼多,還不如模版上放兩個按鈕完事

是的,確實是,當我們為了使用call而使用反而增加了業務代碼的維護成本,正常情況還是建議不要寫出上面那段壞代碼的味道,我們只要明白在什麼時候可以用,什麼可以不用就行,不要為了使用而使用,反而本末倒置。

但是有時候如果業務複雜,你想隔離業務的耦合,達到通用,call能幫你減少不少代碼量

apply

apply也是可以改變this對象


const userInfo = { publicName: 'Jack', getName: () => { console.log(this.publicName, '---useInfo') }}function test(...args) { console.log(args); // ['hello', 'world'] console.log(this.publicName);}test.apply(userInfo, ['hello', 'world'])

apply會立即執行該函數,如果傳入的首個參數是null或者undefined,那麼此時內部this指向的是window

另外還有一個方法可以讓函數立即執行,也能改變當前函數this指向


...var publicName = 'Maic';function test(...args) { console.log(args); console.log(this.publicName);}Reflect.apply(test, {publicName: 'aaa'}, [1,2,3]) // aaa [1,2,3]Reflect.apply(test, window, ['a', 'b', 'c']) // Maic ['a', 'b', 'c']

bind

這也是可以改變this指向,不過會返回一個新函數,我們常常在react中發現這樣用bind顯示綁定方案。

我們寫個簡單的例子,嘗試改變頁面背景,切換body膚色


document.body.addEventListener('click', function () { console.log(this) // body if (this.style.backgroundColor === 'red') { this.style.backgroundColor = 'green' } else { this.style.backgroundColor = 'red'; }})

可以切換背景膚色

以上貌似沒有問題,但是你可能會寫這樣的代碼


document.body.addEventListener('click', () => { console.log(this) if (this.style.backgroundColor === 'red') { this.style.backgroundColor = 'green' } else { this.style.backgroundColor = 'red'; }})

此時內部的this一定指向的window,而且內部訪問style報錯

於是你會改成這樣


const fn = function () { if (this.style.backgroundColor === 'red') { this.style.backgroundColor = 'green' } else { this.style.backgroundColor = 'red'; }}document.body.addEventListener('click', fn)

是的,這樣是可以的,本質上就是一個fn的形參,內部this指向仍然是document.body

於是為了藉助bind,你可以這麼做


const body = document.body;const fn = function () { if (this.style.backgroundColor === 'red') { this.style.backgroundColor = 'green' } else { this.style.backgroundColor = 'red'; }}.bind(body)body.addEventListener('click', fn)

這麼做也是ok的

不知道你有沒有疑問,為什不像下面這麼做呢?


const body = document.body;const fn = function () { if (this.style.backgroundColor === 'red') { this.style.backgroundColor = 'green' } else { this.style.backgroundColor = 'red'; }}body.addEventListener('click', fn.bind(this))

如果你仔細看下,其實fn內部this指向是window,所以這是一個常會犯的錯誤。

還有為啥不是像下面這樣


const body = document.body;const fn = function () { if (this.style.backgroundColor === 'red') { this.style.backgroundColor = 'green' } else { this.style.backgroundColor = 'red'; }}body.addEventListener('click', fn.bind(body))

以上功能沒有任何問題,但是我們每次點擊都會調用bind,從而返回一個新的函數,所以這種方式雖然效果一樣,但是性能遠不如第一種,為了更好理解,你可以寫成下面這樣


const body = document.body;const fn = function () { if (this.style.backgroundColor === 'red') { this.style.backgroundColor = 'green' } else { this.style.backgroundColor = 'red'; }}const callback = fn.bind(body)body.addEventListener('click', callback)
總結

了解this怎麼產生的,通常情況this在非嚴格模式下,指向的是全局window對象,在嚴格模式下,普通函數內的this不是全局對象

迷惑的this指向問題,正常情況this指向的是被調用的那個對象,但是如果是箭頭函數,那麼指向的是全局對象window

bind,call,apply改變this指向

code example[2]

推薦一篇關於阮一峰老師this[3]的博文

參考資料

[1]js-script:https://www.runoob.com/js/js-strict.html

[2]code example:https://github.com/maicFir/lessonNote/tree/master/javascript/05-this

[3]this:https://wangdoc.com/javascript/oop/this.html


在看點這裡
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()