開發者的javascript造詣取決於對【動態】和【異步】這兩個詞的理解水平。javascript
這一期主要分析各類實際開發中各類複雜的this
指向問題。html
嚴格模式是ES5中添加的javascript
的另外一種運行模式,它能夠禁止使用一些語法上不合理的部分,提升編譯和運行速度,但語法要求也更爲嚴格,使用use strict
標記開啓。前端
嚴格模式中this
的默認指向再也不爲全局對象,而是默認指向undefined
。這樣限制的好處是在使用構造函數而忘記寫new
操做符時會報錯,而不會把原本須要綁定在實例上的一堆屬性全綁在window
對象上,在許多沒有正確地綁定this
的場景中也會報錯。java
詞法定義並不影響this
的指向 , 由於this
是運行時肯定指向的。node
2.1 函數定義的嵌套react
function outerFun(){ function innerFun(){ console.log('innerFun內部的this指向了:',this); } innerFun(); } outerFun();
控制檯輸出的this
指向全局對象。ajax
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
指向IronMan的ability屬性所指向的對象,調用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
爲何指向了全局對象,由於這個規則和語法的字面意思是有衝突的。
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
,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
的指向狀況實際上是幾種狀況的集合,與代碼如何編寫有很大關係。
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的方式時,只有最後一次賦值的函數會被調用。
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
指向所在的節點對象,不一樣內核的瀏覽器實現方式有區別。
不一樣的使用方式實質上是伴隨着DOM事件模型升級而發生改變的,現代瀏覽器對於以上幾種模式都是支持的,只有須要兼容老版本瀏覽器時須要考慮對DOM事件模型的支持程度。開發中DOM2級事件模型中addEventListener()
和removeEventListener()
來管理事件監聽函數是最爲推薦的方法。
1. setTimeout( )和setInterval( )
這裏的狀況至關於上文中的回調函數的狀況。
2. 事件監聽
詳見第7章。
3. ajax請求
幾乎沒有遇到過。
4. Promise
這裏的狀況至關於上文中的回調函數的狀況。
箭頭函數是ES6
標準中支持的語法,它的誕生不只僅是由於表達方式簡潔,也是爲了更好地支持函數式編程。箭頭函數內部不綁定this
,arguments
,super
,new.target
,因此因爲做用域鏈的機制,箭頭函數的函數體中若是使用到this
,則執行引擎會沿着做用域鏈去獲取外層的this
。
Nodejs
是一種脫離瀏覽器環境的javascript
運行環境,this
的指向規則上與瀏覽器環境在全局對象的指向上存在必定差別。
1. 全局對象global
Nodejs
的運行環境並非瀏覽器,因此程序裏沒有DOM
和BOM
對象,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
進行前端開發,必定見過下面這樣的代碼:
//假想定義一個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函數系列第二篇》http://www.javashuo.com/article/p-vddoptaa-h.html
[3]《ES6-Class基本語法》http://www.javashuo.com/article/p-qcbunqul-k.html