<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JavaScript 數(shù)據(jù)處理 - 映射表篇

          共 8076字,需瀏覽 17分鐘

           ·

          2022-03-16 13:46

          作者:邊城

          來源:SegmentFault ?思否社區(qū)?


          JavaScript 的常用數(shù)據(jù)集合有列表 (Array) 和映射表 (Plain Object)。列表已經(jīng)講過了,這次來講講映射表。


          由于 JavaScript 的動態(tài)特性,其對象本身就是一個映射表,對象的「屬性名?屬性值」就是映射表中的「鍵?值」。為了便于把對象當(dāng)作映射表來使用,JavaScript 甚至允許屬性名不是標(biāo)識符 —— 任意字符串都可以作為屬性名。當(dāng)然非標(biāo)識符屬性名只能使用?[]?來訪問,不能使用 . 號訪問。


          使用?[]?訪問對象屬性更契合映射表的訪問形式,所以在把對象當(dāng)作映射表使用時,通常會使用?[]?訪問表元素。這個時候?[]?中的內(nèi)容稱為“鍵”,訪問操作存取的是“值”。因此,映射表元素的基本結(jié)構(gòu)稱為“鍵值對”。、


          在 JavaScript 對象中,鍵允許有三種類型:number、string 和 symbol。


          number 類型的鍵主要是用作數(shù)組索引,而數(shù)組也可以認(rèn)為是特殊的映射表,其鍵通常是連續(xù)的自然數(shù)。不過在映射表訪問過程中,number 類型的鍵會被轉(zhuǎn)成 string 類型來使用。


          symbol 類型的鍵用得比較少,一般都是按規(guī)范使用一些特殊的 Symbol 鍵,比如 Symbol.iterator。symbol 類型的鍵通常會用于較為嚴(yán)格的訪問控制,在使用 Object.keys()?和 Object.entries()?訪問相關(guān)元素時,會忽略掉鍵類型是 symbol 類型的元素。


          一、CRUD



          創(chuàng)建對象映射表直接使用?{ }?定義 Object Literal 就行,基本技能,不用詳述。但需要注意的是?{ }?在 JavaScript 也用于封裝代碼塊,所以把 Object Literal 用于表達(dá)式時往往需要使用一對小括號把它包裹起來,就像這樣:({ })。在使用箭頭函數(shù)表達(dá)式直接返回一個對象的時候尤其需要注意這一點。


          對映射表元素的增、改、查都用?[]?運算符。


          如果想判斷某個屬性是否存在,有人習(xí)慣用?!!map[key]?,或者?map[key] === undefined?來判斷。使用前者要注意 JavaScript 假值的影響;使用后者則要注意有可能值本身就是?undefined。如果想準(zhǔn)確地判斷是否存在某個鍵,應(yīng)該使用?in?運算符:


          const?a?=?{?k1:?undefined?};

          console.log(a["k1"]?!==?undefined);??//?false
          console.log("k1"?in?a);??????????????//?true

          console.log(a["k2"]?!==?undefined);??//?false
          console.log("k2"?in?a);??????????????//?false


          類似地,要刪除一個鍵也不是將其值改變?yōu)?undefined?或者?null,而是使用?delete?運算符:


          const?a?=?{?k1:?"v1",?k2:?"v2",?k3:?"v3"?};

          a["k1"]?=?undefined;
          delete?a["k2"];

          console.dir(a);?//?{?k1:?undefined,?k3:?'v3'?}


          使用?delete a["k2"]?操作后?a?的?k2?屬性不復(fù)存在。


          上述兩個示例中,由于 k1、k2、k3 都是合法標(biāo)識符,ESLint 可能會報違反 dot-notation 規(guī)則。這種情況下可以關(guān)閉此規(guī)則,或者改用 . 號訪問(由團(tuán)隊決定處理方式)。


          二、映射表中的列表



          映射表可以看作是鍵值對的列表,所以映射表可以轉(zhuǎn)換成鍵值對列表來處理。

          鍵值對用英語一般稱為 key value pair 或 entry,Java 中用?Map.Entry?來描述;C# 中用?KeyValuePair?來描述;JavaScript 中比較直接,使用一個僅含兩個元素的數(shù)組來表示鍵值對,比如?["key", "value"]。


          在 JavaScript 中,可以使用?Object.entries(it)?來得到一個由?[鍵, 值]?形成的鍵值對列表。


          const?obj?=?{?a:?1,?b:?2,?c:?3?};
          console.log(Object.entries(obj));
          //?[?[?'a',?1?],?[?'b',?2?],?[?'c',?3?]?]


          映射表除了有 entry 列表之外,還可以把鍵和值分開,得到單獨的鍵列表,或者值列表。要得到一個對象的鍵列表,使用?Object.keys(obj)?靜態(tài)方法;相應(yīng)的要得到值列表使用?Object.values(obj)?靜態(tài)方法。


          const?obj?=?{?a:?1,?b:?2,?c:?3?};

          console.log(Object.keys(obj));??????//?[?'a',?'b',?'c'?]
          console.log(Object.values(obj));????//?[?1,?2,?3?]


          三、遍歷映射表



          既然映射表可以看作鍵值對列表,也可以單獨取得鍵或值的列表,那么遍歷映射表的方法也比較多。


          最基本的方法就是用?for?循環(huán)。不過需要注意的是,由于映射表通常不帶序號(索引號),不能通過普通的?for(;;)?循環(huán)來遍歷,而是需要使用 for each 來遍歷。不過有意思的是,for...in?可以用于會遍歷映射表所有的 Key;但在映射表上使用?for...of?會出錯,因為對象“is not iterable”(不可迭代,或不可遍歷)。


          const?obj?=?{?a:?1,?b:?2,?c:?3?};
          for?(let?key?in?obj)?{
          ????console.log(`${key}?=?${obj[key]}`);???//?拿到?key?之后通過?obj[key]?來取值
          }
          //?a?=?1
          //?b?=?2
          //?c?=?3


          既然映射表可以單獨拿到鍵集和值集,所以在遍歷的處理上會比較靈活。但是通常情況下我們一般都會同時使用鍵和值,所以在實際使用中,比較常用的是對映射表的所有 entry 進(jìn)行遍歷:


          Object.entries(obj)
          ????.forEach(([key,?value])?=>?console.log(`${key}?=?${value}`));


          四、從列表到映射表



          前面兩個小節(jié)都是在講映射表怎么轉(zhuǎn)成列表。反過來,要從列表生成映射表呢?


          要從列表生成映射表,最基本的操作是生成一個空映射表,然后遍歷列表,從每個元素中去取到“鍵”和“值”,將它們添加到映射表中,比如下面這個示例:


          const?items?=?[
          ????{?name:?"size",?value:?"XL"?},
          ????{?name:?"color",?value:?"中國藍(lán)"?},
          ????{?name:?"material",?value:?"滌綸"?}
          ];

          function?toObject(specs)?{
          ????return?specs.reduce((obj,?spec)?=>?{
          ????????obj[spec.name]?=?spec.value;
          ????????return?obj;
          ????},?{});
          }

          console.log(toObject(items));
          //?{?size:?'XL',?color:?'中國藍(lán)',?material:?'滌綸'?}


          這是常規(guī)操作。注意到?Object?還提供了一個?fromEntries()?靜態(tài)方法,只要我們準(zhǔn)備好鍵值對列表,使用?Object.fromEntries()?就能快速得到相應(yīng)的對象:


          function?toObject(specs)?{
          ????return?Object.fromEntries(
          ????????specs.map(({?name,?value?})?=>?[name,?value])
          ????);
          }


          五、一個小小的應(yīng)用案例



          數(shù)據(jù)處理過程中,列表和映射表之間往往需要相互轉(zhuǎn)換以達(dá)到較為易讀的代碼或更好的性能。本文前面的內(nèi)容已經(jīng)講到了轉(zhuǎn)換的兩個關(guān)鍵方法:


          • Object.entries()?把映射表轉(zhuǎn)換成鍵值對列表
          • Object.fromEntries()?從鍵值對列表生成映射表

          在哪些情況下可能用到這些轉(zhuǎn)換呢?應(yīng)用場景很多,比如這里就有一個比較經(jīng)典的案例。

          提出問題:

          從后端拿到了一棵樹的所有節(jié)點,節(jié)點之間的父關(guān)系是通過 parentId 字段來描述的。現(xiàn)在想把它構(gòu)建成樹形結(jié)構(gòu)該怎么辦?樣例數(shù)據(jù):

          [
          ?{?"id":?1,?"parentId":?0,?"label":?"第?1?章"?},
          ?{?"id":?2,?"parentId":?1,?"label":?"第?1.1?節(jié)"?},
          ?{?"id":?3,?"parentId":?2,?"label":?"第?1.2?節(jié)"?},
          ?{?"id":?4,?"parentId":?0,?"label":?"第?2?章"?},
          ?{?"id":?5,?"parentId":?4,?"label":?"第?2.1?節(jié)"?},
          ?{?"id":?6,?"parentId":?4,?"label":?"第?2.2?節(jié)"?},
          ?{?"id":?7,?"parentId":?5,?"label":?"第?2.1.1?點"?},
          ?{?"id":?8,?"parentId":?5,?"label":?"第?2.1.2?點"?}
          ]

          一般思路是先建一個空樹(虛根),然后按順序讀取節(jié)點列表,每讀到一個節(jié)點,就從樹中找到正確的父節(jié)點(或根節(jié)點)插入進(jìn)去。這個思路并不復(fù)雜,但實際操作起來會遇到兩個問題

          1. 在已生成的樹中查找某個節(jié)點本身是個復(fù)雜的過程,不管是用遞歸通過深度遍歷查找,還是用隊列通過廣度遍歷查找,都需要寫相對復(fù)雜的算法,也比較耗時;

          2. 對于列表所有節(jié)點順序,如果不能保證子節(jié)點在父節(jié)點之后,處理的復(fù)雜度會大大增加。


          要解決上面兩個問題也不難,只需要先遍歷一遍所有節(jié)點,生成一個?[id => node]?的映射表就好辦了。假設(shè)這些數(shù)據(jù)拿到之后由變量?nodes?引用,那么可以用如下代碼生成映射表:

          const?nodeMap?=?Object.fromEntries(
          ????nodes.map(node?=>?[node.id,?node])
          );

          具體過程就不詳述了,有興趣的讀者可以去閱讀:從列表生成樹 (JavaScript/TypeScript):

          鏈接:https://segmentfault.com/a/1190000040336164

          六、映射表的拆分



          映射表本身不支持拆分,但是我們可以按照一定規(guī)則從中選擇一部分鍵值對出來,組成新的映射表,達(dá)到拆分的目的。這個過程就是?Object.entries()??filter()???Object.fromEntries()。比如,希望把某配置對象中所有帶下劃線前綴的屬性剔除掉:


          const?options?=?{?_t1:?1,?_t2:?2,?_t3:?3,?name:?"James",?title:?"Programmer"?};

          const?newOptions?=?Object.fromEntries(
          ????Object.entries(options).filter(([key])?=>?!key.startsWith("_"))
          );
          //?{?name:?'James',?title:?'Programmer'?}

          不過,對于非常明確地知道要清除掉哪些元素的時候,使用?delete?會更直接。
          這里再舉一個例子:

          提出問題:

          某項目做技術(shù)升級,原來的異步請求是在參數(shù)中傳遞 success fail 回調(diào)事處理異步,新的接口改為 Promise 風(fēng)格,參數(shù)中不再需要 successfail?,F(xiàn)在的問題是:大量應(yīng)用這個異步操作的代碼需要一定的時間來完成遷移,而在這期間,仍需要保證舊接口能正確執(zhí)行。

          為了遷移期間的兼容性,這段代碼需要把參數(shù)對象中的?success?和?fail?拿出來,從原參數(shù)對象中去掉,再把處理過的參數(shù)對象交給新的業(yè)務(wù)處理邏輯。這里去掉?success?和?fail?兩個 entry 的操作就可以用?delete?來完成。

          async?function?asyncDoIt(options)?{
          ????const?success?=?options.success;
          ????const?fail?=?options.fail;
          ????delete?options.success;
          ????delete?options.fail;
          ????try?{
          ????????const?result?=?await?callNewProcess(options);
          ????????success?.(result);
          ????}?catch?(e)?{
          ????????fail?.(e);
          ????}
          }

          這是中規(guī)中矩的做法,花了 4 行代碼來處理兩個特殊 entry。其中前兩句很容易想到可以使用解構(gòu)來簡化:

          const?{?success,?fail?}?=?options;

          但是有沒有發(fā)現(xiàn),后兩句也可以合并進(jìn)去?你看 ——

          const?{?success,?fail,?...opts?}?=?options;

          這里拿到的?opts?可不就是排除了?success?和?fail?兩個 entry 的選項表!
          更進(jìn)一步,我們可以利用解構(gòu)參數(shù)語法把解構(gòu)過程移到參數(shù)列表中去。下面是修改后的?asyncDoIt

          async?function?asyncDoIt({?success,?fail,?...options?}?=?{})?{
          ????//?TODO?try?{?...?}?catch?(e)?{?...?}
          }

          利用解構(gòu)拆分映射表讓代碼看起來非常簡潔,這樣的函數(shù)定義方式可以照搬到箭頭函數(shù)上,作為鏈?zhǔn)綌?shù)據(jù)處理過程中的處理函數(shù)。這樣一來,拆分?jǐn)?shù)據(jù)在定義參數(shù)的時候順手就解決了,代碼整體看起來會非常簡潔清晰。

          七、合并映射表



          合并映射表,基本操作肯定還是循環(huán)添加,不推薦。

          既然 JavaScript 的新特性提供了更便捷的方法,干嘛不用呢!新特性基本上也就兩種:

          • Object.assign()
          • 展開運算符

          語法和接口說明都可以在 MDN 上去看,這里還是用案例來說:

          提出問題

          有一個函數(shù)的參數(shù)是一個選項表,為了方便使用不需要調(diào)用者提供全部選項,沒提供的選項全部采用默認(rèn)選項值。但是一個個去判斷太繁瑣了,有沒有比較簡單的辦法?

          有,當(dāng)然有!用?Object.assign()??。?/span>

          const?defaultOptions?=?{
          ????a:?1,?b:?2,?c:?3,?d:?4
          };

          function?doSomthing(options)?{
          ????options?=?Object.assign({},?defaultOptions,?options);
          ????//?TODO?使用?options
          }

          提出這個問題可能是因為不知道?Object.assign(),一旦知道了,會發(fā)現(xiàn)用起來還是很簡單。不過簡單歸簡單,坑還是有的。

          這里?Object.assign()?的第一個參數(shù)一定要給一個空映射表,否則?defaultOptions?會被修改掉,因為?Object.assign()?會把每個參數(shù)中的 entries 合并到它的第一個參數(shù)(映射表)中。

          為了避免?defaultOptions?被意外修改,可以把它“凍”住:

          const?defaultOptions?=?Object.freeze({
          //?????????????????????^^^^^^^^^^^^^^
          ????a:?1,?b:?2,?c:?3,?d:?4
          });

          這樣一來,Object.assign(defaultOptions, ...)?會報錯。

          另外,使用展開運算符也可以實現(xiàn):

          options?=?{?...defaultOptions,?...options?};

          使用展開運算符更大的優(yōu)勢在于:要添加單個 entry 也很方便,不像?Object.assign()?必須要把 entry 封裝成映射表。

          function?fetchSomething(url,?options)?{
          ????options?=?{
          ????????...defaultOptions,
          ????????...options,
          ????????url,????????//?鍵和變量同名時可以簡寫
          ????????more:?"hi"??//?普通的?Object?Literal?屬性寫法
          ????};
          ????//?TODO?使用?options
          }

          講了半天,上面的合并過程還是有個大坑,不知道你發(fā)現(xiàn)了沒?—— 上面一直在說合并映射表,而不是合并對象。雖然映射表就是對象,但映射表的 entry 就是簡單的鍵值對關(guān)系;而對象不同,對象的屬性存在層次和深度。

          舉例來說,

          const?t1?=?{?a:?{?x:?1?}?};
          const?t2?=?{?a:?{?y:?2?}?};
          const?r?=?Object.assign({},?t1,?t2);????//?{?a:?{?y:?2?}?}

          結(jié)果是?{ a: { y: 2} }?而不是?{ a: { x: 1, y: 2 } }。前者是淺層合并的結(jié)果,合并的是映射表的 entries;后者是深度合并的結(jié)果,合并的是對象的多層屬性。

          手寫深度合并工作量不小,不過 Lodash 有提供?_.merge()?方法,不妨用現(xiàn)成的。_.merge()?在合并數(shù)組的時候可能會不符合預(yù)期,這情況使用?_.mergeWith()?自定義處理數(shù)組合并就好,文檔中就有現(xiàn)成的例子。

          八、Map 類



          JavaScript 也提供了專業(yè)的?Map,和 Plain Object 相比,它允許任意類型的“鍵”,而不局限于 string。

          上面提到的各種操作在 Map 都有對應(yīng)的方法。無需詳述,簡單介紹一下即可:

          • 添加/修改,使用?set()?方法;

          • 通過鍵取值,使用?get()?方法;

          • 根據(jù)鍵刪除,使用?delete()?方法,還有一個?clear()?直接清空映射表;

          • has()?訪求用來判斷是否存在某個鍵值對;

          • size?屬性可以拿到 entry 數(shù),不像 Plain Object 需要用?Object.entries(map).length?來獲取;

          • entries()keys()?和?values()?方法用來獲取 entry、鍵、值的列表,但結(jié)果不是數(shù)組,而是 Iterator;

          • 還有個?forEach()?方法直接用來遍歷,處理函數(shù)不接收整個 entry (即?([k, v])),而是分離的?(value, key, map)。


          總結(jié)



          在 JavaScript 中你用的到底是對象還是映射表呢?說實在的并不太容易說得清楚。作為映射表來說,上面提到的各種方法足夠使用 了,但是作為對象,JavaScript 還提供了更多的工具方法,需要了解可以查查 Object API 和 Reflect API。

          掌握對列表和映射表的操作方法,基本上可以解決日常遇到的各種 JavaScript 數(shù)據(jù)處理問題。像什么數(shù)據(jù)轉(zhuǎn)換、數(shù)據(jù)分組、分組展開、樹形數(shù)據(jù) …… 都不在話下。一般情況下 JavaScript 原生 API 足夠用了,但如果遇到處理起來較為復(fù)雜的情況(比如分組),不妨去查查 Lodash 的 API,畢竟是個專業(yè)的數(shù)據(jù)處理工具。

          -?END -

          瀏覽 57
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  51ⅴ精品国产91久久久久久 | 免费一级片网站 | 影音先锋在线爱爱 | 人妻在线观看 | 天天日天天操天天插 |