博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从 shuffle 看代码品味(面试题)
阅读量:7099 次
发布时间:2019-06-28

本文共 5620 字,大约阅读时间需要 18 分钟。

面试官:小伙子写一个 shuffle?(JavaScript)

shuffle:顾名思义,将数组随机排序,常在开发中用作实现随机功能。

我们来看看一个 shuffle 可以体现出什么代码品味

地址

P.S. 我不会告诉你原文地址的显示效果有多么感人

原文地址:
作者:

更多信息,check:

错误举例

function shuffle(arr) {  return arr.sort(function () {    return Math.random() - 0.5;  });}复制代码

ES6

const shuffle = arr =>  arr.sort(() => Math.random() - 0.5);复制代码

测试代码

// testshuffle([1, 2, 3, 4, 5]);复制代码

请老铁千万不要这样写,这体现了两个错误:

  1. 你的这段代码一定是从网上抄/背下来的,面试官不想考这种能力
  2. 很遗憾,这是错误的,并不能真正地随机打乱数组

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 里有没有变量提升?没有,不仅 constlet 都没有,连 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 有两个不同:

  1. 块作用域,只存在于 {} 中,不像 var 只有函数才能锁住它的作用域
  2. var 有变量提升,let 没有

上代码:

for (var i = 0; i < 10; i++){  setTimeout(() => {    console.log(i)  }, 100)}  // 输出全为 10for (let i = 0; i < 10; i++){  setTimeout(() => {    console.log(i)  }, 100)}// 输出 0 1 2 3 4 5 6 7 8 9复制代码

Why?

  • 每次循环都创建一个块级作用域
  • 所以每次循环改变的就是对局部变量赋值

进阶

光说不练假把式,我们来试用一下第四版的 shuffle 把:

// testshuffle([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 声明的变量没有变量提升,在调用 shuffleswap 的时候他们还木有出生呢~!

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]];};// testshuffle([1, 2, 3, 4, 5]);复制代码

老铁没毛病。但主要逻辑运行代码放在后,次要逻辑函数定义放在前有没有不妥?

这里只有 shuffleswap 两个函数,或许你会觉得区别不明显,那如果代码更长呢? 没错,或许你可以进行模块拆分,但如果像 underscore 那样的代码呢。如果像博主一样写一个 呢?(不是硬广:-D)

有时候我们需要一次自我审问:每次调用函数时都要确认函数声明在调用之前的工作是必须的吗

最终解答

// testshuffle([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 是因为:

我想利用它的变量提升实现主逻辑前置而不用去关心函数的定义位置

进而从上到下,层层逻辑递进。再一次出现这两个词:逻辑简洁好维护

总结

  • 你问:有没有高水平的代码来让面试官眼前一亮?

  • 我答:只有好读又简洁,稳定易维护的代码,没有高水平的代码一说。

  • 你问:说好的代码品味呢?

  • 我答:都藏在每一个细节的处理上:)

转载于:https://juejin.im/post/5aa941c6f265da23826dafea

你可能感兴趣的文章
spark的HA集群搭建
查看>>
Essential Studio for WPF 2018 v3最新版发布(上)
查看>>
Navicat使用教程:获取MySQL中的高级行数(第2部分)
查看>>
关于GDPR的六大理解
查看>>
cordova安卓全面屏适配
查看>>
总结2012展望2013
查看>>
Oracle入门必读
查看>>
Oracle实例和Oracle数据库
查看>>
MySQL群集,主从复制及双主模式
查看>>
SocketChannel / ServerSocketChannel / Selector
查看>>
grep 及正则表达式总结
查看>>
angularjs-currency 过滤器
查看>>
H3C-1000S 内部服务器映射
查看>>
Linux负载均衡软件LVS+keepalived
查看>>
世界500强某知名日企面试题库
查看>>
MySQL MyISAM 库转换为InnoDB的方法
查看>>
使用Python读Excel数据Insert到MySQL
查看>>
linux chkconfig and umask
查看>>
我的友情链接
查看>>
win2008安装win7主题
查看>>