javascript基礎修煉(3)—What's this(下)

来源:https://www.cnblogs.com/dashnowords/archive/2018/08/04/9410498.html
-Advertisement-
Play Games

開發者的javascript造詣取決於對【動態】和【非同步】這兩個詞的理解水平。 這一期主要分析各種實際開發中各種複雜的 指向問題。 一. 嚴格模式 嚴格模式是 ES5 中添加的 的另一種運行模式,它可以禁止使用一些語法上不合理的部分,提高編譯和運行速度,但語法要求也更為嚴格,使用 標記開啟。 嚴格模 ...


開發者的javascript造詣取決於對【動態】和【非同步】這兩個詞的理解水平。

這一期主要分析各種實際開發中各種複雜的this指向問題。

一. 嚴格模式

嚴格模式是ES5中添加的javascript的另一種運行模式,它可以禁止使用一些語法上不合理的部分,提高編譯和運行速度,但語法要求也更為嚴格,使用use strict標記開啟。

嚴格模式中this的預設指向不再為全局對象,而是預設指向undefined。這樣限制的好處是在使用構造函數而忘記寫new操作符時會報錯,而不會把本來需要綁定在實例上的一堆屬性全綁在window對象上,在許多沒有正確地綁定this的場景中也會報錯。

二. 函數和方法的嵌套與混用

詞法定義並不影響this的指向 , 因為this是運行時確定指向的。

2.1 函數定義的嵌套

function outerFun(){
    function innerFun(){
        console.log('innerFun內部的this指向了:',this);
    }
    innerFun();
}
outerFun();

控制台輸出的this指向全局對象。

2.2 對象屬性的嵌套

當調用的函數在對象結構上的定義具有一定深度時,this指向這個方法所在的對象,而不是最外層的對象。

var IronMan = {
    realname:'Tony Stark',
    rank:'1',
    ability:{
        total_types:100,
        fly:function(){
            console.log('IronMan.ability.fly ,作為方法調用時this指向:',this);
        },
        
    }
}
IronMan.ability.fly();

控制台輸出的this指向IronManability屬性所指向的對象,調用fly( )這個方法的對象是IronMan.ability所指向的對象,而不是IronMan所指向的對象。

this作為對象方法調用時,標識著這個方法是如何被找到的。IronMan這個標識符指向的對象信息並不能在運行時找到fly( )這個方法的位置,因為ability屬性中只存了另一個對象的引用地址,而IronMan.ability對象的fly屬性所記錄的指向,才能讓引擎在運行時找到這個匿名方法。

三. 引用轉換

引用轉換實際上並不會影響this的指向,因為它是詞法性質的,發生在定義時,而this的指向是運行時確定的。只要遵循this指向的基本原則就不難理解。

3.1 標識符引用轉換為對象方法引用

var originFun = function (){
    console.log('originFun內部的this為:',this);
}
var ironMan = {
    attack:originFun
};
ironMan.attack();

這裡的this指向其調用者,也就是ironMan引用的對象。

3.2 對象方法轉換為標識符引用

var ironMan = {
    attack:function(){
        console.log('對象方法中this指向了:',this);
    }
}
var originFun = ironMan.attack;
originFun();

這裡的this指向全局對象,瀏覽器中也就是window對象。3.2中的示例被認為是javascript語言的bug,即this指向丟失的問題。同樣的問題也可能在回調函數傳參時發生,本文【第5章】將對這種情況進行詳細說明。

四. 回調函數

javascript中的函數是可以被當做參數傳遞進另一個函數中的,也就有了回調函數這樣一個概念。

4.1 this在回調函數中的表現

  var IronMan = {
       attack:function(findEnemy){
           findEnemy();
       }
  }

  function findEnemy(){
     console.log('已聲明的函數被當做回調函數調用,this指向:',this);
  }

  var attackAction = {
      findEnemy:function(){
        console.log('attackAction.findEnemy本當做回調函數調用時,this指向',this);
      },
      isArmed:function(){
        console.log('check whether the actor is Armed');
      }
  }

  //1.直接傳入匿名函數
  IronMan.attack(function(){
      console.log(this);
  });

  //2.傳入外部定義函數
  IronMan.attack(findEnemy);

  //3.傳入外部定義的對象方法
  IronMan.attack(attackAction.findEnemy);

從控制台列印的結果來看,無論以哪種方式來傳遞迴調函數,回調函數執行時的this都指向了全局變數。

4.2 原理

javascript中函數傳參全部都是值傳遞,也就是說如果調用函數時傳入一個原始類型,則會把這個值賦值給對應的形參;如果傳入一個引用類型,則會把其中保存的記憶體指向的地址賦值給對應的形參。所以在函數內部操作一個值為引用類型的形參時,會影響到函數外部作用域,因為它們均指向記憶體中的同一個函數。詳細可參考[深入理解javascript函數系列第二篇——函數參數]這篇博文。

理解了函數傳參,就很容易理解回調函數中this為何指向全局了,回調函數對應的形參是一個引用類型的標識符,其中保存的地址直接指向這個函數在記憶體中的真實位置,那麼通過執行這個標識符來調用函數就等同於this基本指向規則中的作為函數來調用的情況,其this指向全局對象也就不難理解了。

五. this指針丟失

在第三節和第四節中,通過原理分析就能夠明白為何在一些特定的場合下this會指向全局對象,但是從語言的角度來看,卻很難理解this為什麼指向了全局對象,因為這個規則和語法的字面意思是有衝突的。

5.1 回調函數的字面語境

var name = 'HanMeiMei';
var liLei = {
      name:'liLei',
      introduce:function () {
          console.log('My name is ', this.name);
         }
    };
var liLeiSay = liLei.introduce;
liLeiSay();//同第三節中的引用轉換示例
setTimeout(liLei.introduce,2000);//同第四節中的回調函數示例

上面的代碼從字面上看意義是很明確的,就是希望liLei立刻介紹一下自己,在2秒後再介紹一下他自己。但控制台輸出的結果中,他卻兩次都說自己的名字是HanMeiMei

5.2 this指針丟失

5.1中的示例,也稱為this指針丟失問題,被認為是Javascript語言的設計失誤,因為這種設計在字面語義上造成了混亂。

5.3 this指針修複

方式1-使用bind

為了使代碼的字面語境和實際執行保持一致,需要通過顯示指定this的方式對this的指向進行修複。常用的方法是使用bind( )生成一個確定了this指向的新函數,將上述示例改為如下方式即可修複this的指向:

var liLeiSay = liLei.introduce.bind(liLei);
setTimeout(liLei.introduce.bind(liLei),2000);

bind( )的實現其實並不複雜,是閉包實現高階函數的一個簡單的實例,感興趣的讀者可以自行瞭解。

方式2-使用Proxy

Proxy是ES6中才支持的方法。

//綁定This的函數
function fixThis (target) {
    const cache = new WeakMap();
    //返回一個新的代理對象
    return new Proxy(target, {
        get (target, key) {
          const value = Reflect.get(target, key);
          //如果要取的屬性不是函數,則直接返回屬性值
          if (typeof value !== 'function') {
            return value;
          }
          if (!cache.has(value)) {
            cache.set(value, value.bind(target));
          }
          return cache.get(value);
        }
    });
}

const toggleButtonInstance = fitThis(new ToggleButton());

兩種修複this指向的思路其實很類似,第一種方式相當於為調用的方法創建了一個代理方法,第二種方式是為被訪問的對象創建了一個代理對象

六. this的透傳

實際開發過程中,往往需要在更深層次的函數中獲取外層this的指向。

常規的解決方案是:將外層函數的this賦值給一個局部變數,通會使用_this,that,self,_self等來作為變數名保存當前作用域中的this。由於在javascript中作用域鏈的存在,嵌套的內部函數可以調用外部函數的局部變數,標識符會去尋找距離作用域鏈末端最近的一個指向作為其值,示例如下:

document.querySelector('#btn').onclick = function(){
    //保存外部函數中的this
    var _this = this;
    _.each(dataSet, function(item, index){
        //回調函數的this指向了全局,調用外部函數的this來操作DOM元素
        _this.innerHTML += item;
    });  
}

七. 事件監聽

事件監聽中this的指向情況其實是幾種情況的集合,與代碼如何編寫有很大關係。

7.1 表現

1. 在html文件中使用事件監聽相關的屬性來觸發方法

<button onclick="someFun()">點擊按鈕</button>
<button onclick="someObj.someFun()">點擊按鈕</button>

如果以第一種方式觸發,則函數中的this指向全局;

如果以第二種方式觸發,則函數中的this指向someObj這個對象。

2. 在js文件中直接為屬性賦值

//聲明一個函數 
function callFromHTML() {
          console.log('callFromHTML,this指向:',this);
}
//定義一個對象方法
var obj = {
        callFromObj:function () {
            console.log('callFromObj',this);
        }
      }
//註冊事件監聽-方式1 
document.querySelector('#btn').onclick = function (event) {
          console.log(this);
} 
//註冊事件監聽-方式2
document.querySelector('#btn').onclick = callFromHTML;

//註冊事件監聽-方式3
document.querySelector('#btn').onclick = obj.callFromObj;
 

以上三種註冊的事件監聽響應函數,其this均指向id="btn"的DOM元素。

3. 使用addEventListener方法註冊響應函數

//低版本IE瀏覽器中需要使用另外的方法
document.querySelector('#btn').addEventListener('click',function(event){
    console.log(this);
});
//也可以將函數名或對象方法作為回調函數傳入
document.querySelector('#btn').addEventListener('click',callFromHTML);
document.querySelector('#btn').addEventListener('click',obj.callFromObj);

這種方式註冊的響應函數,其this場景2相同,均指向id="btn"的DOM元素。區別在於使用addEventListener方法添加的響應函數會依次執行,而採用場景2的方式時,只有最後一次賦值的函數會被調用。

7.2 基本原理

1. 通過標簽屬性註冊

<button id="btn" onclick="callFromHTML()">點我</button>
<script>
   function callFromHTML() {
          console.log(document.querySelector('#btn').onclick);
   }
</script>

在html中綁定事件處理程式,然後當按鈕點擊時,在控制台列印出DOM對象的onclick屬性,可以看到:

這種綁定方式其實是將監聽方法包裹在另一個函數中去執行,相當於:

document.querySelector('#btn').onclick = function(event){
    callFromHTML();
}

這樣上述的表現就不難理解了。

2. 通過元素對象屬性註冊

document在javascript中是一個對象,通過其暴露的查找方法返回的節點也是一個對象,那麼方式二綁定的監聽函數在運行時,實際上就是在執行指定節點的onclick方法,根據this指向的基本規則可知其函數體中的this應該指向調用對象,也就是onclick這個方法所在的節點對象。

3. 通過addEventListener方法註冊

這種方式是在DOM2事件模型中擴展的,用於支持多個監聽器綁定的場景。DOM2事件模型的描述中規定了通過這種方式添加的監聽函數執行時的this指向所在的節點對象,不同內核的瀏覽器實現方式有區別。

7.3 使用建議

不同的使用方式實質上是伴隨著DOM事件模型升級而發生改變的,現代瀏覽器對於以上幾種模式都是支持的,只有需要相容老版本瀏覽器時需要考慮對DOM事件模型的支持程度。開發中DOM2級事件模型中addEventListener()removeEventListener()來管理事件監聽函數是最為推薦的方法。

八. 非同步函數

1. setTimeout( )和setInterval( )

這裡的情況相當於上文中的回調函數的情況。

2. 事件監聽

詳見第7章。

3. ajax請求

幾乎沒有遇到過。

4. Promise

這裡的情況相當於上文中的回調函數的情況。

九. 箭頭函數和this

箭頭函數是ES6標準中支持的語法,它的誕生不僅僅是因為表達方式簡潔,也是為了更好地支持函數式編程。箭頭函數內部不綁定this,arguments,super,new.target,所以由於作用域鏈的機制,箭頭函數的函數體中如果使用到this,則執行引擎會沿著作用域鏈去獲取外層的this

十. Nodejs中的this

Nodejs是一種脫離瀏覽器環境的javascript運行環境,this的指向規則上與瀏覽器環境在全局對象的指向上存在一定差異。

1. 全局對象global

Nodejs的運行環境並不是瀏覽器,所以程式里沒有DOMBOM對象,Nodejs中也存在全局作用域,用來定義一些不需要通過任何模塊的載入即可使用的變數、函數或類,全局對象中多為一些系統級的信息或方法,例如獲取當前模塊的路徑,操作進程,定時任務等等。

2. 文件級this指向

Nodejs是支持模塊作用域的,每一個文件都是一個模塊,可通過require( )的方式同步引入,通過module.exports來暴露介面供其他模塊調用。在一個文件中最頂級的this指向當前這個文件模塊對外暴露的介面對象,也就是module.exports指向的對象。示例:

var IronMan = {
    name:'Tony Stark',
    attack: function(){
        
    }
}
exports.IronMan = IronMan;
console.log(this);

在控制台即可看到,this指向一個對象,對象中只有一個屬性IronMan,屬性值為文件中定義的IronMan這個對象。

3. 函數級this指向

this的基本規則中有一條—當作為函數調用時,函數中的this指向全局對象,這一條在nodejs中也是成立的,這裡的this指向了全局對象(此處的全局對象Global對象是有別於模塊級全局對象的)。

思考題— React組件中為什麼要bind(this)

如果你嘗試使用過React進行前端開發,一定見過下麵這樣的代碼:

//假想定義一個ToggleButton開關組件
class ToggleButton extends React.Component{
    constructor(props){
        super(props);
        this.state = {isToggleOn: true};
        this.handleClick = this.handleClick.bind(this); 
        this.handleChange = this.handleChange.bind(this);
    }
    handleClick(){
        this.setState(prevState => ({
            isToggleOn: !preveState.isToggleOn
        }));
    }
    handleChange(){
        console.log(this.state.isToggleOn);
    }
    render(){
        return(
           <button onClick={this.handleClick} onChange={this.handleChange}>
                {this.state.isToggleOn ? 'ON':'OFF'}
            </button>
        )
    }
}

思考題:構造方法中為什麼要給所有的實例方法綁定this呢?(強烈建議讀者先自己思考再看筆者分析)

1. 代碼執行的細節

上例僅僅是一個組件類的定義,當在其他組件中調用或是使用ReactDOM.render( )方法將其渲染到界面上時會生成一個組件的實例,因為組件是可以復用的,面向對象的編程方式非常適合它的定位。根據this指向的基本規則就可以知道,這裡的this最終會指向組件的實例。

組件實例生成的時候,構造器constructor會被執行,此處著重分析一下下麵這行代碼:

this.handleClick = this.handleClick.bind(this);

此時的this指向新生成的實例,那麼賦值語句右側的表達式先查找this.handleClick( )這個方法,由對象的屬性查找機制(沿原型鏈由近及遠查找)可知此處會查找到原型方法this.handleClick( ),接著執行bind(this),此處的this指向新生成的實例,所以賦值語句右側的表達式計算完成後,會生成一個指定了this的新方法,接著執行賦值操作,將新生成的函數賦值給實例的handleClick屬性,由對象的賦值機制可知,此處的handleClick會直接作為實例屬性生成。總結一下,上面的語句做了一件這樣的事情:

把原型方法handleClick( )改變為實例方法handleClick( ),並且強制指定這個方法中的this指向當前的實例。

2. 綁定this的必要性

在組件上綁定事件監聽器,是為了響應用戶的交互動作,特定的交互動作觸發事件時,監聽函數中往往都需要操作組件某個狀態的值,進而對用戶的點擊行為提供響應反饋,對開發者來說,這個函數觸發的時候,就需要能夠拿到這個組件專屬的狀態合集(例如在上面的開關組件ToggleButton例子中,它的內部狀態屬性state.isToggleOn的值就標記了這個按鈕應該顯示ON或者OFF),所以此處強制綁定監聽器函數的this指向當前實例的也很容易理解。

React構造方法中的bind會將響應函數與這個組件Component進行綁定以確保在這個處理函數中使用this時可以時刻指向這一組件的實例。

3. 如果不綁定this

如果類定義中沒有綁定this的指向,當用戶的點擊動作觸發this.handleClick( )這個方法時,實際上執行的是原型方法,可這樣看起來並沒有什麼影響,如果當前組件的構造器中初始化了state這個屬性,那麼原型方法執行時,this.state會直接獲取實例的state屬性,如果構造其中沒有初始化state這個屬性(比如React中的UI組件),說明組件沒有自身狀態,此時即使調用原型方法似乎也沒什麼影響。

事實上的確是這樣,這裡的bind(this)所希望提前規避的,就是第五章中的this指針丟失的問題

例如使用解構賦值的方式獲取某個屬性方法時,就會造成引用轉換丟失this的問題:

const toggleButton = new ToggleButton();

import {handleClick} = toggleButton;

上例中解構賦值獲取到的handleClick這個方法在執行時就會報錯,Class的內部是強制運行在嚴格模式下的,此處的this在賦值中丟失了原有的指向,在運行時指向了undefined,而undefined是沒有屬性的。

另一個存在的限制,是沒有綁定this的響應函數在非同步運行時可能會出問題,當它作為回調函數被傳入一個非同步執行的方法時,同樣會因為丟失了this的指向而引發錯誤。

如果沒有強制指定組件實例方法的this,在將來的使用中就無法安心使用引用轉換作為回調函數傳遞這樣的方式,對於後續使用和協作開發而言都是不方便的。

參考

[1]《javascript高級程式設計(第三版)》

[2]《深入理解javascript函數系列第二篇》https://www.cnblogs.com/xiaohuochai/p/5706289.html

[3]《ES6-Class基本語法》https://www.cnblogs.com/ChenChunChang/p/8296350.html


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

-Advertisement-
Play Games
更多相關文章
  • 代碼可以在 https://pan.baidu.com/s/1uN120-18hvAzELpJCQfbXA 處下載 下麵來 移除 廣告元素,在js目錄下,創建一個 removeAds.js, 用來移除網頁中的廣告元素 修改manifest.json 同時, 發現了一個問題,如果打開的是 https: ...
  • 理解好javascript的變數作用域和鏈式調用機制對用好變數起著關鍵的作用,本文來談談這兩個概念。 ...
  • 關於行內元素(補充一點) 行內元素只能容納文本或其他行內元素。(a特殊a裡面可以放塊級元素) 例子: 關於行高tip: 選擇器的嵌套層級不應大於3級,位置靠後的限定條件應儘可能的精確。 屬性定義必須另起一行。 關於行高的測量: css的三大特性(層疊 優先 繼承) a、層疊性:多種css樣式的疊加 ...
  • 使用html5視頻背景 直到現在,仍然不存在一項旨在網頁上顯示視頻的標準。今天,大多數視頻是通過插件(比如 Flash)來顯示的。然而,並非所有瀏覽器都擁有同樣的插件。HTML5 規定了一種通過 video 元素來包含視頻的標準方法。 瀏覽器支持的視頻格式 當前,video 元素支持Ogg,MPEG ...
  • Require.js與Sea.js的區別 相同之處 和 都是模塊載入器,倡導模塊化開發理念,核心價值是讓 JavaScript 的模塊化開發變得簡單自然。 不同之處 兩者的主要區別如下: RequireJS 想成為瀏覽器端的模塊載入器,同時也想成為 Rhino / Node 等環境的模塊載入器。Se ...
  • 場景: 假如有一天,你的在寫一個前端項目,是關於一份點餐商家電話信息表,你啪塔啪塔地寫完了,突然間項目經理跑過來找你,要求你在每一個商家的電話號碼前都添加一個電話符號,來使得電話號碼更直觀和頁面更美觀。這個時候你就糾結了,這不是折磨人嗎?這不是要我在每個電話號碼前都添加一個<img>標簽?這要整到猴 ...
  • [TOC] 前後端如何通信 前段:客戶端 後端: 伺服器端 所謂的全棧,其實是你可以實現客戶端和伺服器端程式的編寫,而且可以實現倆端之間的通信 客戶端和伺服器端是如何通信的? 本地開發(當前項目可以在本地預覽) 部署到伺服器上,讓別人可以通過功能變數名稱或者外網訪問 購買一臺伺服器(阿裡雲獨立主機,虛擬服務 ...
  • 不知不覺接觸前端的時間已經過去半年了,越來越發覺對知識的學習不應該只停留在會用的層面,這在我學jQuery的一段時間後便有這樣的體會。 雖然jQuery只是一個JS的代碼庫,只要會一些JS的基本操作學習一兩天就能很快掌握jQuery的基本語法並熟練使用,但是如果不瞭解jQUery庫背後的實現原理,相 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...