JS做题易错记录 | 青训营

128 阅读5分钟

本文是学习课程《如何写好JavaScript》以及平时写题写项目中遇到问题的总结。感谢大佬的课程,受益良多,学完整个人都视野开阔了,大佬的代码真的非常好看超级牛,学到真的赚到。

导航

  1. 数组fill()方法易错点
  2. JSON.stringify()易错点
  3. 正则表达式易错点
  4. ACM模式常见输入
  5. 课程范例代码总结:Toggle; 防抖优化; Promise红绿灯

1. 数组fill()方法易错点

Array.prototype.fill(value, start?, end?)

这个问题是在做回溯问题的时候发现的。数组fill()方法用来快速初始化一个数组,但是当我们用来填充引用类型数据的时候,就会出现:改动其中一个数据影响了其他数据。即若fill()的参数为引用类型,则会导致都执行填充同一个引用类型。比如:

let dp = new Array(2).fill(new Array(2).fill(0))
dp[0][1] = 1
dp // [[0, 1],[0, 1]] 全都变啦!一不留神好好的算法就做错了!

// 正确写法
let dp2 = new Array(2).fill(0).map(item => new Array(2).fill(0))
dp2[0][1] = 1
dp2 // [[0, 1],[0, 0]]

在伪数组上调用fill()

伪数组:有length属性

// from MDN doc
const arrayLike = { length: 2 };
console.log(Array.prototype.fill.call(arrayLike, 1));
// { '0': 1, '1': 1, length: 2 }

for in 与 for of

数组有没有填充区别还是蛮大的,在选择题里容易混淆出错。

const a = new Array(4)
for(let item of a){console.log(item)} // 打印 4 个 undefined
for(let item in a){console.log(item)} // 打印 1 个 undefined
Object.keys(a) // []

const b = new Array(4).fill(9)
for(let item of b){console.log(item)} // 4 个 9
for(let item in b){console.log(item)} // 0 1 2 3
Object.keys(b) // ['0', '1', '2', '3']

2. JSON.stringify()易错点

该问题其实就是深拷贝问题容易遇到的错误。具体为:

  1. undefinedFunctionSymbol 会被忽略,单独转化则会变成undefined
  2. NaN, Infinity, null 全变成 null
  3. Map, Set, RegExp, WeakMap,WeakSet,ArrayBuffer类型仅序列化可枚举的属性。
  4. Date会变成字符串
  5. 不支持循环引用对象的拷贝,会报错TypeError

收集到的常考面试手写深拷贝解决方案:

// 递归版本:缺点是没有解决循环引用问题
const deepClone1 = function (obj)  {
    const newObj = {}
    let keys = Object.keys(obj)
    for(let i=0; i<keys.length; i++) {
        const key = keys[i]
        const data = obj[key]
        if (data && typeof data === 'object') {
            newObj[key] = deepClone(data)
        } else {
            newObj[key] = data
        }
    }
    return newObj
}

// 非递归版本
const deepClone2 = function (obj) {
    let copy = Object.create(Object.getPrototypeOf(obj));
    let propNames = Object.getOwnPropertyNames(obj);
    propNames.forEach(function (items) {
        let item = Object.getOwnPropertyDescriptor(obj, items);
        Object.defineProperty(copy, items, item);
})};

3. 正则表达式易错点

正则表达式的全局匹配的 lastIndex 属性,即每执行一次匹配,lastIndex会增加到当前匹配完后的字符的下一个位置。不适合用来找字符串中所有符合条件的子字符串。写算法不能偷懒哦!

'0110'.match(/0(1+)0/g) // ['0110']
'01011010'.match(/0(1+)0/g) // ['010', '010'], 越过了'0110'

4. ACM模式常见输入

大部分厂子用的是ACM mode,不熟悉考场真的会浪费宝贵的机会。希望大家都能准备充分迎接挑战,少依赖本地,学会在线debugger~一起加油

主要的场景有:

  1. 单行输入,最简单,但是需要注意数据范围,有时会越界
  2. 多行输入(第一列为用例组数)每一组用例的开头是数组的元素个数
  3. 多行输入对应多行输出
  4. 多行输入,第一行为测试用例的组数
  5. 多行输入,遇到0 0结束
  6. 多行输入,每行第一个数字代表后面数组的长度,遇到0结束输入
  7. 多行输入,多行输出(输入的行数和每行的个数不定)
// 默认模板
const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;

// solution 写外面比较方便整理思路
const mySolution = (a, b) => { return ans; }
void async function () {
    // 这里就是纯输入问题了,写得少就会很头疼
    while(line = await readline()){
        let tokens = line.split(' ');
        let a = parseInt(tokens[0]); // 这里小心,有些题目是0-9数字组成的长字符串
        let b = parseInt(tokens[1]); // 此时别忘记把 parseInt 删了,直接用 string 别提多省心
        console.log(mySolution(a, b));
    }
}()

// 多行输入(第一列为用例组数)每一组用例的开头是数组的元素个数
/**
 * 2
 * 5 1 2 3 4 5
 * 4 1 2 3 4
 */
void async function () {
    let n = parseInt(await readline())
    while (n--) {
        let lines = await readline().split(' ')
        let n = parseInt(lines[0])
        let sum = 0
        for (let i=1; i < lines.length; i++) {
            sum += parseInt(lines[i])
        }
        console.log(sum)
    }
}

// 3. 多行输入对应多行输出
void async function () {
    while(line = await readline()){
        let tokens = line.split(' ');
        let a = parseInt(tokens[0]);
        let b = parseInt(tokens[1]);
        console.log(a + b);
    }
}()

// 4. 多行输入,第一行为测试用例的组数
/**
 * 2
 * 1 5
 * 10 20
 */
 void async function () {
    let n = parseInt(await readline())
    while(n > 0) {
        let line = readline.split(' ')
        let a = parseInt(line[0])
        let b = parseInt(line[1])
        console.log(a + b)
        n --
    }
}()

// 5. 多行输入,遇到0 0结束
void async function () {
    while(true) {
        let line = await readline().split(' ')
        let a = parseInt(line[0])
        let b = parseInt(line[1])
        if (a !== 0 && b !== 0) {
            console.log(a + b)
        }else {
            break
        }
    }
}()

// 6. 多行输入,每行第一个数字代表后面数组的长度,遇到0结束输入
/**
 * 4 1 2 3 4 
 * 5 1 2 3 4 5
 * 0
 */
 void async function () {
    while(true) {
        let sum = 0
        let lines = await readline().split(' ')
        let n = parseInt(lines[0])
        if (n === 0) {
            break
        } else {
            for (let i=1; i<lines.length; i++) {
                sum += parseInt(lines[i])
            }
        }
        console.log(sum)
    }
}()

// 7. 多行输入,多行输出(输入的行数和每行的个数不定)
/**
 * 1 2 3
 * 4 5 
 * 0 0 0 0 0
 */
 void async function () {
    while (true) {
        let lines = readline()
        if (lines) {
            let arr = lines.split(' ')
            let n = arr.length
            let sum = 0
            for (let i=0; i<n; i++) {
                sum += parseInt(arr[i])
            }
            console.log(sum)
        } else {
            break
        }
    }
}

5. 课程范例代码总结

最后记录下课程中学到的写法和平时遇到的类似功能的写法,真的很佩服各位经验充足的大佬,本菜鸡受益匪浅。

a. Toggle

点击切换状态

function toggle(...actions){
  return function(...args){
    let action = actions.shift();
    actions.push(action);
    return action.apply(this, args);
  }
}

switcher.onclick = toggle(
  evt => evt.target.className = 'off',
  evt => evt.target.className = 'on',
  evt => evt.target.className = 'warn'
  // ....类似可以继续加
);
// 用 css 选择器实现
// #switcher.on {}
// #switcher.on:after {content: 'on';}

b. 防抖优化

青训营课程介绍了节流throttle)和防抖debounce)的写法,并用防抖实现了一个小鸟跟踪的demo。在一些面试经验帖中,也看到关于优化防抖的问题,即:如何利用节流来优化防抖?在掘金小册里我看到佬提出了解决方案:

// delay: 耐心时间
function throttle(fn, delay) {
  // last为上一次触发回调的时间, timer是定时器
  let last = 0, timer = null
  // 将throttle处理结果当作函数返回
  
  return function () { 
    let context = this  // 保留调用时的this上下文
    let args = arguments  // 保留调用时传入的参数
    let now = +new Date()  // 记录本次触发回调的时间
    
    // 判断上次触发的时间和本次触发的时间差是否小于耐心时间
    if (now - last < delay) {
        // 如果时间间隔 < 耐心时间,则为本次触发操作设立一个新的定时器
       clearTimeout(timer)
       timer = setTimeout(function () {
          last = now
          fn.apply(context, args)
        }, delay)
    } else {
        // 如果时间间隔 > 耐心时间,则停止等待给出响应
        last = now
        fn.apply(context, args)
    }
  }
}

// 用新的 throttle 包装 scroll 的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)

c. Promise红绿灯

Promise红绿灯问题是一个比较经典的考题,课程给出了递归写法,async/await写法。以下为结合老师写法的解决方案:

const traffic = document.getElementById('traffic');

function wait(state, time){
  return new Promise(
      resolve => setTimeout(() => resolve(state), time)
    ).then(res => traffic.className = res);
}

async function start(){
  while(1){
    await wait('wait', 1000);
    await wait('stop', 1000);
    await wait('pass', 1000);
  }
}

start();

参考资料

  1. 青训营课程《如何写好JavaScript》
  2. MDN 手册:developer.mozilla.org/zh-CN/docs/…
  3. 掘金小册:前端性能优化原理与实践
  4. JSON.stringfy()