科普 JavaScript,揭開 JavaScript 神祕面紗,直擊 JavaScript 靈魂。此係列文章適合任何人閱讀。javascript
本文章內容爲:php
Array 做爲 JavaScript 語言除 Object 外惟一的複雜數據結構,完全掌握它很是有必要。html
這篇文章的初衷,就是講透數組以及 JavaScript 中 Array 的概念、做用、全部 API 和便捷用法。最終能夠達到融會貫通,在無數用法中找到最正確的那一種,讓 Array 操做變成駕輕就熟。java
千老師寫完這篇文章的時候,已是 2019 年年末,截至文章完成,這些是最新的 ECMAScript 規範、JavaScript 版 v8 的 Array、C++版 V8 的 Array、V8 Array 運行時。node
舒適提示:因爲文章篇幅過長,我以爲你不太可能堅持一次看完。因此建議你先收藏。若是遇到看不懂的內容,或者不想看的內容,能夠快進或者選擇性觀看。git
要學習一個東西,最佳方式就是不斷地拋出問題,經過對問題的探索,一步一步撥開迷霧,尋找真相。程序員
上面這句話是千老師寫的,不具備權威性。因此千老師先提出一個問題,來證實這個觀點是正確的。github
提問到底有多重要?這是個問題。這裏千老師借鑑幾位大神的名言解釋一下這個問題:golang
創造始於問題,有了問題纔會思考,有了思考,纔有解決問題的方法,纔有找到獨立思路的可能。—— 陶行知正則表達式
提出正確的問題,每每等於解決了問題的大半。——海森堡
生活的智慧大概就在於逢事都問個爲何。——巴爾扎克
有教養的頭腦的第一個標誌就是善於提問。——普列漢諾夫
一個聰明人,永遠會發問。——著名程序員千老師
善問者,如攻堅木,先其易者,後其節目。 ——《禮記·學記》
好問則裕,自用則小。——《尚書·仲虺之誥》
敏而好學,不恥下問。——《論語·公冶長》
君子之學必好問,問與學,相輔而行者也。非學,無以至疑;非問,無以廣識。——劉開
知識的問題是一個科學問題,來不得半點虛僞和驕傲,決定的須要的卻是其反面——誠實和謙遜的態度。——毛主席
好,千老師隨便一整理,就整理出十個解釋問題爲何重要的緣由,連偉大的開國領袖毛主席都喜歡問題,很是棒。但這是一篇講解程序的文章,不是學習名言警句的文章,因此你們收一收。只要明白」帶着問題去學習效率是很是高的「這個道理就足夠了。下面轉入正題。
如今千老師先拋出第一個正式的問題,數組究竟是個啥?
這個問題好像很簡單哎,可事實真的是這樣嗎?不服的話,你能夠先把你的答案說出來,等看完本篇文章後,再來對比,是否和原來的答案一致。
這裏千老師偷個懶,拿出 wiki 百科對數組的解釋:
數組數據結構(英語:array data structure),簡稱數組(英語:Array),是由相同類型的元素(element)的集合所組成的數據結構,分配一塊連續的內存來存儲。利用元素的索引(index)能夠計算出該元素對應的存儲地址。
從這句很是官方的解釋裏面,千老師找到幾個關鍵的點:相同類型的元素、連續的內存、索引。
標準數組,是必定要符和上面這三個條件的。其實還有一個條件,wiki 上面沒體現出來,就是固定大小。
數組的設計,最先是出自 C 語言。在後來出現的編程語言中,大都效仿了 C 語言的數組設計。好比 C++、Java、Go 等等。
從這裏開始,千老師就要推翻你傳統思惟中的JavaScript數組概念。只有推翻,才能反向驗證。只有打破,才能破鏡重圓。
先拿 Java 的標準數組舉例。爲何要拿 Java 舉例呢?由於 JavaScript 中沒有「標準數組」。
int arr[] = new int[3]; /*建立一個長度爲3的int類型數組*/
arr[0] = 1; // 給下標0 賦值
arr[1] = 2; // 給下標1 賦值
arr[2] = 3; // 給下標2 賦值
複製代碼
咱們在 Java 中來一點在 JavaScript 的常規操做。
arr[2] = "3"; // error: incompatible types: String cannot be converted to int
複製代碼
看,Java 居然報錯了!這個錯誤的意思是: int 類型的數組,不兼容 String 類型的元素。
若是你一直在使用 JavaScript,而沒用過其它強類型編程語言,你確定以爲這很神奇。數組居然還能夠有類型?趕忙提出第二個問題:數組爲何有類型?
是的,數組有類型,並且數組有類型還有緣由,後面千老師再說爲何數組會有類型。
再來一個 JavaScript 的常規操做。
arr[3] = 1;// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
複製代碼
Java 居然又報錯了!若是你一直在使用 JavaScript,看到這裏估計你已經驚呆了。這個錯誤的意思是:索引超過了數組的最大界限。
這個現象又說明一個問題:數組的長度一旦肯定,就不會再發生改變。
千老師替你提出第三個問題:數組的長度爲何不能夠改變?
看到這裏你確定會想,標準數組這麼多限制,用起來未免也太麻煩了吧?
最後,千老師再補充一個已經被你們習覺得常,甚至已經被忽略掉的的問題:數組的下標爲何是從 0 開始的?
第二題:數組爲何有類型?
由於數組的尋址公式:array[i]Address = headAddress + i * dataTypeSize
啥玩意是尋址公式?
尋址方式就是處理器根據指令中給出的地址信息來尋找有效地址的方式,是肯定本條指令的數據地址以及下一條要執行的指令地址的方法。
我翻譯一下,就是在內存塊中找到這個變量的方法。
這裏涉及到一些計算機原理和數據結構與算法的知識,由於本篇文章的主要內容是講解 JavaScript 的 Array 相關知識,因此千老師不會展開講這些問題。不過考慮到不少人不是科班出身,或者科班出身沒認真學過計算機原理。千老師仍是略微講講尋址是個啥,有啥用。
首先內存地址是個什麼東西?內存地址長這樣:0x12345678
。在編程語言中,咱們建立的變量,都被存到了內存中,你能夠理解成一個hash
,或者是 ECMAScript 中的 object
,0x12345678
是 key
,你建立的變量名和變量的值是 value
。而尋址,就是找到內存地址,把內存地址中的 value
拿出來。換成 JavaScript
表達式大概是這樣:Memory["0x12345678"]
。
大概明白了尋址。接下來再看一下建立數組的過程:
建立數組,就是向內存申請一塊固定大小的空間。這塊空間有多大呢?根據數組的長度和數組的數據類型來獲得的。
好比上面的例子。int 類型是 4 個字節,長度是 3 的 int 類型數組,就須要 3*4=12 個字節的內存空間。
一旦申請成功,CPU 就會把這塊空間鎖住。而且記錄一個這塊空間的內存首地址。也就是上面那個公式的 headAddress。
在以後的訪問中,就能夠經過這個公式來快速尋址。
這解釋通了 數組爲何有類型。
第三題:數組的長度爲何不能夠改變?
由於數組的內存是連續分配的,若是數組的長度發生改變,就意味着數組的佔用內存空間也發生改變。而數組原空間後面的空間有可能被其它值所佔用了,這也是處於安全性的考慮,因此沒法改變。
第四題:數組的下標爲何是從 0 開始的?
若是下標從 1 開始,按照人類十進制的邏輯很是值觀,但對於 CPU 而言,就麻煩了。數組的尋址公式就要改爲:array[i]Address = headAddress + (i-1) * dataTypeSize
,這樣每次對數組的操做,CPU 都會無緣無故多一次減法運算,對性能不利。
看到這,你應該明白,咱們在 JavaScript 中平常使用的 Array 類型,並非「標準數組」。同時也明白了,標準化數組的特徵。
經過上面的四道自問自答,相信你也明白了數組設計成這樣的苦衷。真的是讓咱們一言難盡啊,可是又不得不盡。
若是一直在數組的這麼多限制下編程,不少人確定會被逼瘋。因此聰明的程序員們發明了一種屏蔽底層數組操做的數組容器。
好比 Java 中出鏡率很是高的 ArrayList。而 ECMAScript 中的 Array 類型,一樣也是如此。
這類容器有什麼好處呢?
咱們來操做一下,就能體驗到。
仍是拿 Java 舉例。爲何還要拿 Java 舉例呢?由於只有經過對比才能引發你的深思。
ArrayList arr = new ArrayList(1);// 建立一個初始長度爲 1 的數組
arr.add(1);// 加 1 個數據
arr.add("2");// 再加 1 個 String 類型的數據
System.out.println(arr);// 沒有問題,正常輸出 [1, 2]
複製代碼
能夠看到 Java 的 ArrayList 解決了兩個重要的問題。
1.能夠存儲不一樣類型的數據。
2.能夠自動擴容。
那它是怎麼實現的呢?這塊內容雖然也不屬於本篇文章的範圍內。但千老師仍是忍不住簡單說一下。
不管是 JavaScript 仍是 Java,基本的內存都分爲堆內存 Head 和棧內存 Stack。由於基本數據類型,(很差意思,打斷一下,千老師在這裏提個問題?2019 年,ECMAScript 有幾種基本數據類型?)都是存到棧內存中的。爲何要存到棧內存中呢?這又是個很好的問題。你能夠先猜一下。由於基本數據類型都是固定的值,既然值都是固定的,那麼大小也是固定的。說到這裏,千老師再來提個問題:在 ECMAScript 中,一個 3 個字符的 String 類型變量佔幾個字節?你看,**問題無處不在,就看你有沒有發現問題的眼睛。**這也算是一個小彩蛋,在 ECMAScript2015 之前,ECMAScript5.0 中,採用 Unicode 編碼,中文字符和英文字符都是佔 2 個字節大小。因此上面問題的答案就是 2*3=6 個字節。但 ECMAScript6 之後,答案不一樣了。由於編碼換了,換成 utf8 了。這裏千老師再提一個問題,unicode 和 utf8 有什麼不一樣?嘿嘿,是否是快崩潰了?utf8 是使用 1 到 4 個字節不等的長度進行編碼的。由於老外發現世界上大多數網站都是英文語言的網站,而其餘語言(在老外眼裏,除了英語,其餘統稱爲其餘語言)的網站佔比不多。因此 utf8 中,英文字符只佔 1 個字節,而像其它語言,好比中文,就佔了 3 個字節。因此上面的題目還缺乏一個條件,還要明確 3 個字符都是哪國語言才能給出正確答案。扯遠了,咱們趕忙收回來,繼續講堆和棧的問題。既然基本數據類型的大小都是固定的,那麼放在棧裏面就很好知道數組總共的大小,就能夠申請到連續的內存塊。那麼存儲引用類型的變量時,ECMAScript 是怎麼作的呢?聰明的你確定猜到了,那就是存到堆內存中了。準確的說,是把變量的數據存到堆內存中,而棧內存仍然會存一個東西,那就是堆內存的內存指針,也就是咱們常說的引用。這樣就解釋通了,數組容器怎麼存儲不一樣類型數據的。
關於堆內存和棧內存的詳細介紹,就不展開說了。
若是想詳細瞭解這部份內容,推薦查閱若是想詳細瞭解這部份內容,推薦查閱《JavaScript高級程序設計(第3版)》第四章。
堆棧內存這部份內容並非能夠被單獨拿出來的一個概念,若是想完全學好,就要有系統的去學,才能夠真正理解。基礎很差的同窗,推薦去讀《深刻理解計算機系統(原書第3版)》。這本書在豆瓣上得到了9.8的高分。但實際上,它並非一本傳統意義上「深刻」的書。而是講解「計算機底層」總體脈絡的書。因此它是一本廣度很是高的書,很是適合完善我的的計算機知識體系。
ArrayList 無參構造,會默認建立一個容量爲 10 的數組。每次添加元素,都會檢查容量是否夠用,若是不夠用,在內部建立一個新的數組,該數組的容量爲原數組容量的 1.5 倍。再將原數組的數據都搬移到新數組中去。若是新數組的容量仍是不夠,就會直接建立一個符和所需容量的數組。
這麼幹沒有什麼太大的問題,最大的問題就是性能會受到必定的影響。另外一個是和 JavaScript 無關的問題,線程安全問題。由於建立新數組,遷移數據這個過程須要必定的時間。Java 這種多線程的語言,若是在這個過程當中另外一個線程再去訪問這個 ArrayList,就會出問題。
爲何要解釋 Java 的 ArrayList 呢?由於千老師只看過 ArrayList 的實現源碼,很尷尬。沒看過 JavaScript 的同窗,若是你感興趣,能夠去文章開頭我掛的那個 V8 源碼連接看看 ECMAScript 是怎麼幹的。我猜它的實現和 ArrayList 是一個原理,你看完能夠回來告訴千老師一下,看千老師猜的對不對。雖然千老師沒仔細看過 V8 的實現,但請不要質疑千老師對 JavaScript 的專業程度,也不要胡亂猜想千老師是搞 Java 的。在這裏強調一下,千老師是正兒八經的 JavaScript 程序員,從始至終都是以 JavaScript 做爲第一編程語言。哦不,如今是 TypeScript。
不管是 Java 的 JDK 仍是 ECMAScript 的 V8,歸根結底的實現仍是 C。因此千老師在這裏建議你們:必定不要想不開去看 C 的源碼。
總結一下:凡是被程序員用起來不爽的東西,總會被各類方式改造。直到變成大夥兒都喜歡的樣子爲止。若是你想完全搞明白一件事情,就必須從源頭找起,看看她的原貌,再看看她化妝、整容的全過程,最後看她是如何一步一步蛻繭成蝶。
這是 JavaScript 中最多見的一個數組。
let arr = [
"h",
9,
true,
null,
undefined,
_ => _,
{ a: "b" },
[1, 2],
Symbol(1),
2n ** 1n,
Infinity,
NaN,
globalThis,
Error("hi"),
Math.LN10,
Date(),
/\w+/i,
new Map([["k1", "v1"]]),
new Set([1, 2]),
new DataView(new ArrayBuffer(16)),
new Promise((resolve, reject) => {}),
];
複製代碼
有點亂,但無傷大雅。能夠看到,數組就像是一個巨大的黑洞,能夠存放 ECMAScript 中的任何東西。變量,任何數據類型的數據,包括數組自己,均可以。這一點讓我想起了QQ空間裏面常常出現的遊戲廣告,山海經,吞食天地、無所不能吞的鯤。
爲何能夠這麼幹呢?
由於和上面介紹的同樣,Array 存儲基本類型時,存儲的是值。存儲引用類型時,存儲的是內存引用。
ECMAScript 中的 Array,徹底不像傳統數組。由於它是個對象。
因爲 ECMAScript 是一門弱類型語言,沒有類型系統的強制約束,任何類型的變量都是能夠被掛在上任何屬性的。數組也不例外。
給一個對象添加一個屬性。
let obj = {};
obj["0"] = 1;
複製代碼
給一個數組添加一個元素。
let arr = [];
arr["0"] = 1;
複製代碼
從一個對象中取值。
obj["0"];
複製代碼
從一個數組中取值。
arr["0"];
複製代碼
再舉個例子,以下數組:
["dog", "pig", "cat"];
複製代碼
等價於以下對象:
{
"0": "dog",
"1": "pig",
"2": "cat",
"length": 3
}
複製代碼
在某種程度上來看,Array 和 Object 沒有太明顯的區別。甚至激進點講,Array 和 Object 本質上是一回事。(這句話千老師不承擔任何法律責任,就是隨便說說)
在 JavaScript 中,你徹底能夠把數組理解成是對象的一種高階實現。
JavaScript 中,Array 到底有多麼自由呢?能夠存儲不一樣類型的值,能夠經過負索引訪問,下標能夠超過原始聲明範圍,甚至能夠經過非數字索引。雖然 Array 和數組格格不入,但它畢竟還叫做數組,畢竟仍是和數組有類似之處的,好比 Array 仍然是以"0"
做爲起始下標的。(這是一個冷笑話。)
因此,不要再拿傳統的數組概念來定義 ECMAScript 的 Array。由於它只是長的像而已。
該說的不應說的,該問的不應問的,上面都講完了。
接下來,就讓咱們進入本文最後一部分,從全部的 API 中感覺 Array 的強大。
在千老師寫這篇文章以前,已經有不少人寫過相似的優秀文章了,好比 MDN
不過千老師保證比這些人講的更加生動形象,通俗易懂,風趣十足,別具一格。帶你深刻……淺出 Array 的世界。
雖然這篇文章出現的時間很是晚了,可是沒有辦法。千老師相信後浪能把前浪拍在沙灘上,使勁蹂躪。
目前標準規範中,Array 的全部的屬性和方法加起來,有足足 36 個之多,實在是使人汗顏。
下面先從建立數組一步步講起。
創造的神祕,有如夜間的黑暗,是偉大的。而知識的幻影,不過如晨間之物。——泰戈爾
常規且標準的建立數組的方式有 3 種。
1.直接使用字面量[]建立
let arr1 = [0, 1, 2];
複製代碼
2.使用 Array 構造函數建立
let arr1 = new Array(0, 1, 2);
複製代碼
3.調用 Array 內置方法建立
let arr1 = Array(0, 1, 2);
複製代碼
異同之處:
方法 2 和方法 3 的做用是相同的,由於在 Array 函數實現內部判斷了 this 指針。
new Array
和 Array
有兩種用法,在僅傳遞 1 個參數的時候,建立的不是存儲該值的數組,而是建立一個值爲參數個數的 undefined 元素。
let arr1 = [10]; // [10]
let arr2 = Array(10); // [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
複製代碼
不常規且不標準的建立數組的方式也有。具體幾種千老師也沒統計過,由於這不是很重要。最多見的不常規且不標準用法就是from
,這裏不劇透了,能夠繼續朝下看。
真理是生活,你不該當從你的頭腦裏去尋找。——羅曼·羅蘭
說完建立數組,接着看看怎麼訪問數組。
訪問數組其實很簡單,經過方括號[]
就能夠了。
let arr1 = [0, 1, 2];
arr1[0]; // 0
複製代碼
咱們習慣寫一個number
類型的參數放在方括號裏,由於咱們知道這個數字是元素的數組下標,數組下標是一個number
類型的值,很正常對吧。但其實否則,上面例子中的arr1[0]
在真正被執行的時候,會變成arr1['0']
。會通過toString
方法隱式轉換。爲何會這樣呢?由於ECMAScript
規範規定了。這裏先賣個關子,暫且不講,感興趣的同窗能夠本身去查一查。雖然會有個隱式轉換過程,但通常正常一點的程序員是不會直接使用帶引號的寫法的。
無父何怙,無母何恃?——《詩經》
在 ECMAScript 中,除了 null 和 undefined 外,幾乎全部東西都有這個屬性。代表了該對象是由誰構造出來的。
一般用到這個屬性的場景,就是在判斷對象是否是 Array 的實例。
let arr = [];
arr.constructor === Array; // true
複製代碼
可是很遺憾,這個屬性是可寫的。可寫意味着這個方式並不能百分之百辨別出一個對象是不是 Array 的實例。
let arr = [];
arr.constructor = String;
arr.constructor === Array; // false
arr.constructor === String; // true
複製代碼
你看,原本是由 Array 生出來的變量 arr,經過一行代碼,就改認 String 作父輩了。再次證明了,Array 的實例,都是有錢即是爹,有奶即是孃的貨色。
再看個例子。
let str = "";
str.constructor = Array;
str.constructor === Array; // false
str.constructor === String; // true
複製代碼
str 和 arr 有着鮮明的差異。str 是孝子啊,親爹就是親爹,有錢也無論用。
其實除了 String,其餘幾種基本類型的 constructor 屬性都是能夠改,可是改了沒起做用。
爲何沒起做用呢?由於這涉及到開裝箱的操做。
因此這裏千老師出一道題:原始類型爲何能夠調用方法和訪問屬性?
搞明白這道題,你就能明白上面這個現象是爲何了。這不算是 Array 的知識點,算是知識擴展吧。
你答上來上面這題,千老師再出一道題,經過構造函數建立出來的原始類型和用字面量建立出來的原始類型,有什麼區別?
尺有所短,寸有所長
表明數組元素的個數。
let arr = [0, 1, 2];
arr.length; // 3
複製代碼
這個屬性好像很簡單,沒什麼好講的對吧?
其實還真有點東西能夠給你們講講。
length
最大的妙用,就是直接改變length
屬性能夠刪除元素或增長元素。
let arr = [0, 1, 2];
arr.length = 2;
console.log(arr); // [0, 1]
arr.length = 5;
console.log(arr); // [0, 1, empty × 3]
複製代碼
看到這裏,又出現一個empty
,這是個啥?你們知道嗎?
empty
是一個和undefined
很像,但又有一點細微區別的東西。
能夠作個實驗。
console.log(arr[3] === undefined); // true
複製代碼
在這個實驗裏,咱們發現empty
是全等於undefined
的。
可是它們還存在必定區別。好比下面的實驗。
arr.indexOf(undefined); // -1
arr.filter(item => item === undefined); // []
arr.forEach(item => {
console.log(item);
}); // 1, 2
複製代碼
indexOf
、filter
和forEach
都是不認爲empty
等於undefined
的。會自動忽略掉empty
。
再作兩個實驗。
arr.includes(undefined); // true
arr.map(item => typeof item); // ["number", "number", empty × 3]
複製代碼
可是includes
很很神奇的認爲empty
就是和undefined
一個概念。而在map
中,則會自動保留empty
的空槽。
這裏並非說typeof
很差使,而是typeof item
這條語句,在碰到empty
時直接跳過了,沒有執行。
爲了證明這個事,再單獨拿萬能的 typeof
操做符作個實驗。
console.log(typeof arr[3]); // undefined
複製代碼
這究竟是個怎麼回事呢?千老師在 ECMAScript6 的文檔中發現,明確規定了empty
就是等於undefined
的,在任何狀況下都應該這樣對待empty
。千老師又去翻了下 V8 源碼,果真在 V8 源碼中找到了關於empty
的描述,原來它是一個空的對象引用。
空的對象引用這個東西,在 ECMAScript 中應該是什麼類型呢?ECMAScript 一共就那麼幾個類型,按道理說,它不符合任何類型啊。
沒辦法,undefined
這個尷尬的數據類型就很莫名其妙地、很委屈地成爲了empty
的背鍋俠。
士不能夠不弘毅,任重而道遠
從 ECMAScript1 開始,就一直有一個使人頭疼的問題(固然使人頭疼的問題遠不止一個,我這裏說有一個,並非說只有一個,這裏必須重點提醒一下。),ECMAScript 中充斥着大量的類數組對象。它們像數組,但又不是數組。最典型的像是arguments
對象和getElementsByTagName
。
爲了解決這個問題,不少類庫都有本身的解決方案,如大名鼎鼎的上古時代霸主級 JavaScript 庫jQuery
,就有makeArray
這個方法。
隨着突飛猛進的科技演變,通過無數 JavaScript 愛好者努力拼搏,追求奉獻,經歷了二十多年的滄海桑田,白雲蒼狗。ECMAScript 終於等來了from
這個本身的超級 API。有了這個 API 之後,ECMAScript 不再須要像makeArray
這類第三方解決方案了。ECMAScript 站起來了!說到這,千老師不由想起了那些曾爲 ECMAScript 的自由,開放,擴展,交融而拋頭顱灑熱血的大神們,是他們,在 ECMAScript 遭受屈辱的時刻自告奮勇,以力挽狂瀾之勢救黎民於苦難。在 ECMAScript 發展過程當中,千老師看到了, ECMAScripter 們,勇於直面慘淡的人生,勇於正視淋漓的鮮血,在Java
,C
,C++
,甚至PHP
的鄙視下,在全部人嘴裏的「不就是個腳本語言嗎?」的侮辱中,咱們以燃燒的激情和鮮血凝聚成精神的火炬,點燃了將來。
扯遠了,咱們收回來。吹了那麼多,趕忙繼續學習一下from
的使用吧。
做用:從類數組對象或可迭代對象中建立一個新的數組。
語法:Array.from(arrayLike[, mapFn[, thisArg]])
參數:
arrayLike
:想要轉換成數組的僞數組對象或可迭代對象。
**mapFn
**(可選) :若是指定了該參數,新數組中的每一個元素會執行該回調函數。
**thisArg
**(可選):執行回調函數 mapFn
時 this
對象。
返回值:新的數組實例。
支持 String、Set、Map、arguments 等類型。
還支持經過函數來建立。
// String 轉 Array
let arr1 = Array.from("123"); // ["1", "2", "3"]
// Set 轉 Array
let arr2 = Array.from(new Set([1, 2, 3])); // [1, 2, 3]
// Map 轉 Array
let arr3 = Array.from(
new Map([
[1, 1],
[2, 2],
[3, 3],
])
); // [[1, 1], [2, 2], [3, 3]]
// MapKey 轉 Array
let arr4 = Array.from(
new Map([
[1, 1],
[2, 2],
[3, 3],
]).keys()
); // [1, 2, 3]
// MapValue 轉 Array
let arr5 = Array.from(
new Map([
[1, 1],
[2, 2],
[3, 3],
]).values()
); // [1, 2, 3]
// arguments 轉 Array
function fn() {
return Array.from(arguments);
}
fn(1, 2, 3); // [1, 2, 3]
複製代碼
除了轉換這個做用之外,喜歡探索的程序員又發現了另外幾個神奇的用法。
1.用來建立數組。
let arr = Array.from({ length: 3 });
console.log(arr); // [undefined, undefined, undefined]
複製代碼
from
方法很不錯,並無建立 3 個empty
出來。看來 ECMAScript6 的規範仍是挺好使的,至少 Chrome 聽他的。
還能夠在這裏加一些邏輯,好比生成某個範圍的數字數組。
let arr = Array.from({ length: 3 }, (item, index) => index);
console.log(arr); // [0, 1, 2]
複製代碼
2.淺拷貝數組。
let arr = [1, 2, 3];
let arr2 = Array.from(arr);
複製代碼
3.深拷貝數組。
基於淺拷貝數組,結合 Array.isArray 來實現的。原理很簡單。
function arrayDeepClone(arr) {
return Array.isArray(arr) ? Array.from(arr, arrayDeepClone()) : arr;
}
複製代碼
說到這裏,千老師提一個問題:在 ECMAScript 中,深淺拷貝數組的方法有幾種,有什麼優劣,適合哪些應用場景?
除了這幾個方法之外,還有不少其它場景的妙用,這裏就不一一舉例了。總之from
這個 API 很是靈活,喜歡探索的同窗能夠本身多去嘗試。
假做真時真亦假,真做假時假亦真。
做用:用於判斷某個變量是不是數組對象。
語法:Array.isArray(obj)
參數:
obj
:須要檢測的值。返回值:若是值是 Array
,則爲 true; 不然爲 false。
返回一個 boolean 值。
let arr = [];
Array.isArray(arr); // true
複製代碼
判斷某個變量是否爲數組,還有另外兩個常見的方法。
1.使用instanceof
。
let arr = [];
arr instanceof Array; // true
複製代碼
instanceof
的原理是經過原型鏈來實現的。即判斷左邊對象的原型鏈上是否存在右邊原型。這裏出道題:如何手動實現 instanceof
?
2.使用constructor
。
let arr = [];
arr2.constructor === Array; // true
複製代碼
constructor
屬性保存了實例被建立時的構造方法,但這個屬性是能夠被修改的。
3.使用Object.prototype.toString.call
let arr = [];
Object.prototype.toString.call(arr); // "[object Array]"
複製代碼
Object.prototype.toString.call()
經常使用於判斷 ECMAScript 的內置對象。但這個方法是能夠被重寫的。
這幾種方法各有弊端。但通常強烈推薦直接使用Array.isArray
。由於在iFrame
中,instanceof
和constructor
會失效。而Object.prototype.toString
這種方式又太過繁瑣。
這裏千老師補充一句,這幾種方法的返回值都是能夠被篡改的。因此當有時候代碼不符合預期的時候,不要太相信本身的眼睛,多動動腦子。
下面是篡改的方法,不過千萬不要閒的沒事在本身項目裏亂改哦,省的被領導K。
let arr = [];
Array.isArray = () => false;
Array.isArray(arr); // false
let arr2 = [];
arr2.__proto__ = Object;
arr2 instanceof Array; // false
let arr3 = [];
arr3.constructor = String;
arr3.constructor === Array; // false
let arr4 = [];
Object.prototype.toString = () => "object";
Object.prototype.toString.call(arr4); // "object"
複製代碼
差以毫釐,謬以千里。——《漢書》
這裏再說一個 Array 設計之初的糟粕,由於使用 new Array
或者 Array
的方式建立數組時,會根據參數的個數和類型作出不一樣的行爲。致使你永遠沒法使用new Array
來建立一個只有一個 number 類型的數組。誇張點說,of
方法出現的理由就只有一個,很純粹也很現實,爲了**建立一個只有一個 number 類型的數組。**這麼幹的好處就是統一建立數組的行爲方式。
做用:用於建立一個具備可變數量參數的新數組實例,而不考慮參數的數量或類型。
語法:Array.of(element0[, element1[, ...[, elementN]]])
返回值:新建立的數組實例。
let arr = Array.of(10); // [10]
複製代碼
主要是用於區分 Array
傳遞 1 個 number
類型參數的狀況。這裏必定會建立一個元素等於第一個參數的數組。
你們之後在建立數組必須用到Array
構造方法時,使用of
方法來替代,是一個不錯的方案。
會改變數組自己的值。
失之東隅,收之桑榆。——《後漢書》
存在感極低的 API,甚至找不到應用場景,這就是所謂的過分設計。
做用:淺複製數組的一部分到數組的另外一個位置,並返回它,不會改變數組的長度。
語法:arr.copyWithin(target[, start[, end]])
參數:
arr.length
。返回值:修改後的數組。
let arr1 = [0, 1, 2, 3, 4];
let result = arr1.copyWithin(1, 2, 4); // 截取下標 2-4 的元素,插入到下標 1 的位置
console.log(arr1); // [0, 2, 3, 3, 4]
console.log(result); // [0, 2, 3, 3, 4]
複製代碼
不積跬步,無以致千里;不積小流,無以成江海。——《荀子》
存在感和copyWithin
同樣。
做用:將數組中指定區間的全部元素的值,都替換成某個固定的值。
語法:arr.fill(value[, start[, end]])
參數:
arr.length
。返回值:修改後的數組。
let arr = [0, 1, 2, 3];
let result = arr.fill(1, 2, 3);
console.log(arr); // [0, 1, 1, 3]
console.log(result); // [0, 1, 1, 3]
複製代碼
君子愛財,取之有道。——《論語》
pop
的靈感來源於棧。其實就是棧的標準操做之一,也是最基礎的操做。pop
和push
是一對相愛相殺的好兄弟。
做用:刪除數組的最後一個元素,並返回這個元素。
語法:arr.pop()
返回值:從數組中刪除的元素(當數組爲空時返回undefined
)。
const arr1 = [1, 2, 3];
const result = arr1.pop();
console.log(arr1); // [1, 2]
console.log(result); // [1, 2]
複製代碼
pop
做爲最古老、最基礎的操做,沒有太多花裏胡哨的玩法。
海納百川,有容乃大;壁立千仞,無欲則剛。——林則徐
做爲pop
的孿生兄弟,讓我想起一句話,凡是push
給的,pop
都要拿走。
做用:在數組末尾添加一個元素,並返回操做後數組的 length。
參數:被添加到數組末尾的元素。
語法:arr.push(element1, ..., elementN)
返回值:新的 length
屬性值。
let arr = [1, 2, 3];
let result = arr.push(5, 6, 7);
console.log(arr); // [1, 2, 3, 5, 6, 7]
console.log(result); // 4
複製代碼
pop
的反操做函數,一樣是老牌 API,操做也很是簡單。
三千功名塵與土,八千里雲和月。——岳飛
做用:顛倒數組中元素的排列順序,即原先的第一個變爲最後一個,原先的最後一個變爲第一個。
語法:arr.reverse()
返回值:修改後的數組。
const arr = [1, 2, 3];
const result = arr.reverse();
console.log(arr); // [3, 2, 1]
console.log(result); // [3, 2, 1]
複製代碼
reverse
能夠配合一些其餘 API 來實現字符串的逆轉。
let str = "hello,world!";
str
.split()
.reverse("")
.join(""); // "!dlrow,olleh"
複製代碼
刪除我一輩子中的任何一瞬間,我都不能成爲今天的本身。——芥川龍之介
shift
和pop
的做用是一致的,只不過pop
是刪除數組的最後一個元素。
做用:從數組中刪除第一個元素,並返回該元素的值。此方法更改數組的長度。
語法:arr.shift()
返回值:從數組中刪除的元素; 若是數組爲空則返回undefined
。
const arr1 = [1, 2, 3];
const result = arr1.shift();
console.log(arr1); // [2, 3]
console.log(result); // 1
複製代碼
人是本身行動的結果,此外什麼都不是。——薩特
做用:使用客製化算法對數組的元素進行排序。默認排序順序是在將元素轉換爲字符串,而後比較它們的 UTF-16 代碼單元值序列時構建的
語法:arr.sort([compareFunction])
參數:
compareFunction([firstEl, secondEl]):可選,用來指定按某種順序進行排列的函數。若是省略,元素按照轉換爲的字符串的各個字符的 Unicode 位點進行排序。
firstEl:第一個用於比較的元素。
secondEl:第二個用於比較的元素。
返回值:排序後的數組。
const arr1 = [1, 9, 9, 8, 0, 8, 0, 7];
const result = arr1.sort((x, y) => x - y);
console.log(arr1); // [0, 0, 1, 7, 8, 8, 9, 9]
console.log(result); // [0, 0, 1, 7, 8, 8, 9, 9]
複製代碼
排序的性能,取決於自定義的函數。以及運行引擎。千老師研究過底層引擎的現實,可是發現每一個引擎的實現都不一樣,因此一樣的代碼,運行在不一樣的平臺上面,速度都會有差別。
意志命運每每背道而馳,決心到最後會所有推倒。——莎士比亞
做用:經過刪除或替換現有元素或者原地添加新的元素來修改數組,並以數組形式返回被修改的內容。此方法會改變原數組。
語法:array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
參數:
start
:指定修改的開始位置(從 0 計數)。若是超出了數組的長度,則從數組末尾開始添加內容;若是是負值,則表示從數組末位開始的第幾位(從-1 計數,這意味着-n 是倒數第 n 個元素而且等價於array.length-n
);若是負數的絕對值大於數組的長度,則表示開始位置爲第 0 位。
deleteCount
: 可選,整數,表示要移除的數組元素的個數。若是 deleteCount
大於 start
以後的元素的總數,則從 start
後面的元素都將被刪除(含第 start
位)。若是 deleteCount
被省略了,或者它的值大於等於array.length - start
(也就是說,若是它大於或者等於start
以後的全部元素的數量),那麼start
以後數組的全部元素都會被刪除。若是 deleteCount
是 0 或者負數,則不移除元素。這種狀況下,至少應添加一個新元素。
item1, item2, ...
:可選,要添加進數組的元素,從start
位置開始。若是不指定,則 splice()
將只刪除數組元素。
splice
的意思是剪切,和訪問器方法slice
僅一字之差,可相差的可不是一星半點。slice
是切片的意思。不少人常常弄混它們兩個。我看到有個老外說了一種區分記憶的好辦法,splice
多出來的這個p
,意思是Produces Side Effects
(產生反作用)。相似於這種名字類似,而又大相徑庭的 API,ECMAScript 可不只僅只有這麼一對,好比還有字符串的substr
和substring
。
splice
用法很是多,變化無窮。可是歸根結底一共就 4 種操做。
只須要關注第 1 個參數就能夠。只傳遞一個參數的時候,就意味着截斷。splice
能夠和length
同樣截斷數組。
let array = [0, 1, 2, 3];
array.splice(3);
// 執行結果等同於 array.length = 3;
複製代碼
只須要關注第 1 個參數和第 3 個參數就能夠。第 1 個參數表明從哪開始插入,第 3 個參數表明插入什麼元素。第 2 個參數設置爲0
就表明插入操做。
let array = [0, 1, 2, 3];
array.splice(1, 0, "1"); // 從下標爲1的地方插入元素 '1'
console.log(array); // [0, "1", 1, 2, 3]
複製代碼
只須要關注第 1 個參數和第 2 個參數就能夠。第 1 個參數表明從那開始刪除,第 2 個參數表明刪除幾個元素。
let array = [0, 1, 2, 3];
array.splice(1, 1); // 從下標爲1的地方刪除1個元素
console.log(array); // [0, 2, 3]
複製代碼
這種操做須要關注全部的參數,如同前面所講。
const arr = [1, 2, 3];
const result = arr.splice(1, 2, 10, 11); // 從下標1的位置,刪除2個元素,並加入元素 10 和 11
console.log(arr); // [1, 10, 11]
console.log(result); // [2, 3]
複製代碼
須要注意,splice
還支持負下標。
let array = [0, 1, 2, 3];
array.splice(-2, 1, 1, 2, 3); // [0, 1, 1, 2, 3, 3]
複製代碼
問渠哪得清如許,爲有源頭活水來。——朱熹
做用:將一個或多個元素添加到數組的開頭,並返回該數組的新長度。
語法:arr.unshift(element1, ..., elementN)
參數列表:要添加到數組開頭的元素或多個元素。
返回值:返回添加後數組的 length
屬性值。
沒啥好說的,shift
的反義詞,push
的好兄弟。
const arr = [1, 2, 3];
const result = arr.unshift(5);
console.log(arr); // [5, 1, 2, 3]
console.log(result); // 4
複製代碼
具備數組特徵的對象,就是類數組對象,也被稱爲ArrayLike
。
從第二部分,數組容器這一小節,咱們已經瞭解到在JavaScript中,數組和類數組對象的操做是很是相近的。實際上,還有一個更爲有趣的地方在於,咱們不止能夠把對象看成數組同樣操做,甚至還能夠使用數組的方法來處理對象。
let obj = {
push: function(el) {
return [].push.call(this, el);
},
};
obj.push(2);
console.log(obj);
/** [object Object] { 0: 2, length: 1, push: function(el){ return [].push.call(this, el);} } **/
複製代碼
能夠看到,push
會自動給對象添加一個0
屬性和length
屬性。
再作個實驗。
let obj = {
0: 0,
push: function(el) {
return [].push.call(this, el);
},
};
obj.push(2);
console.log(obj);
複製代碼
發現push
以後,原來的屬性0
被替換成了 2。
這就是push
的規則:push
方法根據 length
屬性來決定從哪裏開始插入給定的值。若是 length
不能被轉成一個數值,則插入的元素索引爲 0,包括 length
不存在時。當 length
不存在時,將會建立它。
下面再看一下length
存在的狀況。
let obj = {
0: 0,
length: 10,
push: function(el) {
return [].push.call(this, el);
},
};
obj.push(2);
console.log(obj);
/** [object Object] { 0: 0, 10: 2, length: 11, push: function(el) { return [].push.call(this, el);} } **/
複製代碼
能夠看到length
的屬性執行了+1
操做,而且它認爲如今數組裏面已經存在 10 個元素了,那麼新加入的 2 將會是第 11 個元素,下標爲 10。push
就是如此愚蠢,是的,他就是這麼愚蠢。
而他的兄弟,pop
具備和push
同樣的行爲。這裏就不展開演示了,你能夠本身拿一個對象擴展試試。
這種行爲被稱做鴨子類型
。那什麼是鴨子類型呢?
鴨子類型(英語:duck typing)在程序設計中是動態類型的一種風格。在這種風格中,一個對象有效的語義,不是由繼承自特定的類或實現特定的接口,而是由"當前方法和屬性的集合"決定。這個概念的名字來源於由詹姆斯·惠特科姆·萊利提出的鴨子測試(見下面的「歷史」章節),「鴨子測試」能夠這樣表述:
「當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就能夠被稱爲鴨子。」
也就是說,在 ECMAScript 中,長得像數組的對象,均可以被數組的方法所操做。這不只僅侷限於pup
和push
兩個方法,其它不少方法均可以適用於這套規則。
不會改變數組自己的值,會返回新的值。
大廈之成,非一木之材也;大海之潤,非一流之歸也。——《東周列國志》
做用:合併兩個或多個數組。此方法不會更改現有數組,而是返回一個新數組。
語法:var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
參數:value*N*
可選
將數組和/或值鏈接成新數組。若是省略了valueN
參數參數,則concat
會返回一個它所調用的已存在的數組的淺拷貝。
返回值:新 Array 實例。
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const result = arr1.concat(arr2, 7, "8", [9]);
console.log(result); // [1, 2, 3, 4, 5, 6, 7, "8", [9]]
複製代碼
concat
主要須要注意 2 個點。第 1 點,能夠鏈接全部東西,並不必定是數組,如上面的例子。
第 2 個點是concat
的操做是淺拷貝,以下。
const arr1 = [1, 2, 3];
const obj = { k1: "v1" };
const arr2 = [[4]];
const result = arr1.concat(obj, arr2);
console.log(result); // [1, 2, 3,{ k1: "v1" }, [4]]
obj.k2 = "v2";
arr2[0].push(5);
console.log(result); // [1, 2, 3,{ k1: "v1", k2: "v2" }, [4, 5]]
複製代碼
無息烏乎生,無絕烏乎續,無無烏乎有? ——宋應星《談天·日說三》
做用:判斷一個數組是否包含一個指定的值,根據狀況,若是包含則返回 true,不然返回 false。
語法:arr.includes(valueToFind[, fromIndex])
參數:
valueToFind
:可選,須要查找的元素值。若是不傳遞,直接返回false
。
fromIndex
:可選,從fromIndex
索引處開始查找 valueToFind
。若是爲負值,則按升序從 array.length + fromIndex
的索引開始搜 (即便從末尾開始往前跳 fromIndex
的絕對值個索引,而後日後搜尋)。默認爲 0。
返回值:boolean 值,表示元素是否存在。
const arr1 = [1, 2, 3];
const result = arr1.includes(1);
console.log(result); // true
const result2 = arr1.includes(22);
console.log(result2); // false
複製代碼
注意:沒法判斷字面量的引用類型,只能判斷基礎類型和引用。
const t = [1];
const arr1 = [t, 2, 3];
const result = arr1.includes(t);
console.log(result); // true
const result2 = arr1.includes([1]);
console.log(result2); // false
複製代碼
includes
是在 EMAScript6 中誕生的。早在 EMAScript5 中,你們喜歡用indexOf
是否等於-1
來判斷某個元素是否存在於數組中。業界也曾經有過一個做用相似的第三方 API,叫作contains
。那麼既然是新出來的 API,而又在作和已存在 API 相似的事情,那麼必定是有緣由的,什麼緣由呢?
先來看一個常規操做。
const arr = [1, 2, 3];
console.log(arr.indexOf(2) !== -1); // true
console.log(arr.includes(2)); // true
複製代碼
好像沒有問題?
那麼再來看一個不通常的操做。
const arr = [NaN];
console.log(arr.indexOf(NaN) !== -1); // false
console.log(arr.includes(NaN)); // true
複製代碼
區別出來了!indexOf
並不能正常匹配到NaN
,由於在 ECMAScript 中,NaN === NaN
的結果是false
。
再來看一個例子。
const arr = [, , , ,];
console.log(arr.indexOf(undefined) !== -1); // false
console.log(arr.includes(undefined)); // true
複製代碼
這兩個大概就是includes
和indexOf
最大的區別吧。
時人莫小池中水,淺處無妨有臥龍。——竇庠《醉中贈符載》
做用:將一個數組的全部元素鏈接成一個字符串並返回這個字符串。若是數組只有一個項目,那麼將返回該項目而不使用分隔符。
語法:arr.join([separator])
參數:
separator
:可選,指定一個字符串來分隔數組的每一個元素。若是須要,將分隔符轉換爲字符串。若是缺省該值,數組元素用逗號(,
)分隔。若是separator
是空字符串(""
),則全部元素之間都沒有任何字符。
返回值:一個全部數組元素鏈接的字符串。若是 arr.length
爲 0,則返回空字符串。
const arr = [1, 2, 3];
const result = arr.join();
console.log(result); // "1,2,3"
複製代碼
join
和字符串的split
的做用幾乎是相反的。
join
的使用須要注意一點,它會將每一個元素先調用toString
再進行拼接。像空數組這種元素,轉成字符串就是""
,因此拼接起來毫無存在感可言。
let arr = ["h", 9, true, null, [], {}];
let result = arr.join("|");
console.log(result); // "h|9|true|||[object Object]"
複製代碼
固然你能夠主動覆蓋它的toString
方法,這樣結果就不同了。
let arr = ["h", 9, true, null, [], {}];
[].__proto__.toString = function() {
return "Array";
};
let result = arr.join("|");
console.log(result); // "h|9|true||Array|[object Object]"
複製代碼
而若是數組中存在可能轉成String
的元素,就會發生異常。好比Symbol
。
let arr = ["h", 9, true, null, Symbol(1)];
arr.join("|"); // TypeError: Cannot convert a Symbol value to a string
複製代碼
勇於浪費哪怕一個鐘頭時間的人,說明他還不懂得珍惜生命的所有價值。——達爾文
做用:從數組中截取一段造成新的數組。接收 2 個參數,第 1 個是開始元素的下標,第二個是結束元素的下標(不包含這個元素)。
語法:arr.slice([begin[, end]])
參數:
begin
:可選,默認爲 0,提取起始處的索引。
end
:可選,默認爲數組length
。提取終止處的索引。(包含begin
,不包含end
)
返回值:一個含有被提取元素的新數組。
const arr1 = [1, 2, 3];
const result = arr1.slice(1, 2); // 從下標1的位置,截取到下標2
console.log(result); // [2]
複製代碼
特殊用法:
slice()
能夠淺拷貝數組。
const result = arr1.slice();
複製代碼
書到用時方恨少、事非通過不知難。——陸游
toSource
不屬於標準 API,僅屬於Firefox
瀏覽器獨有,不建議使用,能夠直接 pass 掉,看下面的toString
。
做用:返回一個字符串,表明該數組的源代碼。
語法:array.toSource()
const array = [1, 2, 3];
array.toSource(); // "[1, 2, 3]"
複製代碼
鯨落海底,哺暗界衆生十五年。——加里·斯奈德
做用:返回一個字符串,表示指定的數組及其元素。
語法:arr.toString()
返回值:一個表示指定的數組及其元素的字符串。
toString
覆蓋了Object
的toString
方法,返回一個字符串,其中包含用逗號分隔的每一個數組元素。當一個數組被做爲文本值或者進行字符串鏈接操做時,將會自動調用其 toString
方法。
和 join
不傳遞參數時做用相同,若是要手動將數組轉成字符串時,建議使用join
,由於更加靈活。
const arr1 = [1, 2, 3];
const result = arr1.toString();
console.log(result); // "1,2,3"
複製代碼
大鵬之動,非一羽之輕也;騏驥之速,非一足之力也。——《潛夫論·釋難》
語法:arr.toLocaleString([locales[,options]]);
參數:
locales
:可選,帶有 BCP 47 語言標記的字符串或字符串數組。
options
:可選,一個可配置屬性的對象,對於數字 Number.prototype.toLocaleString()
,對於日期Date.prototype.toLocaleString()
。
返回值:表示數組元素的字符串。
既然存在了toString
,那麼toLocaleString
是爲了解決什麼問題呢?
除了Array
具備這個 API,Date
、Number
、Object
都存在這個 API。
toLocaleString
和toString
的主要區別就是toLocaleString
的參數了,它能夠將元素轉化成哪一個國家的人類語言。好比一百萬這個數字。西班牙人的表示方式爲1.000.000
,英國人的表示方式爲1,000,000
。而日期也是如此,好比中國大陸用 年年年年/月月/日日 上午 or 下午 12 小時制時分秒,而國外大多數是 日日/月月/年年年年 24 小時制時分秒。
const arr1 = [1000000, new Date()];
const resultENGB = arr1.toLocaleString("en-GB");
const resultESES = arr1.toLocaleString("es-ES");
const resultAREG = arr1.toLocaleString("ar-EG");
const resultZHCN = arr1.toLocaleString("zh-CN");
console.log(resultENGB); // "1,000,000,29/12/2019, 23:40:39"
console.log(resultESES); // "1.000.000,29/12/2019 23:40:39"
console.log(resultAREG); // ١٬٠٠٠٬٠٠٠,٢٩/١٢/٢٠١٩ ١١:٤١:٣١ م
console.log(resultZHCN); // "1,000,000,2019/12/29 下午11:40:39"
複製代碼
從上面的例子中,能夠總結出toLocaleString
存在的根本目的是爲了保證多個國家的用戶瀏覽器來是符合各自習慣的。由於中國人徹底看不懂阿拉伯的數字和日期,阿拉伯人一樣也不容易看懂中國人的日期同樣。
第 2 個參數很是強大,應用場景通常是展現貨幣,它能夠自定義轉換後的樣式。
const arr1 = [1000000, new Date()];
const resultGBP = arr1.toLocaleString("en-GB", {
style: "currency",
currency: "GBP",
});
const resultCNY = arr1.toLocaleString("zh-CN", {
style: "currency",
currency: "CNY",
});
console.log(resultGBP); // "£1,000,000.00,29/12/2019, 23:51:18"
console.log(resultCNY); // "¥1,000,000.00,2019/12/29 下午11:51:18"
複製代碼
能夠看到,設置了鈔票代碼後,就能夠將數字轉換成鈔票的樣式。須要注意一點,人民幣的代碼是CNY
,而不是RMB
。好吧,開個玩笑。
其實在調用 Array 的toLocaleString
時,會自動調用每一個元素的toLocaleString
。可是前面說了,只有數組、日期、數字和對象存在這個 API,那麼其它的類型沒有這個 API 咋辦呢?調用toString
唄。
有些鳥是註定不會被關在牢籠裏的,它們的每一片羽毛都閃耀着自由的光輝。——《肖申克的救贖》
做用:返回在數組中能夠找到一個給定元素的第一個索引,若是不存在,則返回-1。
語法:arr.indexOf(searchElement[, fromIndex])
參數:
searchElement
:要查找的元素。
fromIndex
:可選,開始查找的位置。若是該索引值大於或等於數組長度,意味着不會在數組裏查找,返回-1。若是參數中提供的索引值是一個負值,則將其做爲數組末尾的一個抵消,即-1 表示從最後一個元素開始查找,-2 表示從倒數第二個元素開始查找 ,以此類推。 注意:若是參數中提供的索引值是一個負值,並不改變其查找順序,查找順序仍然是從前向後查詢數組。若是抵消後的索引值仍小於 0,則整個數組都將會被查詢。其默認值爲 0。
在 ECMAScript5 時期,indexOf
還一直在作一件和 includes
相似的事,前面也提到了,就是經過 indexOf
獲得結果是否爲-1 來判斷數組中是否存在某個元素。後來爲了職責單一,創造出了includes
。因此indexOf
目前的用途最主要的仍是獲取數組內第一個與參數匹配的某個下標。
const arr1 = [1, 2, 3];
const result = arr1.indexOf(3);
console.log(result); // 2
const result2 = arr1.indexOf(21);
console.log(result2); // -1
複製代碼
indexOf
匹配元素時,只能正常匹配基本類型的元素。像引用類型,就必需要使用引用來匹配。
const arr1 = [];
const arr2 = [0, [], arr1];
console.log(arr2.indexOf([])); // -1
console.log(arr2.indexOf(arr1)); // 2
複製代碼
indexOf
和lastIndexOf
是一對,其實indexOf
的名字應該叫做firstIndexOf
。
方向是比速度更重要的追求。——白巖鬆
做用:返回指定元素在數組中的最後一個的索引,若是不存在則返回 -1。從數組的後面向前查找,從 fromIndex
處開始。
參數:
searchElement
:被查找的元素。
fromIndex
:可選,今後位置開始逆向查找。默認爲數組的長度減 1(arr.length - 1
),即整個數組都被查找。若是該值大於或等於數組的長度,則整個數組會被查找。若是爲負值,將其視爲從數組末尾向前的偏移。即便該值爲負,數組仍然會被從後向前查找。若是該值爲負時,其絕對值大於數組長度,則方法返回 -1,即數組不會被查找。
返回值:數組中該元素最後一次出現的索引,如未找到返回-1。
和 indexOf 做用幾乎相同,惟一的區別是從後面進行匹配。
const arr1 = [1, 2, 3, 2];
const result = arr1.lastIndexOf(2);
console.log(result); // 3
const result2 = arr1.indexOf(2);
console.log(result2); // 1
複製代碼
即便咱們生活在陰溝裏,咱們也要仰望星空。——電影《少年的你》影評
flat
是2019年新加入的API,因爲「把多維數組攤平」這個需求一直存在,但需求程度不是很高,因此並無被官方特別重視,直到如今纔出現了這個API。
在早期的JavaScript中,事實上,在2019年這個API沒有開放以前,咱們還在使用本身製做的攤平API來實現這個功能,具體能夠看後面的內容,有具體的代碼實現。
flat
的做用很簡單,就是把一個數組攤平而已。攤平到什麼程度,由你而定。
好比這樣一個數組:
let arr = [1, 2, [3, 4, [5, 6, [7, 8, 9]]]];
複製代碼
攤開一層。
let result = arr.flat(1);
console.log(result);// [1, 2, 3, 4, [5, 6, [7, 8, 9]]]
複製代碼
攤開兩層。
let result = arr.flat(2);
console.log(result);// [1, 2, 3, 4, 5, 6, [7, 8, 9]]
複製代碼
若是你不知道這是一個幾維數組,而又想將它們所有攤開,那麼就傳入Infinity
便可。
let result = arr.flat(Infinity);
console.log(result);// [1, 2, 3, 4, 5, 6, 7, 8, 9]
複製代碼
若是直接調用flat()
而不傳遞任何參數,你認爲效果應該是怎樣的呢?是所有攤開嗎?
那你就猜錯了。
若是不傳遞任何參數,那麼效果和傳遞1是同樣的。
再多的才智也沒法阻擋愚蠢和庸碌的空虛——《瑞克與莫蒂》
flatMap
和flat
是一塊兒出生的。你能夠嘗試可否從ECMAScript
神奇的API命名規則上猜想一下它的做用。
也許讓你猜對了。flatMap
的做用就是flat
和map
這兩個API的結合體。
若是你想讓一個數組中每一個元素複製自身而且創造一個爲自身2倍的值加入到下一個下標中。
你須要作兩步,先使用map
獲得這些值,但它們的返回值變成了一個二維數組。
第二步天然就是把這個返回的數組攤平成一維數組了。
let arr = [1, 2, 3];
const result = arr.map((item) => [item, item * 2]).flat();
console.log(result);// [1, 2, 2, 4, 3, 6]
複製代碼
flatMap
的做用是什麼呢?就是把這個鏈式調用的API,合併成一個API。
let arr = [1, 2, 3];
const result = arr.flatMap((item) => [item, item * 2]);
console.log(result);// [1, 2, 2, 4, 3, 6]
複製代碼
看,多麼無聊的API。真佩服ECMA那幫語言學的設計天才們。
這只是一句無足輕重的吐槽,請不要在乎。
著名的做家Kevin Kelly
在一次演講中說過一段話:
關於技術,在最開始時,沒有人知道新的發明最適合用於作什麼,例如艾迪生的留聲機,他本來不知道這能用來幹什麼。留聲機慢慢被應用於兩個場景:一是錄下臨終遺言;二是錄下教堂裏的講話,包括唱歌。後來留聲機主要用於錄製音樂等。
但咱們生活中不乏不少人有這種思想:世界不須要沒有用的創新。
以爲地說,世界上不存在沒有用的創新,全部創新都有它的用途。那些認爲無用的創新無用的人,大概是沒辦法等待漫長的發掘創新用途的過程。其實:存在即合理,合理即存在。
對原始數組進行遍歷。在遍歷過程當中,數組元素的操做不會受到影響。
千般荒涼,以此爲夢;萬般蹀躞,以此爲歸。——餘秋雨
做用:對數組的每一個元素執行一次提供的函數。
語法:arr.forEach(callback(currentValue [, index [, array]])[, thisArg]);
參數:
callback
:爲數組中每一個元素執行的函數,該函數接收三個參數:
currentValue
:數組中正在處理的當前元素。index
:可選,數組中正在處理的當前元素的索引。array
可選,forEach()
方法正在操做的數組。thisArg
:可選,可選參數。當執行回調函數 callback
時,用做 this
的值。
返回值:undefined
雖說forEach
做爲平常開發最爲頻繁的一個 API,但仍然有很是多的細節不被你們所熟知。致使不少同窗在使用forEach
時出現意料以外的現象發生,讓人感到困惑。
下面千老師來分析一下forEach
到底有哪些須要注意的細節。
forEach
在第一次執行時就會肯定執行範圍。在forEach
執行期間,人爲改變數組元素會影響forEach
的執行。在上面咱們學到了,修改數組的方法一共有 9 種。
分別是添加類(push、unshfit、splice)、刪除類(pop、shift、splice)、填充類(copyWithin、fill)、改變順序類(reverse、sort)。除數組自身的方法之外,arr[i] = x
、arr.length = i
和delete array[i]
這幾種方式也會改變數組。
先看一個簡單的例子。
const array = [0, 1, 2, 3, 4, 5];
array.forEach((currentValue, index, array) => {
if (currentValue % 2 === 0) {
array.push(1);
}
console.log(currentValue);
});
console.log(array);
/* 0 1 2 3 4 5 [0, 1, 2, 3, 4, 5, 1, 1, 1] */
複製代碼
能夠看到,在forEach
過程當中,向數組新添加的數據,是不會被遍歷到的。
可是若是在forEach
的過程當中修改數據,forEach
則會讀取遍歷到它的那一刻的值。好比調整一下上面的那個例子。
const array = [0, 1, 2, 3, 4, 5];
array.forEach((currentValue, index, array) => {
if (currentValue % 2 === 0) {
array[index + 1]++;
}
console.log(currentValue);
});
console.log(array);
/* 0 2 3 3 4 6 [0, 2, 3, 3, 4, 6, NaN] */
複製代碼
由於 API 太多,再也不一一舉例。這裏簡單概括總結一下forEach
的規律:
1.forEach
執行開始時,就會肯定執行次數。不管數組長度如何變化,都不會超過這個執行次數。但可能會低於這個次數。
2.forEach
執行過程當中,長度被改變。增加時沒做用,減小時,到達數組最大長度後,就會結束(跳過)遍歷。
3.forEach
執行過程當中,元素被改變。會讀取遍歷到該元素那一刻的值。
4.forEach
不能夠被像map
、filter
同樣被鏈式調用,由於它的返回值是undefined
,而不是個數組。
5.除了拋出異常之外,沒有辦法停止或跳出 forEach()
循環。若是你須要停止或跳出循環,forEach()
方法不是應當使用的工具。最簡單的辦法是使用for
,或者every
、some
等元素。
forEach
和for
在早期的瀏覽器中,forEach
的性能一直都不如for
的性能。因此致使你們的一個錯誤觀點,即便是如今人們仍認爲forEach
的性能不如for
,其實否則,得益於 V8 引擎的優化。現在在較新版的的瀏覽器或者 nodejs 裏面,forEach
和for
的性能都是不相上下的,也許for
會佔據一點性能優點,但這個差距微乎其微。
爲此千老師還特地作了一個實驗,在 Chrome79 版本下, 長度爲 100 萬的數組的性能對比,我運行了 5 次:
let array = Array.from({ length: 1000000 }, (v, i) => {
return i;
});
// for
console.time("log");
for (let i = 0; i < array.length; i++) {}
console.timeEnd("log");
// log: 17.89697265625ms
// log: 12.362060546875ms
// log: 18.535888671875ms
// log: 13.59326171875ms
// log: 13.08984375ms
// forEach
console.time("log");
array.forEach(function(val, i) {});
console.timeEnd("log");
// log: 16.1630859375ms
// log: 19.702392578125ms
// log: 18.179931640625ms
// log: 19.887939453125ms
// log: 20.77197265625ms
複製代碼
能夠看到性能差距很是微小,甚至有時forEach
的性能會賽過for
。
for
是典型的命令式編程產物。而forEach
和其它的迭代方法同樣,都屬於函數式編程。for
的惟一好處就是靈活性,break
和continue
的隨時跳出。**但最大的優勢同時也是最大的缺點。**功能強大就會致使出現一些難以閱讀的複雜代碼。好比下面這段代碼。
for (var i = 0, len = grid.length, j = len - 1, p1, p2, sum; i < len; j = i++) {
p1 = grid[i];
p2 = grid[j];
sum += p1 + p2;
}
複製代碼
而forEach
就不會出現這種狀況,由於它屏蔽了for
的配置條件。
最後千老師給出的建議就是,98%的狀況下都應該優先使用forEach
或者其它迭代方法,剩下 2%的狀況應該是你在意那一點點性能的狀況,這時就須要你本身權衡了。
內外相應,言行相稱。——韓非
做用:返回一個新的Array Iterator對象,該對象包含數組中每一個索引的鍵/值對。
語法:arr.entries()
返回值:一個迭代器(interator
)對象。
文章到這裏,第一次出現interator
這個名詞。千老師相信不少同窗直到這個概念,但更多的同窗可能不知道。這裏有必要講明白Iterator
是什麼。以便更好地理解數組。
其實要講Iterator
能夠再寫一篇文章的,但千老師儘可能控制。簡明扼要的把這個概念講明白就好了。
它是Iterable
對象上的[Symbol.iterator]
屬性。準確地講,Array 也屬於iterable
。
在 ECMAScript6 以前,JavaScript 中的對象沒有辦法區分哪些對象能迭代,哪些對象不能迭代。咱們總會說數組能夠被迭代,但咱們沒辦法說一些類數組對象也能迭代。雖然它們可以被咱們經過一些手段迭代,好比Array.prototype.forEach.call()
。這些都是對象,都是靠咱們的習慣認定一個對象能不能被迭代,並無規範來約束。這樣子很奇怪。因此 ECMAScript6 推出了一個迭代協議,來解決這個問題。
全部具備[Symbol.iterator]
屬性的對象,都屬於可迭代對象(Iterable
)。經過調用可迭代對象上的[Symbol.iterator]
方法就能夠獲得一個迭代器(iterator
)。經過調用迭代器身上的next
方法就能夠實現迭代。
let str = "hello World!";
let iterator = str[Symbol.iterator]();
let _done = false;
while (!_done) {
const { value, done } = iterator.next();
if (!done) console.log(value);
_done = done;
}
/** "h" "e" "l" "l" "o" " " "W" "o" "r" "l" "d" "!" **/
複製代碼
爲何這個屬性的名字這麼奇怪呢?長得並非像length
這種屬性同樣。**要知道 JavaScript 中全部看起來奇怪的設計都是有緣由的。**由於在 ECMAScript2015 以前是沒有迭代器這個概念的。這屬於新加入的概念,爲了保證兼容性,不能隨便在對象原型上添加iterator
這麼個屬性,否則就會致使以前的 JavaScript 代碼產生意想不到的問題。恰好 ECMAScript2016 引入了Symbol
概念。使用Symbol
能夠很好的解決屬性衝突問題。
咱們能夠利用Symbol.iterator
屬性來建立可迭代對象。
class Rand {
[Symbol.iterator]() {
let count = 0;
return {
next: () => ({
value: count++,
done: count > 5,
}),
};
}
}
var rand = new Rand();
var iterator = rand[Symbol.iterator]();
iterator.next(); // {value: 0, done: false}
iterator.next(); // {value: 1, done: false}
// ..
iterator.next(); // {value: 5, done: false}
iterator.next(); // {value: undefined, done: true}
複製代碼
上面的代碼雖然看上去花裏胡哨,其實語法很簡單。Symbol.iterator
方法返回一個帶有next
方法的對象。而next
方法每次調用時會返回一個包含value
和done
屬性的對象,就這麼簡單。
value
表示當前迭代的值,done
表示迭代是否結束。
注意:可迭代對象(iterable
)和迭代器對象(iterator
)不是一回事。惟一的聯繫就是可迭代對象上會包含一個Symbol.iterator
的屬性,它指向一個迭代器對象。
ECMAScript6 還增長了一種新語法,中文叫展開操做符(Spread syntax
)。可迭代對象能夠利用這種操做符將一個可迭代對象展開。
let str = "hello World!";
let iterator = str[Symbol.iterator]();
let _done = false;
console.log(...str);
/* "h" "e" "l" "l" "o" " " "W" "o" "r" "l" "d" "!" */
複製代碼
ECMAScript6 中新添加的for of
語法也是依據iterator
的value
和done
進行循環。
稍微瞭解數據結構的同窗會發現,這玩意不就是一個單向鏈表嗎?還別說,iterator
就是個單向鏈表。
明白了迭代器的概念,咱們回到entries
上面繼續研究。
var arr = [1, 2, 3];
var iterator = arr.entries();
iterator.next();
/* {value: Array(2), done: false} value: (2) [0, 1] done: false */
iterator.next();
/* {value: Array(2), done: false} value: (2) [1, 2] done: false __proto__: Object */
複製代碼
entries
會將數組轉化成迭代器。這個迭代器中每一個迭代出的值都是一個數組,[下標, 值]
的形式。這樣有什麼用呢?其實千老師一時半會也想不到有什麼用,可是當你應該用到它的時候,天然就知道它的應用場景是什麼了。(若是你還能記住有這麼個 API 的話。)
天下之事常成於困約,而敗於奢靡。——陸游
做用:測試一個數組內的全部元素是否都能經過某個指定函數的測試。它返回一個布爾值。
語法:arr.every(callback[, thisArg])
參數:
callback
:用來測試每一個元素的函數,它能夠接收三個參數:
element
:用於測試的當前值。
index
:可選,用於測試的當前值的索引。
array
:可選,調用 every
的當前數組。
thisArg
:執行 callback
時使用的 this
值。
返回值:若是回調函數的每一次返回都爲 truthy 值,返回 true
,不然返回 false
。
every
和some
很像,其實every
和早期的幾個迭代方法都很像,更恰當的說法是,早期的幾個迭代方法都很像。every
和forEach
的區別有兩點,第一點,every
的執行會在不知足條件時中止遍歷。第二點,every
有一個返回值。
const arr = [0, 1, 2, 30, 4, 5];
const result = arr.every(function(item, index) {
console.log(index);
/* 0 1 2 3 */
return item < 10;
});
console.log(result); // false
複製代碼
業精於勤,荒於嬉;行成於思,毀於隨。——韓愈
做用:測試數組中是否是至少有 1 個元素經過了被提供的函數測試。它返回的是一個 Boolean 類型的值。
語法:arr.some(callback(element[, index[, array]])[, thisArg])
參數:
callback
:用來測試每一個元素的函數,接受三個參數:
element
數組中正在處理的元素。
index
可選
數組中正在處理的元素的索引值。
array
可選
some()
被調用的數組。
thisArg
:可選,執行 callback
時使用的 this
值。
返回值:數組中有至少一個元素經過回調函數的測試就會返回true
;全部元素都沒有經過回調函數的測試返回值纔會爲 false。
some
和every
很像,區別在於some
會在碰到第一個符合條件的元素時中止遍歷。因此這裏也沒什麼好說的。把every
的例子拿到這裏就能夠看到區別。
const arr = [0, 1, 2, 30, 4, 5];
const result = arr.some(function(item, index) {
console.log(index);
// 0
// 1
return item < 10;
});
console.log(result); // true
複製代碼
物以類聚,人以羣分。——《易經》
做用:建立一個新數組, 其包含經過所提供函數實現的測試的全部元素。
語法:var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])
參數:callback
:用來測試數組的每一個元素的函數。返回 true
表示該元素經過測試,保留該元素,false
則不保留。它接受如下三個參數:
element
:數組中當前正在處理的元素。index
:可選,正在處理的元素在數組中的索引。array
:可選,調用了 filter
的數組自己。thisArg
:可選,執行 callback
時,用於 this
的值。
返回值:一個新的、由經過測試的元素組成的數組,若是沒有任何數組元素經過測試,則返回空數組。
能夠用一句話理解filter
,取走咱們想要的東西。
const arr = [0, 1, 2, 3, 4, 5];
const result = arr.filter(function(item, index) {
return item % 2 === 0;
});
console.log(result); // [0, 2, 4]
複製代碼
filter
和map
是在 ES6 中最先加入的api
。在沒有filter
時,forEach
一樣能夠實現filter
功能。
const arr = [0, 1, 2, 3, 4, 5];
let result = [];
arr.forEach(function(item, index) {
if (item % 2 === 0) {
result.push(item);
}
});
console.log(result); // true
複製代碼
勇氣通往天堂,怯懦通往地獄。——塞內加
做用:返回數組中知足提供的測試函數的第一個元素的值。不然返回 undefined
。
語法:arr.find(callback[, thisArg])
參數:
callback
:在數組每一項上執行的函數,接收 3 個參數:
element
:當前遍歷到的元素。index
:可選,當前遍歷到的索引。array
:可選:數組自己。thisArg
:可選,執行回調時用做this
的對象。
返回值:數組中第一個知足所提供測試函數的元素的值,不然返回 undefined
。
const arr = [0, 1, 20, 30, 40];
const result = arr.find((item) => item > 10);
console.log(result);// 20
複製代碼
得之,我幸;不得,我命,如此而已。——徐志摩
做用:返回數組中知足提供的測試函數的第一個元素的索引。不然返回-1。
語法:arr.findIndex(callback[, thisArg])
參數:
callback
:針對數組中的每一個元素, 都會執行該回調函數, 執行時會自動傳入下面三個參數:
element
:當前元素。index
:當前元素的索引。array
:調用findIndex
的數組。thisArg
:可選。執行callback
時做爲this
對象的值。
返回值:數組中經過提供測試函數的第一個元素的索引。不然返回-1。
findIndex
返回的結果就是find
返回元素的索引。
const arr = [0, 1, 20, 30, 40];
const result = arr.findIndex((item) => item > 10);
console.log(result);// 2
複製代碼
知人者智,自知者明。勝人者有力,自勝者強。——老子
做用:返回一個包含數組中每一個索引鍵的Array Iterator
對象。
語法:arr.keys()
返回值:一個新的Array迭代器對象。
let arr = ['a', 'b', 'c'];
const result = arr.keys();
for(let key of result){
console.log(key);
}
/* 0 1 2 */
複製代碼
做用就是把數組轉換成了一個存儲了數組所有索引的迭代器。
keys
與Object.keys
不一樣的是,keys
不會忽略empty
元素。
let arr = ['a', ,'b', , 'c', ,];
const result = arr.keys();
for(let key of result){
console.log(key);
}
/* 0 1 2 3 4 5 */
複製代碼
有則改之,無則加勉。——《論語》
做用:建立一個新數組,其結果是該數組中的每一個元素都調用一個提供的函數後返回的結果。
語法:
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
複製代碼
參數:
callback
:生成新數組元素的函數,使用三個參數:
currentValue
:callback
數組中正在處理的當前元素。index
:可選,callback
數組中正在處理的當前元素的索引。array
:可選,map
方法調用的數組。thisArg
:可選,執行 callback
函數時值被用做this
。
返回值:回調函數的結果組成了新數組的每個元素。
map
是ECMAScript2015中最爲最古老的一批API,它的主要做用就是映射一個數組。它的功能使用forEach
一樣可以實現。
好比將一個數組全部元素翻倍。
let arr = [1, 2, 3, 4, 5, 6, 7];
const result = arr.map(function(item) {
return item * 2;
});
console.log(result);// [2, 4, 6, 8, 10, 12, 14]
複製代碼
forEach
一樣可以實現,但比map
稍微麻煩一點。
let arr = [1, 2, 3, 4, 5, 6, 7];
const result = [];
arr.forEach(function(item) {
result.push(item * 2);
});
console.log(result);// [2, 4, 6, 8, 10, 12, 14]
複製代碼
既然二者均可以實現,雖然map
更簡潔。那麼應該在什麼狀況下使用map
呢?
1.是否須要返回一個新的數組。
2.是否須要從回掉函數中獲得返回值。
知足任一條件均可以使用map
,不然使用forEach
或者for...of
。
最多見的用法是從對象數組中提取某些值。
let arr = [
{ name: "dog", age: 11 },
{ name: "cat", age: 4 },
{ name: "小明", age: 15 },
];
const result = arr.map(function(item) {return item.age});
console.log(result); // [11, 4, 15]
複製代碼
當你不知道是否應該使用map
時,也不用糾結,由於map
最大的意義就是能夠簡化一些代碼。
人的一輩子是短的,但若是卑劣地過這一輩子,就太長了。——莎士比亞
做用:對數組中的每一個元素執行一個由您提供的reducer函數(升序執行),將其結果彙總爲單個返回值。
語法:arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
參數:
callback
:執行數組中每一個值 (若是沒有提供 initialValue則第一個值除外
)的函數,包含四個參數:
accumulator
:累計器累計回調的返回值; 它是上一次調用回調時返回的累積值,或initialValue
(見於下方)。
currentValue
:數組中正在處理的元素。
index
:可選,數組中正在處理的當前元素的索引。 若是提供了initialValue
,則起始索引號爲0,不然從索引1起始。
array
:可選,調用reduce()
的數組。
initialValue
:可選,做爲第一次調用 callback
函數時的第一個參數的值。 若是沒有提供初始值,則將使用數組中的第一個元素。 在沒有初始值的空數組上調用 reduce 將報錯。
返回值:函數累計處理的結果。
let arr = [1, 2, 3, 4, 5, 6];
const result = arr.reduce(function(acc, val) {
return acc + val;
});
console.log(result);// 21
複製代碼
和map
、filter
等API相比,reduce
好像有點不同凡響。不一樣在哪裏呢?
reduce
的字面意思是「減小」,實際上,稱呼它爲組合更合適一些。
舉個形象點的例子,健身計算蛋白質、脂肪、碳水化合物。
這是一個食物的養分表。
const nutritionFacts = {
"🥚": {
carbohydrate: 3,// 碳水化合物
protein: 13,// 蛋白質
fat: 9,// 脂肪
calories: 144// 熱量
},
"🍏": {
carbohydrate: 12,
protein: 0,
fat: 0,
calories: 52
},
"🍌": {
carbohydrate: 21,
protein: 1,
fat: 0,
calories: 91
},
"🍞": {
carbohydrate: 58,
protein: 8,
fat: 5,
calories: 312
},
"🥦": {
carbohydrate: 3,
protein: 4,
fat: 1,
calories: 33
},
"🥩": {
carbohydrate: 2,
protein: 20,
fat: 4,
calories: 125
}
};
複製代碼
下面咱們能夠經過reduce
來封裝一個計算熱量的函數。
const calculation = (foods) => {
return foods.reduce((nutrition, food) => {
return {
carbohydrate:
nutritionFacts[nutrition].carbohydrate +
nutritionFacts[food].carbohydrate,
protein: nutritionFacts[nutrition].protein + nutritionFacts[food].protein,
fat: nutritionFacts[nutrition].fat + nutritionFacts[food].fat,
calories:
nutritionFacts[nutrition].calories + nutritionFacts[food].calories
};
});
};
const result = calculation(["🥩", "🥦"]);
console.log(result);
/* { calories: 158, carbohydrate: 5, fat: 5, protein: 24 } */
複製代碼
你能夠多嘗試一下,而後就能從上面的代碼中發現一個BUG。若是沒有發現,請繼續嘗試。
這個BUG就是:若是你只吃了一塊牛肉,那麼它會把牛肉原封不動地返回給你。(這不符合事實,正確答案應該是💩)
const result = calculation(["🥩"]);
console.log(result);// "🥩"
複製代碼
那麼該怎樣修復呢?
首先要肯定緣由,是什麼緣由致使出現了這個現象?你可能發現了,若是reduce
的調用者的length
爲1時,它不會去調用callback
的邏輯,而是直接返回該元素。
那麼照着這個思路,改造方法大致上是根據傳入的參數foods
的length
來給定返回值,若是length
是1的話,直接返回它對應的養分成分。能夠寫出如下代碼:
if (foods.length === 1) {
const { carbohydrate, protein, fat, calories } = nutritionFacts[foods[0]];
return {
carbohydrate,
protein,
fat,
calories
};
} else if (foods.length < 1) {
// 原來的邏輯
}
複製代碼
但這樣明顯感受到很笨拙,有沒有更加睿智的作法呢?固然是有的,別忘了reduce
還存在第二個參數。若是不傳遞第二個參數,第一個參數就要被做爲初始狀態。這種狀況下,第一個值就要被跳過。不過一旦有了第二個參數,第一個值就不會被跳過。因此咱們能夠用第二個參數更加優雅的解決這個問題。
nutritionFacts["💧"] = {carbohydrate: 0, protein: 0, fat: 0, calories: 0};// 添加 💧 默認全部養分都爲0
const calculation = (foods) => {
return foods.reduce((nutrition, food) => {
return {
carbohydrate:
nutritionFacts[nutrition].carbohydrate +
nutritionFacts[food].carbohydrate,
protein: nutritionFacts[nutrition].protein + nutritionFacts[food].protein,
fat: nutritionFacts[nutrition].fat + nutritionFacts[food].fat,
calories:
nutritionFacts[nutrition].calories + nutritionFacts[food].calories
};
}, "💧");
};
複製代碼
聰明的同窗發現了一個等式。
["🥩", "🥦"].reduce(reducer, initialState);
[initialState, "🥩", "🥦"].reduce(reducer);
複製代碼
上面這兩種用法,效果是相等的。
若是你用過Redux或者Rxjs,那麼從上面的代碼中,你應該看到了熟悉的東西。reducer
和initialState
。
是的,它們都是使用的同一種思想和原理。
你能夠這麼想,Redux中的reduce
並非同步自動完成的,而是異步手動激活的。reducer
的第一個參數currentState
就是當前的基礎狀態。每次發動不一樣的action
時,會觸發一次reduce
的調用。經過reducer
的第二個參數currentValue
和reducer
的邏輯來改變currentState
,currentValue
對應的是Redux中Action
傳遞過來的參數type
和payload
。
你可能有點聽不明白,不要緊,你遲早會明白的。
for
同樣能夠實現reduce
。好比計算碳水化合物。
let foods = ["🥩", "🥦"];
let result = {};
for(let i = 0; i < foods.length; i++) {
result.carbohydrate = (result.carbohydrate || 0) + nutritionFacts[foods[i]].carbohydrate;
}
console.log(result);// { carbohydrate: 5 }
複製代碼
從上面的例子中能夠看到,for
比reduce
多了一個變量來存儲上一次計算的結果值。
reduce
其實也存在這麼一個值,只不過得益於函數式編程的好處,它不會被你直接看到。可能如今你並不能感覺到reduce
比for
有什麼太大的優點。可是當你面對一份數百行的代碼文件時,reduce
自動替你維護這個變量的優點又很容易體現出來了。
還須要注意一個點,reduce
從字面意思看是減小,實際上它並非減小。由於它的回調函數中的第一個參數能夠是任何值,好比數組或者對象。既然是數組或者是對象,那麼就是一個能夠無限擴展的數據結構。因此,千萬不要被reduce
的字面意思騙過去了。
let arr = [0, 1, 2, 3, 4, 5];
const result = arr.reduce((accumulator, currentValue) => {
accumulator.push(currentValue * 2);
return accumulator;
}, []);
console.log(result);// [0, 2, 4, 6, 8, 10]
複製代碼
看,reduce
能夠實現相似於map
的功能。一樣的,reduce
也能夠實現forEach
、filter
等功能。
不要回避苦惱和困難,挺起身來向它挑戰,進而克服它。——池田大做
做用:接受一個函數做爲累加器(accumulator)和數組的每一個值(從右到左)將其減小爲單個值。
語法:arr.reduceRight(callback(accumulator, currentValue[, index[, array]])[, initialValue])
參數:
callback
:一個回調函數,用來操做數組中的每一個元素,可接受四個參數:
accumulator
:上一次調用回調的返回值,或提供的 initialValue。
currentValue
:當前被處理的元素。index
:可選,數組中當前被處理的元素的索引。array
:可選,調用 reduceRight()
的數組。initialValue
:可選,值用做回調的第一次調用的累加器。若是未提供初始值,則將使用並跳過數組中的最後一個元素。在沒有初始值的空數組上調用reduce或reduceRight就會建立一個TypeError。
返回值:執行以後的返回值。
reduceRight
和reduce
是一對雙胞胎,不一樣之處是從後面朝前迭代。能夠類比indexOf
和lastIndexOf
。
我和誰都不爭,和誰爭我都不屑。——蘭德
做用:返回一個新的 Array Iterator
對象,該對象包含數組每一個索引的值。
語法:arr.values()
返回值:一個新的 Array
迭代對象。
let arr = ['a', 'b', 'c'];
const iterator = arr.values();
for(let item of iterator){
console.log(item);
}
/* "a" "b" "c" */
複製代碼
一切特立獨行的人格都意味着強大——加繆
做用:@@iterator
屬性和 Array.prototype.values()
屬性的初始值是同一個函數對象。
語法:arr[Symbol.iterator]()
返回值:與values
相同。
通常不建議用這個方法,直接用values
就行了。
let arr = ['a', 'b', 'c'];
const iterator = arr[Symbol.iterator]();
for(let item of iterator){
console.log(item);
}
/* "a" "b" "c" */
複製代碼
忘記過去就意味着背叛。—— 列寧
做用:返回一個給定對象自身可枚舉屬性的鍵值對數組,其排列與使用 for...in
循環遍歷該對象時返回的順序一致(區別在於 for-in 循環還會枚舉原型鏈中的屬性)。
語法:Object.entries(obj)
參數:obj
:能夠返回其可枚舉屬性的鍵值對的對象。
返回值:給定對象自身可枚舉屬性的鍵值對數組。
let animals = {"dog": "🐶", "cat": "🐱", "bird": "🐦", "wolf": "🐺"};
const result = Object.entries(animals);
console.log(result);// [["dog", "🐶"], ["cat", "🐱"], ["bird", "🐦"], ["wolf", "🐺"]]
複製代碼
對象轉換成數組後,元素的排列順序並不取決於對象的定義順序,但這個順序是和for...in
保持一致的。
entries
常見的應用場景有兩個。
for(let [key, value] of Object.entries(animals)) {
console.log(`${key}:${value}`);
}
/* "dog:🐶" "cat:🐱" "bird:🐦" "wolf:🐺" */
複製代碼
或者使用forEach
,結果是相同的。
Object.entries(animals).forEach(([key, value]) => console.log(`${key}: ${value}`));
/* "dog: 🐶" "cat: 🐱" "bird: 🐦" "wolf: 🐺" */
複製代碼
const map = new Map(Object.entries(animals));
console.log(map.size); // 4
console.log(map.has('dog')); // true
console.log(map.get('cat')); // "🐱"
複製代碼
從善如登,從惡如崩。一一《國語》
做用:返回一個由一個給定對象的自身可枚舉屬性組成的數組,數組中屬性名的排列順序和使用 for...in
循環遍歷該對象時返回的順序一致 。
語法:Object.keys(obj)
參數:obj
:要返回其枚舉自身屬性的對象。
返回值:一個表示給定對象的全部可枚舉屬性的字符串數組。
keys
就是把一個對象全部可枚舉的屬性名收集到一個數組中。
let animals = {"dog": "🐶", "cat": "🐱", "bird": "🐦", "wolf": "🐺"};
const result = Object.keys(animals);
console.log(result);// ["dog", "cat", "bird", "wolf"]
複製代碼
功崇唯志,業廣唯勤。一一《尚書》
做用:返回一個給定對象自身的全部可枚舉屬性值的數組。
語法:Object.values(obj)
參數:obj
:被返回可枚舉屬性值的對象。
返回值:一個包含對象自身的全部可枚舉屬性值的數組。
values
的工做方式和entries
十分相似。它們都會自動忽略原型鏈和不可枚舉的屬性。
let animals = {"dog": "🐶", "cat": "🐱", "bird": "🐦", "wolf": "🐺"};
const result = Object.values(animals);
console.log(result);// ["🐶", "🐱", "🐦", "🐺"]
複製代碼
values
能夠輕鬆將對象轉換爲Set。
let animals = {"dog": "🐶", "cat": "🐱", "bird": "🐦", "wolf": "🐺"};
const set = new Set(Object.values(animals));
console.log(set); // Set(4) {"🐶", "🐱", "🐦", "🐺"}
複製代碼
values
與keys
是一對功能類似的API。
若是不忘記許多,人生沒法再繼續。——巴爾扎克
做用:把鍵值對列表轉換爲一個對象。
語法:Object.fromEntries(iterable);
參數:iterable
:可迭代對象,相似 Array
、 Map
或者其它實現了可迭代協議的對象。
返回值:一個由該迭代對象條目提供對應屬性的新對象。
fromEntries
是2019年新加入的API,fromEntries
是entries
的反轉函數,設計初衷也是爲了解決entries
的使用後,數據被保留在數組中沒法逆轉的問題。
let animals = {"dog": "🐶", "cat": "🐱", "bird": "🐦", "wolf": "🐺"};
const animalsList = Object.entries(animals);
const animals2 = Object.fromEntries(animalsList);
console.log(animals2);
/* { bird: "🐦", cat: "🐱", dog: "🐶", wolf: "🐺" } */
複製代碼
fromEntries
的做用不只限於將Array
轉換爲Object
,它能夠將全部可迭代對象轉換爲Object,如Set
和Map
。
無事時不可或倦,人賢者視其自修。——左宗棠
做用:使用指定的分隔符字符串將一個String
對象分割成子字符串數組,以一個指定的分割字串來決定每一個拆分的位置。
語法:str.split([separator[, limit]])
參數:
separator
:指定表示每一個拆分應發生的點的字符串。separator
能夠是一個字符串或正則表達式。 若是純文本分隔符包含多個字符,則必須找到整個字符串來表示分割點。若是在str中省略或不出現分隔符,則返回的數組包含一個由整個字符串組成的元素。若是分隔符爲空字符串,則將str原字符串中每一個字符的數組形式返回。
limit
:一個整數,限定返回的分割片斷數量。當提供此參數時,split 方法會在指定分隔符的每次出現時分割該字符串,但在限制條目已放入數組時中止。若是在達到指定限制以前達到字符串的末尾,它可能仍然包含少於限制的條目。新數組中不返回剩下的文本。
返回值:返回源字符串以分隔符出現位置分隔而成的一個Array。
const str = "hello,world!";
const result = str.split(',');
console.log(result);// ["hello", "world!"]
複製代碼
使用split
切割字符串返回的數組,每一個元素都是源字符串的子串。
若是想把這個切割後的數組還原,能夠用join
。
const str2 = result.join(',');
console.log(str2);// "hello,world!"
複製代碼
在ECMAScript5.0時期,API並無那麼多。你能夠去看《JavaScript高級程序設計(第3版)》,一共就那麼幾個關鍵的API。
從如今來看,不多有人可以在不借助文檔的狀況下徹底清楚什麼情景下應該用什麼API。要知道,上面洋洋灑灑介紹的這麼多API,僅僅是Array這麼一個數據類型本身而已。並且,你肯定上面這些內容你都記住並理解了嗎?我相信你並無記住多少。你或許如今正在煩惱該怎麼記住它們。
千老師的建議是:不要刻意的背誦它們,這樣沒有意義。
若是你是一個長期和JavaScript接觸的工做者;或者是一位JavaScript資深愛好者,能夠適當的在歷史項目的重構和新項目的開發中使用這些功能。不過要注意Transplier的支持。**練習是加深記憶的一個絕佳手段。**當你練習到必定程度後,就能夠去找同道中人去交流、分享彼此的心得。不要只停留在本身的知識框架內,多出去走走。
若是你並不常常接觸JavaScript,或者說JavaScript並不做爲你的惟一語言,好比你是一個同時掌握了其它語言的全棧工程師,好比Java。那你更沒有必要記住所有的API了。由於Java那邊還有更多、更重要的東西須要去記憶。
可是,不管如何你都必須記住幾個關鍵且核心的API。
哪些是關鍵且核心的呢?ECMAScript5.0時代的那些API以及ECMAScript2015第一批加入的API。記住這些你就能夠解決全部場景。至於ECMAScript2015以後的API,其目的大可能是爲了修補一些歷史問題,或者爲了實現單一職責的設計而刻意設計出來的。雖然有趣,但未必真的有用。
因爲 Array 的 API 實在是太多,沒有認真學習和理解,加上大量的實踐和經驗。是很難記住全部的 API 的。針對這種狀況,千老師纔會寫出這篇文章。可是這篇文章的內容稍微有一點兒多。千老師還研究了一下如何偷懶的辦法。好比如今你並無記住和理解全部的 API,但你要用,並且要用對。怎麼辦呢?每次都查文檔但是很是慢的。
不要緊,一位美女程序員 Sarah Drasner 開源了一個 Array 利器ArrayExplorer。你只須要選擇你須要的 Array 操做,就能夠獲得對應的 API。
其實呢,千老師以爲若是**做爲一名通常的工程師,沒有必須把 Array 學精通的必要。固然,若是你想做爲一名不通常的工程師,那麼千老師仍是建議你把 Array 學精通。**因此不要僥倖,是福不是禍,是禍躲不過。千老師相信,每一名讀這篇文章的你,都不會只是一名通常的工程師。
得益於 ECMAScript 靈活的原型系統,每種數據類型均可以經過原型擴展的方式增長新的功能。
這一點是和 Java 有很是大的區別的。Java 要經過繼承來實現對數組的擴展。
class MyArrayList extends ArrayList{
public void hello(){
System.out.println("hello");
}
}
複製代碼
而且在此以後使用數組時,就不能再用 ArrayList 了,而要改用 MyArrayList。
MyArrayList arr = new MyArrayList();
arr.add(1);
arr.add("2");
arr.hello();// hello
複製代碼
除此以外還有另外一種方式,那就是直接修改 JDK。
而 ECMAScript 就簡單多了。擴展以後不影響原使用方式。
Array.prototype.hello = function() {
console.log("hello");
}
[].hello();// hello
複製代碼
固然 ECMASCript 也支持繼承的方式來擴展。
class MyArray extends Array {
hello() {
console.log("hello");
}
}
複製代碼
壞處顯而易見,建立數組的方式能經過new
關鍵字,字面量的方式就行不通了。而依照咱們的習慣,99%的人都是喜歡使用字面量的。
var arr = new MyArray();
arr.hello(); // hello
var arr2 = [];
arr2.hello(); // Uncaught TypeError: [].hello is not a function
複製代碼
在原型擴展這方面,prototype.js 算是業界鼻祖了。緊隨其後的 right.js、ext.js、undersore.js 等框架都紛紛效仿。曾經有很長一段時間,幾乎每一個庫都要對 Array 進行擴展。就好像不擴展一下 Array,都不敢稱本身是個 JavaScript 庫似的。Array 已經有三十多個屬性和方法了,大家在擴展 Array 時,有替 Array 想一下嗎?固然他們都沒想,但 ECMA 替它想了。因此如今的原生 Array 已經很是很是強大,而這些庫反而不行了。
如今來講,除了 lodash.js 之外,其它同類型的庫幾乎都不見蹤跡。但它們所擴展的方法確實是剛需,由於這些方法都是你們在寫業務時碰到的決解方案,因此仍然值得咱們去學習。若是你以爲如今學這些沒有太大意義,說明你寫的代碼量仍是不夠,多寫寫複雜業務,遲早會碰到使用場景。
千老師推薦一個比較成熟的 array 擴展庫,d3-array。這個庫很完善,能夠直接拿到項目中使用。
下面展現一些比較常見的擴展方法的實現。
做用:根據下標移除元素。
實現:
Array.prototype.remove = function(index) {
return !!this.splice(index, 0);
};
複製代碼
用法:
var arr = ["a", "b", "c"];
const result = arr.remove(1);
console.log(result); // true
console.log(arr); // ['a', 'c']
複製代碼
做用:根據元素移除元素
實現:
Array.prototype.removeAt = function(item) {
const index = this.indexOf(item);
if (index !== -1) {
return !!this.splice(index, 1);
}
return false;
};
複製代碼
用法:
var arr = ["a", "b", "c"];
const result = arr.removeAt(1);
console.log(result); // true
console.log(arr); // ['b', 'c']
複製代碼
做用:對數組進行洗牌,打亂數組元素。
洗牌算法相對來講是一個比較複雜的 API。它是一個很是古老的問題,能夠追溯到 1938 年的 Knuth shuffle 算法,那時的咱們尚未出生。
Knuth shuffle 的操做步驟大體爲:
1.記錄從 1 到 length-1 的數字。
2.從 1 到剩餘未遍歷數字之間選擇隨機數 k。
3.從尾端開始計數,每次剔除被隨機取到的 k,並將其放置到數組末尾。
4.重複第 2-3 步,直到全部數字都被遍歷。
其核心是使用Math
對象提供的random
函數。
在 npm 上已經有 N 個實現好的庫,knuth-shuffle-seeded和knuth-shuffle。
實現:
Array.prototype.shuffle = function() {
for (len = this.length - 1; i > 0; i--) {
rand = Math.floor(Math.random() * i);
temp = this[rand];
this[rand] = this[i];
this[i] = temp;
}
};
複製代碼
用法:
let arr = [0, 1, 2, 3, 4, 5, 6];
arr.shuffle();
console.log(arr); // [5, 4, 6, 1, 0, 2, 3]
複製代碼
做用:從數組中隨機取出一個元素。
實現:
Array.prototype.random = function() {
return this[Math.floor(Math.random() * this.length)];
};
複製代碼
用法:
let arr = [0, 1, 2, 3, 4, 5, 6];
const result = arr.random();
console.log(result);
複製代碼
做用:對數組進行平坦化處理,返回一個一維數組。
實現:
Array.prototype.flatten = function() {
let result = [];
this.forEach(function(item) {
if (Array.isArray(item)) {
result = result.concat(item.flatten());
} else {
result.push(item);
}
});
return result;
};
複製代碼
用法:
let arr = [0, [1, 2], 3, [[[4], 5], 6]];
const result = arr.flatten();
console.log(result); // [0, 1, 2, 3, 4, 5, 6]
複製代碼
在ECMAScript2019以後,原生Array擁有了flat
API,就再也不須要這個第三方API了。但在更高的環境中仍能夠使用。
做用:對數組進行去重,返回一個沒有重複元素的數組。
在 ECMAScript6 以前,unique
的實現相對麻煩,可是在 ECMAScript6 以後,有了新的數據結構Set
,unique
的實現就很是簡單了。
使用Set
實現:
Array.prototype.unique = function() {
let set = new Set(this);
return [...set];
};
複製代碼
ECMAScript5 實現:
Array.prototype.unique = function() {
var result = [];
loop: for (let i = 0; i < this.length; i++) {
for (let j = i + 1; j < this.length; j++) {
if (this[i] === this[j]) {
continue loop;
}
}
result.push(this[i]);
}
return result;
};
複製代碼
用法:
let arr = [0, 1, 1, 1, 2, 3, 3];
const result = arr.unique();
console.log(result); // [0, 1, 2, 3]
複製代碼
做用:去除數組中的 null 與 undefined,並返回新數組。
實現:
Array.prototype.compact = function() {
return this.filter(function(item) {
return item != null;
});
};
複製代碼
用法:
let arr = [0, null, 1, , undefined, 2];
const result = arr.compact();
console.log(result); // [0, 1, 2]
複製代碼
做用:取得數組中每一個對象的某個屬性,並組成數組返回。
實現:
Array.prototype.pluck = function(propertyName) {
let result = [];
this.forEach(function(item) {
if (propertyName in item) {
result.push(item[propertyName]);
}
});
return result;
};
複製代碼
用法:
let arr = [
{ name: "dog", age: 11 },
{ name: "cat", age: 4 },
{ name: "小明", age: 15 },
];
const result = arr.pluck("age");
console.log(result); // [11, 4, 15]
複製代碼
做用:根據指定條件進行分組,返回對象。
實現:
Array.prototype.groupBy = function(key) {
return this.reduce((acc, i) => {
(acc[i[key]] = acc[i[key]] || []).push(i);
return acc;
}, {});
}
複製代碼
使用:
let arr = [
{
name: "金庸",
profession: "做家"
},
{
name: "李小龍",
profession: "武術家"
},
{
name: "古龍",
profession: "做家"
}
];
const result = arr.groupBy("profession");
console.log(result);
/* { 做家: [ { name: "金庸", profession: "做家" }, { name: "古龍", profession: "做家" } ], 武術家: [ { name: "李小龍", profession: "武術家" } ] } */
複製代碼
這個API的靈感來源於SQL中的GROUP BY
操做。
做用:兩個數組取並集。
實現思路和unique
很是類似,兩個數組拼接起來,再去重便可。
實現:
Array.prototype.union = function(arr) {
let set = new Set(this.concat(arr));
return [...set];
};
複製代碼
用法:
const result = [1, 2, 3, 4, 5].union([2, 3, 4, 5, 6]);
console.log(result);// [1, 2, 3, 4, 5, 6]
複製代碼
做用:兩個數組取交集。
實現:
Array.prototype.intersect = function(arr) {
return this.filter(function(item) {
return ~arr.indexOf(item);
});
};
複製代碼
用法:
const result = [1, 2, 3, 4, 5].intersect([2, 3, 4, 5, 6]);
console.log(result);// [2, 3, 4, 5]
複製代碼
做用:兩個數組取差集。
Array.prototype.diff = function(arr) {
let result = [];
for(let i = 0;i < this.length; i++) {
if(!arr.includes(this[i])) {
result.push(this[i]);
}
}
return result;
}
複製代碼
用法:
let arr = ['🐇', '🐘', '🐿️', '🐑'];
let arr2 = ['🐇', '🐑', '🐒', '🐄'];
const result = arr.diff(arr2);
console.log(result);// ["🐘", "🐿️"]
複製代碼
做用:取數組中最小值,僅用於數字數組。
實現思路是利用Math.min
和apply
這兩個函數的組合。
Array.prototype.min = function() {
return Math.min.apply(0, this);
}
複製代碼
用法:
const result = [1, 2, 3, 4, 5].min();
console.log(result);// 1
複製代碼
做用:取數組中最大值,僅用於數字數組。
max
的實現思路類同於min
。
Array.prototype.max = function() {
return Math.max.apply(0, this);
}
複製代碼
用法:
const result = [1, 2, 3, 4, 5].max();
console.log(result);// 5
複製代碼
做用:將一個大數組分紅N個小數組。
Array.prototype.chunk = function(size) {
let chunked = [];
let arr = this.slice();
while(arr.length) {
chunked.push(arr.splice(0, size));
}
return chunked;
}
複製代碼
用法:
const arr = [1, 2, 3, 4, 5, 6, 7];
const result1 = arr.chunk(2);
console.log(result1);// [[1, 2], [3, 4], [5, 6], [7]]
const result2 = arr.chunk(3);
console.log(result2);// [[1, 2, 3], [4, 5, 6], [7]]
const result3 = arr.chunk(4);
console.log(result3);// [[1, 2, 3, 4], [5, 6, 7]]
複製代碼
上面的全部例子都是經過擴展原型鏈來實現的。若是你不想修改原型鏈,也能夠經過其它方式來實現這些功能,好比把它們都寫成函數的形式或者寫成一個類。
寫成類的例子,在這一小節的最開始已經講過了。這裏主要講講改寫成函數的方式,其實很簡單,拿remove
來舉例。
原型鏈的實現方式:
Array.prototype.remove = function(index) {
return !!this.splice(index, 0);
};
複製代碼
改寫成函數的方式:
function remove(arr, index) {
return !!arr.splice(index, 0);
};
複製代碼
用法:
remove([1, 2, 3, 4], 1);
複製代碼
只須要將函數的原參數列表的首部添加一個數組參數,將函數內部的this
改成傳入的參數便可,確實很是簡單,這樣作的好處就是不會改變數組原有的使用習慣。
不乏有不少對代碼有潔癖的人。他們對代碼有着一絲不苟的追求,喜歡更爲優雅的實現方式。而拉姆達表達式(lambda),則是一個讓代碼簡化到極致的手段。
好比使用lambda
對chunk
實現進行簡化。
Array.prototype.remove = index => !!arr.splice(index, 0);
複製代碼
這樣簡單多了。可是好像並非很明顯,由於原來的實現就很短。那麼咱們再來看另外一個例子吧。
使用Math.ceil
對chunk
進行改造。
const chunk = (arr, n) => [...new Array(Math.ceil(arr.length / n))].map((x, i) => arr.slice(i * n, (i + 1) * n ));
複製代碼
如今能看出一些對比了嗎?N行代碼被轉換爲1行。
凡是都有兩面性,雖然看上去簡潔多了,可是要讀懂這行代碼,成本要比原來的寫法高一些。
編程是一門藝術,既然是藝術,就沒有絕對的優劣。由於每一個人對做品的理解不一樣。
其實若是你足夠仔細,就能發現其實全部的擴展API或多或少都有依賴原有API或者其它API。將原來須要兩步或者更多步驟的操做,組合成一個便於操做的API。若是你玩過英雄聯盟,應該知道里面有一個英雄叫作銳雯,她有一個連招叫作光速QA。這個連招能夠在極短的時間內打出爆炸性的傷害,但操做很是繁瑣,須要「鼠標右鍵點擊敵人進行普攻-鍵盤Q鍵釋放技能-鼠標右鍵點擊地板取消Q技能硬直」,而後重複此操做3次。年齡稍大的人,手速跟不上,是很難零失誤打出這種連招的。可是中年人每每更加聰明,他們利用鼠標宏的功能,將連招的操做提早拼接好,錄入鼠標中,當須要使用時,只須要按一下宏按鈕,就能夠輕鬆實現這一套繁瑣的操做。這個道理和擴展API十分類似。依照這個思路,你能夠想象並製做出更多的實用性API。
在靈魂四問中,咱們知道了數組的內存範圍是固定的,但 JavaScript 根本不在意這些。
在語言層面上,既然 JavaScript 無論不顧,那麼在引擎層面上,又是怎麼處理的呢?
在 V8 的內部,數組的元素都被稱爲elements
。爲了優化性能,V8 將數組進行了特殊處理。並且還對類型進行了更爲精確的區分。好比 number 類型的元素,在語言層面,JavaScript 只有一個typeof
的操做符來得知一個變量是否爲數字,而沒法得知是否爲整數型、浮點型或者雙精度類型。在 V8 內部,一個數組內,所有元素都爲整數型的話,那麼這個數組的類型就被標記爲PACKED_SMI_ELEMENTS
。若是隻存在整數型和浮點型的元素類型,那麼這個數組的類型爲PACKED_DOUBLE_ELEMENTS
。除此之外,一個數組包含其它的元素,都被標記爲PACKED_ELEMENTS
。而這些數組類型並不是一成不變,而是在運行時隨時更改的。可是數組的類型只能從特定種類變動爲普通種類。即初始爲PACKED_SMI_ELEMENTS
的數組,只能過渡爲PACKED_DOUBLE_ELEMENTS
或者PACKED_ELEMENTS
。而PACKED_DOUBLE_ELEMENTS
只能過渡爲PACKED_ELEMENTS
。至於初始就是PACKED_ELEMENTS
類型的數組,就沒法再過渡了。
而上述的這三種類型,都屬於密集(壓縮)數組。與之相對應的,是稀疏數組,標記爲HOLEY_ELEMENTS
,稀疏數組一樣具備三種類型。任何一種PACKED
均可以過渡到HOLEY
。
爲何有這兩種區分呢?由於密集數組要比稀疏數組在操做上效率更高。
什麼是密集數組?其實就是一個內存塊連續的堆棧。
什麼是稀疏數組?其實就是一個內存塊散列的鏈表。
那在語言層面上,什麼是稀疏數組?就是存在empty
元素的數組。
let array = [0, 1, 2]; // PACKED_SMI_ELEMENTS
array.push(2.1); // PACKED_DOUBLE_ELEMENTS
array.push("3"); // PACKED_ELEMENTS
array[10] = "10"; // HOLEY_ELEMENTS
複製代碼
數組的類型越靠下,性能就越差。並且數組的類型過渡,只會由上到下,而不會從下到上。一個被標記爲HOLEY_ELEMENTS
的數組,即便把empty
填充後,也不會再標記爲PACKED_ELEMENTS
。
這一小部分的優化其實意義不是很大,只是可以將性能提高到極致而已。
let array = [0, 1, 2];
array[10];
複製代碼
讀取超出數組長度的數據產生的影響就是執行代價昂貴的原型鏈查找。
在 jQuery 某些地方,會存在這種模式的循環代碼。
for (let i = 0, item; (item = items[i]) != null; i++) {
doSomething(item);
}
複製代碼
這段代碼的意思就是讀取數組中全部元素,而後再讀取一個。直到遇到 undefined 或者 null 元素時結束。
替代方案有三種,傳統的for
,可迭代對象的for-of
和forEach
。如今的 V8,for-of
和forEach
的性能與for
至關。
更糟糕的一種狀況是,在該數組的原型鏈中找到了這個值。
let arr = [1, 2, 3];
arr.__proto__ = { "10": "gg" };
console.log(arr[10]); // "gg"
複製代碼
let array = [+0, 1, 2]; // PACKED_SMI_ELEMENTS
array.push(-0); // PACKED_DOUBLE_ELEMENTS
複製代碼
避免使用-0
、NaN
、Infinity
,由於它們任何一個元素進入數組後都會致使數組的類型變爲PACKED_DOUBLE_ELEMENTS
。
雖然在 JavaScript 中不少對象和數組極爲類似,並且咱們能夠本身用對象建立相似數組的對象。這些在上面咱們都講過也演示過了。但終究仍是有區別的。
好比這樣一段代碼:
let arrayLike = {};
arrayLike[0] = "a";
arrayLike[1] = "b";
arrayLike[2] = "c";
arrayLike.length = 3;
Array.prototype.forEach.call(arrayLike, (value, index) => {
console.log(`${index}: ${value}`);
});
複製代碼
雖然運行起來邏輯沒有問題,可是比數組直接調用forEach
要慢得多,由於在 V8 中,forEach
已經高度優化。
提升性能的方案就是,先將相似數組的對象轉化爲數組,在進行數組方法的調用。雖然有一次轉換的成本,可是換來的性能優化是值得的,尤爲是在數組上執行大量操做時。
const actualArray = Array.prototype.slice.call(arrayLike, 0);
actualArray.forEach((value, index) => {
console.log(`${index}: ${value}`);
});
複製代碼
另外一個比較常見的狀況是arguments
。 在之前,要輸出參數,咱們通常都會這麼作。
const logArgs = function() {
Array.prototype.forEach.call(arguments, (value, index) => {
console.log(`${index}: ${value}`);
});
};
logArgs("a", "b", "c");
複製代碼
但在 ES2015 中,咱們能夠藉助rest
參數解決這個問題。
const logArgs = (...args) => {
args.forEach((value, index) => {
console.log(`${index}: ${value}`);
});
};
logArgs("a", "b", "c");
複製代碼
現在咱們徹底沒有任何理由,直接使用arguments
對象。除非你的代碼要運行在 ES5 上面。
HOLEY
在現實世界的編碼中,訪問密集數組和稀疏數組的性能差別很是小,可能小到沒法察覺,甚至沒法測量。可是,保持極致的編碼習慣仍然重要。
let array = new Array(3);
array[0] = 1;
array[1] = 2;
array[2] = 3;
複製代碼
這個構造函數會創造一個HOLEY_SMI_ELEMENTS
類型的數組。按照上面 V8 的介紹,數組的類型過渡只會朝下,而不會朝上。因此一旦被標記爲HOLEY
,就永遠都會是HOLEY
。哪怕是把這些empty
都填充上也無濟於事。
更好的方式是使用字面量來建立數組。
let array = [1, 2, 3];
複製代碼
這可能會在該數組某些操做上起到優化代碼的做用。
做者注:關於 V8 這一部分,並不是千老師原創。算是譯做,原文是從V8 官方博客上面看到的,做者是Mathias Bynens。千老師讀完以爲不錯,就加了一些我的的理解,借鑑到個人博客上來了。有興趣的同窗能夠去看看原文。
這篇文章的內容是簡單了介紹 JavaScript 的 Array。
其實 Array 並不難,它只是組織數據的一種方式。
全部編程語言裏,數據的種類都是同樣的。數字、字符和布爾值。在人類的概念裏,只有這三種。其它的類型都是基於這三種的變種。固然一些其它類型不算此列,如比特,緩衝。這些準確地講不算數據類型。由於它們都沒法和現實世界中的任何東西對應起來,它們屬於計算機的數據類型。而其它組織數據的方式,如數組和對象,都是對這三種數據的組織和封裝。因此它們不屬於基本的數據類型,而應該稱之爲數據結構。
這一點和拳擊或詠春很是像。拳擊的世界裏,人只有兩隻手,出拳的不一樣無非就是方向和角度。各個門派都會給它們取不一樣的名字,但歸根結底拳只能分紅三種,一種是直拳,一種是勾拳,一種是擺拳。而詠春也有三板斧——攤膀伏。這都和三大數據類型不謀而合。身體的移動和不一樣的出拳順序,以及不一樣的速度與力量,就構成了被稱爲」組合拳「或者」招式「一類的東西,這類事物和數組或對象是一個做用。很有道生一,一輩子2、二生3、三生萬物的味道。
武術會衍生。功夫之王、MMA 先驅、20 世紀最偉大的中國人李小龍所創立的截拳道,也是衍生自中國傳統武術,詠春拳,又夾雜了哲學思想、西洋拳、空手道、柔術等多種技藝。
數據也會衍生,數字能夠衍生出int
,float
,double
,long
、bigint
。或者按進制,衍生出諸如int8
、int16
、int32
、int64
一類的東西。
每一種語言都有本身的思想,golang 喜歡簡潔,JavaScript 喜歡複雜,C 喜歡無限制,Java、C++喜歡面向對象思想。就像太極拳、泰拳、相撲、摔跤、拳擊同樣,都有本身的規則和思想。它們都有本身認爲正確的那一套東西,從而造成一套系統。好比中國有些傳統武術認爲」起腿三分輸。「,而跆拳道和泰拳則持有徹底相反的意見。針對它們本身認爲的缺陷、弊端、優點,作出一些本身的東西,這就是衍生出來的產物。
每一個派系的程序員都有着本身的一套語言體系,作 Java 的會關心float
、double
、int
、long
。作 golang 的會關心uint
、int
、float
和位數。作 JavaScript 的程序員只會關心number
。但幾乎每個程序員都知道一個叫作 JSON 的東西。
你們思考一個問題,JSON 爲何是業界通用的東西?
有兩個緣由。
一是由於它足夠簡單,同時又知足全部需求。JSON 一共只有數字、字符串、布爾值、對象、數組和 null 六種數據類型,但它能知足全部語言。不管是寫配置文件仍是做爲數據交互格式。
二是由於它的全稱叫作 JavaScript Object Notation。早期 Ajax 時代,數據交互不少都是使用 xml 的。但 xml 的寫法過於繁瑣,並且解析起來性能也很差。JSON 出現,直接擊潰 xml,成爲標準。Web2.0 至今,幾乎 90%以上的網站都是使用 JSON。等到後來移動端時代來臨,沒有發明出更好的數據交互類型,而是直接採用了 JSON。這又是爲何呢?一是歷史緣由,二是不必。技術發展得快是由於老技術有痛點。而 JSON 並無,固然也多是痛點不明顯,你們都默認接受了。哪怕是如今比較熱門的新 API 交互語言 Graphql,仍然是使用 JSON 做爲基礎數據格式。
因此,但願你們不要被某種語言的類型系統限制住本身。每一個語言的思想多少都有差別,特色也不一樣,這些都是對的。這都是風格,而不是錯誤。每一個人都有本身喜歡的風格。這和性格、年齡、心態、經歷都有關係。有些喜歡泰拳的人認爲太極拳都是花架子,不具有實戰能力,有些喜歡太極拳的人認爲泰拳太粗魯,傷身體。對錯難分難解,視角不一樣、思想不一樣、初衷不一樣。在程序員世界裏,有些 Java 程序員認爲 JavaScript 由於太過靈活而顯得太亂,一樣會有些 JavaScript 認爲 Java 由於太過於模板化而顯得太煩。這些都無可避免。一樣有些人在喜歡 JavaScript 的同時也喜歡 Java,因此有了 TypeScript。
最後,告誡你們一句:你練習了多少東西,你就能獲得多少東西。