前端面试手撕编程之ES【防抖节流,深拷贝,setTimeout、数组去重,浮点数精度,Object】

js中字符串,json数据的处理【匹配url、邮箱、电话,版本号,千位分割,判断回问】-CSDN博客


手写实现js类/方法【改变this、map、Object、发布订阅、promise】-CSDN博客

目录

防抖debounce

被打断的时间+原定time执行一次

并且执行的是最后一次触发的事件

应用

search搜索联想

window触发resize

节流throttle

间隔至少time执行

无视打断,time内执行的是第一次触发的事件

应用

鼠标不断点击触发

监听滚动事件

深拷贝

A.JSON.parse() 和 JSON.stringify()

B.遍历+递归

arr instanceof Array ?  [] :  {}

C.改进版

Symbol属性:Object.getOwnPropertySymbols

不可枚举属性:Reflect.ownKeys

循环引用:WeakMap

setTimeout()

倒计时

js:递归调用

react:useEffect(()=>{},[cnt])

requestAnimationFrame代替settimeout更精准

setTimeout、setInterval最短时长为4ms

setTimeout固定时长后执行

setInterval间隔固定时间重复执行

setTimeout模拟实现setInterval

setInterval模拟实现setTimeout

数组去重

[...new Set(arr)]

arr.filter((value, index, self) => self.indexOf(value) === index)

浮点数不精确性

原因:部分十进制小数在二进制中无限循环

解决

面试:toFixed+精度

业务:第三方库

bignumber.js:处理大数字和高精度运算

Object

new :Fn=[...arguments].shift()

*寄生组合式继承:call,create,constructor

Object.defineProperty(obj, prop, descriptor)


防抖debounce

被打断的时间+原定time执行一次

并且执行的是最后一次触发的事件

触发事件后在 n 秒内函数至多只能执行一次,如果在 n 秒内又触发了事件,会重计算函数执行时间,延迟执行

本该在-----执行

但是--再次触发

于是-------执行

必须等待完time,即才能执行,time期间触发都会从0计时

function debounce(fun,time) {
        let flag // 定义状态
        return function () {
            clearTimeout(flag)// 在执行之前 清除 定时器的 flag 不让他执行
            flag = setTimeout(() => {
                fun.call(this,arguments)//拿到正确的this对象,即事件发生的dom
            }, time)
        }
    }

应用

search搜索联想

用户在不断输入值时,用防抖来节约请求资源

window触发resize

不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

节流throttle

间隔至少time执行

无视打断,time内执行的是第一次触发的事件

连续触发事件但是在 n 秒中只执行一次函数。

定时在-----执行

在--再次触发,无效

最终在-----执行

两种方式可以实现,分别是时间戳版和定时器版。

function throttle(fun, time) {
        let flag // 定义一个空状态
        return function () { // 内部函数访问外部函数形成闭包
            if (!flag) { // 状态为空执行
                flag = setTimeout(() => {
                    fns.apply(this, arguments) // 改变this指向 把 event 事件对象传出去
                    flag = null // 状态为空
                }, time)
            }
        }
      }

应用

鼠标不断点击触发

mousedown(单位时间内只触发一次)

监听滚动事件

比如是否滑到底部自动加载更多,用throttle来判断

深拷贝

A.JSON.parse() 和 JSON.stringify()

  • 数据类型限制:不能复制、undefinedDateSymbol函数等等其他的js数据类型,其实也很好理解,因为JSON只支持以上列举的数据类型,其他要么被忽略要么报错
  • 丢失原型的信息:最终拷贝的对象原型只会是Object.prototype
  • 不能处理循环引用:对象包含循环引用,JSON.stringify() 将会导致错误。
  • 性能问题:对于大型复杂对象深度嵌套的对象,容易产生性能问题
const originalObject = { a: 1, b: { c: 2 } };

// 通过将对象转换为字符串,再将其解析回对象来实现深拷贝
const deepCopy = JSON.parse(JSON.stringify(originalObject));

console.log(deepCopy); // 输出深拷贝后的对象

B.遍历+递归

arr instanceof Array ?  [] :  {}

function deepClone(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  const res = Array.isArray(obj) ? [] : {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      res[key] = deepClone(obj[key]);
    }
  }

  return res;
}
  1. 要拷贝的属性只能是对象自身的,不能是原型链上的

  2. 拷贝的属性不仅仅是string类型的,还有symbol类型的

  3. 对象身上可能会有循环引用,需要处理,而不是陷入死循环

C.改进版

Symbol属性:Object.getOwnPropertySymbols

Symbols 在 for...in 迭代中不可枚举。

Object.getOwnPropertyNames() 不会返回 symbol 对象的属性

// 复制 Symbol 类型属性
  const symbols = Object.getOwnPropertySymbols(obj);
  for (const symbolKey of symbols) {
    res[symbolKey] = deepClone(obj[symbolKey]);
  }

不可枚举属性:Reflect.ownKeys

Reflect.ownKeys简单来说会返回一个数组,属性包括:对象自身的所有属性(包括所有可枚举的不可枚举的stringsymbol类型

 // 拿到对象身上所有的属性,返回一个数组
  const keys = Reflect.ownKeys(obj);

  for (const key of keys) {
    res[key] = deepClone(obj[key]);
  }

循环引用:WeakMap

在每次对复杂数据类型进行深拷贝前保存其值,如果下次又出现了该值,就不再进行拷贝,直接截止。

WeakMap 中的键是弱引用,当键对象在其他地方没有被引用时,它可以被垃圾回收,这有助于防止内存泄漏

function deepClone(obj, clones = new WeakMap()) {
  // 如果是原始类型或 null,直接返回
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  // 检查是否已经克隆过该对象,防止循环引用
  if (clones.has(obj)) {
    return clones.get(obj);
  }

  // 判断是否数组还是普通对象
  const res = Array.isArray(obj) ? [] : {};

  // 将当前对象添加到克隆Map中
  clones.set(obj, res);

  // 拿到所有 key
  const keys = Reflect.ownKeys(obj);

  for (const key of keys) {
    res[key] = deepClone(obj[key], clones);
  }

  return res;
}

// test
const obj = {
  foo: "bar",
  num: 42,
  arr: [1, 2, 3],
  obj: { dd: true },
  [Symbol("symbol属性")]: "hello",
};

// 循环引用
obj.newObj = obj;

const copy = deepClone(obj);
console.log(copy);

setTimeout()

倒计时

​//返回值timeoutID是一个正整数,表示定时器的编号。
let timeoutID = scope.setTimeout(function[, delay]),//delay表示最小等待时间,真正等待时间取决于前面排队的消息
clearTimeout(timeoutID) //取消该定时器。

js:递归调用

var c = 10; // 设置初始倒计时时间为10秒
var t; // 声明一个变量用来存储 setTimeout 的返回值

function timedCount() {
    c -= 1; // 每次调用函数,倒计时时间减一秒

    if (c === 0) {
        clearTimeout(t); // 当倒计时为零时,清除定时器
        console.log("倒计时结束"); // 可以在这里添加任何你想要执行的操作,比如提示用户倒计时结束
        return;
    }

    console.log("剩余时间:" + c + "秒"); // 在控制台打印剩余时间,你也可以将其显示在页面上的某个元素中

    t = setTimeout(function () {
        timedCount();
    }, 1000); // 每隔一秒调用一次自身,实现倒计时
}

timedCount(); // 调用函数开始倒计时

react:useEffect(()=>{},[cnt])

import React, { useState, useEffect } from 'react';

const Countdown = ({ initialCount }) => {
  const [count, setCount] = useState(initialCount);

  useEffect(() => {
    // 如果倒计时已经结束,则不再减少计时
    if (count === 0) return;

    // 设置定时器
    const timerId = setTimeout(() => {
      setCount(count - 1);
    }, 1000);

    // 清理函数
    return () => clearTimeout(timerId);
  }, [count]);

  return (
    <div>
      <h1>倒计时: {count} 秒</h1>
    </div>
  );
};

export default Countdown;

requestAnimationFrame代替settimeout更精准

startTime = Date.now();

    requestAnimationFrame(function update() {
        var currentTime = Date.now();
        var deltaTime = currentTime - startTime;

        if (deltaTime >= 1000) { // 每秒更新一次
            startTime = currentTime;
            timedCount(); // 递归调用自身,实现倒计时
        } else {
            requestAnimationFrame(update); // 继续请求下一帧
        }
    });

setTimeout、setInterval最短时长为4ms

setTimeout固定时长后执行

setInterval间隔固定时间重复执行

setTimeout模拟实现setInterval

// 使用闭包实现
function mySetInterval(fn, t) {
  let timer = null;
  function interval() {
    fn();
    timer = setTimeout(interval, t);
  }
  interval();
  return {
    // cancel用来清除定时器
    cancel() {
      clearTimeout(timer);
    }
  };
}

setInterval模拟实现setTimeout

function mySetTimeout(fn, time) {
  let timer = setInterval(() => {
    clearInterval(timer);
    fn();
  }, time);
}

// 使用
mySetTimeout(() => {
  console.log(1);
}, 2000);

数组去重

[...new Set(arr)]

arr.filter((value, index, self) => self.indexOf(value) === index)

浮点数不精确性

原因:部分十进制小数在二进制中无限循环

Java、Python、C++、JS都是基于 IEEE 754 标准的双精度浮点数来表示数字

0.2的二进制表示是无限循环的,近似等于0.0011001100110011001100

 

 IEEE-754 标准下双精度浮点数由三部分组成,分别如下:

  • sign(符号): 占 1 bit, 表示正负;
  • exponent(指数): 占 11 bit,表示范围;
  • mantissa(尾数): 占 52 bit,表示精度,多出的末尾如果是 1 需要进位;

阅读 JavaScript 浮点数陷阱及解法可以了解到公式的由来。

解决

面试:toFixed+精度

 /*** method **
 *  add / subtract / multiply /divide
 * floatObj.add(0.1, 0.2) >> 0.3
 * floatObj.multiply(19.9, 100) >> 1990
 *
 */
var floatObj = function() {

    /*
     * 判断obj是否为一个整数
     */
    function isInteger(obj) {
        return Math.floor(obj) === obj
    }

    /*
     * 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100
     * @param floatNum {number} 小数
     * @return {object}
     *   {times:100, num: 314}
     */
    function toInteger(floatNum) {
        var ret = {times: 1, num: 0}
        if (isInteger(floatNum)) {
            ret.num = floatNum
            return ret
        }
        var strfi  = floatNum + ''
        var dotPos = strfi.indexOf('.')
        var len    = strfi.substr(dotPos+1).length
        var times  = Math.pow(10, len)
        var intNum = Number(floatNum.toString().replace('.',''))
        ret.times  = times
        ret.num    = intNum
        return ret
    }

    /*
     * 核心方法,实现加减乘除运算,确保不丢失精度
     * 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除)
     *
     * @param a {number} 运算数1
     * @param b {number} 运算数2
     * @param digits {number} 精度,保留的小数点数,比如 2, 即保留为两位小数
     * @param op {string} 运算类型,有加减乘除(add/subtract/multiply/divide)
     *
     */
    function operation(a, b, digits, op) {
        var o1 = toInteger(a)
        var o2 = toInteger(b)
        var n1 = o1.num
        var n2 = o2.num
        var t1 = o1.times
        var t2 = o2.times
        var max = t1 > t2 ? t1 : t2
        var result = null
        switch (op) {
            case 'add':
                if (t1 === t2) { // 两个小数位数相同
                    result = n1 + n2
                } else if (t1 > t2) { // o1 小数位 大于 o2
                    result = n1 + n2 * (t1 / t2)
                } else { // o1 小数位 小于 o2
                    result = n1 * (t2 / t1) + n2
                }
                return result / max
            case 'subtract':
                if (t1 === t2) {
                    result = n1 - n2
                } else if (t1 > t2) {
                    result = n1 - n2 * (t1 / t2)
                } else {
                    result = n1 * (t2 / t1) - n2
                }
                return result / max
            case 'multiply':
                result = (n1 * n2) / (t1 * t2)
                return result
            case 'divide':
                result = (n1 / n2) * (t2 / t1)
                return result
        }
    }

    // 加减乘除的四个接口
    function add(a, b, digits) {
        return operation(a, b, digits, 'add')
    }
    function subtract(a, b, digits) {
        return operation(a, b, digits, 'subtract')
    }
    function multiply(a, b, digits) {
        return operation(a, b, digits, 'multiply')
    }
    function divide(a, b, digits) {
        return operation(a, b, digits, 'divide')
    }

    // exports
    return {
        add: add,
        subtract: subtract,
        multiply: multiply,
        divide: divide
    }
}();

业务:第三方库

bignumber.js:处理大数字和高精度运算

Object

new :Fn=[...arguments].shift()

"_new"函数,该函数会返回一个对象,

该对象的构造函数为函数参数、原型对象为函数参数的原型,核心步骤有:

  1. 创建一个新对象
  2. 获取函数参数
  3. 将新对象的原型对象和函数参数的原型连接起来
  4. 将新对象和参数传给构造器执行
  5. 如果构造器返回的不是对象,那么就返回第一个新对象
const _new = function() {
    const object1 = {}
    const Fn = [...arguments].shift()
    object1.__proto__ = Fn.prototype
    const object2 = Fn.apply(object1, arguments)
    return object2 instanceof Object ? object2 : object1
}

*寄生组合式继承:call,create,constructor

通过寄生组合式继承使"Chinese"构造函数继承于"Human"构造函数。要求如下:
1. 给"Human"构造函数的原型上添加"getName"函数,该函数返回调用该函数对象的"name"属性
2. 给"Chinese"构造函数的原型上添加"getAge"函数,该函数返回调用该函数对象的"age"属性

  1. 在"Human"构造函数的原型上添加"getName"函数
  2. 在”Chinese“构造函数中通过call函数借助”Human“的构造器来获得通用属性
  3. Object.create函数返回一个对象,该对象的__proto__属性为对象参数的原型。此时将”Chinese“构造函数的原型和通过Object.create返回的实例对象联系起来
  4. 最后修复"Chinese"构造函数的原型链,即自身的"constructor"属性需要指向自身
  5. 在”Chinese“构造函数的原型上添加”getAge“函数
function Human(name) {
    this.name = name
    this.kingdom = 'animal'
    this.color = ['yellow', 'white', 'brown', 'black']
}
Human.prototype.getName = function() {
    return this.name
}

function Chinese(name,age) {
    Human.call(this,name)//call函数借助”Human“的构造器来获得通用属性
    this.age = age
    this.color = 'yellow'
}

//返回的对象__proto__属性为对象参数的原型
Chinese.prototype = Object.create(Human.prototype)//使用现有的对象来作为新创建对象的原型
//修复"Chinese"构造函数的原型链,即自身的"constructor"属性需要指向自身
Chinese.prototype.constructor = Chinese

Chinese.prototype.getAge = function() {
    return this.age
}

Object.defineProperty(obj, prop, descriptor)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值