前言
本文寫作目的在於,對上次面試中未手寫出來的map函式做一個收尾工作。其內容以map函式作為線,將其涉及到的眾多知識點穿針引線梳理一下,並賦予本人學習及寫作時的所感所想。既是所感所想,想必難免存在一些個人拙見,望各位大佬不吝指正,還望輕噴!!!
map函式是個黑盒
還記得初識JS的map函式時
[1,2,3].map((e, i, arr) => {
return 2*e; //[2,4,6]
})
複製程式碼
大學簡單學過C語言後,第一次看到這個用法就感覺特別神奇,完全不知道它怎麼運作,僅對它形成了一個大致的輪廓:你想基於原陣列生成一個怎樣的新陣列,只要把邏輯寫在回撥函式裡就好了。對我而言它完全就像一個黑盒。但不覺間潛意識裡卻模糊了一些概念(已然與未然/主動與被動的關係)。
對不起,map函式實現不來
- 上次面試時終於被這個黑盒給安排了。面試官讓我手寫map函式,我懵逼!儘管感覺好像能懟出來,卻總差了點什麼。被摁在地板摩擦之後,發現自己沒做出來確實是有原因。
- 一方面,寫這篇文章的時候發現,實現map所需要的知識點我都有涉獵,但卻僅把它當作理論指導,沒有把它與實踐聯絡起來。今天就讓我來學以致用一下。
- 另一方面,對於一些概念有些許誤區,儘管這些誤區看似可有可無微乎其微,但卻真真切切的影響我很多。那就讓這篇文章歡送這些不速之客。
map黑盒背後的利益集團
以我個人入門JS的心路歷程來看,倘若對以下知識有所涉獵瞭解,就算不是十分熟練也能輕鬆實現map函式。知識點如下: (僅以實現map而做簡單講解,詳情內容請自閱其他文獻)
1. 資料型別與儲存
JS中基本資料型別和引用資料型別是不同的。當我們把一個儲存基本資料型別值的變數A賦值給另一個變數B時,本質是值傳遞,兩個變數儲存兩個獨立的值。但若是引用資料型別,本質是地址傳遞,那麼此時兩個變數儲存的是同一個資料的地址,因此A、B會互相影響。
- 而函式是引用資料型別,其儲存在堆記憶體中,將其賦值給一變數,則該變數儲存的是函式在堆記憶體中的起始地址,以此來引用函式。
- 在這裡還想多扯一下,關於賦值,深、淺拷貝的問題,對比研究一下能很快掌握。另外說到儲存不得不提提垃圾回收機制,都可以偷偷學一下,串一下。
2. 函式是一等公民
我們知道JS是一門多正規化語言,其中就包括函數語言程式設計。因此在JS中函式就像任何其他引用資料型別一樣可以把它們存在陣列裡,當作函式引數傳遞,賦值給變數,作為物件的屬性值等。
- 作為物件的屬性值 我們定義了一個物件f,並將一個函式myFn賦值給了f中的屬性fn,則此時我們的f.fn屬性就已經指向了該函式,並可以通過f.fn()完成對函式的呼叫。
- 當作函式引數傳遞 這裡我們宣告瞭一個fn函式,其接受一個函式作為引數並執行。我們又宣告瞭一個callback函式。接著將callback作為引數傳入fn中,並執行fn函式,其結果就是callback在fn函式中執行了。 如果你真的會意該部分,那麼對你而言,map函式的金鐘罩將會變成最後一塊遮羞布了。
3. 原型與原型鏈 / new建構函式呼叫的過程
在JS中,當我們用 var arr = [1,2,3] 建立一個陣列並將其賦值給變數arr時,該方式本質上與var arr = new Array(1,2,3) 是沒有區別的。(這裡突然意識到,還涉及到new建構函式呼叫的知識,優秀的你應該是知道該知識點的!!) 那麼此時arr表示的陣列就可以稱為Array的一個例項,該例項的_proto_屬性是指向建構函式Array的原型物件(也就是Array.prototype所表示的一個物件)
- 現在就讓我們一步步揭開map的神祕面紗
- 當訪問屬性的時候,會先在本例項物件(arr)中搜尋屬性,若未找到則會通過原型鏈繼續搜尋其指標指向的原型物件(Array.prototype)是否有該屬性,OK找到了。沒錯我們平常用的map函式一般都是通過原型鏈查詢到的Array.prototype.map所指向的函式。
- 這裡再多扯一些,我們建立一個陣列並將其起始地址賦值給h,同理得g。但是h卻不等於g。因為對於引用資料型別,g、h變數儲存的是堆記憶體中該資料的起始地址,而記憶體中同時開闢了兩個資料地址,因此不等。那麼也就是說,這裡arr._proto_指向的物件與Array.prototype指向的物件是堆記憶體中同一個引用資料型別。而arr.map通過原型鏈查詢到的即是Array.prototype.map指向的函式因此也必然是相等的。
- 再多扯一點,關於物件中屬性的讀取與修改,與作用域變數的讀取與修改還是有很大不同的。感興趣的話可以研究一下,對比記憶很快就掌握了。
4. this的指向性問題
關於JS函式裡this的指向問題就不再概述了,大致分為四個規則加一個特殊的箭頭函式。現在對於 [1,2,3].map(callback) 我們大概明白了,通過[1,2,3].map以原型鏈查詢的方式找到了在Array.prototype.map裡的函式,然後將callback函式作為引數傳入map函式中以達到後期呼叫並執行相關邏輯的目的,但是我們怎麼在呼叫的函式中找到原陣列?沒錯通過函式中的this。
- 由this指向規則中的隱含轉換知(this本質是函式執行時建立的執行上下文裡的一個物件,因此this的指向由呼叫點決定),當我們通過a.fn()呼叫a.fn指向的函式時,函式中的this就指向物件a。同理,當實現map函式時也可通過此原理來找到原陣列,即實現的map函式裡的this就指向例項陣列本身。[1,2,3].map()則函式裡的this指向該[1,2,3]陣列。
5. 回撥函式
學習JS時才第一次接觸回撥函式,一度覺得自己挺懂回撥,後來發現自己真的是根本不懂,還以為自己很懂!現在讓我們看看map中回撥的真容吧。
-
一直以來我都把回撥函式理解成主動性,但事實上傳入的回撥函式是被動性的。想一下平時我們為了實現某個功能定義了一個函式,然後傳參執行該函式。但回撥函式本質只是一個函式宣告,之所以會執行相關的邏輯是因為之後會給該回撥函式傳入引數並呼叫該回撥函式,它是被呼叫的。
-
那麼這裡又涉及到已然性和未然性。原生的map函式是被定義過的,當呼叫map函式實現相關邏輯時,它內部執行流程就會將陣列每個元素的(item/元素值, index/元素索引, arr/原陣列)傳入回撥函式callback並以callback(item, index, arr)的形式呼叫。因此,我們知道該回撥會被傳入指定的引數並呼叫。所以,我們在僅需要做的宣告傳入的回撥函式時,可以把此時回撥函式的引數當做對應的陣列中元素的值,在此基礎上實現相關邏輯。實際上,就是把宣告回撥裡的引數當做map執行時內部呼叫回撥時傳入的引數(item, index, arr)進行操作
-
其實以上兩點總結來說就是以往我們都是先宣告函式,再傳參呼叫。而現在我們在理解map函式時遇到的事實卻是,已經確定了將回撥函式傳入map中呼叫時,將會在map函式內呼叫該回撥函式,且該回撥函式是被傳入了固定引數的狀態下呼叫的。因此可以說我們已經確定了內部會自動執行該回撥,就差宣告回撥並傳入map中執行了。所以現在對回撥函式的理解是,會(hui)被呼叫的函式。
- 如上圖,我們定義了一個sumTwoItemFlag函式,它接受一個回撥函式並將物件a,b傳入該回撥執行,所以當我們執行sumTwoItemFlag函式時要傳入一個宣告的回撥函式,並在回撥裡完成相應的邏輯。這就是之前贅述的,已經確定好回撥函式呼叫時傳入的引數,我們已經知道此時回撥裡的引數就是sumTwoItemFlag中的a,b物件。在此基礎上,我們只需要傳入回撥的時候把回撥裡的引數當做是a、b,並執行我們想要的邏輯就可以了。
- 其本質上就是反其道而行之。但卻能達到我們思維裡的先宣告再呼叫的正常邏輯,且其更加靈活。因為雖然回撥函式執行時傳入的引數是固定的,但是對於map函式來說傳入的回撥函式卻是靈活多變的,所以可以根據個人傳入回撥的不同,達到靈活實現陣列操作的目的,真正是一本萬利呀!回撥牛逼!!!
- 再囉嗦一局,該部分知識點配合上篇文章推薦知識清單中的用promise實現jsonp更絲滑哦。(該部分好像很囉嗦很重複,但還是選擇了囉嗦重複,那你就把它當做強調吧)
手寫程式碼
相信看完內容的你已經對map這個有了很清晰的認識了吧。其實我覺得如果以上能掌握,那麼以後絕大多數手寫方法的題應該都不成問題了。那麼接下來就讓我貼出手寫map的程式碼吧。(寫文章真是個累人的活啊,貼出來把,寫不動了!)
map實現
Array.prototype.myMap = function(callback, context) {
var arr = this;
var res = [];
context = context ? context : window;
for(let i = 0; i < arr.length; i++) {
let tem = callback.call(context, arr[i], i, arr);
res.push(tem);
}
return res;
}
複製程式碼
reduce的實現
相信只要你稍微動動靈活的小腦袋肯定也能實現一個reduce函式吧。
Array.prototype.myReduce = function(callback) {
var arr = this;
var res; <!--用arguments捕獲第二個引數因為其值可能是null,NaN之類-->
if(typeof(callback) !== "function") throw new Error("not a function");
if(arguments.length < 2 && arr.length === 0) throw new Error("empty array with no initial value");
if(arguments.length < 2 && arr.length === 1) return arr[0];
if(arguments.length > 1 && arr.length === 0) return arguments[1];
res = arguments.length > 1? arguments[1] : arr.shift();
for(let i = 0; i < arr.length; i++) {
res = callback(res, arr[i], i, arr);
}
return res;
}
複製程式碼
map的reduce實現
Array.prototype._myMap = function(callback, context) {
context = context ? context : window;
return this.reduce((accum, item, index, arr) =>
[...accum, callback.call(context, item, index, arr)]
, []);
}
複製程式碼
總結
- 千萬別鑽進牛角尖。相信你也看出來了,上述只是map函式等的簡易版實現,關於該方法實現map函式,其邊界情況真的是太多了。而我就有幸((┬_┬))鑽入了牛角尖,意圖實現一個理想的map。其結果就是花費了太多時間卻收效甚微。看了原始碼之後頓感自己真是傻!
- 學習的時候方向不能搞錯啊! 為什麼說自己傻呢?我在實現的時候還是在用原生map手動測試邊界情況,花費來大量時間之後,終於覺得搞不定了,要去看看原始碼。看了原始碼之後就開始懷疑人生了。其實仔細想想也能明白,沒必要實現一個完美的map啊,你就算仿了一個完美的map又能說明什麼?想搞明白就去看原始碼啊,還在那跟個××一樣意圖從表面探測真相,還是手動的。另一方面,面試官出這個題也是想考察你的基本功,也不可能真是讓你完美實現啊。所以學習的時候千萬不能搞錯方向,更不要鑽進不錯誤的牛角尖。
- 想探究一個技術的真容,如果不懂,真的搞不定的話,就去學習原始碼。 這可以說是唯一欣慰的一點收穫吧。以前學習webpack的時候也想弄明白這個黑盒的真容,當時也是手動由表入裡的探索,結果最後實在是進行不下去了,結果就收工了,好在當時還是有所收穫的。這篇文章後將更加堅定了我以研究原始碼作為日後學習各種黑盒的決心。
展望
- 感覺對箭頭函式還是有一點不熟練,準備再研究一哈。
- 接下來想再探探webpack的真容,目前想的是從原始碼方面入手,如果太難的話就再補充補充涉及到的前置知識,再繼續攻略原始碼。
- 昨天在看node開發實戰裡的爬蟲實戰時,驚覺以前似懂非懂的模組呼叫並摻雜著一些回撥的邏輯業務,居然能看懂,不再半遮半掩了。感覺寫文章真的是對以前純輸入的一種很好的輸出方式。不僅能認識新朋友,更能對多所學知識進行一個梳理,總結。現在已經從純輸入過渡到想輸出的階段了,以後會繼續堅持下去。但是也要深深的明白,根據能量守恆,這些輸出是建立在以前輸入的基礎上,所以未來的日子也不能忘記充電呀!!!
- 本以為QQ截圖會保留在本地,但在寫這篇文章的時候居然驚奇地發現用QQ截圖拖入該編輯頁面居然會有該圖片的地址,且在非本地情況下輸入網址後真的有該圖片,感覺自己真的對網路一無所知。粗略的想了一下(瞎猜的),大概是截圖成功就會將該圖片放入儲存我QQ對應資料的資料庫中吧,然後可以通過該url訪問此圖片。之後會想要了解清楚。原來習以為常的QQ截圖背後竟有如此不為人知的操作,真是該對日常理所應當的事更上上心了呀。