重讀 ES6 — 陣列、物件的擴充套件

weixin_34146805發表於2017-09-03

上一篇《讀 ES6 — 字串、數值、正則的擴充套件》 將新增 API 做了一些梳理,隔離了一些複雜而低頻的知識點,從有限的篇幅分析,可以窺探到 JavaScript 內部程式碼組織更趨於合理、而本身 API 則變得更加易用和強大。

本篇繼續沿著上篇的分析方向,來整理下 ES6 基本型別中的陣列物件 新增特性。(函式稍特別,將單獨來梳理)

因為它們在 JavaScript 語言中的地位無比重要,無論是 ES5 還是 ES6 中都應該優先掌握它們。本篇著重梳理 ES6 相對於 ES5 的增量知識,但是有些增量使用起來仍然很費勁,用到一個知識點恐怕先得弄清一大片才行——好比對朋友說了一個謊(大家應該都有這樣幹過吧......),就得用好幾個謊才能圓回來。

比如當你開始用起 Array.from() 你可能會遇到 陣列的空位的概念,然後猛然一驚還有這事,那豈不是我曾經有段程式用錯了埋了 bug? 比如函式的尾呼叫優化,是什麼鬼呀?還比如物件 Object 搞了一大堆 getOwnPropertyDescriptor()setPrototypeOf()getPrototypeOf() 這些看著 prototype 就頭疼的方法......

確實,這些東西有點繁瑣,不過沒關係,如之前的理念我們暫且先把它們隔離起來,關在鐵籠子裡,等我們先打完小怪,在來收拾它們。(怎麼感覺和一款叫 “軒轅劍...” 的遊戲劇情有點相似)。另外,就是很多 API 可能初期有點抗拒,只要用起來了就覺得太 TM 順手絲滑了。

陣列的擴充套件

一如既往,囉嗦完畢開始進入正題。陣列新增的特性有:

  • 新增方法(包括在名稱空間 Array 上和陣列例項上)
  • spread 運算子
  • 陣列空位的概念

新增方法

陣列例項的新增方法:

在陣列內自我覆蓋: copyWithin()
見好就收: find() 和 findIndex()
是否包含了某項: includes()
自我填充: fill()
花式遍歷: entries()、keys() 和 values()

以及名稱空間 Array 上的方法:

陣列產自近親: Array.from()
化零為整: Array.of()

新增方法都很簡單,和上一篇總結的特徵規律是相似的。比如封裝高頻的寫法:

// ES5 陣列是否存在某項
var isExist = [1, NaN, 3, 6, 8].indexOf(6) !== -1;
// => 這樣寫總感覺不夠直觀
// => 經常還要查 `indexOf` 的 `O` 是大寫還是小寫有木有!
// => 因為 `typeof` 的是小寫!

// 莫名其妙 `NaN` 的存在性判斷不出來
[1, NaN, 3, 6, 8].indexOf(NaN);
// => -1

// ES6 陣列是否存在某項就沒有上述的問題
let isExist = [1, NaN, 3, 6, 8].includes(NaN)

另外比較重要的特徵就是:

  • 幾乎都是純函式
  • 更偏向宣告式程式設計
  • API 的行為統一明確

首先,為什麼說幾乎都是 “純函式”(相關知識若有需要,可以閱讀本人的另一篇專題文章《走向 JavaScript 函數語言程式設計》)? 新增的方法都滿足相同輸入恆有相同輸出,但是還有一點點 “副作用”,就是改變了外部變數即當前陣列本身。但總的來說,比起 ES5,已向 “確定性” API 邁出了一大步。

於是,將稍有 “副作用” 的方法改成 “純函式”,是非常容易的:

// 帶副作用的 fill 方法會改變陣列 arr
arr.fill(val, start, end);

// 很容易封裝成純函式,並且實現一定的柯里化
function fill(arr) {
    let inner_arr = arr.slice(0);
    return function (val, start, end) {
        return inner_arr.fill(val, start, end);
    }
}

其次,偏向於宣告式程式設計。ES6 看起來是要圍剿 for迴圈,讓它們少出頭露面的辦法就是提供新的 API來隱藏它們。

比如,在一次 “尋找 NaN” 的活動中,這樣的呼叫是不是更 “宣告式”,更絲滑順暢?

//在陣列中尋找 NaN
let index = [1, 2, 3, NaN, 6, 8].findIndex(Number.isNaN);
// => 3

let item = [1, 2, 3, NaN, 6, 8].find(Number.isNaN);
// => NaN

至於統一而確定的行為,在上例中 includes 以及 Array.from() 都有體現。對於一個重量級的語言,尤其是欲意 “征服全宇宙” 的 JavaScript 來說,定當以極為挑剔的眼光審視它,而 ES6 已經做了很多。

spread 運算子

spread 運算子(擴充套件運算子)是三個點(...)。這個新增項非常能讓人接受,而且它已經蔓延到了除數字本身外的 函式引數(類似陣列)、物件字串等等型別上。

以一個簡單的例子,看看用 ... 書寫帶來的良好的閱讀體驗:

// 合併陣列,通過 concat() 連線起來
var compose = first_arr.concat(second_arr).concat(third_arr);

//毫無雜質的 ...
let compose = [...first_arr, ...second_arr, ...third_arr];

陣列空位

最後陣列的空位概念,本文不打算去說明。知識點本身比較簡單,但是牽扯到太多的驗證,比較難梳理,感覺像是揮不走的蒼蠅。所以,最好的辦法是在程式設計實踐中再去 “拍” 它。

物件的擴充套件

其實物件的擴充套件並不複雜,歸結起來差不多以下內容:

  • 為了更好的賦值運算
    • 屬性的簡寫
    • 擴充套件運算子
  • Object 名稱空間的新增方法
    • 常用方法
    • 物件的 prototype 方法
    • 屬性的描述方法

為了更好的賦值運算

屬性的簡寫、擴充套件運算子以及與此緊密相關的解構賦值,可以說為舊的 JavaScript 開創了一批新的賦值運算方式,讓 賦值運算 擺脫了一板一眼的 =號運算的方式。

var lang = {name: 'ECMA2015', shortName: 'ES6'};

//ES5 寫法比較冗餘
var name = lang.name;
var shortName = lang.shortName;

//ES6 寫法更加簡明
let {name, shortName} = lang;

對一個 ES6 模組的匯入 (import) 和匯出 (export) ,也能充分凸顯這種 賦值運算 的便利性。

// a.js
const version = '1.0.0';
let fn1 = () => {};
let fn2 = () => {};
export { version, fn1, fn2 };  //注:為了節約空間,就寫成一行了

//b.js
import { version, fn1 } from './a';

// 如果不能解構賦值
import a from './a';
let version = a.verion;
let fn1 = a.fn1;

屬性的簡寫

上述 賦值運算,和一個符號有關,那就是 ES6 的大括號{},與 ES5 不同的是,ES6{}不僅能開闢一塊作用域,又能進行 模式匹配運算,有如自帶魔法一般。

先來看看一個有意思的例子:將任意一個變數變成物件。

// ES5 需要獲取形參名
function var2obj(x) {
    var obj = {};
    var str_fn = var2obk.toString();
    var key = str_fn.split(')')[0].split('(')[1];
    obj[key] = x;
    return obj;
}
var x = 'unkown';
var myObj = var2obj(x);
// => {x: 'unkown'};

更為完整的情形,請參考 這裡 。但在 ES6{} 眼裡,完全是另一番景象。甚至可以通過這個方式,輕易的獲取到所有形參名。(此處不去延伸)

function var2obj (x) {
    return {x};
}
var x = 'unkown';
var myObj = var2obj(x);
// => {x: 'unkown'};

let y = 'yes';
var anotherObj = var2obj(y);
// => {y: 'yes'};

大括號 {} 自帶運算魔法。解析時,能將 {x} 一分為二,自動展開為 {x: x}。反之,將物件的鍵值合成一個變數項,寫在 {} 中,就是屬性的簡寫。

// 簡寫形式
let {name, age} = {name: 'jeremy', age: 18};

// 展開形式
let {name: name, age: age} = {name: 'jeremy', age: 18};

簡寫物件的解構賦值,可以看做是先轉化成上述的 “展開形式” ,然後再開始匹配賦值的。

因此,解構賦值 = 號左邊的任何鍵名,都必須來自 = 號右邊物件中的某個元素,否則將無法識別(undefined)。換句話說,= 號右邊物件能夠訪問到的屬性,都是可以被解構和賦值給 = 號左邊的,包括它原型鏈上的屬性。

function Corder (name) {
    this.name = name;
}
Corder.prototype.age = 18;
let {name, age} = new Corder('jeremy');
// => age 18

擴充套件運算子

物件也有擴充套件運算子 (...),和陣列的是類似的,簡單理解就是剝離了一層大括號 {},將物件鍵值對直接暴露出來。

擴充套件運算並不難,但是有一個和解構賦值不同的特徵,是它不會將原型鏈上的屬性、方法暴露出來。準確的說,擴充套件運算子是取出物件自身(不包括其原型鏈上)的所有可遍歷屬性。

let {...aCoder} = new Corder('jeremy');
console.log(aCoder);
// => {name: ''jeremy'}

let aCoder = Object.assign({}, new Corder('jeremy'));
// => {name: ''jeremy'}

可見,...Object.assign 都沒有將原型鏈上的 age 取出來。

對了,這裡提到了 取出物件自身(不包括其原型鏈上)的所有可遍歷屬性,不禁腦袋中又蹦出一個問題:到底哪些運算或方法,只獲取到物件自身的可遍歷的屬性?又有哪些是可以獲取到物件原型鏈上的屬性呢?

面對 ES6 不勝列舉的新增特性,資訊量的暴增,往往讓人煩躁不堪。情緒性的鄙夷油然而生:看吧,就為了解決些小問題,卻弄出這麼一大堆東西來,有意思嗎!

Object 的新增方法

資訊量暴增,確有其事。不過本文意在梳理,解決的問題就是從這些繁雜的資訊中,提取容易的、有利的為自己所用,其他晦澀麻煩的暫且一併鎖在鐵籠子裡。

從總體上看,ES6 的新增的特性並非沒有瑕疵(筆者實際程式設計中也曾遇到過,以後的篇幅有機會再提),而且很多粒度很小,辨識起來的確很麻煩。但單單從剛才的提問來說,遍歷自身屬性還是原型鏈屬性,ES6 並沒有給我們製造麻煩。

第一類在 Object 名稱空間上新增方法:

  • Object.assign(target[, source1, source2, ...])
  • Object.keys(obj)
  • Object.values(obj)
  • Object.entries(obj)
  • Object.is(a, b)
  • Object.getOwnPropertyDescriptors(obj)

除了 Object.is(a, b) 之外,它們都是處理屬性自身的可遍歷的屬性的方法。Own 是專屬自有屬性的特定命名,所以帶 OwnAPI 只遍歷到自有屬性就很好識別了,包括 ES5obj.hasOwnProperty() 就是這個規則。此外,前文的 ... 擴充套件運算子是也歸類於此。

第二類在 Object 名稱空間上新增方法:

  • Object.setPrototypeOf(obj, proto)
  • Object.getPrototypeOf(obj)

這兩個方法是直接對原型鏈物件的 setget,雖不用於遍歷,但屬於和原型鏈直接打交道的方法。真正能遍歷到原型鏈的還是 ES5 已有的 for in 迴圈。

到此,問題就非常明確了,仍然只有極少數的 API 可以遍歷或者直接操作原型鏈,其他絕大多數新增 API,都只明確的限定在物件自有屬性的遍歷上。

屬性的描述方法

物件的每個屬性都有一個描述物件(Descriptor)。屬性的描述物件在 ES5就具備了,但當初很少接觸到這個概念。具體上,它包含以下專案:

{
    value: 'attr', //屬性值
    writable: true, // 可寫
    enumerable: true, //可列舉可遍歷
    configurable: true //可配置
}

描述物件之於屬性,好比於原子細分成用若干個質子、中子來描述。描述物件用來對外界開放處置屬性的許可權,這在設計健壯的 API 是非常有用的。

為此而新增的方法有:

  • Object.getOwnPropertyDescriptors(obj)
  • Object.getOwnPropertyDescriptor(obj, key)

上文曾提過,該方法只會遍歷到物件自身屬性。至於其他方面,延伸暫無必要。

ES6 之於遍歷

很多時候,提到獲取陣列、物件中的元素時候,總會說來個 for 迴圈,來遍歷一下。陣列、物件大部分的消費方式就是通過遍歷。類似的,字串也有遍歷,甚至 Generator + yield 也是對狀態的遍歷。看過或用過這麼多 ES6API,不知您是否注意到,作為如此共性的 遍歷 這事兒,ES6 其實開放了一個底層概念:Iterator 遍歷器。

讀過 underscore.js 原始碼的同學,不難發現它在 遍歷 問題上也是採用了統一的 口徑 ——即用陣列的 for i++ 方式,物件的遍歷 for in 最終也是落腳在陣列遍歷上。

這兩個問題擺在一起,能為我們設計 API 提供怎樣的啟示呢?資訊量略大,容我想好再說,哈哈~

最後, Iterator 遍歷器這個偏底層的概念,本文還是老套路,把它先關在籠子裡,以後再來拜會。

相關文章