做爲 Javascript 的標準對象之一,數組是很是底層並且實用的數據結構。雖然結構很簡單,可是用好卻不簡單,包括我一開始學習 JS 的時候看到一堆原生方法也是很矇蔽,怎麼能有這麼多方法。並且數組的各類方法各有其特色和使用場景,若是你還停留在 for 循環一把梭的階段,也就是數組元素拼接,遍歷等操做都是用 for 循環來完成的階段,那麼這篇文章很是適合你,或者你也能夠推給你的坑逼同事︿( ̄︶ ̄)︿。javascript
一個原則:能用字面量構造的類型儘可能用字面量構造。例如對象,數組,字符串等一票基本類型,[1, 2, 3]
比起 new Array(1, 2, 3)
,可讀性和精簡程度都好。數組的每一個逗號後面都加個空格,想要成爲優秀的程序員必須注重細節。java
所謂擴展運算符就是三個點那個操做符:...
,當咱們構造一個新數組須要其它數組中的元素的時候,可使用擴展運算符。node
// 一個無反作用的 push
Array.prototype.purePush = function(...elements) {
return [...this, ...elements];
};
console.log([1, 3, 1, 4].purePush(5, 2, 0)); // => [ 1, 3, 1, 4, 5, 2, 0 ]
複製代碼
arguments, nodelist 等類數組轉換成數組的方式有不少種,我第一次看到下面這種 ES5 類數組轉數組的方式也是很懵逼。python
function testArrayLikeToArray() {
var args;
console.log(arguments); // => { [Iterator] 0: 'a', 1: 'b', 2: 'c', [Symbol(Symbol.iterator)]: [λ: values] }
// 能夠經過下標和訪問,還能夠訪問 length
console.log(arguments[0]); // => a
console.log(arguments.length);
// 返回 false 說明不是數組
console.log(Array.isArray(arguments)); // => false
args = Array.prototype.slice.call(arguments);
console.log(args); // => [ 'a', 'b', 'c' ]
console.log(Array.isArray(args)); // => true
}
testArrayLikeToArray('a', 'b', 'c');
複製代碼
主要是這行代碼:程序員
args = Array.prototype.slice.call(arguments);
複製代碼
Array.prototype
也能夠直接用數組實例如空數組字面量 []
來代替,只要能獲取到數組原型上的 slice 就能夠。上面的代碼將 slice 函數的 this 指向 arguments 爲何就能夠返回類數組對應的數組呢?web
我沒有研究過 slice 的具體實現,猜想是下面這樣的:面試
Array.prototype.mySlice = function(start=0, end) {
const array = this;
const end = end === undefined ? array.length : end;
const resultArray = [];
if (array.length === 0) return resultArray;
for (let index = start; index < end; index++) {
resultArray.push(array[index]);
}
return resultArray;
}
複製代碼
我想 slice 內部實現可能就是會像我上面的代碼同樣只須要一個 length 屬性,遍歷元素返回新數組,因此調用 slice 時將其 this 指向類數組能正常工做。redux
args = Array.from(arguments);
複製代碼
args = [...args];
複製代碼
Array.from 和擴展運算符的做用對象均可以是可迭代對象和類數組,因此類數組也能夠正常被轉換。建議使用後面兩種 ES6 的方式,咱們學習新知識就是拿來用的,若是不拿來用那就沒意義了,不用咱們也不容易記住。數組
在平時的開發中咱們常常須要對對象的 keys,values,entries 操做,對應到 Object 的方法就是 Object.keys, Object.values, Object.entries。這 3 個 API 返回的都是數組而不是像 Map 返回 Iterator,我以爲緣由是由於是由於對象通常鍵值對都是有限的,比較少,直接返回數組並不會佔用多少內存,而 Map 不同,map 通常是服務於大量鍵值對的,若是直接返回數組那樣太浪費內存了,採用迭代器更合適,由於迭代器並不會產生數組,它遍歷的是原可迭代對象。這裏直接看幾個案例就行了:數據結構
// 判斷對象是否爲空
const isEmpty = (obj) => obj.keys().length !== 0;
複製代碼
// 字符串轉數組
console.log(Object.values('abc')); // => [ 'a', 'b', 'c' ]
複製代碼
有人可能還在用下面這種方式遍歷對象:
const me = {
name: 'ly',
age: 21,
};
me.__proto__.sex = 'man';
for (const key in me) {
me.hasOwnProperty(key) && console.log(`${key}: ${me[key]}`);
}
// =>
// name: ly
// age: 21
複製代碼
那就太 low 了,建議使用下面這種方式:
const me = {
name: 'ly',
age: 21,
};
me.__proto__.sex = 'man';
// 命令式風格
for (const [key, value] of Object.entries(me)) {
console.log(`${key}: ${value}`);
}
// 函數式風格
// Object.entries(me).forEach(([key, value]) => console.log(`${key}: ${value}`));
// =>
// name: ly
// age: 21
複製代碼
上面這種方式利用了 Object.entries 讓咱們能夠獲取鍵值對數組,在結合 for of 循環和數組解構讓你直接在循環中訪問 key 和 value。
set 能夠看作 key 和 value 相同的 Map,Map 轉換成數組仍是利用 map.keys(), map.values(), map.entries(),前面也說過,Map 這三接口返回的是 iterator,而不是數組,而 Array.from 和 擴展運算能夠又能夠將 iterator 和類數組轉換成數組,so,通常 Map 轉換成數組就像下面這樣:
const LANGUAGE_CODE_MAPPER = Object.freeze(
new Map([['en', 'English'], ['zh', 'Chinese'], ['ja', 'Japanese'], ['fr', 'French']])
);
// React 渲染 options,固然這裏擴展運算符也能夠用 Array.from 代替
// 先獲取 iterator,再將 iterator 轉換成數組
const options = [...LANGUAGE_CODE_MAPPER.entries()].map(([code, language]) => <Option value={{key}}>{{language}}</Option>)
複製代碼
方法有不少,上面已經講過能夠用 Object.values(),因爲字符串也是 iterator,因此也可使用 Array.from 和擴展運算符來轉換。
console.log(Array.from('abcd'));
console.log([...'abcd']);
console.log('abcd'.split(''));
console.log([].slice.call('abcd'));
// 使用 Reflect.apply 改變 slice 函數指向
console.log(Reflect.apply([].slice, 'abcd', []));
// =>
// [ 'a', 'b', 'c', 'd' ]
// [ 'a', 'b', 'c', 'd' ]
// [ 'a', 'b', 'c', 'd' ]
// [ 'a', 'b', 'c', 'd' ]
// [ 'a', 'b', 'c', 'd' ]
複製代碼
雖然寫了不少,可是綜合考慮就用 split 好了,兼容器,可讀性,簡潔性都不差。這裏寫了這麼多種方式只是爲了擴展下思路而已,事實上方式有不少,做爲一個優秀的程序員,應該學會總結,並將總結的最實踐應用於平時的編碼之中。
可使用 array.fill:
const arr = Array(6).fill(6);
console.log(arr); // => [ 6, 6, 6, 6, 6, 6 ]
複製代碼
Array.from 也是個高階函數,能夠接受一個函數來處理每一個元素。
// length 設置爲你須要獲取的整數的個數,index + 1 這個 1 能夠替換爲你要設置的第一個數的大小
const continuousIntegers = Array.from({ length: 10 }, (__, index) => index + 1);
console.log(continuousIntegers); // => [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
複製代碼
console.log(Array.from({ length: 27 }, (__, index) => String.fromCodePoint('a'.codePointAt(0) + index)));
// =>
// ['a'
// ...
// 'z']
複製代碼
new Array(6) 結果等於 Array(6),可是 Array.of(6) 是返回把 6 做爲第一個元素的數組。
控制檯測試一下,一目瞭然,Array.from({ length: 4 }) 返回的是包含4個值爲 undefined 的元素的數組,而 Array(4) 返回的是包含4個空元素的數組。
說到這你可能不明白空元素和undefined 有什麼區別,空元素是沒法被 forEach, map 等函數遍歷到到的,因此我在構造可遍歷的數組時是使用 Array.from(length: n) 而不是 Array(n),例如上面的構造連續整數和字母的示例,你換成 Array(length) 就不行。
數組方法有不少,要想把知識點記得牢,除了多用多思考,學會分類也是很重要的。
刪除
,插入
元素的方法,當你要刪除元素或者在哪一個位置插入一個元素,找它準沒錯這部份內容是寫給初學者的,讀者選擇性閱讀。
上面列了這麼多,按照用途分類我都列了 14 類,要是像 JSON 那樣就 stringify 和 parse 兩個經常使用的方法那該多爽。我剛開始學習 js 數組的時候也很鬱悶,怎麼這麼多的方法。其實不要緊,方法可能是好事,方法意味着方便,合適的場景選擇合適的方法能夠大大減小咱們的代碼量,可讀性也會提升很多。
回到標題,這麼多方法該如何選擇呢?首先,我以爲做爲一個初學者,你好歹把這些方法挨個看一邊,知道它們是幹嗎用的,它們的用法是怎麼樣的。我每次學習一門新語言的時候,都會花一到兩週的時間去學習基礎語法,基礎必須紮實,你絕大數狀況寫的代碼都是基礎語法,其實我也以爲 js 語法很簡單,我當初作筆記就有點煩,以爲寫那些之前 java, python 就搞過的 map,slice 沒啥意思,寫個錘子,可是我仍是認真的每一個 API 都本身敲一遍,有些坑你本身不敲一篇你永遠不會知道,你不敲一下 typeof null 你還真想不到這筆返回 'object'。
過一遍以後固然是忘得差很少了,不要緊,有個印象就行,好像那個 reverse 函數是用來翻轉數組的,可是究竟是返回新數組仍是直接改變原數組,這個忘了,有關係嗎?不要緊的其實,記住翻轉就夠了,至於會不會返回新數組,臨時測試一下就行了。
而後就是多敲代碼,多用。前面也說了,學習新語法你以爲有用的就必定要用上,好比你之前不知道能夠用 forEach 這個高階函數來遍歷數組,你學了以後,以爲這個不錯哦,那你就用上唄,下次敲代碼,碰到一個場景你要遍歷一個數組,就用 forEach,就不要去用 for 循環了。固然了可能有些狀況下 forEach 並不適用好比你要提早 break,誒,你看,你這不就發現 forEach 的一個坑,不能提早退出,面試也常常會問 forEach 遍歷和 for 循環有啥區別啊,這不就是一個區別嘍,for 循環可使用 break 提早退出。重要的是說三遍:
學了新知識,必定要用,
學了新知識,必定要用,
學了新知識,必定要用。
我每次在一篇文章中 get 到新技能以後,好比以前看人家用正則配合 map 處理多條件分支,我在項目中碰到多分支的狀況就會考慮用上。若是學了用不上咋辦,忘了它,都用不上學它幹嗎,好比學 Java web 如今還學 jsp,學 js 你去了解 with 語句有啥用啊?包括以前在知乎看到有人發了個貼說面試官問它原型鏈,其實我以爲問原型鏈除非人家簡歷寫着熟悉或精通原生 js,你問人家原型鏈,那無可厚非,可是就由於人家原型鏈沒搞明白就否定人家,認爲人家技術不行,我以爲這是不該該的,你本身平時寫業務代碼須要用上原型鏈的機會多很少沒B數嗎?
其實上面說的還不是最關鍵的,最關鍵的是學會思考,學會總結,思考多了,總結多了,天然就能如魚得水,從容應對。你在用那些方法的時候有沒有像我在分類中描述的那樣總結出那些方法各自的用途?若是你要在刪除某個索引位置的元素或者在某個索引位置插入元素那確定是用 splice 了,有沒有總結過 forEach,map,filter,reduce, some, every 其實均可以用來遍歷?
因此總的來講學習一個新知識比較好的方式是先腳踏實地的把基礎弄懂,而後就是多用,用的過程當中多思考,多中結。
雖然 Javascript 這門語言沒有提供標準的數據結構類,可是其實處處都有數據結構的影子,對象其實自己就是 Map,對象還能夠看作 Tree,隊列和棧其實均可以用數組來實現,它們的區別只是行爲上不同而已。push,pop 能夠用來實現棧:
const stack = [];
stack.push('a');
stack.push('b');
// 符合棧後進先出的特色
console.log(stack.pop()); // => b
console.log(stack.pop()); // => a
複製代碼
實現隊列其實也很簡單,使用 push 和 shift 便可:
const queue = [];
queue.push('a');
queue.push('b');
// 符合隊列先進先出的特色
console.log(queue.shift()); // => a
console.log(queue.shift()); // => b
複製代碼
const testArray = ['c', 'd', 'd'];
// 刪除操做,第一個參數表示刪除的起始下標, 第二個參數表示刪除的個數
testArray.splice(1, 1);
// 第三個參數及其日後表示插入的元素,能夠結合結構賦值使用
const ab = ['a', 'b'];
testArray.splice(0, 0, ...ab);
console.log(testArray); // => [ 'a', 'b', 'c', 'd' ]
複製代碼
其實 Javascript 這門語言自己能夠說早期是設計的很是糟糕了,你沒有單獨的 remove 和 insert 方法就算了,惟一一個能夠用來插入刪除的 API 還設計的這麼反直覺。正常人的思惟確定是但願返回一個新數組,而不是修改原數組,假設設計者是基於性能考慮纔不返回一個修改後的新數組,那麼爲什麼又要返回一個保存了刪除了元素的數組。我若是須要刪除的那幾個元素我直接 slice 很差嗎?返回一個數組很容易讓人誤解這是返回修改後新數組。
const arr = [1, 2, 3];
console.log(arr.reverse()); // => [ 3, 2, 1 ]
console.log(arr); // => [ 3, 2, 1 ]
// 竟然返回原數組!!!
console.log(arr.reverse() === arr); // => true
複製代碼
reverse 和 splice 同樣都被設計成有反作用的不純函數。問題是爲嘛要返回一個新數組,容易讓人誤解返回的是新數組,我以爲壓根就不用返回新數組,你返回 undefined 那就等於告訴使用者這裏是修改原數組,多好啊。
有些人可能不知道 slice 實際上是能夠接受負數的:
const arr = ['a', 'b', 'c', 'd'];
console.log(arr.slice(-3)); // => [ 'b', 'c', 'd' ]
console.log(arr.slice(-3, -1)); // => [ 'b', 'c' ]
複製代碼
查找元素的方法有不少,indexOf/lastIndexOf/includes/find/findIndex。常常看到有人寫相似下面這樣的代碼:
const arr = ['a', 'b', 'c', 'd'];
const isExists = arr.indexOf('k') != -1;
if (isExists) {
console.log('數組中存在 k');
}
複製代碼
若是你要判斷數組中是否包含某個元素,明顯 ES7 引入的 includes 函數更合適,可讀性好多了。
const handleSelect = newValue => {
if (this.state.includes(newValue)) {
message.error(`您已經選擇過${newValue},不能重複選擇!`);
}
};
複製代碼
lastIndexOf 在一些須要反向查找的狀況下仍是頗有用的。其實 includes 叫查找元素還不太準確,find 纔是真正的查找元素,find 返回的是元素自己,因此當你須要在數組中查找元素而且返回這個元素的時候你能夠考慮使用 find。有 indexOf 不夠嗎爲何還有個 findIndex 呢?確實不夠,有些場景下,使用函數式的高階函數能夠大大提升接口的靈活性。
const students = [{ Chinese: 120, English: 138 }, { Chinese: 140, English: 110 }, { Chinese: 108, English: 120 }];
const chineseHighest = students.find(stu => stu.Chinese === 140);
console.log(chineseHighest); // => { Chinese: 140, English: 110 }
複製代碼
上面這個查找語文分數爲 140 的學生用 indexOf 是不可能作的,使用高階函數,讓使用者傳遞一個函數去完成接口中的關鍵步驟這樣的方式來擴展接口靈活性在 Javascript 處處可見,像 findIndex,sort 都是這樣,因此我以前在面美團的時候被問到說函數式有什麼用啊,這不就是嗎?提升接口的靈活性。
至於啥時候用 findIndex, 那確定是當你須要被查找元素的下標而不是元素自己嘍。
數組的 join 方法其實挺經常使用的,來舉幾個案例:
console.log(`${Array(35).fill('*').join('')} 我是美麗的分割線 ${Array(35).fill('*').join('')}`);
// => *********************************** 我是美麗的分割線 ***********************************
複製代碼
let url = 'http://xxx.com/xxx';
const params = {
name: 'lyreal666',
age: 21,
};
const query = [];
Object.keys(params).forEach(key => {
query.push(`${key}=${params[key]}`);
});
url += `?${query.join('&')}`;
console.log(url);
複製代碼
個人同事寫過相似上面的代碼,其實有不少能夠吐槽的地方。上面的代碼目的是爲了獲得一個 queryString,其實最開始的數據時個對象,爲何最後就能將對象的內容拼接成字符串呢?這其實你要這樣想:不要把對象只是當成一個對象,對象是能夠轉換成數組的,因此對象也能夠其實也能夠充分利用數組的那些接口,包括這裏的 join。
其實我平時轉換 queryString 我是使用標準對象 URLSearchParams
:
new URLSearchParams({ name: 'ly', age: 21 }).toString(); // => "name=ly&age=21"
複製代碼
一行代碼解決,不使用標準對象也能夠優雅的實現,其實能夠 join 都不用,後面會提到。
sort 這裏補充一個坑:js 數組的 sort 默認是按照元素的字符串碼值排序。當須要對數字數字排序須要自定義比較函數。
const arr = [111, 11, 8, 2, 7, 4, 3];
console.log(arr.sort()); // => [ 11, 2, 3, 4, 7, 8 ]
console.log(arr.sort((x, y) => x - y)); // => [ 2, 3, 4, 7, 8, 11 ]
複製代碼
flat 是 ES2019 的語法,用於數組的扁平化,可使用 reduce 和 concat 偏平到最裏層的 flat。思路是這樣的:
你要獲得一個最終的結果 result 數組,你必須遍歷每個元素將其合併到結果數組,又考慮到遍歷到的元素可能自己也是個數組,那就將其先扁平化再合併,也就是使用遞歸,扁平化數組元素後返回的是一個數組。哪一個 API 能夠將一個數組並將其合併到 result 數組呢?天然是 concat。那遍歷收集每一個數組元素獲得一個和初始值類型相同的狀況選擇那個接口呢,固然是 reduce,一步到位就是下面的代碼:
const flat = array => array.reduce((pre, current) => pre.concat(Array.isArray(current) ? flat(current) : current), []);
const testArray = [1, [2, [3, 4], 5, [6, 7], 8], 9];
console.log(flat(testArray)); // => [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
複製代碼
reduceRight 和 reduce 只是遍歷的方向不一樣,因此後面不會單獨講 reduceRight。
關於上面第二點,我舉個栗子:
const sleep = seconds =>
new Promise(resolve => {
setTimeout(() => resolve(), seconds * 1000);
});
const crawl = async url => {
console.log(`start crawl url: ${url}...`);
await sleep(3);
console.log(`crawl url: ${url} end...`);
};
const urls = ['http://a.com', 'http://b.com', 'http://c.com', 'http://d.com', 'http://e.com'];
// forEach
console.time('Test forEach');
urls.forEach(crawl);
console.timeEnd('Test forEach');
// for loop
(async function testForLoop() {
console.time('Test for loop');
for (const url of urls) await crawl(url);
console.timeEnd('Test for loop');
})();
// =>
// start crawl url: http://a.com...
// start crawl url: http://b.com...
// start crawl url: http://c.com...
// start crawl url: http://d.com...
// start crawl url: http://e.com...
// Test forEach: 2.837ms
// start crawl url: http://a.com...
// crawl url: http://a.com end...
// crawl url: http://b.com end...
// crawl url: http://c.com end...
// crawl url: http://d.com end...
// crawl url: http://e.com end...
// crawl url: http://a.com end...
// start crawl url: http://b.com...
// crawl url: http://b.com end...
// start crawl url: http://c.com...
// crawl url: http://c.com end...
// start crawl url: http://d.com...
// crawl url: http://d.com end...
// start crawl url: http://e.com...
// crawl url: http://e.com end...
// Test for loop: 15012.782ms
複製代碼
均可以用來遍歷,區別在於返回值和用途不一樣。
filter/map 返回的都是新數組,並不會修改原數組,除非你在遍歷過程當中修改了原數組的元素,很是不建議在遍歷操做中修改原數組,除非你清楚的知道修改的後果是什麼。
固然是看你需不須要返回值啦,forEach 就是純粹的遍歷,Map 是用來映射原數組的。
來看幾個案例:
仍是上面那個獲取 queryString 的,我用 reduce 來改寫一下,思考過程是這樣的:我最終要獲得一個字符串,這個字符串是遍歷全部的元素獲得的,顯然不能用 map,map 是返回數組, reduce 能夠啊,reduce 是掃描一遍每個元素合成一個值啊。
let url = 'http://xxx.com/xxx';
const params = {
name: 'lyreal666',
age: 21,
};
// 使用 map 來實現
// url += '?' + Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
// 使用 reduce
url += Object.entries(params).reduce(
(pre, [key, value]) => `${pre}${pre === '?' ? '' : '&'}${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
'?'
);
console.log(url); // => http://xxx.com/xxx?name=lyreal666&age=21
複製代碼
連乘或連加
const add = (...operands) => operands.reduce((pre, current) => pre + current, 0);
console.log(add(2, 3, 3)); // => 8
const pow = (x, y = 2) => Array.from({ length: y }).reduce(pre => pre * x, 1);
console.log(pow(3)); // => 9
console.log(pow(2, 3)); // => 8
複製代碼
再來看看 redux 中的 compose
const compose = (...funcs) => {
if (funcs.length === 0) {
return args => args;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
};
const a = arg => {
console.log(`call a, arg: ${arg}`);
};
const b = arg => {
console.log(`call b, arg: ${arg}`);
return 'a';
};
const c = arg => {
console.log(`call c, arg: ${arg}`);
return 'b';
};
const composedFunc = compose(
a
b,
c
);
// 即執行效果爲 arrgs => a(b(c(args)))
composedFunc('c');
// =>
// call c, arg: c
// call b, arg: b
// call a, arg: a
複製代碼
因此 reduce 使用的時候通常就是當你須要經過遍歷一個數組計算出一個值得時候。
這倆和短路或,短路與很像。當你須要判斷數組中的是否有一個或多個元素知足條件時考慮使用 some,只要一個元素知足就會退出遍歷。當你須要判斷數組中的元素是都都知足某個條件時使用 every,只要有一個元素不知足就會退出。
some 和 every實際上是能夠相互轉換的,你想啊,數組中有一個或多個瞞住條件 condition 是否是數組中全部元素都知足 !condition 的結果取反。
看一個🌰:
// 小明的分數
const grades = [80, 59, 80];
// 完蛋,要被老爸打屁屁
const isOver = grades.some((grade) => grade < 60);
// 等同於
// const isOver = !grades.every((grade) => grade >= 60);
複製代碼
在使用這幾個高階函數的時候咱們常常是鏈式調用的,例如:
// level 爲 0 表示管理員
const users = [
{ name: 'ly', level: 2 },
{ name: 'Bob', level: 1 },
{ name: 'Lily', level: 0 },
{ name: 'Tom', level: 3 },
];
const customerNames = users
.filter(user => user.level !== 0)
.map(customer => customer.name)
.join(', ');
console.log(customerNames); // => ly, Bob, Tom
複製代碼
比起 for 循環不但節省代碼,邏輯還更清晰,並且由於處理過程是分步的,出錯的時候咱們只須要關注出錯的那步調用,若是是 for 循環的話代碼每每是糅雜在一塊兒的,分析起來涉及面比較廣。看看 for 循環版本的:
// level 爲 0 表示管理員
const users = [
{ name: 'ly', level: 2 },
{ name: 'Bob', level: 1 },
{ name: 'Lily', level: 0 },
{ name: 'Tom', level: 3 },
];
let str = '';
for (const user of users) {
if (user.level !== 0) {
str += `${str === '' ? '' : ', '}${user.name}`;
}
}
console.log(str); // => ly, Bob, Tom
複製代碼
這篇文章算是我對 Javascript 數組的一些總結和經驗之談,可能後續還會補充一些內容。但願能給讀者一些啓發,尤爲是其中我提到的一些學習方法,但願對一些初學者可以起到一些指導做用。
若是文章內容有什麼錯誤或者不當之處,歡迎在評論區指出。感謝您的閱讀,若是文章對您有所幫助或者啓發,不妨點個贊,關注一下唄。
本文爲原創內容,首發於我的博客,轉載請註明出處。