徹底理解js中的this指向

2021-03-02 字節愛好者

可以說this與閉包、原型鏈一樣,屬於JavaScript開發中老生常談的問題了,百度一搜,this相關的文章鋪天蓋地。可開發好幾年,被幾道this題安排明明白白的人應該不在少數(我就是其一)。我覺得this概念抽象,變化多端總是讓人暈頭轉向,但平心它並不是有多難,今天我們就從this綁定的五種場景(默認綁定、隱式綁定、顯式綁定、new綁定、箭頭函數綁定)出發,靜下心來好好聊聊這個 this,本文開始。

this默認綁定this默認綁定我們可以理解為函數調用時無任何調用前綴的情景,它無法應對我們後面要介紹的另外四種情況,所以稱之為默認綁定,默認綁定時this指向全局對象(非嚴格模式):

function fn1() {    let fn2 = function () {        console.log(this); //window        fn3();    };    console.log(this); //window    fn2();};
function fn3() { console.log(this); //window};
fn1();

這個例子中無論函數聲明在哪,在哪調用,由於函數調用時前面並未指定任何對象,這種情況下this指向全局對象window。

但需要注意的是,在嚴格模式環境中,默認綁定的this指向undefined,來看個對比例子:

function fn() {    console.log(this); //window    console.log(this.name);};
function fn1() { "use strict"; console.log(this); //undefined console.log(this.name);};
var name = '聽風是風';
fn(); fn1() //TypeError: Cannot read property 'a' of undefined

再例如函數以及調用都暴露在嚴格模式中的例子:

"use strict";var name = '聽風是風';function fn() {    console.log(this); //undefined    console.log(this.name);//報錯};fn();

最後一點,如果在嚴格模式下調用不在嚴格模式中的函數,並不會影響this指向,來看最後一個例子:

var name = '聽風是風';function fn() {    console.log(this); //window    console.log(this.name); //聽風是風};
(function () { "use strict"; fn();}());

 this隱式綁定

1.隱式綁定

什麼是隱式綁定呢,如果函數調用時,前面存在調用它的對象,那麼this就會隱式綁定到這個對象上,看個例子:

function fn() {    console.log(this.name);};let obj = {    name: '聽風是風',    func: fn};obj.func() //聽風是風

如果函數調用前存在多個對象,this指向距離調用自己最近的對象,比如這樣:

function fn() {    console.log(this.name);};let obj = {    name: '行星飛行',    func: fn,};let obj1 = {    name: '聽風是風',    o: obj};obj1.o.func() //行星飛行

那如果我們將obj對象的name屬性注釋掉,現在輸出什麼呢?

function fn() {    console.log(this.name);};let obj = {    func: fn,};let obj1 = {    name: '聽風是風',    o: obj};obj1.o.func() //??

這裡輸出undefined,大家千萬不要將作用域鏈和原型鏈弄混淆了,obj對象雖然obj1的屬性,但它兩原型鏈並不相同,並不是父子關係,由於obj未提供name屬性,所以是undefined。

既然說到原型鏈,那我們再來點花哨的,我們再改寫例子,看看下面輸出多少:

function Fn() {};Fn.prototype.name = '時間跳躍';
function fn() { console.log(this.name);};
let obj = new Fn();obj.func = fn;
let obj1 = { name: '聽風是風', o: obj};obj1.o.func() //?

這裡輸出時間跳躍,雖然obj對象並沒有name屬性,但順著原型鏈,找到了產生自己的構造函數Fn,由於Fn原型鏈存在name屬性,所以輸出時間跳躍了。

番外-作用域鏈與原型鏈的區別:

當訪問一個變量時,解釋器會先在當前作用域查找標識符,如果沒有找到就去父作用域找,作用域鏈頂端是全局對象window,如果window都沒有這個變量則報錯。

當在對象上訪問某屬性時,首選i會查找當前對象,如果沒有就順著原型鏈往上找,原型鏈頂端是null,如果全程都沒找到則返一個undefined,而不是報錯。

 2.隱式丟失

在特定情況下會存在隱式綁定丟失的問題,最常見的就是作為參數傳遞以及變量賦值,先看參數傳遞:

var name = '行星飛行';let obj = {    name: '聽風是風',    fn: function () {        console.log(this.name);    }};
function fn1(param) { param();};fn1(obj.fn);//行星飛行

這個例子中我們將 obj.fn 也就是一個函數傳遞進 fn1 中執行,這裡只是單純傳遞了一個函數而已,this並沒有跟函數綁在一起,所以this丟失這裡指向了window。

第二個引起丟失的問題是變量賦值,其實本質上與傳參相同,看這個例子:

var name = '行星飛行';let obj = {    name: '聽風是風',    fn: function () {        console.log(this.name);    }};let fn1 = obj.fn;fn1(); //行星飛行

注意,隱式綁定丟失並不是都會指向全局對象,比如下面的例子:

var name = '行星飛行';let obj = {    name: '聽風是風',    fn: function () {        console.log(this.name);    }};let obj1 = {    name: '時間跳躍'}obj1.fn = obj.fn;obj1.fn(); //時間跳躍

雖然丟失了 obj 的隱式綁定,但是在賦值的過程中,又建立了新的隱式綁定,這裡this就指向了對象 obj1。

this顯式綁定顯式綁定是指我們通過call、apply以及bind方法改變this的行為,相比隱式綁定,我們能清楚的感知 this 指向變化過程。來看個例子:

let obj1 = {    name: '聽風是風'};let obj2 = {    name: '時間跳躍'};let obj3 = {    name: 'echo'}var name = '行星飛行';
function fn() { console.log(this.name);};fn(); //行星飛行fn.call(obj1); //聽風是風fn.apply(obj2); //時間跳躍fn.bind(obj3)(); //echo

比如在上述代碼中,我們分別通過call、apply、bind改變了函數fn的this指向。

在js中,當我們調用一個函數時,我們習慣稱之為函數調用,函數處於一個被動的狀態;而call與apply讓函數從被動變主動,函數能主動選擇自己的上下文,所以這種寫法我們又稱之為函數應用。

注意,如果在使用call之類的方法改變this指向時,指向參數提供的是null或者undefined,那麼 this 將指向全局對象。

let obj1 = {    name: '聽風是風'};let obj2 = {    name: '時間跳躍'};var name = '行星飛行';
function fn() { console.log(this.name);};fn.call(undefined); //行星飛行fn.apply(null); //行星飛行fn.bind(undefined)(); //行星飛行

另外,在js API中部分方法也內置了顯式綁定,以forEach為例:

let obj = {    name: '聽風是風'};
[1, 2, 3].forEach(function () { console.log(this.name);//聽風是風*3}, obj);

番外call、apply與bind有什麼區別?

1.call、apply與bind都用於改變this綁定,但call、apply在改變this指向的同時還會執行函數,而bind在改變this後是返回一個全新的boundFcuntion綁定函數,這也是為什麼上方例子中bind後還加了一對括號 ()的原因。

2.bind屬於硬綁定,返回的 boundFunction 的 this 指向無法再次通過bind、apply或 call 修改;call與apply的綁定只適用當前調用,調用完就沒了,下次要用還得再次綁。

3.call與apply功能完全相同,唯一不同的是call方法傳遞函數調用形參是以散列形式,而apply方法的形參是一個數組。在傳參的情況下,call的性能要高於apply,因為apply在執行時還要多一步解析數組。

描述一請參照上面已有例子。

描述二請參照下方例子,我們嘗試修改 boundFunction 的 this 指向:

let obj1 = {    name: '聽風是風'};let obj2 = {    name: '時間跳躍'};var name = '行星飛行';
function fn() { console.log(this.name);};fn.call(obj1); //聽風是風fn(); //行星飛行fn.apply(obj2); //時間跳躍fn(); //行星飛行let boundFn = fn.bind(obj1);//聽風是風boundFn.call(obj2);//聽風是風boundFn.apply(obj2);//聽風是風boundFn.bind(obj2)();//聽風是風

描述三請參考以下例子:

let obj = {    name: '聽風是風'};
function fn(age,describe) { console.log(`我是${this.name},我的年齡是${age},我非常${describe}!`);};fn.call(obj,'26','帥');//我是聽風是風,我的年齡是26,我非常帥fn.apply(obj,['26','帥']);//我是聽風是風,我的年齡是26,我非常帥

new綁定

準確來說,js中的構造函數只是使用new 調用的普通函數,它並不是一個類,最終返回的對象也不是一個實例,只是為了便於理解習慣這麼說罷了。

那麼new一個函數究竟發生了什麼呢,大致分為三步:

1.以構造器的prototype屬性為原型,創建新對象;

2.將this(可以理解為上句創建的新對象)和調用參數傳給構造器,執行;

3.如果構造器沒有手動返回對象,則返回第一步創建的對象

這個過程我們稱之為構造調用,我們來看個例子

function Fn(){    this.name = '聽風是風';};let echo = new Fn();echo.name//聽風是風

在上方代碼中,構造調用創建了一個新對象echo,而在函數體內,this將指向新對象echo上(可以抽象理解為新對象就是this)。

this綁定優先級

我們先介紹前四種this綁定規則,那麼問題來了,如果一個函數調用存在多種綁定方法,this最終指向誰呢?這裡我們直接先上答案,this綁定優先級為:

顯式綁定 > 隱式綁定 > 默認綁定

new綁定 > 隱式綁定 > 默認綁定

為什麼顯式綁定不和new綁定比較呢?因為不存在這種綁定同時生效的情景,如果同時寫這兩種代碼會直接拋錯,所以大家只用記住上面的規律即可。

function Fn(){    this.name = '聽風是風';};let obj = {    name:'行星飛行'}let echo = new Fn().call(obj);//報錯 call is not a function

那麼我們結合幾個例子來驗證下上面的規律,首先是顯式大於隱式:

//顯式>隱式let obj = {    name:'行星飛行',    fn:function () {        console.log(this.name);    }};obj1 = {    name:'時間跳躍'};obj.fn.call(obj1);// 時間跳躍

其次是new綁定大於隱式:

//new>隱式obj = {    name: '時間跳躍',    fn: function () {        this.name = '聽風是風';    }};let echo = new obj.fn();echo.name;//聽風是風

箭頭函數的this

ES6的箭頭函數是另類的存在,為什麼要單獨說呢,這是因為箭頭函數中的this不適用上面介紹的四種綁定規則。

準確來說,箭頭函數中沒有this,箭頭函數的this指向取決於外層作用域中的this,外層作用域或函數的this指向誰,箭頭函數中的this便指向誰。有點吃軟飯的嫌疑,一點都不硬朗,我們來看個例子:

function fn() {    return () => {        console.log(this.name);    };}let obj1 = {    name: '聽風是風'};let obj2 = {    name: '時間跳躍'};let bar = fn.call(obj1); // fn this指向obj1bar.call(obj2); //聽風是風

為啥我們第一次綁定this並返回箭頭函數後,再次改變this指向沒生效呢?

前面說了,箭頭函數的this取決於外層作用域的this,fn函數執行時this指向了obj1,所以箭頭函數的this也指向obj1。除此之外,箭頭函數this還有一個特性,那就是一旦箭頭函數的this綁定成功,也無法被再次修改,有點硬綁定的意思。

當然,箭頭函數的this也不是真的無法修改,我們知道箭頭函數的this就像作用域繼承一樣從上層作用域找,因此我們可以修改外層函數this指向達到間接修改箭頭函數this的目的。

function fn() {    return () => {        console.log(this.name);    };};let obj1 = {    name: '聽風是風'};let obj2 = {    name: '時間跳躍'};fn.call(obj1)(); // fn this指向obj1,箭頭函數this也指向obj1fn.call(obj2)(); //fn this 指向obj2,箭頭函數this也指向obj2

那麼到這裡,對於this的五種綁定場景就全部介紹完畢了,如果你有結合例子練習下來,我相信你現在對於this的理解一定更上一層樓了。

那麼通過本文,我們知道默認綁定在嚴格模式與非嚴格模式下this指向會有所不同。

我們知道了隱式綁定與隱式丟失的幾種情況,並簡單複習了作用域鏈與原型鏈的區別。

相對隱式綁定改變的不可見,我們還介紹了顯式綁定以及硬綁定,簡單科普了call、apply與bind的區別,並提到當綁定指向為null或undefined時this會指向全局(非嚴格模式)。

我們介紹了new綁定以及new一個函數會發生什麼。

最後我們了解了不太合群的箭頭函數中的this綁定,了解到箭頭函數的this由外層函數this指向決定,並有一旦綁定成功也無法再修改的特性。

希望在面試題中遇到this的你不再有所畏懼,到這裡,本文結束。

前端必看的電子書

獲取方式:

關注公眾號,後臺回覆:電子書

原文連結:https://www.cnblogs.com/echolun/p/11962610.html

相關焦點

  • 徹底理解js中this的指向
    ,只有函數執行的時候才能確定this到底指向誰,實際上this的最終指向的是那個調用它的對象(這句話有些問題,後面會解釋為什麼會有問題,雖然網上大部分的文章都是這樣說的,雖然在很多情況下那樣去理解不會出什麼問題,但是實際上那樣理解是不準確的,所以在你理解this的時候會有種琢磨不透的感覺),那麼接下來我會深入的探討這個問題。
  • js中對函數的深入理解(下)
    在js函數中,有一個特殊的對象this,this引用的就是函數執行的環境對象,當在全局作用域中的時候,this指向的對象就是window;這裡getName函數的作用域是window,當我們直接調用該函數的時候,this指向的是window對象
  • 三年經驗的前端,如何理解JS三座大山
    在 js 中,所有對象都是 Object 的實例,並繼承 Object.prototype 的屬性和方法,但是有一些是隱性的。我們來看一下原型的規則:所有的引用類型(包括數組,對象,函數)都具有對象特性;可自由擴展屬性。
  • JS 中的 with 關鍵字
    var hostName = hostname;    var url = href;}在這段代碼中,使用了with語句關聯了location對象,這就以為著在with代碼塊內部,每個變量首先被認為是一個局部變量,如果局部變量與location對象的某個屬性同名,則這個局部變量會指向location對象屬性。
  • 都0202年了,還在問JS中this的指向
    this是JavaScript中定義的一個關鍵字,是指向調用該this的對象的。this本身也是一個對象,可以在函數執行時保存該函數裡相關的值;其存在的目的是利用this指向的不同可在不同的對象環境中執行這個函數,達到復用的效果。換句話說,this的指向並不是在函數定義時確定的,而是在調用的時候確定的,即函數的調用方式決定了this的指向。
  • JS 中幾種輕鬆處理'this'指向方式
    我喜歡在JS中更改函數執行上下文的指向,也稱為 this 指向。function execute(func) {  return func();}execute(function() { return 10 }); 現在,繼續理解圍繞this錯誤的本質:方法分離。1.方法分離問題假設有一個類Person包含欄位firstName和lastName。
  • 在 Node.js 7 中甩掉 Callback Hell
    在幾個月之前,V8 引擎就實現了對 async/await 關鍵字的支持,Node.js 7中的 V8 經過幾次更新,終於在上一個 night build 版本中加入對為了解決 callback hell(回調地獄),程式設計師們一直在努力,從最早的回調函數,到 Promise 對象,再到 Generator 函數,每次都有所改進,但又讓人覺得不徹底 —— 因為使用它們之前都需要理解抽象的底層運行機制。
  • 【專業技術】關於JS的prototype
    概述:在接觸JS的過程中,隨著理解的深入會逐漸的理解一些比較深奧的理論或者知識
  • JavaScript學習- js中的typeof/instanceof
    之前我講了js中原型和原型鏈,那衍生出來了一些其他的知識面類型判斷。我主要講三個我們經常用到的,並且在面試中很大機率會被問到的知識。
  • 前端:js中修改this的指向方法整理
    call使用方法:fun.call(thisArg[,arg1[, arg2[, ……]]])該方法傳遞一個thisArgs和一個參數列表,thisArgs指定了函數在運行中的調用者,也就是函數中的this對象,而參數列表會被傳入調用函數中。
  • Per.js 2.6 版本發布,執行速度超快的 JS 響應式框架
    Per.js 2.6版本今天發布了,那麼讓我們來看看這次版本更新了哪些內容:dom方法增加info屬性,用來定義值,同時方法中的this指向info
  • 前端開發為什麼要學Node.js?小白如何深入理解Node.js?
    隨著網際網路的高速發展以及市場需求推動,Node已經成為前端知識棧必備技能之一,千鋒廣州Web大前端老師也發現很多企業在招聘中也會著重考察求職者對Node的掌握程度。有人好奇從事Web前端為什麼要學習Node.js?今天千鋒廣州Web前端培訓老師就給大家詳細的分析一下。
  • 乾貨 | 快速讀懂 JS 原型鏈
    ");const student2 = new Student("xiaohong");student1.study();student2.study();上面的代碼中,我們創建了 2 個 Student 實例,每個實例都有一個 study 方法,用來列印 "study js"。
  • 由一道題徹底弄懂 JS 繼承
    沒關係,先看下圖來理解:(原型鏈關係一目了然)原型上的方法ES6中繼承的實質是:先由父類( SuperClass)構造出實例對象this,這也是為什麼必須先調用父類的 super()方法(子類沒有自己的this對象,需先由父類構造)然後在子類的構造函數中,修改this(進行加工),譬如讓它指向子類原型( SubClass.prototype),這一步很關鍵,否則無法找到子類原型(注,子類構造中加工這一步的實際做法是推測出的
  • js this指針詳細介紹
    this指針指向哪裡?何時使用this?在JavaScript中,在創建函數時,系統默認會生成的兩個隱式參數:this和arguments。函數執行時,自動生成的一個內部對象,只能在函數內部使用。this指針指向與該函數調用進行隱式關聯的一個對象,該對象被稱為「函數上下文」。this是js的一個關鍵字,隨著函數使用場合不同,this的值會發生變化。
  • Per.js 2.5 版本發布,執行速度超快的 JS 響應式框架
    Per.js 2.5版本今天發布了,那麼讓我們來看看這次版本更新了哪些內容:修復data屬性bug修復for屬性響應式bug
  • 【Node.js系列】Express 介紹
    ',{msg:'訂單首頁'})})router.get('/list',function(req,res,next){    res.send('訂單列表')})//導出module.exports=router;頁面文件寫好以後需要在在app.js中進行掛載,var
  • CommonJS/ AMD/CMD/ES6模塊規範講解
    不過其實在發出來之前自己心裡也沒有太多底,不知道文章中說的一些東西對不對...唔...因此也是做好了被噴的準備,如果確實有理解不到位的地方還請大大們能夠指出,一起學習一下哈。我是這樣理解的:module這個變量它代表的就是整個模塊,也就是m1.js。
  • Backbone.js在大型單頁面應用中的應用實踐
    首先,Backbone.js的精髓是它定義前端MVC的方式和編碼哲學,並依據這些規定了如何去給代碼分層,因此Backbone.js能夠讓前端工程在可維護性和擴展性上都得到質的提升;同時,由於其良好且易於理解的結構,各個模塊之間都是鬆散耦合的,雖然目前官方並沒有提供根據實際需求build文件的功能,但如果你願意,完全可以自己手工刪掉源碼中的Bakcbone.View只使用Model和Collection
  • Node.js模塊化你所需要知道的事
    filename、children甚至都無需解釋,通過字面意思就可以理解。當然,當Node.js本地的node_modules目錄中找到了find-me模塊,就不會再去後續的目錄中繼續尋找了。Wrapping就是包裝,包裝的對象就是所有我們在模塊中寫的代碼。也就是我們引用模塊時,其實經歷了一層『透明』的包裝。要了解這個包裝過程,首先要理解exports和module.exports之間的區別。