新聞中心
前言
開(kāi)寫(xiě)前大家先來(lái)理解一下指向:指向,即目標(biāo)方向、所對(duì)的方位。

網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)!專(zhuān)注于網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、成都小程序開(kāi)發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了樊城免費(fèi)建站歡迎大家使用!
很多人剛剛接觸前端甚至一些“老”前端都經(jīng)常會(huì)在JavaScript中所謂的難點(diǎn),如this,原型,繼承,閉包等這些概念中迷失了自我。接下來(lái)這篇文章會(huì)把我自己對(duì)于JavaScript中這些點(diǎn)通過(guò)指向的概念做個(gè)總結(jié)并分享給大家,希望可以幫助大家更好的了解這些所謂的難點(diǎn)。
一、this
this是什么?其實(shí)它本身就是一種指向。this指向可以分為以下幾種情況
- 普通調(diào)用,this指向?yàn)檎{(diào)用者
- call/apply調(diào)用,this指向?yàn)楫?dāng)前thisArg參數(shù)
- 箭頭函數(shù),this指向?yàn)楫?dāng)前函數(shù)的this指向
這個(gè)怎么理解呢?接下來(lái)我會(huì)一一做解析。
1、普通調(diào)用
通俗理解一下,就是誰(shuí)調(diào)用,則this便指向誰(shuí)。這里又大致分為幾種情況,分別為
1.1、對(duì)象方法的調(diào)用
即某方法為某對(duì)象上的一個(gè)屬性的屬性,正常情況當(dāng)改方法被調(diào)用的時(shí)候,this的指向則是掛載該方法的對(duì)象。廢話不多說(shuō),直接看代碼可能會(huì)更好的理解。
var obj = {
a: 'this is obj',
test: function () {
console.log(this.a);
}
}
obj.test();
// this is obj1.2、“單純”函數(shù)調(diào)用
即該函數(shù)為自己獨(dú)立的函數(shù),而不是掛載到對(duì)象上的屬性(window除外),也不會(huì)被當(dāng)成構(gòu)造函數(shù)來(lái)使用,而僅僅是當(dāng)成函數(shù)來(lái)使用,此時(shí)的this指向則是window對(duì)象。例子如下
var a = 'this is window'
function test () {
console.log(this.a);
}
test();
// this is window這個(gè)我們來(lái)理解一下,其實(shí)也很簡(jiǎn)單,我們都知道,window對(duì)象是全局對(duì)象。其實(shí)整個(gè)代碼塊等同于
window.a = 'this is window'
window.test = function test () {
console.log(this.a);
// 此時(shí)是window為調(diào)用者,即this會(huì)指向window
}
window.test();1.3、構(gòu)造函數(shù)調(diào)用
即該函數(shù)被當(dāng)成構(gòu)造函數(shù)來(lái)調(diào)用,此時(shí)的this指向該構(gòu)造器函數(shù)的實(shí)例對(duì)象。我們來(lái)看一個(gè)例子,先上一個(gè)屬于第二種情況的例子
function test () {
this.a = 'this is test';
console.log(this.a);
console.log(this);
}
test();
// this is test
// Window {}按照上面的來(lái)理解,此時(shí)的this的確指向window對(duì)象,但是如果我換種形式,將其換成構(gòu)造函數(shù)來(lái)調(diào)用呢,結(jié)果又會(huì)如何呢,直接上代碼
function Test () {
this.a = 'this is test';
console.log(this.a);
console.log(this);
}
var test = new Test();
// this is test
// Test {a: 'this is test'}OK,好像的確沒(méi)有問(wèn)題了,此時(shí)的this的確指向了該構(gòu)造函數(shù)的實(shí)例對(duì)象。具體這里的一些解釋后面我會(huì)在原型鏈繼承里面詳細(xì)講解。
2、call/apply調(diào)用
2.1、call調(diào)用
call方法形式,fun.call(thisArg[, arg1[, arg2[, ...]]])
- thisArg,當(dāng)前this指向
- arg1[, arg2[, ...]],指定的參數(shù)列表
詳細(xì)介紹請(qǐng)猛戳MDN
示例代碼如下
function Test () {
this.a = 'this is test';
console.log(this.a);
console.log(this);
}
function Test2 () {
Test.call(this);
}
var test = new Test2();
// this is test
// Test2 {a: 'this is test'}2.2、apply調(diào)用
和call類(lèi)似,唯一的一個(gè)明顯區(qū)別就是call參數(shù)為多個(gè),apply參數(shù)則為兩個(gè),第二個(gè)參數(shù)為數(shù)組或類(lèi)數(shù)組形式, fun.apply(thisArg, [argsArray])
- thisArg,當(dāng)前this指向
- 一個(gè)數(shù)組或者類(lèi)數(shù)組對(duì)象,其中的數(shù)組元素將作為單獨(dú)的參數(shù)傳給fun函數(shù)
詳細(xì)介紹請(qǐng)猛戳MDN
但是終究apply里面的數(shù)組參數(shù)會(huì)轉(zhuǎn)變?yōu)閏all方法的參數(shù)形式,然后去走下面的步驟,這也是為什么call執(zhí)行速度比apply快。這邊詳情有篇文章有介紹,點(diǎn)擊鏈接。
另外,提及到call/apply,怎么能不提及一下bind呢,bind里面的this指向,會(huì)永遠(yuǎn)指向bind到的當(dāng)前的thisArg,即context上下文環(huán)境參數(shù)不可重寫(xiě)。這也是為什么a.bind(b).call(c),最終的this指向會(huì)是b的原因。至于為什么,其實(shí)就是bind實(shí)現(xiàn)實(shí)際上是通過(guò)閉包,并且配合call/apply進(jìn)行實(shí)現(xiàn)的。具體的請(qǐng)參考bind MDN里面的用法及 Polyfill實(shí)現(xiàn)。
3、箭頭函數(shù)
首先需要介紹的一點(diǎn)就是,在箭頭函數(shù)本身,它是沒(méi)有綁定本身的this的,它的this指向?yàn)楫?dāng)前函數(shù)的this指向。怎么理解呢,直接上個(gè)代碼看下
function test () {
(() => {
console.log(this);
})()
}
test.call({a: 'this is thisArg'})
// Object {a: 'this is thisArg'}這樣看聯(lián)想上面的call/apply調(diào)用的理解,好像是沒(méi)有問(wèn)題了,那如果我設(shè)置一個(gè)定時(shí)器呢,會(huì)不是this指向會(huì)變成Window全局對(duì)象呢?答案肯定是不會(huì)的,因?yàn)榧^函數(shù)里面的this特殊性,它依舊會(huì)指向當(dāng)前函數(shù)的this指向。不多BB,直接看代碼
function test () {
setTimeout(() => {
console.log(this);
}, 0)
}
test.call({a: 'this is obj'})
// Object {a: 'this is obj'}當(dāng)然普通函數(shù)使用setTimeout的話會(huì)讓this指向指向Window對(duì)象的。demo代碼如下
function test () {
setTimeout(function () {
console.log(this);
}, 0)
}
test.call({a: 'this is obj'})
// Window {...}這里可能會(huì)牽扯到setTimeout的一些點(diǎn)了,具體這里我就不講了,想深入了解的猛戳這里
箭頭函數(shù)里面還有一些特殊的點(diǎn),這里由于只提及this這一個(gè)點(diǎn),其他比如不綁定arguments,super(ES6),抑或 new.target(ES6),他們都和this一樣,他會(huì)找尋到當(dāng)前函數(shù)的arguments等。
關(guān)于箭頭函數(shù)里面的this這里也有詳細(xì)的介紹,想深入了解的可以自行閱讀
- 英文原版(需翻墻)
- 中文翻譯版
二、原型/原型鏈
其實(shí)我們一看到原型/原型鏈都能和繼承聯(lián)想到一起,我們這里就把兩塊先拆開(kāi)來(lái)講解,這里我們就先單獨(dú)把原型/原型鏈拎出來(lái)。首先我們自己?jiǎn)栆幌伦约?,什么是原型?什么是原型鏈?/p>
- 原型:即每個(gè)function函數(shù)都有的一個(gè)prototype屬性。
- 原型鏈:每個(gè)對(duì)象和原型都有原型,對(duì)象的原型指向原型對(duì)象,而父的原型又指向父的父,這種原型層層連接起來(lái)的就構(gòu)成了原型鏈。
好像說(shuō)的有點(diǎn)繞,其實(shí)一張圖可以解釋一切
那么這個(gè)東西有怎么和指向這個(gè)概念去聯(lián)系上呢?其實(shí)這里需要提及到的一個(gè)點(diǎn),也是上面截圖中存在的一個(gè)點(diǎn),就是__proto__,我喜歡把其稱(chēng)為原型指針。終歸到頭,prototype只不過(guò)是一個(gè)屬性而已,它沒(méi)有什么實(shí)際的意義,最后能做原型鏈繼承的還是通過(guò)__proto__這個(gè)原型指針來(lái)完成的。我們看到的所謂的繼承只不過(guò)是將需要繼承的屬性掛載到繼承者的prototype屬性上面去的,實(shí)際在找尋繼承的屬性的時(shí)候,會(huì)通過(guò)__proto__原型指針一層一層往上找,即會(huì)去找__proto__原型指針?biāo)囊粋€(gè)指向??磦€(gè)demo
function Test () {
this.a = 'this is Test';
}
Test.prototype = {
b: function () {
console.log("this is Test's prototype");
}
}
function Test2 () {
this.a = 'this is Test2'
}
Test2.prototype = new Test();
var test = new Test2();
test.b();
console.log(test.prototype);
console.log(test);其執(zhí)行結(jié)果如下
更多關(guān)于繼承的點(diǎn),這里就不提及了,我會(huì)在繼承這一章節(jié)做詳細(xì)的講解。那么“單獨(dú)”關(guān)于原型/原型鏈的點(diǎn)就這些了。
總結(jié):原型即prototype,它只是所有function上的一個(gè)屬性而已,真正的“大佬”是__proto__,“大佬”指向誰(shuí),誰(shuí)才能有言語(yǔ)權(quán)(當(dāng)然可能因?yàn)椤按罄小边^(guò)于霸道,所以在ECMA-262之后才被Standard化)。
三、繼承
關(guān)于繼承,之前我有寫(xiě)過(guò)一篇博文對(duì)繼承的一些主流方式進(jìn)行過(guò)總結(jié)。想詳細(xì)了解的請(qǐng)點(diǎn)擊傳送門(mén)。這里我們通過(guò)指向這個(gè)概念來(lái)重新理解一下繼承。這里咱就談兩個(gè)萬(wàn)變不離其宗的繼承方式,一個(gè)是構(gòu)造函數(shù)繼承,一個(gè)是原型鏈繼承。
1、構(gòu)造函數(shù)繼承
其實(shí)就是上面提及到的通過(guò)call/apply調(diào)用,將this指向變成thisArg,具體看上面的解釋?zhuān)@里直接上代碼
function Test () {
this.a = 'this is test';
console.log(this.a);
console.log(this);
}
function Test2 () {
Test.apply(this)
// or Test.apply(this)
}
var test = new Test2();
// this is test
// Test2 {a: 'this is test'}2、原型鏈繼承
一般情況,我們做原型鏈繼承,會(huì)通過(guò)子類(lèi)prototype屬性等于(指向)父類(lèi)的實(shí)例。即
Child.prototype = new Parent();
那么這樣的做法具體是怎么實(shí)現(xiàn)原型鏈繼承的呢?
首先在講解繼承前,我們需要get到一個(gè)點(diǎn),那就是對(duì)象{ }它內(nèi)部擁有的一些屬性,這里直接看張圖
如上圖所示,我們看到對(duì)象{ }它本身?yè)碛械膶傩跃褪巧厦嫖覀兲峒暗降腳_proto__原型指針以及一些方法。
接下來(lái)我先說(shuō)一下new關(guān)鍵字具體做的一件事情。其過(guò)程大致分為三步,如下
var obj= {}; // 初始化一個(gè)對(duì)象obj
obj.__proto__ = Parent.prototype; // 將obj的__proto__原型指針指向父類(lèi)Parent的prototype屬性
Parent.call(obj); // 初始化Parent構(gòu)造函數(shù)從這里我們看出來(lái),相信大家也能理解為什么我在上面說(shuō)__proto__才是真正的“大佬”。
這里我額外提一件我們經(jīng)常干的“高端”的事情,那就是通過(guò)原型prototype做monkey patch。即我想在繼承父類(lèi)方法的同時(shí),完成自己獨(dú)立的一些操作。具體代碼如下
function Parent () {
this.a = 'this is Parent'
}
Parent.prototype = {
b: function () {
console.log(this.a);
}
}
function Child () {
this.a = 'this is Child'
}
Child.prototype = {
b: function () {
console.log('monkey patch');
Parent.prototype.b.call(this);
}
}
var test = new Child()
test.b()
// monkey patch
// this is Child這個(gè)是我們對(duì)于自定義的類(lèi)進(jìn)行繼承并重寫(xiě),那么如果是類(lèi)似Array,Number,String等內(nèi)置類(lèi)進(jìn)行繼承重寫(xiě)的話,結(jié)果會(huì)是如何呢?關(guān)于這個(gè)話題我也有寫(xiě)過(guò)一篇博文進(jìn)行過(guò)講解,傳送門(mén)
四、閉包
對(duì)于閉包,我曾經(jīng)也做過(guò)總結(jié)和分享,簡(jiǎn)單的一些東西和概念這里不提及了,想了解的可以猛戳這里。和原型鏈那章一張,這里會(huì)摒棄掉原來(lái)的一些看法,這里我依舊通過(guò)代入指向這個(gè)概念來(lái)進(jìn)行理解。
一般情況下,我們理解閉包是這樣的:“為了可以訪問(wèn)函數(shù)內(nèi)的局部變量而定義的內(nèi)部函數(shù)”。
JavaScript語(yǔ)言特性,每一個(gè)function內(nèi)都有一個(gè)屬于自己的執(zhí)行上下文,即特定的context指向。
內(nèi)層的context上下文總能訪問(wèn)到外層context上下文中的變量,即每次內(nèi)部的作用域可以往上層查找直到訪問(wèn)到當(dāng)前所需訪問(wèn)的變量。例子如下
var a = 'this is window'
function test () {
var b = 'this is test'
function test2 () {
var c = 'this is test2';
console.log(a);
console.log(b);
console.log(c);
}
test2();
}
test();
// this is window
// this is test
// this is test2但是如果反過(guò)來(lái)訪問(wèn)的話,則不能進(jìn)行訪問(wèn),即變量訪問(wèn)的指向是當(dāng)前context上下文的指向的相反方向,且不可逆。如下
function test () {
var b = 'this is test';
}
console.log(b); // Uncaught ReferenceError: b is not defined這里用一個(gè)非常常見(jiàn)的情況作為例子,即for循環(huán)配合setTimeout的異步任務(wù),如下
function test () {
for (var i = 0; i < 4; i++) {
setTimeout(function () {
console.log(i);
}, 0)
}
}
test();看到上面的例子,我們都知道說(shuō):“答案會(huì)打印4次4”。那么為什么會(huì)這樣呢?我想依次打印0,1,2,3又該怎么做呢?
相信很多小伙伴們都會(huì)說(shuō),用閉包呀,就能實(shí)現(xiàn)了呀。對(duì)沒(méi)錯(cuò),的確用閉包就能實(shí)現(xiàn)。那么為什么出現(xiàn)這種情況呢?
這里我簡(jiǎn)單提一下,首先這邊牽扯到兩個(gè)點(diǎn),一個(gè)就是for循環(huán)的同步任務(wù),一個(gè)就是setTimeout的異步任務(wù),在JavaScript線程中,因?yàn)楸旧鞪avaScript是單線程,這個(gè)特點(diǎn)決定了其正常的腳本執(zhí)行順序是按照文檔流的形式來(lái)進(jìn)行的,即從上往下,從左往右的這樣方向。每次腳本正常執(zhí)行時(shí),但凡遇到異步任務(wù)的時(shí)候,都會(huì)將其set到一個(gè)task queue(任務(wù)隊(duì)列)中去。然后在執(zhí)行完同步任務(wù)之后,再來(lái)執(zhí)行隊(duì)列任務(wù)中的異步任務(wù)。
當(dāng)然對(duì)于不同的異步任務(wù),執(zhí)行順序也會(huì)不一樣,具體就看其到底屬于哪個(gè)維度的異步任務(wù)了。這里我就不詳細(xì)扯Event Loop了,想更詳細(xì)的了解請(qǐng)戳這里
回到上面我們想要實(shí)現(xiàn)的效果這個(gè)問(wèn)題上來(lái),我們一般處理方法是利用閉包進(jìn)行參數(shù)傳值,代碼如下
function test () {
for (var i = 0; i < 4; i++) {
(function (e) {
setTimeout(function () {
console.log(e);
}, 0)
})(i)
}
}
test();
// 0 -> 1 -> 2 -> 3循環(huán)當(dāng)中,匿名函數(shù)會(huì)立即執(zhí)行,并且會(huì)將循環(huán)當(dāng)前的 i 作為參數(shù)傳入,將其作為當(dāng)前匿名函數(shù)中的形參e的指向,即會(huì)保存對(duì) i 的引用,它是不會(huì)被循環(huán)改變的。
當(dāng)然還有一種常見(jiàn)的方式可以實(shí)現(xiàn)上面的效果,即從自執(zhí)行匿名函數(shù)中返回一個(gè)函數(shù)。代碼如下
function test () {
for(var i = 0; i < 4; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 0)
}
}
test();更多高階閉包的寫(xiě)法這里就不一一介紹了,想了解的小伙伴請(qǐng)自行搜索。
文章到此差不多就要結(jié)束了
可是我也沒(méi)辦法,的確要結(jié)束了。下面給整篇博文做個(gè)總結(jié)吧
總結(jié)
首先基本上JavaScript中所涉及的所謂的難點(diǎn),在本文中都通過(guò)指向這個(gè)概念進(jìn)行了通篇的解讀,當(dāng)然這是我個(gè)人對(duì)于JavaScript的一些理解,思路僅供參考。如果有什么不對(duì)的地方,歡迎各位小伙伴指出。
其實(shí)寫(xiě)該博文的好多次,我想把所有的知識(shí)點(diǎn)全部串起來(lái)進(jìn)行講解,但又怕效果不好,所以做了一一的拆解,也進(jìn)行了混合的運(yùn)用。具體能領(lǐng)悟到多少,就要看小伙伴你們自己的了。
網(wǎng)站名稱(chēng):從指向看JavaScript中的難點(diǎn)
網(wǎng)站網(wǎng)址:http://www.dlmjj.cn/article/dpjhpdo.html


咨詢
建站咨詢
