标签: Javascript

  • Vue 3.0 核心特性技术日志

    作为从 Vue 1.0 一路使用过来的开发者,Vue 3.0 并非简单的版本迭代,而是在核心架构、性能、开发体验上的全面升级,同时兼容了 2.x 的大部分核心思想,既保留了我们熟悉的开发范式,又解决了 2.x 长期存在的痛点。以下从核心架构、语法特性、性能优化、工程化等维度,结合 2.x 对比梳理关键变化。

    一、核心架构:从 Options API 到 Composition API

    1. Vue 2.x:Options API 为主

    Vue 2.x 采用「选项式 API」,将代码按 datamethodscomputedwatch生命周期钩子 等选项划分,优点是上手简单,但存在明显痛点:

    • 逻辑碎片化:一个业务逻辑(如「用户登录状态管理」)的代码会分散在 data(状态)、methods(方法)、watch(监听)、mounted(初始化)等多个选项中,组件越大,逻辑越难追踪;
    • 代码复用受限:mixins 是主要复用方式,但存在命名冲突、来源不清晰、逻辑依赖隐晦的问题;
    • TypeScript 支持薄弱:Options API 天生不契合 TS 的类型推导,需要额外的类型声明,开发体验差。

    2. Vue 3.0:Composition API(组合式 API)+ 保留 Options API

    Vue 3.0 推出「组合式 API」,核心是 setup 函数(组件的入口),允许按业务逻辑维度组织代码,而非按选项类型:

    
    <template>
    
    <div>{{ count }}</div>
      <button @click="increment">+1</button>
    </template>
    
    <script setup>
    // Vue 3.0 组合式 API(<script setup> 语法糖)
    import { ref, computed, onMounted } from 'vue'
    
    // 状态定义(替代 2.x 的 data)
    const count = ref(0)
    
    // 方法定义(替代 2.x 的 methods)
    const increment = () => {
      count.value++
    }
    
    // 计算属性(替代 2.x 的 computed)
    const doubleCount = computed(() => count.value * 2)
    
    // 生命周期(替代 2.x 的 mounted)
    onMounted(() => {
      console.log('组件挂载完成,count 初始值:', count.value)
    })
    </script>

    核心优势

    • 逻辑聚合:一个业务逻辑的所有代码(状态、方法、监听、生命周期)可集中写在一处,组件结构更清晰;
    • 灵活复用:通过「组合函数(Composables)」复用逻辑(如 useUser()useRequest()),替代 mixins,解决命名冲突和依赖问题;
    • 完美支持 TS:基于函数式的写法天然契合 TS 类型推导,无需额外声明即可获得完整的类型提示。

    兼容说明

    • Vue 3.0 完全保留 Options API,2.x 的代码可直接迁移(仅需少量适配),支持「渐进式升级」—— 既可以在老组件中继续用 Options API,也可以在新组件中用 Composition API。

    二、响应式系统:从 Object.defineProperty 到 Proxy

    1. Vue 2.x:响应式的痛点

    Vue 2.x 基于 Object.defineProperty 实现响应式,存在天然限制:

    • 无法监听数组下标修改(如 arr[0] = 1)和数组长度修改(如 arr.length = 0),需通过 Vue.set/this.$set 或数组变异方法(push/splice)触发更新;
    • 无法监听对象新增属性/删除属性(如 this.obj.newKey = 1),同样需要 Vue.set/this.$delete
    • 响应式初始化时需要递归遍历对象,性能随对象层级加深下降。

    2. Vue 3.0:Proxy 重构响应式

    Vue 3.0 改用 ES6 的 Proxy 实现响应式,彻底解决上述问题:

    • 原生支持监听数组下标/长度修改、对象新增/删除属性,无需 $set/$delete
    • 非侵入式:无需修改原对象,而是代理对象,初始化时无需递归遍历(懒代理,访问属性时才递归),性能大幅提升;
    • 支持 RefReactive 两种响应式方式:
      • ref:用于基础类型(字符串、数字等),通过 .value 访问(模板中自动解包,无需 .value);
      • reactive:用于对象/数组,返回代理对象,直接访问属性即可。

    示例对比

    // Vue 2.x
    export default {
      data() {
        return {
          arr: [1, 2, 3],
          obj: { name: 'Vue2' }
        }
      },
      methods: {
        updateData() {
          // 无效:无法监听数组下标修改
          this.arr[0] = 100
          // 有效:需用 $set
          this.$set(this.arr, 0, 100)
    
          // 无效:无法监听对象新增属性
          this.obj.age = 3
          // 有效:需用 $set
          this.$set(this.obj, 'age', 3)
        }
      }
    }
    
    // Vue 3.0(Composition API)
    import { ref, reactive } from 'vue'
    const arr = ref([1, 2, 3])
    const obj = reactive({ name: 'Vue3' })
    
    const updateData = () => {
      // 有效:原生支持数组下标修改
      arr.value[0] = 100
      // 有效:原生支持对象新增属性
      obj.age = 3
    }

    三、性能优化:体积、渲染、编译全方位提升

    1. 体积更小

    • Vue 2.x 运行时体积约 22KB(gzip);
    • Vue 3.0 通过「树形摇树(Tree-shaking)」重构核心代码,仅引入使用的功能(如仅用响应式、不用编译器),最小体积约 10KB(gzip),体积减少 50% 以上。

    2. 渲染更快

    • 虚拟 DOM 重构:Vue 3.0 优化了虚拟 DOM 的生成和对比算法,引入「静态标记」—— 对模板中不变的节点(如纯文本、静态标签)做标记,对比时直接跳过,渲染性能提升约 55%;
    • 编译优化:Vue 3.0 的模板编译器会提前分析模板结构,生成更高效的渲染函数,更新性能提升约 133%。

    3. 内存占用更低

    Vue 3.0 优化了组件实例的内存占用,同场景下内存使用减少约 50%。

    四、其他关键变化(对比 Vue 2.x)

    1. 生命周期钩子

    Vue 3.0 保留了 2.x 的生命周期概念,但命名略有调整(Composition API 中),且新增 setup 替代 beforeCreatecreated

    Vue 2.x Vue 3.0(Composition API) 说明
    beforeCreate setup(开始) setup 执行时,组件实例未创建
    created setup(结束) setup 执行完,组件实例已创建
    beforeMount onBeforeMount 挂载前
    mounted onMounted 挂载后
    beforeUpdate onBeforeUpdate 更新前
    updated onUpdated 更新后
    beforeDestroy onBeforeUnmount 卸载前(命名更语义化)
    destroyed onUnmounted 卸载后(命名更语义化)

    2. 模板语法

    • 支持多根节点:Vue 2.x 模板要求唯一根节点,3.0 支持多根节点(Fragment),无需包裹 `
      `;
    • 更灵活的 v-model:Vue 2.x 中 v-model 本质是 value + input 事件,3.0 支持自定义绑定属性和事件,且支持多个 v-model:

      <!-- Vue 2.x:仅能绑定一个 v-model,且固定 value/input -->
      <Child v-model="name" />
      
      <!-- Vue 3.0:支持多个 v-model,自定义属性和事件 -->
      <Child v-model:name="name" v-model:age="age" />
    • 移除 filter:Vue 3.0 废弃了 2.x 的 filter 过滤器,推荐用计算属性或方法替代(减少 API 复杂度)。

    3. 全局 API 调整

    Vue 2.x 的全局 API(如 Vue.componentVue.directive)会污染全局环境,3.0 改为「实例化 API」:

    // Vue 2.x
    import Vue from 'vue'
    Vue.component('MyComponent', { /* ... */ })
    Vue.directive('my-dir', { /* ... */ })
    new Vue({ el: '#app' })
    
    // Vue 3.0
    import { createApp } from 'vue'
    const app = createApp({ /* 根组件 */ })
    app.component('MyComponent', { /* ... */ })
    app.directive('my-dir', { /* ... */ })
    app.mount('#app')

    优势:多个 Vue 实例可独立配置,避免全局配置冲突(如多应用嵌入场景)。

    4. 废弃/不推荐的特性

    • 废弃 Vue.prototype:改用 app.config.globalProperties 挂载全局属性;
    • 废弃 $on/$off/$once:移除实例事件总线,推荐用第三方库(如 mitt)替代;
    • 不推荐 filters:用计算属性/方法替代;
    • 不推荐 mixins:用 Composition API 的组合函数替代。

    五、迁移建议(从 Vue 2.x 到 3.0)

    1. 渐进式迁移:先保留 Options API,逐步在新功能中使用 Composition API,降低迁移成本;
    2. 工具适配:升级脚手架到 Vite(Vue 3.0 推荐)或 Vue CLI 5+,安装 @vue/compat 兼容包,可临时兼容 2.x 代码;
    3. 重点检查
      • 替换 Vue.set/$setVue.delete/$delete(3.0 无需);
      • 调整生命周期钩子(如 beforeDestroyonBeforeUnmount);
      • 替换 filter 为计算属性/方法;
    4. TypeScript 接入:新组件建议用 <script setup lang="ts">,充分利用 3.0 的 TS 优势。

    总结

    1. 核心架构:Vue 3.0 的 Composition API 解决了 2.x Options API 逻辑碎片化、复用难、TS 支持差的问题,同时兼容 2.x 写法,支持渐进式升级;
    2. 响应式系统:基于 Proxy 重构,彻底解决 2.x Object.defineProperty 无法监听数组/对象新增属性的痛点,性能和易用性大幅提升;
    3. 性能与体验:体积更小、渲染更快,模板语法更灵活(多根节点、多 v-model),全局 API 更合理,是对 Vue 2.x 核心痛点的全面优化,同时保留了 Vue 一贯的「易用、渐进式」设计理念。
  • 【React】学习笔记-基础篇

    虚拟 DOM 的原理是什么?为什么比直接操作 DOM 高效?

    • 虚拟 DOM 是一个轻量的 JS 对象,用来描述真实 DOM 的层次和属性。
    • 通过 Diff 算法最小化 DOM 操作,减少重绘和回流。

    JSX 是什么?和普通 JavaScript 的区别?

    • JSX 是语法糖,最终会被 Babel 编译为 React.createElement() 调用。

    数据流

    • 单向数据流,自上而下传递。
    • 父组件通过 props 把数据传递给子组件;
    • 子组件可以通过回调函数通知父组件。

    Props

    • PropTypes 定义类型
    • defaultProps 定义默认值
    • this.props.children 表示组件的所有子节点
    • 不要把 props 复制到 state 中,要尽可能把 props 当作数据源(避免反模式)

    高阶组件(HOC)

    • 用于复用组件逻辑。本质上是一个函数,通过接收一个组件并返回一个新组件

    PureComponent 和 React.memo

    • PureComponent 会对 props 和 state 进行浅比较
    • memo 只对 props 进行浅比较

    `

    ` 和 “ (空标签)的区别 – 只有 ` ` 可以带 key 属性,这在列表渲染时非常重要 #### 类组件和函数组件的区别? – 语法与结构 “`js class MyComponent extends React.Component { render() { return
    Hello, {this.props.name}
    ; } } function MyComponent(props) { return
    Hello, {props.name}
    ; } “` – 状态管理 – 类组件通过 `this.state` 定义状态,`this.setState()` 更新状态 – 函数组件使用 Hooks 的 `useState` 管理状态 – 生命周期与副作用 – 类组件通过生命周期处理副作用 – 函数组件通过 `useEffect` 替代生命周期 – `this` 的绑定问题 – 类组件通过手动绑定事件函数的 `this` “`js class Button extends React.Component { handleClick() { console.log(this); } // this 可能为 undefined render() { return ; } } “` – 函数组件没有 this,直接访问 props 和状态 “`js function Button() { const handleClick = () => console.log(“No this!”); return ; } “` – 性能优化 – 类组件使用 `shouldComponentUpdate` 或 `PureComponent` 避免不必要的渲染 – 函数组件使用 `React.memo` 进行浅比较 ### 【状态管理】 #### React 中的状态提升(Lifting State Up)是什么? – 把状态提升到父组件进行管理,子组件通过 props 读取数据。 #### Context API 的作用?如何避免不必要的渲染? – Context(上下文) 是 React 提供的跨层级组件数据传递方案,用于解决多层组件嵌套时(逐层传递 props)的问题。 – 核心作用 – 跨组件共享数据(如主题、用户信息、全局配置等) – 避免中间组件透传 props(减少冗余代码) #### MobX / Redux – 全局状态管理,把状态抽离到组件之外。 **Redux** * 工作流程: 1. 定义 Store:集中管理全局状态(createStore)。 2. 派发 Action:组件通过 dispatch(action) 发出状态修改请求(action 是一个描述“发生了什么”的普通对象)。 3. 执行 Reducer:根据 action.type,纯函数 reducer 计算新状态(不可变更新)。 4. 更新视图:Store 通知订阅者(如 React 组件),触发重新渲染。 * 核心概念: – 单向数据流:View → Action → Reducer → Store → View – 三大原则:单一数据源、状态只读、纯函数修改。 * 总结: – 组件 dispatch → Reducer 处理 → Store 更新 → 视图同步 **MobX** * 工作流程: 1. 定义 Observable State:用 @observable 或 makeObservable 标记可变状态。 2. 修改 State:在 @action 函数中更新状态(直接赋值/修改对象属性)。 3. 自动追踪依赖:组件用 observer 包裹,自动订阅其依赖的状态。 4. 触发更新:状态变化时,依赖它的组件自动重渲染。 * 核心概念: – 响应式编程:像 Excel 公式一样自动更新(无需手动 dispatch/reducer)。 – 直接修改状态(但通过 action 规范变更)。 * 总结: – 响应式状态,自动追踪依赖,修改即更新 ### 【性能优化】 #### React 的渲染机制是怎样的?如何避免不必要的渲染? – 使用 React.memo、useMemo、useCallback、避免内联对象/函数。 #### React 的 key 属性有什么作用? – 帮助 React 识别元素变化,优化列表渲染。 #### 实现代码分割(Code Splitting) – 动态 import() 语法 – 动态 import() 是 ES2020 引入的语法,返回一个 Promise: “`js // 普通模块中使用 import(“./math”).then(math => { console.log(math.add(16, 26)); }); // React 组件中使用 import(“./OtherComponent”).then(OtherComponent => { // 使用加载的组件 }); “` – React.lazy + Suspense – React.lazy 和 Suspense 来实现组件的懒加载: “`js import React, { Suspense } from ‘react’; const OtherComponent = React.lazy(() => import(‘./OtherComponent’)); function MyComponent() { return (
    Loading…
    }> ); } “`
  • WEB前端-JS篇

    小数精度处理

    • 使用 toFixed 取后几位小数
    • 把小数转为整数再进行运算
    • 使用精度运算库,如:big.js

    for…infor…of 的区别

    • for-in 历遍对象,通常获取 key(类似 Object.keys)
    • for-of 历遍可迭代对象,如 ArrayMapSet

    任务队列(Task Queue)

    • 先执行同步再执行异步
    • 核心作用:处理异步代码的调度和执行
    • 宏任务:setTimeoutsetIntervalI/O
    • 微任务:Promise 回调(.then / .catch/ .finally)、MutationObserver 回调等
    • 微任务优先级高于宏任务
    • 调用 fetch 会同步执行并立即返回一个 promise

    call | apply | bind

    均用于改变函数内部 this 的指向

    const obj = { name: "Alice" };
    
    function greet() {
      console.log(`Hello, ${this.name}!`);
    }
    
    greet.call(obj);      // Hello, Alice!
    greet.apply(obj);     // Hello, Alice!
    const boundGreet = greet.bind(obj);
    boundGreet();         // Hello, Alice!

    ES 新特性:

    变量声明

    letconst

    箭头函数

    简化函数语法,自动绑定 this

    模板字符串

    `Hello ${name}`

    解构赋值

    const { age, name } = user;
    const [first, second] = [1, 2];

    默认参数

    扩展运算符

    const arr1 = [1, 2];
    const arr2 = [...arr1, 3]; // [1, 2, 3]; // 合并数组
    const objCopy = { ...originalObj }; // 合并对象

    Promise 和 异步处理

    Promise.all – 等待所有 Promise 对象都执行完成 Promise.race – 返回第一个完成的 Promise(无论成功失败) Promise.any – 返回第一个成功的 Promise

    模块化:importexport

    Set, Map

    let s = new Set(); s.add(‘a’).add(‘b’)
    let m = new Map(); m.set(’age’, 16)

    数组的 includes,检查是否包含某元素

    async / await

    底层实现机制: async/await 的核心是通过 Generator 函数和自动执行器实现的:

    1. async 函数本质上是一个返回 Promise 的 Generator 函数;
    2. await 相当于 yield,暂停函数执行;
    3. 有一个自动执行器负责处理 Promise 的 resolve/reject 并继续执行。

    Generator 函数的语法糖,省去手动 next() 的步骤

    Object.values() 和 Object.entries()

    Object.values({ a: 1, b: 2 }); // [1, 2]
    Object.entries({ a: 1, b: 2 }); // [["a", 1], ["b", 2]]

    字符串填充:padStart() 和 padEnd()

    "5".padStart(2, "0"); // "05"

    空值合并运算符

    const value = input ?? "default";

    动态导入模块

    const module = await import('./module.js')

    replaceAll 替换所有匹配的子串

    "a.b.c".replaceAll(".", "-"); // "a-b-c"

    require 和 import

    • 分别属于 CommonJS 和 ES 两种不同模块系统
    • require 动态加载,运行时解析
    • import 静态加载,编译时解析
    • Proxy
      • 数据绑定与响应式系统:通过 set 拦截属性赋值,触发副作用(如重新渲染)
      • 属性访问控制和校验:限制对象属性的访问或赋值

    微信小程序:

    OpenID & UnionID

    OpenID

    • 用户在当前小程序中的唯一标识
    • 用途:用于识别用户在当前小程序内的身份(如用户数据存储、业务逻辑处理)
    • 获取:通过 wx.login() 获取 code,后端用 code 调用微信接口换取 OpenID

    UnionID

    • 用户在微信的唯一标识
    • 用途:实现跨小程序、公众号用户身份统一
    • 获取:通过 wx.getUserInfo() 获取 encryptedData, iv,发送到后端解密,或者通过按钮触发授权

    性能优化:

    1. 文本压缩(css, js, html)
    2. 图片压缩
    3. 合并资源(合并文本、雪碧图)
    4. 图片懒加载
    5. 合并请求
    6. 缓存资源(设置浏览器头、设置 html head)
    7. 使用 CDN
    8. gzip 压缩
    9. 减少 DOM 操作
    10. CSS 样式置顶、JS 脚本置底
    11. 防抖和节流
      • 防抖:保证N秒内执行一次,如果N秒内触发将再执行并重新计算时间
      • 节流:保证N秒内只能触发一次
  • 【Typescript】接口、类

    接口

    TypeScript的核心原则之一是对值所具有的结构进行类型检查。

    interface LabelledValue {
      label: string;
    }
    
    function printLabel(labelledObj: LabelledValue) {
      console.log(labelledObj.label);
    }
    
    let myObj = {size: 10, label: "Size 10 Object"};
    printLabel(myObj);

    可选属性

    interface SquareConfig {
        width: number
        height: number
        color?: string // 可选属性
    }

    只读属性

    只能在刚创建时修改其值

    interface Point {
        readonly x: number;
        readonly y: number;
    }

    额外的属性检查

    如果 SquareConfig带有上面定义的类型的color和width属性,并且还会带有任意数量的其它属性,可以这样定义:

    interface SquareConfig {
        width: number
        color?: string
        [propName: string]: any
    }

    函数类型

    interface SearchFunc {
      (source: string, subString: string): boolean
    }
    
    // 对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 
    let mySearch: SearchFunc;
    mySearch = function(src: string, sub: string): boolean {
      let result = src.search(sub);
      return result > -1;
    }

    可索引的类型

    interface StringArray {
      [index: number]: string;
    }
    
    let myArray: StringArray;
    myArray = ["Bob", "Fred"];
    
    let myStr: string = myArray[0];

    类类型

    interface ClockInterface {
        currentTime: Date;
        setTime(d: Date);
    }
    
    // 通过 implements 描述
    class Clock implements ClockInterface {
        currentTime: Date;
        setTime(d: Date) {
            this.currentTime = d;
        }
        constructor(h: number, m: number) { }
    }

    继承接口

    interface Shape {
        color: string;
    }
    
    interface PenStroke {
        penWidth: number;
    }
    
    interface Square extends Shape, PenStroke {
        sideLength: number;
    }
    
    let square = 
    <Square>{};
    square.color = "blue"
    square.sideLength = 10
    square.penWidth = 5

    混合类型

    一个对象可以同时做为函数和对象使用,并带有额外的属性

    interface Counter {
        (start: number): string;
        interval: number;
        reset(): void;
    }
    
    function getCounter(): Counter {
        let counter = 
    <Counter>function (start: number) { };
        counter.interval = 123;
        counter.reset = function () { };
        return counter;
    }
    
    let c = getCounter();
    c(10);
    c.reset();
    c.interval = 5.0;

    class Animal {
        move(distanceInMeters: number = 0) {
            console.log(`Animal moved ${distanceInMeters}m.`);
        }
    }
    
    class Dog extends Animal {
        bark() {
            console.log('Woof! Woof!');
        }
    }
    
    const dog = new Dog();
    dog.bark();
    dog.move(10);
    dog.bark();

    继承

    通过 extends 关键字

    class Animal {
        name: string;
        constructor(theName: string) { this.name = theName; }
        move(distanceInMeters: number = 0) {
            console.log(`${this.name} moved ${distanceInMeters}m.`);
        }
    }
    
    class Snake extends Animal {
        constructor(name: string) { super(name); }
        move(distanceInMeters = 5) {
            console.log("Slithering...");
            super.move(distanceInMeters);
        }
    }

    公共,私有与受保护的修饰符

    public(默认)

    在TypeScript里,成员都默认为 public。

    class Animal {
        public name: string;
    }
    private

    当成员被标记成private时,它就不能在声明它的类的外部访问。

    class Animal {
        private name: string
        constructor(theName: string) { this.name = theName; }
    }
    
    new Animal('Cat').name // 错误: 'name' 是私有的.
    protected

    protected与private修饰符的行为很相似,但有一点不同, protected成员在派生类中仍然可以访问。

    class Person {
        protected name: string
        constructor(name: string) { this.name = name }
    }
    
    class Employee extends Person {
        private department: string
        public getElevatorPitch() {
            return `my name is ${this.name} and I work in ${this.department}.`
        }
    }
    
    let dick = new Employee("Howard", "Sales")
    console.log(dick.getElevatorPitch());
    console.log(dick.name); // 错误

    readonly修饰符

    readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

    class Octopus {
        readonly name: string;
        constructor (theName: string) {
            this.name = theName;
        }
    }
    let dad = new Octopus("Man with the 8 strong legs");
    dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

    存取器

    TypeScript支持通过getters/setters来截取对对象成员的访问。

    let passcode = "secret passcode";
    
    class Employee {
        private _fullName: string;
    
        get fullName(): string {
            return this._fullName;
        }
    
        set fullName(newName: string) {
            if (passcode && passcode == "secret passcode") {
                this._fullName = newName;
            }
            else {
                console.log("Error: Unauthorized update of employee!");
            }
        }
    }
    
    let employee = new Employee();
    employee.fullName = "Bob Smith";
    if (employee.fullName) {
        alert(employee.fullName);
    }

    抽象类

    抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。

    abstract class Department { ... }
    class AccountingDepartment extends Department { ... }
    
    let department: Department // 允许创建一个对抽象类型的引用
    department = new Department() // 错误: 不能创建一个抽象类的实例
    department = new AccountingDepartment() // 允许对一个抽象子类进行实例化和赋值
  • 【JS】小数精度处理技巧

    JavaScript 使用 IEEE 754 双精度浮点数表示所有数字,这会导致一些常见的小数精度问题,例如 0.1 + 0.2 !== 0.3。以下是处理 JavaScript 小数精度的常用技巧:

    1. 四舍五入方法

    // toFixed方法(返回字符串)
    let num = 0.1 + 0.2; // 0.30000000000000004
    let fixed = num.toFixed(2); // "0.30"
    
    // 使用Math.round四舍五入
    function round(value, decimals) {
      return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
    }
    round(1.005, 2); // 1.01

    2. 精度运算库

    使用专门的数学库处理精确计算:

    • decimal.js —— 功能相对简单,适合基本的高精度计算需求
    • big.js —— 支持三角函数、指数、对数等高级数学运算
    // 使用decimal.js示例
    import Decimal from 'decimal.js';
    let sum = new Decimal(0.1).add(new Decimal(0.2)); // 0.3

    3. 整数运算技巧

    将小数转换为整数进行计算,再转换回去:

    function add(num1, num2) {
      const multiplier = Math.pow(10, Math.max(getDecimalLength(num1), getDecimalLength(num2)));
      return (num1 * multiplier + num2 * multiplier) / multiplier;
    }
    
    function getDecimalLength(num) {
      const decimalStr = num.toString().split('.')[1];
      return decimalStr ? decimalStr.length : 0;
    }

    4. 比较数字时的容差方法

    function numbersEqual(a, b, tolerance = 1e-10) {
      return Math.abs(a - b) < tolerance;
    }

    5. 格式化显示

    // 使用Intl.NumberFormat
    const formatter = new Intl.NumberFormat('en-US', {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2
    });
    formatter.format(0.1 + 0.2); // "0.30"

    6. 避免常见陷阱

    • 不要直接比较浮点数
    • 对于货币计算,通常使用整数表示分(如用100表示1.00元)
    • 大整数计算考虑使用BigInt类型

    对于简单的显示问题,toFixed可能足够;对于复杂的财务计算,建议使用专门的库。

  • 【JS】判断登录设备是移动端还是PC端

    基于 navigator.userAgent 的 User Agent 检测

    function isMobileDevice () {
      const ua = navigator.userAgent || navigator.vendor || window.opera;
      const mobileRegex = /android|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile|windows phone|phone|webos|kindle|tablet/i;
      return mobileRegex.test(ua.toLowerCase());
    }

    通过 navigator.userAgent 获取浏览器标识字符串‌。正则表达式匹配移动端关键词(如 android、iphone、mobile 等)‌。返回 true 表示移动端,false 表示 PC 端。

    ‌基于 navigator.userAgent 的插件方案‌

    插件:mobile-detect.js

    插件通过解析 navigator.userAgent 中的设备标识符(如 Android、iPhone、Windows 等)实现设备判断‌。 支持更细分的设备类型检测(如平板、手机、PC)‌。

    import MobileDetect from 'mobile-detect';
    
    const md = new MobileDetect(navigator.userAgent);
    
    if (md.mobile()) {
      console.log("移动端登录设备(手机/平板)");
    } else if (md.tablet()) {
      console.log("平板设备");
    } else {
      console.log("PC 端设备");
    }
  • 【Webpack】学习笔记

    构建作用

    构建工具就是将源代码转换成可执行的 JavaScript、CSS、HTML 代码,包括以下内容:

    • 代码转换:将 TypeScript 编译成 JavaScript、将 SCSS 编译成 CSS 等;
    • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等;
    • 代码分割:提取多个页面的公共代码,提取首屏不需要执行部分的代码,让其异步加载;
    • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要通过构建功能将模块分类合并成一个文件;
    • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器;
    • 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过;
    • 自动发布:更新代码后,自动构建出线上发布代码并传输给发布系统。

    基本概念

    • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。

    • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。

    • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。

    • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。

    • Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

    • module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。

    • filename 指列在 entry 中,打包后输出的文件的名称;

    • chunkFilename 指未列在 entry 中,却又需要被打包出来的文件的名称。

    • webpackChunkName 是为预加载的文件取别名,

    • webpackPrefetch 会在浏览器闲置下载文件,

    • webpackPreload 会在父 chunk 加载时并行下载文件。

    • hash 计算与整个项目的构建相关;

    • chunkhash 计算与同一 chunk 内容相关;

    • contenthash 计算与文件内容本身相关。(注:chunkhash, contenthash不能同时和HMR使用)

    • source-map 大而全,啥都有,就因为啥都有可能会让 webpack 构建时间变长,看情况使用;

    • cheap-module-eval-source-map 这个一般是开发环境(dev)推荐使用,在构建速度报错提醒上做了比较好的均衡;

    • cheap-module-source-map 一般来说,生产环境是不配 source-map 的,如果想捕捉线上的代码报错,我们可以用这个。

    流程概括

    Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

    1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
    2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
    3. 确定入口:根据配置中的 entry 找出所有的入口文件;
    4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
    5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
    6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
    7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

    在以上过程中,Webpack 会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

    流程细节

    Webpack 的构建流程可以分为以下三大阶段:

    1. 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
    2. 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
    3. 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

    关键的配置

    • context:上下文。
    • entry:入口文件,是所有依赖关系的入口,webpack 从这个入口开始静态解析,分析模块之间的依赖关系。
    • output:打包输出的配置。
    • devtools:SourceMap 选项,便于开发模式下调试。
    • watch:监听模式,增量更新,开发必备!
    • profile:优化。
    • cache:webpack构建的过程中会生成很多临时的文件,打开 cache 可以让这些临时的文件缓存起来,从而更快的构建。
    • module.loaders:如前文介绍,loaders 用来对文件做预处理。这样 webpack 就可以打包任何静态文件。
    • resolve.alias:模块别名,这样可以更方便的引用模块。
    • plugins:如前文介绍,webpack 的一些内置功能均是以插件的形式提供。
  • 【JS】事件循环(Event Loop)

    核心概念

    事件循环是 JavaScript 处理异步操作的机制,它使得单线程的 JS 能够实现非阻塞 I/O 操作。

    三大关键组成部分

    1. 调用栈(Call Stack):

      • 执行同步代码的地方
      • 后进先出(LIFO)结构
    2. 任务队列(Task Queues):

      • 宏任务队列:setTimeout、setInterval、I/O 等
      • 微任务队列:Promise.then、MutationObserver 等
    3. 事件循环机制:

      • 不断检查调用栈和任务队列
      • 决定何时执行哪个任务

    执行流程

    1. 从宏任务队列取出一个任务执行(如 script 整体代码)
    2. 执行过程中:
      • 同步代码立即执行
      • 遇到异步操作,注册相应回调
    3. 当前宏任务执行完毕:
    • 立即执行所有微任务
    • 如果微任务产生新微任务,继续执行直到队列清空
    1. 必要时进行UI 渲染
    2. 从宏任务队列取下一个任务,开始新循环

    执行顺序示例

    console.log('1. 同步代码'); // 同步
    
    setTimeout(() => console.log('4. 宏任务'), 0); // 宏任务
    
    Promise.resolve().then(() => console.log('3. 微任务')); // 微任务
    
    console.log('2. 同步代码'); // 同步
    
    /* 输出顺序:
    1. 同步代码
    2. 同步代码
    3. 微任务
    4. 宏任务
    */

    重要特点

    • 微任务优先级高于宏任务:每执行完一个宏任务,就会清空微任务队列
    • 避免阻塞:长时间同步代码会阻塞事件循环
    • 不同环境差异:Node.js 与浏览器实现有细微差别

    理解事件循环是掌握 JavaScript 异步编程的基础,有助于编写高效、无阻塞的代码。

  • 【JS】常用正则表达式

    数字

    判断数字
    ^[0-9]*$
    n位的数字
    ^\d{n}$
    至少n位的数字
    ^\d{n,}$
    m-n位的数字:
    ^\d{m,n}$
    零和非零开头的数字
    ^(0|[1-9][0-9]*)$
    非零开头的最多带两位小数的数字
    ^([1-9][0-9]*)+(\.[0-9]{1,2})?$
    带1-2位小数的正数或负数
    ^(\-)?\d+(\.\d{1,2})$
    正数、负数、和小数
    ^(\-|\+)?\d+(\.\d+)?$
    有两位小数的正实数
    ^[0-9]+(\.[0-9]{2})?$
    有1~3位小数的正实数
    ^[0-9]+(\.[0-9]{1,3})?$
    非零的正整数
    ^[1-9]\d*$
    ^([1-9][0-9]*){1,3}$
    ^\+?[1-9][0-9]*$
    非零的负整数
    ^\-[1-9][]0-9"*$
    ^-[1-9]\d*$
    非负整数
    ^\d+$
    ^[1-9]\d*|0$
    非正整数
    ^-[1-9]\d*|0$
    ^((-\d+)|(0+))$
    非负浮点数
    ^\d+(\.\d+)?$
    ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
    非正浮点数
    ^((-\d+(\.\d+)?)|(0+(\.0+)?))$
    ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
    正浮点数
    ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$
    ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
    负浮点数
    ^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$
    ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
    浮点数
    ^(-?\d+)(\.\d+)?$
    ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$

    字符

    汉字
    ^[\u4e00-\u9fa5]{0,}$
    英文和数字
    ^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
    长度为3-20的所有字符
    ^.{3,20}$
    由26个英文字母组成的字符串
    ^[A-Za-z]+$
    由26个大写英文字母组成的字符串
    ^[A-Z]+$
    由26个小写英文字母组成的字符串
    ^[a-z]+$
    由数字和26个英文字母组成的字符串
    ^[A-Za-z0-9]+$
    由数字、26个英文字母或者下划线组成的字符串
    ^\w+$ 或 ^\w{3,20}$
    中文、英文、数字包括下划线
    ^[\u4E00-\u9FA5A-Za-z0-9_]+$
    中文、英文、数字但不包括下划线等符号
    ^[\u4E00-\u9FA5A-Za-z0-9]+$
    ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
    可以输入含有^%&’,;=?$\"等字符
    [^%&',;=?$\x22]+
    禁止输入含有~的字符
    [^~\x22]+

    常用验证

    Email地址
    ^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
    域名
    [a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?
    Internet URL
    [a-zA-z]+://[^\s]*
    ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
    国内电话号码(0511-4405222、021-87888822)
    \d{3}-\d{8}|\d{4}-\d{7}
    身份证号(15位、18位数字),最后一位是校验位,可能为数字或字符X
    (^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)
    帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线)
    ^[a-zA-Z][a-zA-Z0-9_]{4,15}$
    密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线)
    ^[a-zA-Z]\w{5,17}$
    强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在 8-10 之间)
    ^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[a-zA-Z0-9]{8,10}$
    强密码(必须包含大小写字母和数字的组合,可以使用特殊字符,长度在8-10之间)
    ^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$
    日期格式
    ^\d{4}-\d{1,2}-\d{1,2}
    一年的12个月(01~09和1~12)
    ^(0?[1-9]|1[0-2])$
    一个月的31天(01~09和1~31)
    ^((0?[1-9])|((1|2)[0-9])|30|31)$
    xml文件
    ^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
    中文字符的正则表达式
    [\u4e00-\u9fa5]
    双字节字符
    [^\x00-\xff]

    (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))

    空白行的正则表达式
    \n\s*\r

    (可以用来删除空白行)

    HTML标记的正则表达式
    <(\S*?)[^>]*>.*?|<.*? /> 

    ( 首尾空白字符的正则表达式:^\s|\s$或(^\s)|(\s$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)

    中国邮政编码
    [1-9]\d{5}(?!\d)

    (中国邮政编码为6位数字)

    IP地址
    ((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))
    钱的输入格式:
    • 有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":
        ^[1-9][0-9]*$
    • 这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:
        ^(0|[1-9][0-9]*)$
    • 一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:
        ^(0|-?[1-9][0-9]*)$
    • 这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧。下面我们要加的是说明可能的小数部分:
        ^[0-9]+(.[0-9]+)?$
    • 必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:
        ^[0-9]+(.[0-9]{2})?$
    • 这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:
        ^[0-9]+(.[0-9]{1,2})?$
    • 这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:
        ^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$
    • 1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:
        ^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$

    备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里