開發(fā)者的javascript造詣取決于對【動態(tài)】和【異步】這兩個詞的理解水平。
十年的乳源網(wǎng)站建設(shè)經(jīng)驗,針對設(shè)計、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時及時工作處理。全網(wǎng)整合營銷推廣的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動調(diào)整乳源建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計,從而大程度地提升瀏覽體驗。創(chuàng)新互聯(lián)從事“乳源網(wǎng)站設(shè)計”,“乳源網(wǎng)站推廣”以來,每個客戶項目都認真落實執(zhí)行。
這一期主要分析各種實際開發(fā)中各種復(fù)雜的this
指向問題。
嚴格模式是ES5中添加的javascript
的另一種運行模式,它可以禁止使用一些語法上不合理的部分,提高編譯和運行速度,但語法要求也更為嚴格,使用use strict
標記開啟。
嚴格模式中this
的默認指向不再為全局對象,而是默認指向undefined
。這樣限制的好處是在使用構(gòu)造函數(shù)而忘記寫new
操作符時會報錯,而不會把本來需要綁定在實例上的一堆屬性全綁在window
對象上,在許多沒有正確地綁定this
的場景中也會報錯。
詞法定義并不影響this
的指向 , 因為this
是運行時確定指向的。
2.1 函數(shù)定義的嵌套
function outerFun(){
function innerFun(){
console.log('innerFun內(nèi)部的this指向了:',this);
}
innerFun();
}
outerFun();
控制臺輸出的this
指向全局對象。
2.2 對象屬性的嵌套
當(dāng)調(diào)用的函數(shù)在對象結(jié)構(gòu)上的定義具有一定深度時,this
指向這個方法所在的對象,而不是最外層的對象。
var IronMan = {
realname:'Tony Stark',
rank:'1',
ability:{
total_types:100,
fly:function(){
console.log('IronMan.ability.fly ,作為方法調(diào)用時this指向:',this);
},
}
}
IronMan.ability.fly();
控制臺輸出的this
指向IronMan的ability屬性所指向的對象,調(diào)用fly( )
這個方法的對象是IronMan.ability
所指向的對象,而不是IronMan
所指向的對象。
this
作為對象方法調(diào)用時,標識著這個方法是如何被找到的。IronMan
這個標識符指向的對象信息并不能在運行時找到fly( )
這個方法的位置,因為ability屬性中只存了另一個對象的引用地址,而IronMan.ability
對象的fly屬性所記錄的指向,才能讓引擎在運行時找到這個匿名方法。
引用轉(zhuǎn)換實際上并不會影響this
的指向,因為它是詞法性質(zhì)的,發(fā)生在定義時,而this
的指向是運行時確定的。只要遵循this指向的基本原則就不難理解。
3.1 標識符引用轉(zhuǎn)換為對象方法引用
var originFun = function (){
console.log('originFun內(nèi)部的this為:',this);
}
var ironMan = {
attack:originFun
};
ironMan.attack();
這里的this
指向其調(diào)用者,也就是ironMan
引用的對象。
3.2 對象方法轉(zhuǎn)換為標識符引用
var ironMan = {
attack:function(){
console.log('對象方法中this指向了:',this);
}
}
var originFun = ironMan.attack;
originFun();
這里的this
指向全局對象,瀏覽器中也就是window
對象。3.2中的示例被認為是javascript語言的bug,即this指向丟失的問題。同樣的問題也可能在回調(diào)函數(shù)傳參時發(fā)生,本文【第5章】將對這種情況進行詳細說明。
javascript中的函數(shù)是可以被當(dāng)做參數(shù)傳遞進另一個函數(shù)中的,也就有了回調(diào)函數(shù)這樣一個概念。
4.1 this在回調(diào)函數(shù)中的表現(xiàn)
var IronMan = {
attack:function(findEnemy){
findEnemy();
}
}
function findEnemy(){
console.log('已聲明的函數(shù)被當(dāng)做回調(diào)函數(shù)調(diào)用,this指向:',this);
}
var attackAction = {
findEnemy:function(){
console.log('attackAction.findEnemy本當(dāng)做回調(diào)函數(shù)調(diào)用時,this指向',this);
},
isArmed:function(){
console.log('check whether the actor is Armed');
}
}
//1.直接傳入匿名函數(shù)
IronMan.attack(function(){
console.log(this);
});
//2.傳入外部定義函數(shù)
IronMan.attack(findEnemy);
//3.傳入外部定義的對象方法
IronMan.attack(attackAction.findEnemy);
從控制臺打印的結(jié)果來看,無論以哪種方式來傳遞回調(diào)函數(shù),回調(diào)函數(shù)執(zhí)行時的this
都指向了全局變量。
4.2 原理
javascript中函數(shù)傳參全部都是值傳遞,也就是說如果調(diào)用函數(shù)時傳入一個原始類型,則會把這個值賦值給對應(yīng)的形參;如果傳入一個引用類型,則會把其中保存的內(nèi)存指向的地址賦值給對應(yīng)的形參。所以在函數(shù)內(nèi)部操作一個值為引用類型的形參時,會影響到函數(shù)外部作用域,因為它們均指向內(nèi)存中的同一個函數(shù)。詳細可參考[深入理解javascript函數(shù)系列第二篇——函數(shù)參數(shù)]這篇博文。
理解了函數(shù)傳參,就很容易理解回調(diào)函數(shù)中this
為何指向全局了,回調(diào)函數(shù)對應(yīng)的形參是一個引用類型的標識符,其中保存的地址直接指向這個函數(shù)在內(nèi)存中的真實位置,那么通過執(zhí)行這個標識符來調(diào)用函數(shù)就等同于this基本指向規(guī)則中的作為函數(shù)來調(diào)用的情況,其this
指向全局對象也就不難理解了。
在第三節(jié)和第四節(jié)中,通過原理分析就能夠明白為何在一些特定的場合下this
會指向全局對象,但是從語言的角度來看,卻很難理解this
為什么指向了全局對象,因為這個規(guī)則和語法的字面意思是有沖突的。
5.1 回調(diào)函數(shù)的字面語境
var name = 'HanMeiMei';
var liLei = {
name:'liLei',
introduce:function () {
console.log('My name is ', this.name);
}
};
var liLeiSay = liLei.introduce;
liLeiSay();//同第三節(jié)中的引用轉(zhuǎn)換示例
setTimeout(liLei.introduce,2000);//同第四節(jié)中的回調(diào)函數(shù)示例
上面的代碼從字面上看意義是很明確的,就是希望liLei立刻介紹一下自己,在2秒后再介紹一下他自己。但控制臺輸出的結(jié)果中,他卻兩次都說自己的名字是HanMeiMei。
5.2 this指針丟失
5.1中的示例,也稱為this指針丟失問題,被認為是Javascript語言的設(shè)計失誤,因為這種設(shè)計在字面語義上造成了混亂。
5.3 this指針修復(fù)
方式1-使用bind
為了使代碼的字面語境和實際執(zhí)行保持一致,需要通過顯示指定this的方式對this
的指向進行修復(fù)。常用的方法是使用bind( )
生成一個確定了this
指向的新函數(shù),將上述示例改為如下方式即可修復(fù)this
的指向:
var liLeiSay = liLei.introduce.bind(liLei);
setTimeout(liLei.introduce.bind(liLei),2000);
bind( )
的實現(xiàn)其實并不復(fù)雜,是閉包實現(xiàn)高階函數(shù)的一個簡單的實例,感興趣的讀者可以自行了解。
方式2-使用Proxy
Proxy是ES6
中才支持的方法。
//綁定This的函數(shù)
function fixThis (target) {
const cache = new WeakMap();
//返回一個新的代理對象
return new Proxy(target, {
get (target, key) {
const value = Reflect.get(target, key);
//如果要取的屬性不是函數(shù),則直接返回屬性值
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());
兩種修復(fù)
this
指向的思路其實很類似,第一種方式相當(dāng)于為調(diào)用的方法創(chuàng)建了一個代理方法
,第二種方式是為被訪問的對象創(chuàng)建了一個代理對象
。
實際開發(fā)過程中,往往需要在更深層次的函數(shù)中獲取外層this
的指向。
常規(guī)的解決方案是:將外層函數(shù)的this
賦值給一個局部變量,通會使用_this
,that
,self
,_self
等來作為變量名保存當(dāng)前作用域中的this
。由于在javascript
中作用域鏈的存在,嵌套的內(nèi)部函數(shù)可以調(diào)用外部函數(shù)的局部變量,標識符會去尋找距離作用域鏈末端最近的一個指向作為其值,示例如下:
document.querySelector('#btn').onclick = function(){
//保存外部函數(shù)中的this
var _this = this;
_.each(dataSet, function(item, index){
//回調(diào)函數(shù)的this指向了全局,調(diào)用外部函數(shù)的this來操作DOM元素
_this.innerHTML += item;
});
}
事件監(jiān)聽中this
的指向情況其實是幾種情況的集合,與代碼如何編寫有很大關(guān)系。
1. 在html文件中使用事件監(jiān)聽相關(guān)的屬性來觸發(fā)方法
<button onclick="someFun()">點擊按鈕</button>
<button onclick="someObj.someFun()">點擊按鈕</button>
如果以第一種方式觸發(fā),則函數(shù)中的this
指向全局;
如果以第二種方式觸發(fā),則函數(shù)中的this
指向someObj這個對象。
2. 在js文件中直接為屬性賦值
//聲明一個函數(shù)
function callFromHTML() {
console.log('callFromHTML,this指向:',this);
}
//定義一個對象方法
var obj = {
callFromObj:function () {
console.log('callFromObj',this);
}
}
//注冊事件監(jiān)聽-方式1
document.querySelector('#btn').onclick = function (event) {
console.log(this);
}
//注冊事件監(jiān)聽-方式2
document.querySelector('#btn').onclick = callFromHTML;
//注冊事件監(jiān)聽-方式3
document.querySelector('#btn').onclick = obj.callFromObj;
以上三種注冊的事件監(jiān)聽響應(yīng)函數(shù),其this
均指向id="btn"
的DOM元素。
3. 使用addEventListener
方法注冊響應(yīng)函數(shù)
//低版本IE瀏覽器中需要使用另外的方法
document.querySelector('#btn').addEventListener('click',function(event){
console.log(this);
});
//也可以將函數(shù)名或?qū)ο蠓椒ㄗ鳛榛卣{(diào)函數(shù)傳入
document.querySelector('#btn').addEventListener('click',callFromHTML);
document.querySelector('#btn').addEventListener('click',obj.callFromObj);
這種方式注冊的響應(yīng)函數(shù),其this
與場景2相同,均指向id="btn"
的DOM元素。區(qū)別在于使用addEventListener
方法添加的響應(yīng)函數(shù)會依次執(zhí)行,而采用場景2的方式時,只有最后一次賦值的函數(shù)會被調(diào)用。
1. 通過標簽屬性注冊
<button id="btn" onclick="callFromHTML()">點我</button>
<script>
function callFromHTML() {
console.log(document.querySelector('#btn').onclick);
}
</script>
在html中綁定事件處理程序,然后當(dāng)按鈕點擊時,在控制臺打印出DOM對象的onclick
屬性,可以看到:
這種綁定方式其實是將監(jiān)聽方法包裹在另一個函數(shù)中去執(zhí)行,相當(dāng)于:
document.querySelector('#btn').onclick = function(event){
callFromHTML();
}
這樣上述的表現(xiàn)就不難理解了。
2. 通過元素對象屬性注冊
document
在javascript中是一個對象,通過其暴露的查找方法返回的節(jié)點也是一個對象,那么方式二綁定的監(jiān)聽函數(shù)在運行時,實際上就是在執(zhí)行指定節(jié)點的onclick
方法,根據(jù)this指向的基本規(guī)則可知其函數(shù)體中的this
應(yīng)該指向調(diào)用對象,也就是onclick
這個方法所在的節(jié)點對象。
3. 通過addEventListener
方法注冊
這種方式是在DOM2事件模型中擴展的,用于支持多個監(jiān)聽器綁定的場景。DOM2事件模型的描述中規(guī)定了通過這種方式添加的監(jiān)聽函數(shù)執(zhí)行時的this
指向所在的節(jié)點對象,不同內(nèi)核的瀏覽器實現(xiàn)方式有區(qū)別。
不同的使用方式實質(zhì)上是伴隨著DOM事件模型升級而發(fā)生改變的,現(xiàn)代瀏覽器對于以上幾種模式都是支持的,只有需要兼容老版本瀏覽器時需要考慮對DOM事件模型的支持程度。開發(fā)中DOM2級事件模型中addEventListener()
和removeEventListener()
來管理事件監(jiān)聽函數(shù)是最為推薦的方法。
1. setTimeout( )和setInterval( )
這里的情況相當(dāng)于上文中的回調(diào)函數(shù)的情況。
2. 事件監(jiān)聽
詳見第7章。
3. ajax請求
幾乎沒有遇到過。
4. Promise
這里的情況相當(dāng)于上文中的回調(diào)函數(shù)的情況。
箭頭函數(shù)是ES6
標準中支持的語法,它的誕生不僅僅是因為表達方式簡潔,也是為了更好地支持函數(shù)式編程。箭頭函數(shù)內(nèi)部不綁定this
,arguments
,super
,new.target
,所以由于作用域鏈的機制,箭頭函數(shù)的函數(shù)體中如果使用到this
,則執(zhí)行引擎會沿著作用域鏈去獲取外層的this
。
Nodejs
是一種脫離瀏覽器環(huán)境的javascript
運行環(huán)境,this
的指向規(guī)則上與瀏覽器環(huán)境在全局對象的指向上存在一定差異。
1. 全局對象global
Nodejs
的運行環(huán)境并不是瀏覽器,所以程序里沒有DOM
和BOM
對象,Nodejs
中也存在全局作用域,用來定義一些不需要通過任何模塊的加載即可使用的變量、函數(shù)或類,全局對象中多為一些系統(tǒng)級的信息或方法,例如獲取當(dāng)前模塊的路徑,操作進程,定時任務(wù)等等。
2. 文件級this指向
Nodejs
是支持模塊作用域的,每一個文件都是一個模塊,可通過require( )
的方式同步引入,通過module.exports
來暴露接口供其他模塊調(diào)用。在一個文件中最頂級的this
指向當(dāng)前這個文件模塊對外暴露的接口對象,也就是module.exports
指向的對象。示例:
var IronMan = {
name:'Tony Stark',
attack: function(){
}
}
exports.IronMan = IronMan;
console.log(this);
在控制臺即可看到,this
指向一個對象,對象中只有一個屬性IronMan
,屬性值為文件中定義的IronMan
這個對象。
3. 函數(shù)級this指向
this的基本規(guī)則中有一條—當(dāng)作為函數(shù)調(diào)用時,函數(shù)中的this
指向全局對象,這一條在nodejs
中也是成立的,這里的this
指向了全局對象(此處的全局對象Global對象是有別于模塊級全局對象的)。
如果你嘗試使用過React
進行前端開發(fā),一定見過下面這樣的代碼:
//假想定義一個ToggleButton開關(guān)組件
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>
)
}
}
思考題:構(gòu)造方法中為什么要給所有的實例方法綁定this呢?(強烈建議讀者先自己思考再看筆者分析)
1. 代碼執(zhí)行的細節(jié)
上例僅僅是一個組件類的定義,當(dāng)在其他組件中調(diào)用或是使用ReactDOM.render( )
方法將其渲染到界面上時會生成一個組件的實例,因為組件是可以復(fù)用的,面向?qū)ο蟮木幊谭绞椒浅_m合它的定位。根據(jù)this指向的基本規(guī)則就可以知道,這里的this
最終會指向組件的實例。
組件實例生成的時候,構(gòu)造器constructor
會被執(zhí)行,此處著重分析一下下面這行代碼:
this.handleClick = this.handleClick.bind(this);
此時的this
指向新生成的實例,那么賦值語句右側(cè)的表達式先查找this.handleClick( )
這個方法,由對象的屬性查找機制(沿原型鏈由近及遠查找)可知此處會查找到原型方法this.handleClick( )
,接著執(zhí)行bind(this)
,此處的this
指向新生成的實例,所以賦值語句右側(cè)的表達式計算完成后,會生成一個指定了this
的新方法,接著執(zhí)行賦值操作,將新生成的函數(shù)賦值給實例的handleClick
屬性,由對象的賦值機制可知,此處的handleClick
會直接作為實例屬性生成??偨Y(jié)一下,上面的語句做了一件這樣的事情:
把原型方法handleClick( )
改變?yōu)閷嵗椒?code>handleClick( ),并且強制指定這個方法中的this
指向當(dāng)前的實例。
2. 綁定this的必要性
在組件上綁定事件監(jiān)聽器,是為了響應(yīng)用戶的交互動作,特定的交互動作觸發(fā)事件時,監(jiān)聽函數(shù)中往往都需要操作組件某個狀態(tài)的值,進而對用戶的點擊行為提供響應(yīng)反饋,對開發(fā)者來說,這個函數(shù)觸發(fā)的時候,就需要能夠拿到這個組件專屬的狀態(tài)合集(例如在上面的開關(guān)組件ToggleButton
例子中,它的內(nèi)部狀態(tài)屬性state.isToggleOn
的值就標記了這個按鈕應(yīng)該顯示ON或者OFF),所以此處強制綁定監(jiān)聽器函數(shù)的this
指向當(dāng)前實例的也很容易理解。
React構(gòu)造方法中的bind會將響應(yīng)函數(shù)與這個組件Component進行綁定以確保在這個處理函數(shù)中使用this時可以時刻指向這一組件的實例。
3. 如果不綁定this
如果類定義中沒有綁定this
的指向,當(dāng)用戶的點擊動作觸發(fā)this.handleClick( )
這個方法時,實際上執(zhí)行的是原型方法,可這樣看起來并沒有什么影響,如果當(dāng)前組件的構(gòu)造器中初始化了state
這個屬性,那么原型方法執(zhí)行時,this.state
會直接獲取實例的state
屬性,如果構(gòu)造其中沒有初始化state
這個屬性(比如React中的UI組件),說明組件沒有自身狀態(tài),此時即使調(diào)用原型方法似乎也沒什么影響。
事實上的確是這樣,這里的bind(this)
所希望提前規(guī)避的,就是第五章中的this指針丟失的問題。
例如使用解構(gòu)賦值的方式獲取某個屬性方法時,就會造成引用轉(zhuǎn)換丟失this的問題:
const toggleButton = new ToggleButton();
import {handleClick} = toggleButton;
上例中解構(gòu)賦值獲取到的handleClick
這個方法在執(zhí)行時就會報錯,Class的內(nèi)部是強制運行在嚴格模式下的,此處的this
在賦值中丟失了原有的指向,在運行時指向了undefined
,而undefined
是沒有屬性的。
另一個存在的限制,是沒有綁定this
的響應(yīng)函數(shù)在異步運行時可能會出問題,當(dāng)它作為回調(diào)函數(shù)被傳入一個異步執(zhí)行的方法時,同樣會因為丟失了this
的指向而引發(fā)錯誤。
如果沒有強制指定組件實例方法的
this
,在將來的使用中就無法安心使用引用轉(zhuǎn)換或作為回調(diào)函數(shù)傳遞這樣的方式,對于后續(xù)使用和協(xié)作開發(fā)而言都是不方便的。
[1]《javascript高級程序設(shè)計(第三版)》
[2]《深入理解javascript函數(shù)系列第二篇》https://www.cnblogs.com/xiaohuochai/p/5706289.html
[3]《ES6-Class基本語法》https://www.cnblogs.com/ChenChunChang/p/8296350.html
名稱欄目:javascript基礎(chǔ)修煉(3)—What'sthis(下)
當(dāng)前URL:http://bm7419.com/article22/pssccc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站建設(shè)、網(wǎng)站導(dǎo)航、面包屑導(dǎo)航、外貿(mào)建站、、網(wǎng)站維護
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)