面試官:小夥子寫一個 shuffle?(JavaScript)
shuffle:顧名思義,將陣列隨機排序,常在開發中用作實現隨機功能。
我們來看看一個 shuffle 可以體現出什麼程式碼品味。
地址
P.S. 我不會告訴你原文地址的顯示效果有多麼感人
原文地址:
rayjune.me/2018/03/13/…
作者:rayjune
更多資訊,check: rayjune.me/about
錯誤舉例
function shuffle(arr) {
return arr.sort(function () {
return Math.random() - 0.5;
});
}
複製程式碼
ES6
const shuffle = arr =>
arr.sort(() => Math.random() - 0.5);
複製程式碼
測試程式碼
// test
shuffle([1, 2, 3, 4, 5]);
複製程式碼
請老鐵千萬不要這樣寫,這體現了兩個錯誤:
- 你的這段程式碼一定是從網上抄/背下來的,面試官不想考這種能力
- 很遺憾,這是錯誤的,並不能真正地隨機打亂陣列
Why? Check: https://blog.oldj.net/2017/01/23/shuffle-an-array-in-javascript/
思考
下面來到了第一反應:思考問題。
陣列隨機化 -> 要用到 Math.random
-> 看來每個元素都要 random 一下 -> 處理 arr.length
的取整要用到 Math.floor
-> 交換陣列元素位置需要用到 swap
一切正常的話你的方向應該轉向經典的 Fisher–Yates shuffle 演算法思路了 :》
第一版
由此有了第一版程式碼:
function shuffle(arr) {
var i;
var randomIndex;
for (i = arr.length - 1; i > 0; i--) {
randomIndex = Math.floor(Math.random() * (i + 1));
swap(arr, i, randomIndex);
}
return arr;
}
複製程式碼
- 為什麼用 randomIndex 不用 j? -> 更有意義的變數命名
- 為什麼要把 i 和 randomIndex 的宣告放在最前方? -> ES5 裡的變數提升(ES6 裡有沒有變數提升?沒有,不僅
const
和let
都沒有,連class
也沒有。但是import
命令具有提升效果,會提升到整個模組的頭部,首先執行) - 為什麼第 3 行和第 5 行中留一個空行 & 為什麼第 8 行和第 10 行之間留一個空行?將宣告的變數、函式體、
return
分開。三段式結構,一目瞭然的邏輯,使程式碼更加清晰易維護
需要注意的是這裡的 randomIndex
處理是 Math.floor(Math.random() * (index + 1))
,index + 1
,看起來好彆扭,可以用 Math.ceil
來替換嗎:
function shuffle(arr) {
var i;
var randomIndex;
for (i = arr.length - 1; i > 0; i--) {
randomIndex = Math.ceil(Math.random() * i);
swap(arr, i, randomIndex);
}
return arr;
}
複製程式碼
多謝評論區的 @旅行者2號 大神提醒,這是不行的。因為:
- Math.random() 產生的隨機數範圍是 [0,1),
Math.ceil
會將 (0, 1) 範圍的數字都化為 1,只有 0 才化為 0。這樣會導致index
為 0 的元素很難被randomIndex
隨機到。
什麼,JavaScript 中木有這麼基礎的 swap
函式?
寫一個,使邏輯更加清晰 & 重複利用:
function swap(arr, indexA, indexB) {
var temp;
temp = arr[indexA];
arr[indexA] = arr[indexB];
arr[indexB] = temp;
}
複製程式碼
第二版
一點點小的改動:
function shuffle(arr) {
arr.forEach(function (curValue, index) {
var randomIndex = Math.floor(Math.random() * (index + 1));
swap(arr, index, randomIndex);
});
return arr;
}
複製程式碼
用 arr.forEach
替代原本的 for
迴圈。(我會告訴你 array.forEach
的返回值是 undefined
這一點容易出錯嘛)
此外不希望有人質疑:JS 由於函式呼叫棧空間有限,用 for
迴圈不是比 forEach
效率更高嗎?
拿出這段話壓壓驚:
”We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.” -- Donald Knuth
JavaScript 天生支援函數語言程式設計(functional programing),放下腦海中的 CPP-OOP,請好好珍惜它。
有了 High-order function & First-class function 的存在,編寫程式碼的邏輯愈發清晰,簡潔好維護。
第三版
且慢,同學不寫一個 ES6 版本的嗎?
const shuffle = (arr) => {
arr.forEach((element, index) => {
const randomIndex = Math.floor(Math.random() * (index + 1);
swap(arr, index, randomIndex);
});
return arr;
};
複製程式碼
使用 ES6 的箭頭函式(arrow function),邏輯的表達更為簡潔、清晰、好維護。(我會告訴你箭頭函式還因為本身繫結的是外部的 this
,解決了一部分 this
繫結的問題嘛。注意我沒有說全部)。
順便也用 ES6 重寫一下 swap
函式把。簡介的語法,更強大的表現力,誰用誰喜歡:
const swap = (arr, indexA, indexB) => {
[arr[indexA], arr[indexB]] = [arr[indexB], arr[indexA]];
};
複製程式碼
怎麼樣,ES6 的物件解構賦值(Destructuring)燃不燃?好用不好用?
第四版
其實程式碼到第三版已然 OK 了,但還是有一處紕漏:
在 arr.forEach((curValue, index) => { ... })
中 index = 0
時,randomIndex
的值也只能為 0,顯然這個時候 swap(arr, 0, 0)
的操作是沒必要的:
const shuffle = (arr) => {
arr.forEach((element, index) => {
if (index !== 0) {
const randomIndex = Math.floor(Math.random() * (index + 1));
swap(arr, index, randomIndex);
}
});
return arr;
};
複製程式碼
那麼我們原先的 for
迴圈語句有木有這種問題呢?ES6 重寫一下:
const shuffle = (arr) => {
for (let i = arr.length - 1; i > 0; i--) {
const randomIndex = Math.floor(Math.random() * (i + 1));
swap(arr, i, randomIndex);
}
return arr;
}
複製程式碼
其中的迴圈終止條件是 i > 0
,自動排除掉 i = 0
的情況了,所以在 for
迴圈中是沒有問題的。
既然在迴圈中用 let
代替了 var
,我們來回顧一下兩者的區別吧:
let
相比較 var
有兩個不同:
- 塊作用域,只存在於
{}
中,不像var
只有函式才能鎖住它的作用域 var
有變數提升,let
沒有
上程式碼:
for (var i = 0; i < 10; i++){
setTimeout(() => {
console.log(i)
}, 100)
}
// 輸出全為 10
for (let i = 0; i < 10; i++){
setTimeout(() => {
console.log(i)
}, 100)
}
// 輸出 0 1 2 3 4 5 6 7 8 9
複製程式碼
Why?
- 每次迴圈都建立一個塊級作用域
- 所以每次迴圈改變的就是對區域性變數賦值
進階
光說不練假把式,我們來試用一下第四版的 shuffle
把:
// test
shuffle([1, 2, 3, 4, 5]);
const shuffle = (arr) => {
arr.forEach((element, index) => {
if (index !== 0) {
const randomIndex = Math.floor(Math.random() * (index + 1));
swap(arr, index, randomIndex);
}
});
return arr;
};
const swap = (arr, indexA, indexB) => {
[arr[indexA], arr[indexB]] = [arr[indexB], arr[indexA]];
};
複製程式碼
出現呼叫錯誤,const
宣告的變數沒有變數提升,在呼叫 shuffle
和 swap
的時候他們還木有出生呢~!
So 這樣?
const shuffle = (arr) => {
arr.forEach((element, index) => {
if (index !== 0) {
const randomIndex = Math.floor(Math.random() * (index + 1));
swap(arr, index, randomIndex);
}
});
return arr;
};
const swap = (arr, indexA, indexB) => {
[arr[indexA], arr[indexB]] = [arr[indexB], arr[indexA]];
};
// test
shuffle([1, 2, 3, 4, 5]);
複製程式碼
老鐵沒毛病。但主要邏輯執行程式碼放在後,次要邏輯函式定義放在前有沒有不妥?
這裡只有 shuffle
和 swap
兩個函式,或許你會覺得區別不明顯,那如果程式碼更長呢?
沒錯,或許你可以進行模組拆分,但如果像 underscore 那樣的程式碼呢。如果像博主一樣寫一個 indexeddb-crud 呢?(不是硬廣:-D)
有時候我們需要一次自我審問:每次呼叫函式時都要確認函式宣告在呼叫之前的工作是必須的嗎?
最終解答
// test
shuffle([1, 2, 3, 4, 5]);
function shuffle(arr) {
arr.forEach((element, index) => {
if (index !== 0) {
const randomIndex = Math.floor(Math.random() * (index + 1));
swap(arr, index, randomIndex);
}
});
return arr;
}
function swap(arr, indexA, indexB) {
[arr[indexA], arr[indexB]] = [arr[indexB], arr[indexA]];
}
複製程式碼
為啥用 ES5 的方式來寫 function,Airbnb 的 ES6 規範建議不是用 const
+ 箭頭函式來替代傳統的 ES5 function
宣告式嗎?
我們來看 const
+ 箭頭式函式宣告帶來了什麼,失去了什麼:
- 帶來了更加規範簡介的函式定義,向外的一層
this
繫結 - 失去了更加自由的邏輯展現(呼叫不能放在宣告之前)
子曰:
- 程式設計規範是人定的,而你是有選擇的
- 軟體開發不是遵循教條,程式碼世界本沒有標準答案
在這裡用傳統 ES5 function
是因為:
我想利用它的變數提升實現主邏輯前置,而不用去關心函式的定義位置。
進而從上到下,層層邏輯遞進。再一次出現這兩個詞:邏輯簡潔、好維護。
總結
-
你問:有沒有高水平的程式碼來讓面試官眼前一亮?
-
我答:只有好讀又簡潔,穩定易維護的程式碼,沒有高水平的程式碼一說。
-
你問:說好的程式碼品味呢?
-
我答:都藏在每一個細節的處理上:)