摘要: 前言 開寫前你們先來理解一下指向:指向,即目標方向、所對的方位。 不少人剛剛接觸前端甚至一些「老」前端都常常會在JavaScript中所謂的難點,如this,原型,繼承,閉包等這些概念中迷失了自我。html
開寫前你們先來理解一下指向:指向,即目標方向、所對的方位。前端
不少人剛剛接觸前端甚至一些「老」前端都常常會在JavaScript中所謂的難點,如this,原型,繼承,閉包等這些概念中迷失了自我。接下來這篇文章會把我本身對於JavaScript中這些點經過指向的概念作個總結並分享給你們,但願能夠幫助你們更好的瞭解這些所謂的難點。數組
this是什麼?其實它自己就是一種指向。this指向能夠分爲如下幾種狀況閉包
這個怎麼理解呢?接下來我會一一作解析。app
通俗理解一下,就是誰調用,則this便指向誰。這裏又大體分爲幾種狀況,分別爲異步
即某方法爲某對象上的一個屬性的屬性,正常狀況當改方法被調用的時候,this的指向則是掛載該方法的對象。廢話很少說,直接看代碼可能會更好的理解。函數
var obj = { a: 'this is obj', test: function () { console.log(this.a); } } obj.test(); // this is obj
即該函數爲本身獨立的函數,而不是掛載到對象上的屬性(window除外),也不會被當成構造函數來使用,而僅僅是當成函數來使用,此時的this指向則是window對象。例子以下oop
var a = 'this is window' function test () { console.log(this.a); } test(); // this is window
這個咱們來理解一下,其實也很簡單,咱們都知道,window對象是全局對象。其實整個代碼塊等同於ui
window.a = 'this is window' window.test = function test () { console.log(this.a); // 此時是window爲調用者,即this會指向window } window.test();
即該函數被當成構造函數來調用,此時的this指向該構造器函數的實例對象。咱們來看一個例子,先上一個屬於第二種狀況的例子this
function test () { this.a = 'this is test'; console.log(this.a); console.log(this); } test(); // this is test // Window {}
按照上面的來理解,此時的this的確指向window對象,可是若是我換種形式,將其換成構造函數來調用呢,結果又會如何呢,直接上代碼
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,好像的確沒有問題了,此時的this的確指向了該構造函數的實例對象。具體這裏的一些解釋後面我會在原型鏈繼承裏面詳細講解。
call方法形式,fun.call(thisArg[, arg1[, arg2[, ...]]])
詳細介紹請猛戳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'}
和call相似,惟一的一個明顯區別就是call參數爲多個,apply參數則爲兩個,第二個參數爲數組或類數組形式, fun.apply(thisArg, [argsArray])
詳細介紹請猛戳MDN
可是終究apply裏面的數組參數會轉變爲call方法的參數形式,而後去走下面的步驟,這也是爲何call執行速度比apply快。這邊詳情有篇文章有介紹,點擊連接。
另外,說起到call/apply,怎麼能不說起一下bind呢,bind裏面的this指向,會永遠指向bind到的當前的thisArg,即context上下文環境參數不可重寫。這也是爲何a.bind(b).call(c),最終的this指向會是b的緣由。至於爲何,其實就是bind實現其實是經過閉包,而且配合call/apply進行實現的。具體的請參考bind MDN裏面的用法及 Polyfill實現。
首先須要介紹的一點就是,在箭頭函數自己,它是沒有綁定自己的this的,它的this指向爲當前函數的this指向。怎麼理解呢,直接上個代碼看下
function test () { (() => { console.log(this); })() } test.call({a: 'this is thisArg'}) // Object {a: 'this is thisArg'}
這樣看聯想上面的call/apply調用的理解,好像是沒有問題了,那若是我設置一個定時器呢,會不是this指向會變成Window全局對象呢?答案確定是不會的,由於箭頭函數裏面的this特殊性,它依舊會指向當前函數的this指向。很少BB,直接看代碼
function test () { setTimeout(() => { console.log(this); }, 0) } test.call({a: 'this is obj'}) // Object {a: 'this is obj'}
固然普通函數使用setTimeout的話會讓this指向指向Window對象的。demo代碼以下
function test () { setTimeout(function () { console.log(this); }, 0) } test.call({a: 'this is obj'}) // Window {...}
這裏可能會牽扯到setTimeout的一些點了,具體這裏我就不講了,想深刻了解的猛戳這裏
箭頭函數裏面還有一些特殊的點,這裏因爲只說起this這一個點,其餘好比不綁定arguments,super(ES6),抑或 new.target(ES6),他們都和this同樣,他會找尋到當前函數的arguments等。
關於箭頭函數裏面的this這裏也有詳細的介紹,想深刻了解的能夠自行閱讀
其實咱們一看到原型/原型鏈都能和繼承聯想到一塊兒,咱們這裏就把兩塊先拆開來說解,這裏咱們就先單獨把原型/原型鏈拎出來。首先咱們本身問一下本身,什麼是原型?什麼是原型鏈?
好像說的有點繞,其實一張圖能夠解釋一切
那麼這個東西有怎麼和指向這個概念去聯繫上呢?其實這裏須要說起到的一個點,也是上面截圖中存在的一個點,就是__proto__,我喜歡把其稱爲原型指針。終歸到頭,prototype只不過是一個屬性而已,它沒有什麼實際的意義,最後能作原型鏈繼承的仍是經過__proto__這個原型指針來完成的。咱們看到的所謂的繼承只不過是將須要繼承的屬性掛載到繼承者的prototype屬性上面去的,實際在找尋繼承的屬性的時候,會經過__proto__原型指針一層一層往上找,即會去找__proto__原型指針它的一個指向。看個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);
其執行結果以下
更多關於繼承的點,這裏就不說起了,我會在繼承這一章節作詳細的講解。那麼「單獨」關於原型/原型鏈的點就這些了。
總結:原型即prototype,它只是全部function上的一個屬性而已,真正的「大佬」是__proto__,「大佬」指向誰,誰纔能有言語權(固然可能由於「大佬」過於霸道,因此在ECMA-262以後才被Standard化)。
關於繼承,以前我有寫過一篇博文對繼承的一些主流方式進行過總結。想詳細瞭解的請點擊傳送門。這裏咱們經過指向這個概念來從新理解一下繼承。這裏咱就談兩個萬變不離其宗的繼承方式,一個是構造函數繼承,一個是原型鏈繼承。
其實就是上面說起到的經過call/apply調用,將this指向變成thisArg,具體看上面的解釋,這裏直接上代碼
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'}
通常狀況,咱們作原型鏈繼承,會經過子類prototype屬性等於(指向)父類的實例。即
Child.prototype = new Parent();
那麼這樣的作法具體是怎麼實現原型鏈繼承的呢?
首先在講解繼承前,咱們須要get到一個點,那就是對象{ }它內部擁有的一些屬性,這裏直接看張圖
如上圖所示,咱們看到對象{ }它自己擁有的屬性就是上面咱們說起到的__proto__原型指針以及一些方法。
接下來我先說一下new關鍵字具體作的一件事情。其過程大體分爲三步,以下
var obj= {}; // 初始化一個對象obj obj.__proto__ = Parent.prototype; // 將obj的__proto__原型指針指向父類Parent的prototype屬性 Parent.call(obj); // 初始化Parent構造函數
從這裏咱們看出來,相信你們也能理解爲何我在上面說__proto__纔是真正的「大佬」。
這裏我額外提一件咱們常常乾的「高端」的事情,那就是經過原型prototype作monkey patch。即我想在繼承父類方法的同時,完成本身獨立的一些操做。具體代碼以下
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
這個是咱們對於自定義的類進行繼承並重寫,那麼若是是相似Array,Number,String等內置類進行繼承重寫的話,結果會是如何呢?關於這個話題我也有寫過一篇博文進行過講解,傳送門
對於閉包,我曾經也作過總結和分享,簡單的一些東西和概念這裏不說起了,想了解的能夠猛戳這裏。和原型鏈那章一張,這裏會摒棄掉原來的一些見解,這裏我依舊經過代入指向這個概念來進行理解。
通常狀況下,咱們理解閉包是這樣的:「爲了能夠訪問函數內的局部變量而定義的內部函數」。
JavaScript語言特性,每個function內都有一個屬於本身的執行上下文,即特定的context指向。
內層的context上下文總能訪問到外層context上下文中的變量,即每次內部的做用域能夠往上層查找直到訪問到當前所需訪問的變量。例子以下
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
可是若是反過來訪問的話,則不能進行訪問,即變量訪問的指向是當前context上下文的指向的相反方向,且不可逆。以下
function test () { var b = 'this is test'; } console.log(b); // Uncaught ReferenceError: b is not defined
這裏用一個很是常見的狀況做爲例子,即for循環配合setTimeout的異步任務,以下
function test () { for (var i = 0; i < 4; i++) { setTimeout(function () { console.log(i); }, 0) } } test();
看到上面的例子,咱們都知道說:「答案會打印4次4」。那麼爲何會這樣呢?我想依次打印0,1,2,3又該怎麼作呢?
相信不少小夥伴們都會說,用閉包呀,就能實現了呀。對沒錯,的確用閉包就能實現。那麼爲何出現這種狀況呢?
這裏我簡單提一下,首先這邊牽扯到兩個點,一個就是for循環的同步任務,一個就是setTimeout的異步任務,在JavaScript線程中,由於自己JavaScript是單線程,這個特色決定了其正常的腳本執行順序是按照文檔流的形式來進行的,即從上往下,從左往右的這樣方向。每次腳本正常執行時,但凡遇到異步任務的時候,都會將其set到一個task queue(任務隊列)中去。而後在執行完同步任務以後,再來執行隊列任務中的異步任務。
固然對於不一樣的異步任務,執行順序也會不同,具體就看其到底屬於哪一個維度的異步任務了。這裏我就不詳細扯Event Loop了,想更詳細的瞭解請戳這裏
回到上面咱們想要實現的效果這個問題上來,咱們通常處理方法是利用閉包進行參數傳值,代碼以下
function test () { for (var i = 0; i < 4; i++) { (function (e) { setTimeout(function () { console.log(e); }, 0) })(i) } } test(); // 0 -> 1 -> 2 -> 3
循環當中,匿名函數會當即執行,而且會將循環當前的 i 做爲參數傳入,將其做爲當前匿名函數中的形參e的指向,即會保存對 i 的引用,它是不會被循環改變的。
固然還有一種常見的方式能夠實現上面的效果,即從自執行匿名函數中返回一個函數。代碼以下
function test () { for(var i = 0; i < 4; i++) { setTimeout((function(e) { return function() { console.log(e); } })(i), 0) } } test();
更多高階閉包的寫法這裏就不一一介紹了,想了解的小夥伴請自行搜索。
文章到此差很少就要結束了
但是我也沒辦法,的確要結束了。下面給整篇博文作個總結吧
首先基本上JavaScript中所涉及的所謂的難點,在本文中都經過指向這個概念進行了通篇的解讀,固然這是我我的對於JavaScript的一些理解,思路僅供參考。若是有什麼不對的地方,歡迎各位小夥伴指出。
其實寫該博文的好屢次,我想把全部的知識點所有串起來進行講解,但又怕效果很差,因此作了一一的拆解,也進行了混合的運用。具體能領悟到多少,就要看小夥伴大家本身的了。