从 shuffle 看代码品味(面试题)
面试官:小伙子写一个 shuffle?(JavaScript)
shuffle:顾名思义,将数组随机排序,常在开发中用作实现随机功能。
我们来看看一个 shuffle 可以体现出什么代码品味。
错误举例
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/comment-page-1/#comment-1466
思考
下面来到了第一反应:思考问题。
数组随机化 -> 要用到 Math.random -> 看来每个元素都要 random 一下 -> 处理 arr.length 要用到 Math.floor -> 需要用到 swap
第一版
由此有了第一版代码:
function shuffle(arr) {
var i;
var randomIndex;
for (i = arr.length - 1; i > 0; i--) {
randomIndex = Math.floor(Math.random() * i);
swap(arr, i, randomIndex);
}
return arr;
}
为什么用 randomIndex 不用 j? -> 更有意义的变量命名
为什么要把 i 和 randomIndex 的声明放在最前方? -> ES5 里的变量提升(ES6 里有没有变量提升?没有,不仅 const 和 let 都没有,连 class 也没有。但是 import 命令具有提升效果,会提升到整个模块的头部,首先执行)
为什么第 3 行和第 5 行中留一个空行 & 为什么第 8 行和第 10 行之间留一个空行?将声明的变量、函数体、return 分开。三段式结构,一目了然的逻辑,使代码更加清晰易维护
什么,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);
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)燃不燃?好用不好用?
进阶
光说不练假把式,我们来试用一下第三版的 shuffle 把:
// test
shuffle([1, 2, 3, 4, 5]);
const shuffle = arr => {
arr.forEach((element, index) => {
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) => {
const randomIndex = Math.floor(Math.random() * (index + 1));
swap(arr, index, randomIndex);
});
};
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 呢?(此处是硬广)
有时候我们需要一次自我审问:每次调用函数时都要确认函数声明在调用之前的工作是必须的吗?
最终解答
// test
shuffle([1, 2, 3, 4, 5]);
function shuffle(arr) {
arr.forEach((element, index) => {
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 是因为:
我想利用它的变量提升实现主逻辑前置,而不用去关心函数的定义位置。
进而从上到下,层层逻辑递进。再一次出现这两个次:逻辑简洁、好维护。
总结
你问:有没有高水平的代码来让面试官眼前一亮?
我答:只有好读又简洁,稳定易维护的代码,没有高水平的代码一说。
你问:说好的代码品味呢?
我答:都藏在每一个细节的处理上:)
地址
P.S. 我不会告诉你原文地址的显示效果更好喔:)
原文地址:https://www.rayjune.me/2018/03/13/see-code-taste-from-shuffle/
共同学习,写下你的评论
评论加载中...
作者其他优质文章