JavaScript代码优化

shift

看下面的代码,这是一个依赖库中源码里发现的。

immediatePush(data: BreadcrumbPushData): BreadcrumbPushData[] {
    data.time || (data.time = getTimestamp())
    if (this.stack.length >= this.maxBreadcrumbs) {
      this.shift()
    }
    this.stack.push(data)
    this.stack.sort((a, b) => a.time - b.time)
    logger.log(this.stack)
    return this.stack
  }

  private shift(): boolean { // 删除第一个元素,为什么要返回一个布尔值
    return this.stack.shift() !== undefined
  }
  

删除第一个元素,为什么要返回一个布尔值?这个布尔值并未使用。

如果数组为空,shift() 方法将返回 undefined。在某些情况下,你可能需要知道 shift() 是否成功移除了元素,这就需要检查返回值。

这种设计模式在处理队列(FIFO)或栈(LIFO)数据结构时很常见,因为它允许你了解操作是否成功,以及在操作后数组的状态。

console

在 JavaScript 中,如果你在全局作用域中重写了 console 对象,并且这个重写涉及到了 console.log 或其他 console 方法,那么在某些情况下,这可能会导致无限递归。这种情况通常发生在你尝试记录(log)信息到控制台(console),但是 console 对象已经被替换为一个会触发自身调用的函数。

例如,如果你有一个 logger 对象,它监听 console.log 并执行一些额外的逻辑,然后调用原始的 console.log 方法,这可能会导致无限递归:

// 监听控制台的报错
const consolePlugin: BasePluginType<BrowserEventTypes, BrowserClient> = {
  name: BrowserEventTypes.CONSOLE,
  monitor(notify) {
    if (!('console' in _global)) {
      return
    }
    const logType = ['log', 'debug', 'info', 'warn', 'error', 'assert']
    logType.forEach(function (level: string): void {
      if (!(level in _global.console)) return
      replaceOld(_global.console, level, function (originalConsole: () => any): Function {
        return function (...args: any[]): void {
          if (originalConsole) {
            notify(BrowserEventTypes.CONSOLE, { args, level })
            originalConsole.apply(_global.console, args)
          }
        }
      })
    })
  },
  transform(collectedData: ConsoleCollectType) {
    return collectedData
  },
  consumer(transformedData: ConsoleCollectType) {
    if (globalVar.isLogAddBreadcrumb) {
      addBreadcrumbInBrowser.call(this, transformedData, BrowserBreadcrumbTypes.CONSOLE, Severity.fromString(transformedData.level))
    }
  }
}


/**
 * 如果用户输入console,并且logger是打开的会造成无限递归,执行callback前,会把监听console的行为关掉
 *
 * @export
 * @param {Function} callback
 */
export function silentConsoleScope(callback: Function) {
  globalVar.isLogAddBreadcrumb = false
  callback()
  globalVar.isLogAddBreadcrumb = true
}

为了避免这种情况,你可以在执行回调之前临时替换 console.log 方法,执行回调后再恢复原来的 console.log 方法。这样可以确保在执行回调时不会触发递归调用。

const originalConsoleLog = console.log;

console.log = function(...args) {
  // 执行一些额外的逻辑
  originalConsoleLog.apply(console, args); // 这里可能会触发递归
};

有没有人好奇,为啥需要重写console对象,如果是监控控制台报错呢?

如何确保传参的类型完全一致?

首先,我们思考一下,为什么设计库和依赖的时候,要保证类型一致呢?

那肯定是不一致,会报错呀?

这样想一定没有问题,如果应该是数字类型,我传入一个对象,那怎么计算,怎么进行加减乘除,对吧?(类型安全)

那再深入一点,为了更方便维护,一个库和依赖,比如在GitHub上开源,那可以有很多人都能更改的,如果你没有指定类型,那其他人怎么知道,这里该是什么类型?你说可以看代码呀,是的,但是这需要花费时间,所以可以提效的事情,为什么不做好点呢?(代码可读性,可维护)

然后库和依赖,都是给其他人一起使用的,如果你知道了要传递什么类型,其他人是不是就不会乱传其他类型了,如果不想正常使用库功能的除外。(文档和示例的清晰度)

只要是人写的代码,都可能有bug和功能不足的时候,对吧,你能保证你的功能就完美无缺吗?不可能的,对吧,所以这个时候,就会需要开发新功能,那这个时候,新功能是不是就会要用到一些已存在的变量或常量呢?那这个时候,你是不是要保证它的类型?(重用和扩展)

然后一些组件库和功能库,你是否看到过test文件夹,或者是__tests__文件,这是干嘛的呀?知道jest的就应该能猜到了吧,自动化测试,就是模拟你这个库的使用,看看是否正常,所以类型不一致,这里就会很难受了,还有查问题的时候,调试功能,类型变来变去,是不是会很难受。(测试和调试)

好了,有没有人会问,类型怎么变来变去,就问在在空字符串的前面加一个加号(+)输出,会是什么类型呢?(隐式类型转换)

最后,类型一致,对编译器和运行环境是有优化的,因为能避免不必要的类型检查和转换操作(性能优化)

好了,既然它如此重要,我们下面就来看看源码中,如何保证类型一致吧


/**
   * 绑定配置项
   *
   * @param {Partial<O>} [options={}]
   * @memberof BaseTransport
   */
bindOptions(options: Partial<O> = {}): void {
  const { dsn, beforeDataReport, service, maxDuplicateCount, backTrackerId, configReportUrl } = options
  const functionType = ToStringTypes.Function
  const optionArr = [
    [service, 'service', ToStringTypes.String],
    [dsn, 'dsn', ToStringTypes.String],
    [maxDuplicateCount, 'maxDuplicateCount', ToStringTypes.Number],
    [beforeDataReport, 'beforeDataReport', functionType],
    [backTrackerId, 'backTrackerId', functionType],
    [configReportUrl, 'configReportUrl', functionType]
  ]
  validateOptionsAndSet.call(this, optionArr) // 类型一致才绑定到对应的类中,类型不正确就使用默认值
}


export function validateOptionsAndSet(this: any, targetArr: [any, string, ToStringTypes][]) {
  targetArr.forEach(
    ([target, targetName, expectType]) => toStringValidateOption(target, targetName, expectType) && (this[targetName] = target)
  )
}


export function toStringValidateOption(target: any, targetName: string, expectType: ToStringTypes): boolean {
  if (toStringAny(target, expectType)) return true
  typeof target !== 'undefined' && logger.error(`${targetName}期望传入:${expectType}类型,当前是:${nativeToString.call(target)}类型`)
  return false
}

原来indexOf要这样用

这个函数看着很简单,但是用到了取反运算符!,也用到了~,有没有区别不了这两者的,请看下文:

/**
 * 原子字符中是否包含目标字符
 *
 * @export
 * @param {string} origin 原字符
 * @param {string} target 目标字符
 * @return {*}  {boolean}
 */
export function isInclude(origin: string, target: string): boolean {
  return !!~origin.indexOf(target)
}

  1. origin.indexOf(target):这个方法返回 target 在 origin 中第一次出现的位置的索引。如果 target 不在 origin 中,它返回 -1。
  2. ~ 操作符(按位非):这是一个一元操作符,它对操作数的每一位进行按位非操作。在 JavaScript 中,任何非零数字的按位非结果都是 0,而零(0)的按位非结果是 -1。因此,如果 origin.indexOf(target) 返回 -1(表示 target 不在 origin 中),按位非操作会得到 0。如果返回的是任何其他值(表示 target 在 origin 中),按位非操作会得到 1。
  3. ! 操作符(逻辑非):这是一个逻辑操作符,它将操作数转换为布尔值,然后取反。在 JavaScript 中,0 被视为 false,而任何非零值被视为 true。因此,无论 ~ 操作的结果是什么,! 操作都会将其取反。如果 ~ 的结果是 0(false),! 操作后得到 true;如果 ~ 的结果是 1(true),! 操作后得到 false。

所以,整个表达式 !!~origin.indexOf(target) 的目的是:

  • 如果 target 在 origin 中,indexOf 返回一个非 -1 的值,按位非操作得到 1,然后逻辑非操作得到 false。
  • 如果 target 不在 origin 中,indexOf 返回 -1,按位非操作得到 0,然后逻辑非操作得到 true。

这个表达式实际上是一个巧妙的技巧,用来简化代码并直接得到一个布尔值,表示 target 是否包含在 origin 中.


 ~的作用
 -1 ---> 0
 0 ---> -1
 1 ---> -2
 2 ---> -3

 !取反变成布尔值
 0 ---> false0 ---> true

获取时间戳

这个大家应该都能想到new Date().getTime(),那你有考虑过还有其他方法吗?来看看源码中用了什么方法?为啥用了这种方式呢?


/**
 * 获取当前的时间戳
 *
 * @export
 * @return {*}  {number}
 */
export function getTimestamp(): number {
  return Date.now()
}

Date为一个时间相关对象

new Date().getTime() Date.now()
new Date()创建一个新时间对象,接着调用该对象的getTime()方法,获取时间戳 now()是Date的一个静态方法,获取时间戳
更快

附链接:stackoverflow上关于这个比较的讨论

既然如此,那么以后我都用Date.now()好了。然后,我又喵到一个同样是统计时间的方式——performance.now();

除了上面两种以外,还有一种获取时间戳的方式,performance是一个很有趣的接口,可以获取当前页面与性能相关的信息,包含三个只读对象:navigation/timing/memory,若干方法:比如now();

附链接:Performance–MDN,推荐去看一下

从统计浏览器加载性能来说,performance.now()比Date.now()好很多,前者精确度更高(千分之五秒),更可靠(不会受浏览器时间影响),然而目前支持率不太够(android 4.3及以下,ios 8.4及以下)。

所以为了防止在移动端出现兼容问题,所以就不使用performance.now();

由于Date.now()性能优于new Date().getTime(),所以就使用Date.now()。

好了,今天就分享到这里啦,大家还看到源码里有哪些优化方案呢?欢迎在评论区讨论哈。

原文链接:https://juejin.cn/post/7351300892614656063 作者:0522Skylar

(0)
上一篇 2024年3月29日 上午10:21
下一篇 2024年3月29日 上午10:32

相关推荐

发表回复

登录后才能评论