关闭

前端常见问题分析,你学会了吗?

发表于:2023-7-05 09:46

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:廿夕    来源:政采云技术

#
前端
  在前端开发过程中,常常遇到各种各样的问题和坑点。尤其是随着技术的不断发展和更新,新的问题也不断涌现。对于初学者而言,这些问题往往让人感到十分困惑和无助。因此,本文将旨在探讨一些前端开发过程中常见的问题和坑点以及解决方法,帮助读者更加深入地了解前端开发,并解决实际工作中遇到的问题。
  数据类型
  数字
  1. 进制转化问题:
  /**
  *  为什么 010 会是 8
  */
  const num1 = 09 // 9
  const num2 = 010 // 8
  这边是因为 0 开始的数字js会尝试先把它转成八进制的数字。如果你出现大于 8 的数字,他知道不是八进制还给你转十进制。纯粹的八进制应该用 0o ,类似的还有  0b  二进制和 0x 十六进制,但是他们写的不符合转换条件的话会直接报错。
  2. 精度丢失问题:
  0.1+0.2 // 0.30000000000000004
  2.55.toFixed(1) // '2.5'
  2.45.toFixed(1) // '2.5'
  2.toFixed(1) // Uncaught SyntaxError: Invalid or unexpected token
  2..toFixed(1) // '2.0'
  js 计算有精度问题呢?大家一定都是知道的, 今天就是来简单解释一下为什么会出现丢失精度的问题。这边其实分两部分,存储和展示。存储的时候 JavaScript 是以 64 位二进制补码的方式来存储。由于改方式是以 2 为底进行表示的,所以执行某些运算时容易出现误差、溢出、无限循环等问题。
  我们可以发现本来应该是 11001100 无限循环被截断了,尾号 11001 的时候1被舍去了,然后进了一位 最后存储成了上图的 1101 的样子。所以 0.1 其实存的比 0.1 要大一点点,0.2 也是一样,而 0.3 比实际小一些。所以计算 0.1+0.2 的时候其实是拿二进制计算的,两个都偏大的数字相加 误差被近一步的放大了。
  下面这张图可以看到他们真实存下来的数据转成十进制的样子。实际显示的时候会做近似处理,js 会判断一个数字特别像 0.1 它就显示 0.1 了。
  toFixed 问题也是一样。
  还有就是有时候我们对一个数字使用 .toFixed .toString 会报错。
  0.toString() // Uncaught SyntaxError: Invalid or unexpected token
  // 我们期待的是它会隐性转换让我们调用 Number 构造函数上的方法,
  // 但是程序会以为你在写一个小数,小数还不合规,所以报错了,
  // 解决方法就是拿变量装一下,或者 0..toString()
  在 JavaScript 中,采用 64 位二进制补码表示数值类型,即双精度浮点数。符号位(S)、指数位(E)和尾数位(M)的比特数分别为 1 位、11 位和 52 位。在使用 IEEE 754 标准表示双精度浮点数时,使用一些特殊的位表示:其中一个隐含位表示数字 1,在正常项中省略,因此一共有 53 位表示有效数字。
  符号位:在数值类型的二进制补码表示中,第一位表示符号位,0 表示正号,1 表示负号。
  指数位:在数值类型的二进制补码表示中,指数位用来表示科学计数法的指数部分。在双精度浮点数中,指数部分使用11个位表示,其中 10 个位表示二进制整数,在运算前需要减去 2^n 的形式,剩下一位表示符号,1表示负指数,0表示正指数。可表示 -1023 ~ 1024 之间的范围。
  尾数位:尾数位用来表示实数的小数部分。在双精度浮点数中,尾数部分使用 52 个位表示。这意味着 JavaScript 浮点数的精度是有限的,并且可能会发生舍入误差。
  长度问题:
  function fn(a,b,c){
    return a+b+c
  }
  fn.length // 3 一般来说fn的长度是形参的个数 但是形参有默认值就不同
  function fn1(a = 1,b,c){
    return a+b+c
  }
  fn1.length // 0
  function fn2(a,b=1,c){
    return a+b+c
  }
  fn2.length //1
  // 它只会统计首个默认之前的参数
  对象排序问题:
  a.b=1
  a.a=1
  a.c=1
  a[2]=2
  a[12]=2
  a[1]=2
  // 结果 {1: 2, 2: 2, 12: 2, b: 1, a: 1, c: 1}
  // 对象的内部key value的存储顺序是这样的
  // 如果属性可以转number,提前上来,按升序排列,其他的字符串属性按添加的先后顺序
  赋值中断问题:js 里没有事务的机制,不会恢复到操作之前的状态。如果中途失败了,之前赋值和操作过的数据是保留的,失败后的操作不执行。
  异步
  定时器不准:这里说的不准还不是说一点小误差。定时器由于渲染主进程阻塞也好,延时任务嵌套过深也好,事件循环优先级被排队到后边也好。这些都可以认为是“误差”,但是如果说你 setIntervel 是 10ms,结果它间隔 n 秒调一次函数,那可不是误差了,可能直接会产生 bug。
  这个问题的原因是:用户在使用谷歌浏览器的过程中将窗口最小化或切换到其他应用程序中去,浏览器会将当前标签页和其中的 JavaScript 定时器挂起,这将导致定时器延迟调用。通常情况下,浏览器会尽可能保持定时器的准确性,并在恢复标签页后立即执行延迟的定时器。但是,如果计算机负载过重或其他原因导致 JavaScript 的执行速度变慢,定时器可能会更加延迟。经过测试,新版本的浏览器上基本都是至少 1 秒一次。
  详细参考 https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout
  竞态问题:异步的竞态问题也是开发中经常遇到的问题。举个例子用户输入搜索就请求相应的商品,用户很快的输入了“手机壳” 3 个字;“手”“手机”“手机壳” 3 个不同参数的请求几乎同时发了出去,异步请求很难保证哪个请求,先回来哪个请求后回来。那加上防抖呢?其实这也不是防抖该应用的场景,弱网环境下请求 5 秒、10 秒返回都说不准,防抖防几秒都不太合适。我根据个人的经验总结了 3 种方式:
  1. 链式调用
  // 让要执行的异步函数通过一个链式方式调用
  export class SequenceQueue {
    promise = Promise.resolve();
    excute (promise) {
      this.promise = this.promise.then(() => promise);
      return this.promise;
  }
  };
  2. 设置一个叠加器,每次调用就累加,回调函数内就可以知道当前是不是“最新”。antd 里有一段例子如下
  const fetchRef = useRef(0);
  const debounceFetcher = useMemo(() => {
    const loadOptions = (value: string) => {
      fetchRef.current += 1;
      const fetchId = fetchRef.current;
      setOptions([]);
      setFetching(true);
      fetchOptions(value).then((newOptions) => {
        if (fetchId !== fetchRef.current) {
          // for fetch callback order
          return;
        }
        setOptions(newOptions);
        setFetching(false);
      });
    };
    return debounce(loadOptions, debounceTimeout);
  }, [fetchOptions, debounceTimeout]);
  可以进一步封装,将请求封装成 request( url, [option], [queueName] ), 通过外部传入来指定需要竞态的映射名。也就是将上述的叠加器放在一个 Map 里,使用 queueName 做 Map 的 key。
  “
  如果作为通用的请求中间件封装,处于内存优化考虑,此处可以将 Map 优化成 weakMap。Map 键值对是强引用,如有一个键被引用,那么GC是无法回收对应的值的,weakmap 不存在这样的问题,但要注意 weakMap 只能使用对象做 key。
  3. 新请求发出的时候取消老的请求。一般来说请求发出去了是追不回来的。但是 fetch 和原生 ajax 提供了 abort 之类的取消方法。如果你项目的请求是 fetch 或 XMLHttpRequest 就可以用他们自带的方式取消。需要注意的是,如果请求已经被发送到服务器,并且请求体数据已被上传,那么 abort() 方法就无法中止请求。大多数情况项目用的可能是 axios、uni.request 等其他更热门的请求库,这时候我们可以利用 promise.race 来封装一个可以取消的请求,传一个自定义能带取消方法的 promise 进 promise.race 来控制 真正要执行的 promise 函数提前取消。
  // 封装
  function cancelableRequest(requestPromise) {
    const cancelToken = {};
    const cancelablePromise = new Promise((resolve, reject) => {
      cancelToken.cancel = () => {
        reject(new Error('Request was canceled'));
      };
      Promise.race([requestPromise, cancelToken])
        .then(resolve)
        .catch(reject);
    });
    return { promise: cancelablePromise, cancel: cancelToken.cancel };
  }
  // 使用
  const mockApi= () =>
    new Promise(resolve => {
      setTimeout(() => {
        resolve([{ title: 'Post 1' }, { title: 'Post 2' }, { title: 'Post 3' }]);
      }, 3000);
    });
  const { promise, cancel } = cancelableRequest(mockApi());
  promise
    .then(posts => console.log(posts))
    .catch(error => console.error(error.message));
  // 取消请求
  cancel();
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号