重讀 ES6 — 陣列、物件的擴充套件
上一篇《讀 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
是專屬自有屬性的特定命名,所以帶 Own
的 API
只遍歷到自有屬性就很好識別了,包括 ES5
的 obj.hasOwnProperty()
就是這個規則。此外,前文的 ...
擴充套件運算子是也歸類於此。
第二類在 Object
名稱空間上新增方法:
- Object.setPrototypeOf(obj, proto)
- Object.getPrototypeOf(obj)
這兩個方法是直接對原型鏈物件的 set
和 get
,雖不用於遍歷,但屬於和原型鏈直接打交道的方法。真正能遍歷到原型鏈的還是 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
也是對狀態的遍歷。看過或用過這麼多 ES6
的 API
,不知您是否注意到,作為如此共性的 遍歷
這事兒,ES6
其實開放了一個底層概念:Iterator
遍歷器。
讀過 underscore.js
原始碼的同學,不難發現它在 遍歷
問題上也是採用了統一的 口徑
——即用陣列的 for i++
方式,物件的遍歷 for in
最終也是落腳在陣列遍歷上。
這兩個問題擺在一起,能為我們設計 API
提供怎樣的啟示呢?資訊量略大,容我想好再說,哈哈~
最後, Iterator
遍歷器這個偏底層的概念,本文還是老套路,把它先關在籠子裡,以後再來拜會。
相關文章
- 重學ES6 陣列擴充套件(2)陣列套件
- ES6之陣列的擴充套件陣列套件
- es6 陣列擴充套件方法陣列套件
- es6陣列擴充套件的學習陣列套件
- ES6入門之陣列的擴充套件陣列套件
- ES6 物件的擴充套件物件套件
- 陣列的擴充套件 —— ES6基礎總結(一)陣列套件
- PHP的SPL擴充套件庫(二)物件陣列與陣列迭代器PHP套件物件陣列
- es6-陣列擴充套件陣列套件
- 重學ES6 函式的擴充套件(上)函式套件
- ES6物件的擴充套件及新增方法。物件套件
- es6 字串的擴充套件字串套件
- 《深入理解ES6》4.擴充套件的物件功能套件物件
- ES6語法學習筆記之陣列與擴充套件運算子筆記陣列套件
- es6陣列去重(連重複的物件也可以去掉)陣列物件
- ES6之字串擴充套件字串套件
- ES6數字擴充套件套件
- ES6之正則的擴充套件套件
- ES6之函式的擴充套件函式套件
- ES6字串的擴充套件字串套件
- 非推倒重來式的讀/寫伸縮擴充套件套件
- es6-物件擴充套件物件套件
- 擴充套件javascript原生物件套件JavaScript物件
- 《深入理解ES6》筆記——擴充套件物件的功能性(4)筆記套件物件
- ES6入門之字串的擴充套件字串套件
- 陣列物件去重陣列物件
- ES6 -- String 擴充套件方法解析套件
- (精華)2020年7月3日 JavaScript高階篇 ES6(陣列的擴充套件方法)JavaScript陣列套件
- JavaScript 原生物件及擴充套件JavaScript物件套件
- JavaScript String 物件擴充套件方法JavaScript物件套件
- ES6各大資料型別的擴充套件大資料資料型別套件
- ES6入門之函式的擴充套件函式套件
- ES6入門之數值的擴充套件套件
- ES6入門之正則的擴充套件套件
- es6陣列去重複陣列
- repeat和tile擴充陣列陣列
- 深入理解ES6 ---- 正則擴充套件套件
- js物件陣列去重JS物件陣列