JavaScript代码优化

shift

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

js 复制代码
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 方法,这可能会导致无限递归:

js 复制代码
// 监听控制台的报错
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 方法。这样可以确保在执行回调时不会触发递归调用。

js 复制代码
const originalConsoleLog = console.log;

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

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

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

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

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

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

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

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

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

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

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

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

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

js 复制代码
/**
   * 绑定配置项
   *
   * @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要这样用

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

js 复制代码
/**
 * 原子字符中是否包含目标字符
 *
 * @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 中.

js 复制代码
 ~的作用
 -1 ---> 0
 0 ---> -1
 1 ---> -2
 2 ---> -3

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

获取时间戳

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

js 复制代码
/**
 * 获取当前的时间戳
 *
 * @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()。

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

相关推荐
lovingsoft6 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly17 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝20 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦21 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡42 分钟前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队1 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端