五四青年节,今天要学习。汇总5道难度不高但可能遇到的JS手写编程题

《五四青年节,今天要学习。汇总5道难度不高但可能遇到的JS手写编程题》

壹 引

时间一晃,今天已是五一假期最后一天了,没有出门,没有太多惊喜与意外。今天五四青年节,脑子里突然想起鲁迅先生以及悲欢并不相通的话,我的五一经历了什么呢,忍不住想说那大概是,父母教育孩子大声嚷嚷,隔壁装修电钻嗡嗡作响,戴上耳机敲着键盘书写每个白天晚上。

《五四青年节,今天要学习。汇总5道难度不高但可能遇到的JS手写编程题》

矫情完,那么回归本文正题,我在之前其实已经更新了大部分JS常考手写题,比如手写bind new apply call promise,节流防抖等等,这些知识中有些较为复杂,所以都用了单独的篇幅去介绍,那自然还剩一些好理解一点的手写题,考虑到一个一篇文章浪费篇幅,也没必要,这里就统一整理,我们直接开始。

贰 实现trim方法

String.prototype.trim()方法能移除字符串前后的所有空格,这在于我们做表单验证时非常实用,那么现在要求手动实现trim方法应该怎么做?

我们知道,String上有一个replace,用于将指定规则的字符串替换成我们想要的字符,因为空格也是字符,所以本质上只用将空格替换成空即可,但需要注意的是,像 hello echo 这种我们其实只需要将前后的空格去掉,而不能去除两个单词之间的空格。

现在的问题就是怎么匹配前后的空格的问题了,这里就需要借用正则表达式了,先上实现:

const trim = (s) => s.replace(/^\s+|\s+$/g, '');
console.log(trim('   hello echo    ')) // 'hello echo'
console.log(trim('echo    ')) // 'echo'

因为replace接受一个正则用于表示目标字符的规则,所以假设大家有疑问,肯定就是疑问这段正则是什么意思,这里我给大家解释下:

  • \s表示匹配空格,而+表示量词,等价于{1,}(至少一次或者多个),因此\s+表示一个或者多个空格。
  • 管道符|,你可以理解成JS中的||,表示满足前后条件其一均可
  • 脱字符^和美元符$在这里表示匹配开头和匹配尾部,毕竟我们也说了不要匹配字符串中间的空格
  • 修饰符g表示全局匹配,毕竟一个字符串可能很长,我就是希望全局替换。

因此这段正则意思就是,全局匹配,匹配目标是字符串开头或结尾的1个或者多个空格,用图来表示:

《五四青年节,今天要学习。汇总5道难度不高但可能遇到的JS手写编程题》

另外,若大家对于^$有疑惑,可以读一读博主JS 正则表达式$详解,脱字符与美元符$同时写表示什么意思?一文。若对于正则感兴趣,可以阅读博主从零开始学正则系列文章,学好正则在某些时刻真的非常非常有帮助。

另外,除了replace之外,其实还有个replaceAll方法,从一个最基本的例子来区分两者的区别:

' ec ho '.replace(' ', '');//'ec ho '  只替换了开头第一个空格
' ec ho '.replaceAll(' ', '');//'echo'  开头结尾中间所有空格全没了,自带全局匹配

叁 实现字符串翻转(实现reverse)

本题其实是leetcode 344. 反转字符串,题目要求给定:

输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

我:return s.reverse()

面试官:滚~

言归正传,本题可以理解为如何实现一个reverse方法,首先可以想到的就是创建一个空数组,倒序遍历塞到新数组,然后返回:

const reverse = (arr) => {
  const ans = [];
  while (arr.length) {
    ans.push(arr.pop())
  };
  return ans;
}
console.log(reverse(["h", "e", "l", "l", "o"])) // ['o', 'l', 'l', 'e', 'h']

当然本题其实有个限制,题目其实是希望你直接修改原数组,不要新建额外的数组去返回,也不卖关子,其实本题的原意是考核双指针的用法,我们来看两个简单的例子:

比如1,2,3要变成3,2,1,是不是只用交换1,3的位置即可:

《五四青年节,今天要学习。汇总5道难度不高但可能遇到的JS手写编程题》

而像1,2,3,4变成4,3,2,1,我们其实只用做1,42,3互换即可:

《五四青年节,今天要学习。汇总5道难度不高但可能遇到的JS手写编程题》

所以我们完全可以定义两个指针,一个从索引0开始,一个从length-1开始,两两互换,然后一个递增一个递减:

const arr = ["h", "e", "l", "l", "o"];
var reverseString = function (s) {
  var i = 0,
    j = s.length - 1;
  for (; i < j; i++, j--) {
    // 解构赋值
    [s[i], s[j]] = [s[j], s[i]];
  };
};
reverseString(arr)
console.log(arr); // ['o', 'l', 'l', 'e', 'h']

肆 数组去重

老生常谈的问题,这里不做太多赘述,ES5我们可以通过filter + indexOf,实现如下:

const arr = [1, 2, 3, 3, 4, 4];
const unique = (arr) => arr.filter((ele, index) => index === arr.indexOf(ele));
console.log(unique(arr)) // [1,2,3,4];

我们知道filter前两个参数,第一个表示当前元素,第二个是当前元素的索引,而indexOf只能找到一个元素第一次出现的索引,所以我们通过index === arr.indexOf(ele))来找到第一次出现的每个元素。

打个比方,以上面的arr的例子,第一个3出现时index2,而arr.indexOf(3)也是2,符合条件所以把这个3加入到了新数组。

遍历到第二个3index3,而indexOf找出的还是2,不相等,这个3就不要了。

ES6的Set实现就更简单了:

const unique = (arr) => [...new Set(arr)]

这里就不多解释了。

伍 实现flat方法

我们知道数组flat方法可用于数组降维,比如:

[[1],[2]].flat(); // [1,2]

同时flat接受一个参数,表示要降维几次,默认是1,像[[[1]]]很明显需要降维2次,因此可以传递2达到效果:

[[[1]]].flat(2); // [1]

那假设我也不清楚这个数组是几维,但是我就是要打瓶怎么办?这里可以传递Infinity,表示无限大,多少维度都给你降了:

[[[[[[[[[[1]]]]]]]]]].flat(Infinity); //[1]

那么我们自己怎么实现false呢?考虑到数组的元素也可能是数组,这里我们就可以考虑递归,只要当前元素是数组,开始递归,直到访问到真正的元素后,再塞入到新数组,递归完成一层层往上返回即可。

我知道,对于递归陌生的同学可能脑子里知道是怎么回事,但就是写不出来,这里我们再回忆下递归的准则:

  • 我们需要定义一个函数,它会自己调用自己
  • 只用想好当前需要做什么,递归会帮你做相同的事情
  • 想好你递归的条件,什么时候跳出,毕竟你不可能一直递归下去
  • 需要做返回值吗?不返回能不能解决

我们假定有个数组[1,2,3],需要你拷贝一份,可以怎么做?不假思索,新建数组,遍历后一一塞进去:

const traverse = (arr) => {
  const arr_ = [];
  arr.forEach((ele) => {
    arr_.push(ele)
  })
  return arr_;
}

结合上面的例子比递归的特性,我们提炼下信息,注意,你永远只需要考虑当前需要做什么,递归的操作它自然会帮你做相同的操作,我们需要做什么?

递归要做什么操作?很显然是遍历数组,如果当前元素不是数组,直接push,如果是数组,递归一次,到这就不用继续在脑子里递归了,递归自然会帮你做好。

递归跳出条件是什么?上面也说了,只有是数组时才会递归,不需要考虑跳出。

需要考虑返回吗?需要,因为我们每次都新建了一个数组,用于装当前普通元素,如果递归了需要返回给上一层。

直接写代码:

const flat = (arr) => {
  const arr_ = [];
  arr.forEach((ele) => {
    // 是数组吗?
    Array.isArray(ele) ?
      arr_.push.apply(arr_, flat(ele)) :// 是数组就递归,不断降维
      arr_.push(ele);
  })
  return arr_;
}

或者:

const flat = (arr) => {
  const arr_ = [];
  arr.forEach((ele) => {
    arr_.push.apply(arr_,
      Array.isArray(ele) ?
      flat(ele) : [ele]
    )
  })
  return arr_;
}

为啥要用apply呢,因为flat递归后返回一个数组,push不能直接压入数组啊,我是希望压入一个个的元素,而apply参数正好是个数组,所以才用了这种写法。

陆 实现千位分隔符

千位分隔符在银行账号上非常常见,比如12345678分隔后就是12,345,678,相当于从后往前数,每三位加一个逗号。怎么做呢?一种做法是将数字转为字符串,之后分割成数组,然后倒序遍历,每三个塞一个逗号即可,思路比较简单,直接上代码:

const fn = (num) => {
  const s = (num + '').split('');
  const ans = [];
  let len = s.length - 1;
  // 用于统计遍历次数,每三次塞一个逗号
  let n = 0;
  while (len >= 0) {
    ans.unshift(s[len]);
    n += 1;
    // 考虑123456789的情况
    if (n % 3 === 0 && len !== 0) {
      ans.unshift(',');
    };
    len--;
  };
  return ans.join('');
};

console.log(fn(12345678)); // 12,345,678
console.log(fn(123456789)); // 123,456,789

需要注意塞逗号这里的len !== 0,这是因为不加这个判断123456789会变成,123,456,789,这里就不多说了。

第二种做法当然还是我们的正则,同理,我们从后往前数,每三个位置塞一个逗号,同时过滤开始的位置:

const fn = (num) => (num + '').replace(/(?!^)(?=(\d{3})+$)/g, ',');

关于正则如何实现千位运算符,我在从零开始学正则(三),理解正则的分组与反向引用文章开头有讲,如果摊开讲,又涉及到正则位置概念的普及,反向负向先行断言等,比较复杂,所以我还是建议大家自行学习,有问题可以留言问我,我再做解释。

对了,我突然想到,千位运算符得考虑小数点的情况,这里我说说正则怎么做,毕竟数组的实现本质上还是截取小数点前面的做相同的操作最后再拼接而已,对于正则,我们还是一样先匹配小数点前面的部分,在对这一步做我们上面的千位运算替换即可,实现为:

// 先匹配小数点前面的数字部分,再执行千位运算符替换,两个replace搞定
const fn = (num) => (num + '').replace(/\d+/, (n) => n.replace(/(?!^)(?=(\d{3})+$)/g, ','));

console.log(fn(12345678.123)); // 12,345,678.123
console.log(fn(123456789.123)); // 123,456,789.123

柒 总

那么到这里,五道简单的手写编程题搞定,今天是五四,因为想起了鲁迅先生,所以借用他的话做文章的结尾。

愿中国青年都摆脱冷⽓,只是向上⾛,不必听⾃暴⾃弃者流的话。能做事的做事,能发声的发声。有⼀分热,发⼀分光,就令萤⽕⼀般,也可以在⿊暗⾥发⼀点光,不必等候炬⽕。—-鲁迅

那么到这里,本文结束。

点赞