一個map函數引起的血案

前言


本文寫做目的在於,對上次面試中未手寫出來的map函數作一個收尾工做。其內容以map函數做爲線,將其涉及到的衆多知識點穿針引線梳理一下,並賦予本人學習及寫做時的所感所想。既是所感所想,想必不免存在一些我的拙見,望各位大佬不吝指正,還望輕噴!!!node

map函數是個黑盒


還記得初識JS的map函數時webpack

[1,2,3].map((e, i, arr) => {
    return 2*e;  //[2,4,6]
})
複製代碼

大學簡單學過C語言後,第一次看到這個用法就感受特別神奇,徹底不知道它怎麼運做,僅對它造成了一個大體的輪廓:你想基於原數組生成一個怎樣的新數組,只要把邏輯寫在回調函數裏就行了。對我而言它徹底就像一個黑盒。但不覺間潛意識裏卻模糊了一些概念(已然與未然/主動與被動的關係)。web

對不起,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所表示的一個對象)json

  • 如今就讓咱們一步步揭開map的神祕面紗

想一想我第一次看到上圖的時候非常懵逼,明明我只聲明瞭一個數組,怎麼它竟然擁有map函數這樣的屬性?還有_proto_這個屬性是什麼?不行我暈了。(相信聰明的你必定沒問題!!!)後來學習了原型鏈後才明白。

  • 當訪問屬性的時候,會先在本實例對象(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中回調的真容吧。promise

  • 一直以來我都把回調函數理解成主動性,但事實上傳入的回調函數是被動性的。想一下平時咱們爲了實現某個功能定義了一個函數,而後傳參執行該函數。但回調函數本質只是一個函數聲明,之因此會執行相關的邏輯是由於以後會給該回調函數傳入參數並調用該回調函數,它是被調用的。bash

  • 那麼這裏又涉及到已然性和未然性。原生的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截圖背後竟有如此鮮爲人知的操做,真是該對平常理所應當的事更上上心了呀
相關文章
相關標籤/搜索