徹底搞明白this

来源:https://www.cnblogs.com/baller/archive/2020/06/03/13036816.html
-Advertisement-
Play Games

this是我們在書寫代碼時最常用的關鍵詞之一,即使如此,它也是JavaScript最容易被最頭疼的關鍵詞。那麼this到底是什麼呢? 如果你瞭解執行上下文,那麼你就會知道,其實this是執行上下文對象的一個屬性: executionContext = { scopeChain:[ ... ], VO ...


this是我們在書寫代碼時最常用的關鍵詞之一,即使如此,它也是JavaScript最容易被最頭疼的關鍵詞。那麼this到底是什麼呢?

如果你瞭解執行上下文,那麼你就會知道,其實this是執行上下文對象的一個屬性:

executionContext = {
    scopeChain:[ ... ],
    VO:{
        ...
    },
    this:  ? 
}

執行上下文中有三個重要的屬性,作用域鏈(scopeChain)、變數對象(VO)和this。

this是在進入執行上下文時確定的,也就是在函數執行時才確定,並且在運行期間不允許修改並且是永久不變的

在全局代碼中的this

在全局代碼中this 是不變的,this始終是全局對象本身。

var a = 10; 
this.b = 20;
window.c = 30;

console.log(this.a);
console.log(b);
console.log(this.c);

console.log(this === window) // true
// 由於this就是全局對象window,所以上述 a ,b ,c 都相當於在全局對象上添加相應的屬性

如果我們在代碼運行期嘗試修改this的值,就會拋出錯誤:

this = { a : 1 } ; // Uncaught SyntaxError: Invalid left-hand side in assignment
console.log(this === window) // true

函數代碼中的this

在函數代碼中使用this,才是令我們最容易困惑的,這裡我們主要是對函數代碼中的this進行分析。

我們在上面說過this的值是,進入當前執行上下文時確定的,也就是在函數執行時並且是執行前確定的。但是同一個函數,作用域中的this指向可能完全不同,但是不管怎樣,函數在運行時的this的指向是不變的,而且不能被賦值。

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

foo();  // window
var obj={
    a: 1,
    bar: foo,
}
obj.bar(); // obj

函數中this的指向豐富的多,它可以是全局對象、當前對象、或者是任意對象,當然這取決於函數的調用方式。在JavaScript中函數的調用方式有一下幾種方式:作為函數調用、作為對象屬性調用、作為構造函數調用、使用apply或call調用。下麵我們將按照這幾種調用方式一一討論this的含義。

作為函數調用

什麼是作為函數調用:就是獨立的函數調用,不加任何修飾符。

function foo(){
    console.log(this === window); // true
    this.a = 1;
    console.log(b); // 2
}
var b = 2;
foo();
console.log(a); // 1

上述代碼中this綁定到了全局對象window。this.a相當於在全局對象上添加一個屬性 a 。

在嚴格模式下,獨立函數調用,this的綁定不再是window,而是undefined

function foo() {
    "use strict";
    console.log(this===window); // false
    console.log(this===undefined); // true
}
foo();

這裡要註意,如果函數調用在嚴格模式下,而內部代碼執行在非嚴格模式下,this 還是會預設綁定為 window。

function foo() {
    console.log(this===window); // true
}


(function() {
    "use strict";
    foo();
})()

對於在函數內部的函數獨立調用 this 又指向了誰呢?

function foo() {
    function bar() {
        this.a=1;
        console.log(this===window); // true
    }
    bar()
}
foo();
console.log(a); // 1

上述代碼中,在函數內部的函數獨立調用,此時this還是被綁定到了window。

總結:當函數作為獨立函數被調用時,內部this被預設綁定為(指向)全局對象window,但是在嚴格模式下會有區別,在嚴格模式下this被綁定為undefined。

作為對象屬性調用

var a=1;
var obj={
    a: 2,
    foo: function() {
        console.log(this===obj); // true
        console.log(this.a); // 2
    }
}
obj.foo();

上述代碼中 foo屬性的值為一個函數。這裡稱 foo 為 對象obj 的方法。foo的調用方式為 對象 . 方法 調用。此時 this 被綁定到當前調用方法的對象。在這裡為 obj 對象。

再看一個例子:

var a=1;
var obj={
    a: 2,
    bar: {
        a: 3,
        foo: function() {
            console.log(this===bar); // true
            console.log(this.a); // 3
        }
    }
}
obj.bar.foo();

遵循上面說的規則 對象 . 屬性 。這裡的對象為 obj.bar 。此時 foo 內部this被綁定到了 obj.bar 。 因此 this.a 即為 obj.bar.a 。  

再來看一個例子: 

var a=1;
var obj={
    a: 2,
    foo: function() {
        console.log(this===obj); // false
        console.log(this===window); // true
        console.log(this.a); // 1
    }
}

var baz=obj.foo;
baz();

這裡 foo 函數雖然作為對象obj 的方法。但是它被賦值給變數 baz 。當baz調用時,相當於 foo 函數獨立調用,因此內部 this被綁定到 window。

使用apply或call調用

apply和call為函數原型上的方法。它可以更改函數內部this的指向。

var a=1;
function foo() {
    console.log(this.a);
}
var obj1={
    a: 2
}
var obj2={
    a: 3
}
var obj3={
    a: 4
}
var bar=foo.bind(obj1);
bar();// 2  this => obj1
foo(); // 1  this => window
foo.call(obj2); // 3  this => obj2
foo.call(obj3); // 4  this => obj3

當函數foo 作為獨立函數調用時,this被綁定到了全局對象window,當使用bind、call或者apply方法調用時,this 被分別綁定到了不同的對象。  

作為構造函數調用

var a=1;
function Person() {
    this.a=2;  // this => p;
}
var p=new Person();
console.log(p.a); // 2

上述代碼中,構造函數 Person 內部的 this 被綁定為 Person的一個實例。

總結:

當我們要判斷當前函數內部的this綁定,可以依照下麵的原則:

  • 函數是否在是通過 new 操作符調用?如果是,this 綁定為新創建的對象
var bar = new foo();     // this => bar;
  • 函數是否通過call或者apply調用?如果是,this 綁定為指定的對象
foo.call(obj1);  // this => obj1;
foo.apply(obj2);  // this => obj2;
  • 函數是否通過 對象 . 方法調用?如果是,this 綁定為當前對象
obj.foo(); // this => obj;
  • 函數是否獨立調用?如果是,this 綁定為全局對象。
foo(); // this => window

DOM事件處理函數中的this

1). 事件綁定

<button id="btn">點擊我</button>

// 事件綁定

function handleClick(e) {
    console.log(this); // <button id="btn">點擊我</button>
}
document.getElementById('btn').addEventListener('click',handleClick,false);  //   <button id="btn">點擊我</button>
        
document.getElementById('btn').onclick= handleClick; //  <button id="btn">點擊我</button>

根據上述代碼我們可以得出:當通過事件綁定來給DOM元素添加事件,事件將被綁定為當前DOM對象。

2).內聯事件

<button onclick="handleClick()" id="btn1">點擊我</button>
<button onclick="console.log(this)" id="btn2">點擊我</button>

function handleClick(e) {
    console.log(this); // window
}

//第二個 button 列印的是   <button id="btn">點擊我</button>

我認為內聯事件可以這樣理解:

//偽代碼

<button onclick=function(){  handleClick() } id="btn1">點擊我</button>
<button onclick=function() { console.log(this) } id="btn2">點擊我</button>

這樣我們就能理解上述代碼中為什麼內聯事件一個指向window,一個指向當前DOM元素。(當然瀏覽器處理內聯事件時並不是這樣的)

定時器中的this

定時器中的 this 指向哪裡呢?

function foo() {
    setTimeout(function() {
        console.log(this); // window
    },1000)
}
foo();  

再來看一個例子

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        console.log(this.name); // erdong
        setTimeout(function() {
            console.log(this.name); // chen
        },1000)
    }
}
obj.foo();

到這裡我們可以看到,函數 foo 內部this指向為調用它的對象,即:obj 。定時器中的this指向為 window。那麼有什麼辦法讓定時器中的this跟包裹它的函數綁定為同一個對象呢?

1). 利用閉包:

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        console.log(this.name)
        var that=this;
        setTimeout(function() {
            console.log(that.name); // window
        },1000)
    }
}
obj.foo();

利用閉包的特性,函數內部的函數可以訪問含義訪問當前詞法作用域中的變數,此時定時器中的 that 即為包裹它的函數中的 this 綁定的對象。在下麵我們會介紹利用 ES6的箭頭函數實現這一功能。

當然這裡也可以適用bind來實現:

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        console.log(this.name);
        setTimeout(function() {
            console.log(this.name); // window
        }.bind(this),1000)
    }
}
obj.foo();

被忽略的this


如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call 、apply或者bind,這些值在調用時會被忽略,實例 this 被綁定為對應上述規則。

var a=1;
function foo() {
    console.log(this.a); // 1  this => window
}
var obj={
    a: 2
}
foo.call(null);

 

var a=1;
function foo() {
    console.log(this.a); // 1  this => window
}
var obj={
    a: 2
}
foo.apply(null);

  

var a=1;
function foo() {
    console.log(this.a); // 1  this => window
}
var obj={
    a: 2
}
var bar = foo.bind(null);
bar();

bind 也可以實現函數柯里化:

function foo(a,b) {
    console.log(a,b); // 2  3
}
var bar=foo.bind(null,2);
bar(3);

更複雜的例子:  

 var foo={
    bar: function() {
        console.log(this);
    }
};

foo.bar(); // foo
(foo.bar)(); // foo

(foo.bar=foo.bar)(); // window
(false||foo.bar)();  // window
(foo.bar,foo.bar)();  // window

上述代碼中:

foo.bar()為對象的方法調用,因此 this 綁定為 foo 對象。

(foo.bar)() 前一個() 中的內容不計算,因此還是 foo.bar()

(foo.bar=foo.bar)() 前一個 () 中的內容計算後為 function() { console.log(this); } 所以這裡為匿名函數自執行,因此 this 綁定為 全局對象 window

後面兩個實例同上。

這樣理解會比較好:

(foo.bar=foo.bar)  括弧中的表達式執行為 先計算,再賦值,再返回值。
(false||foo.bar)()    括弧中的表達式執行為 判斷前者是否為 true ,若為true,不計算後者,若為false,計算後者並返回後者的值。
(foo.bar,foo.bar)   括弧中的表達式之行為分別計算 “,” 操作符兩邊,然後返回  “,” 操作符後面的值。

箭頭函數中的this


 

箭頭函數時ES6新增的語法。

有兩個作用:

  1. 更簡潔的函數
  2. 本身不綁定this

代碼格式為:

// 普通函數
function foo(a){
    // ......
}
//箭頭函數
var foo = a => {
    // ......
}

//如果沒有參數或者參數為多個

var foo = (a,b,c,d) => {
    // ......
}

我們在使用普通函數之前對於函數的this綁定,需要根據這個函數如何被調用來確定其內部this的綁定對象。而且常常因為調用鏈的數量或者是找不到其真正的調用者對 this 的指向模糊不清。在箭頭函數出現後其內部的 this 指向不需要再依靠調用的方式來確定。

箭頭函數有幾個特點(與普通函數的區別)

  1. 箭頭函數不綁定 this 。它只會從作用域鏈的上一層繼承 this。
  2. 箭頭函數不綁定arguments,使用reset參數來獲取實參的數量。
  3. 箭頭函數是匿名函數,不能作為構造函數。
  4. 箭頭函數沒有prototype屬性。
  5. 不能使用 yield 關鍵字,因此箭頭函數不能作為函數生成器。

這裡我們只討論箭頭函數中的this綁定。

用一個例子來對比普通函數與箭頭函數中的this綁定:

var obj={
    foo: function() {
        console.log(this); // obj
    },
    bar: () => {
        console.log(this); // window
    }
}
obj.foo();
obj.bar();

上述代碼中,同樣是通過對象 . 方法調用一個函數,但是函數內部this綁定確是不同,只因一個數普通函數一個是箭頭函數。

用一句話來總結箭頭函數中的this綁定:

個人上面說的它會從作用域鏈的上一層繼承 this ,說法並不是很正確。作用域中存放的是這個函數當前執行上下文與所有父級執行上下文的變數對象的集合。因此在作用域鏈中並不存在 this 。應該說是作用域鏈上一層對應的執行上下文中繼承 this 。

箭頭函數中的this繼承於作用域鏈上一層對應的執行上下文中的this

var obj={
    foo: function() {
        console.log(this); // obj
    },
    bar: () => {
        console.log(this); // window
    }
}
obj.bar();

上述代碼中obj.bar執行時的作用域鏈為:

scopeChain = [
    obj.bar.AO,
    global.VO
]

根據上面的規則,此時bar函數中的this指向為全局執行上下文中的this,即:window。

再來看一個例子:

var obj={
    foo: function() {
        console.log(this); // obj
        var bar=() => {
            console.log(this); // obj
        }
        bar();
    }
}
obj.foo();

在普通函數中,bar 執行時內部this被綁定為全局對象,因為它是作為獨立函數調用。但是在箭頭函數中呢,它卻綁定為 obj 。跟父級函數中的 this 綁定為同一對象。

此時它的作用域鏈為:

 scopeChain = [
     bar.AO,
     obj.foo.AO,
     global.VO
 ]

這個時候我們就差不多知道了箭頭函數中的this綁定。

繼續看例子:

var obj={
    foo: () => {
        console.log(this); // window
        var bar=() => {
            console.log(this); // window
        }
        bar();
    }
}
obj.foo();

這個時候怎麼又指向了window了呢?

我們還看當 bar 執行時的作用域鏈:

 scopeChain = [
     bar.AO,
     obj.foo.AO,
     global.VO
 ]

當我們找bar函數中的this綁定時,就會去找foo函數中的this綁定。因為它是繼承於它的。這時 foo 函數也是箭頭函數,此時foo中的this綁定為window而不是調用它的obj對象。因此 bar函數中的this綁定也為全局對象window。

我們在回頭看上面關於定時器中的this的例子:

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        console.log(this.name); // erdong
        setTimeout(function() {
            console.log(this); // chen
        },1000)
    }
}
obj.foo();

這時我們就可以很簡單的讓定時器中的this與foo中的this綁定為同一對象: 

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        console.log(this.name); // erdong
        setTimeout(() =>  {
            console.log(this.name); // erdong
        },1000)
    }
}
obj.foo();

  

如需轉載請註明出處。 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # 從零開始的前端生活-理解content(一) 替換元素 定義 我們把通過修改某個屬性值呈現的內容就可以被替換的元素被稱為"替換元素"。 比如 <img src="xxx.jpg"> ,我們只需修改屬性src的值,內容就會發生變化,這就是替換元素。 類似的有 <video>、<iframe>、< ...
  • web前端需要學習什麼知識?以下是我的一些分享,希望對你有幫助 想學好web前端,該從哪裡入手學習呢?零基礎學習web前端學習路線圖從哪裡可以找到呢?這裡為大家整理完整的零基礎 前端學習路線分享給大家。 適合零基礎學員的web前端學習路線分享給大家: 1、HTML5介紹 內容包括:(互聯網發展趨勢、 ...
  • Vue+Element 踩坑 1. 獲取後臺返回的數據,是個數組,迴圈後展示出來 <el-table-column :label="$t('common.cardModel.guz')" sortable="custom" prop="assetNo"> <template slot-scope=" ...
  • 1、概念 所謂盒子模型,就是把HTML頁面中的佈局元素看作是一個矩形的盒子,也就是一個盛裝內容的容器。css盒子模型本質上就是一個盒子,封裝周圍的HTML元素,它包括:邊框、外邊距、內邊距和實際內容 2、邊框 屬性:寬度、樣式、顏色 (1)普通方式 <html> <head> <meta chars ...
  • 據官網介紹這個電子錶格插件,是一款純前端類似excel的線上表格,功能強大、配置簡單、完全開源. 官網鏈接: Luckysheet官網 線上DEMO 特性包含: 表格設置,包括凍結行列、合併單元格、篩選、排序、查詢、條件格式、批註; 支持數據分析功能包括透視表、分列、矩陣操作、內置385個計算函數; ...
  • 三角形 利用border-color支持transparent這一特性,隱藏三條邊框,實現三角形。 <style> .triangle { width: 0; height: 0; border-style: solid; box-sizing: border-box; border-width: ...
  • 前言 瀑布流佈局是前端領域中一個很常見的需求,由於圖片的高度是不一致的,所以在多列佈局中預設佈局下很難獲得滿意的排列。 我們的需求是,圖片高度不規律的情況下,在兩列佈局中,讓左右兩側的圖片總高度儘可能的接近,這樣的佈局會非常的美觀。 註意,本文的目的僅僅是討論演算法在前端中能如何運用,而不是說瀑布流的 ...
  • 首先簡單介紹下自己,目前七年前端實際項目經驗,有一線大廠經驗,也去過國外。算的上是前端老鳥了,這篇文章是個人的經驗和心得,不吹不黑,中肯的態度來說,希望能夠幫到大家。 都2020年了,我現在學前端還來的及麽? 這是很多前端新人經常問我的一個問題,有學弟學妹,也有想轉行的朋友。 我的意識里,“大前端時 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...