跳到主要内容

总结-JavaScript

设计模式

模式名作用简述JS 示例
单例模式保证一个类只有一个实例全局状态管理器、EventBus
工厂模式创建对象时,隐藏复杂的构造逻辑Axios 请求实例封装
观察者模式一对多通知机制plugin 生命周期触发、Vue 响应式
发布-订阅模式解耦多个模块的事件传递EventEmitter、Redux 的中间件
策略模式封装一组可互换算法表单校验规则、折扣策略
装饰器模式动态给对象添加功能中间件链、函数增强(如节流、防抖)
适配器模式让两个接口不兼容的模块协作接口格式转换、封装旧 API
命令模式将操作封装成对象,支持撤销/重做编辑器撤销、浏览器前进后退
代理模式控制对对象的访问Vue 3 的 Proxy 实现响应式

插件机制中常见的生命周期 Hook 调用方式,核心体现的是观察者模式:主程序维护一套 Hook 系统,插件注册为观察者,在特定生命周期被自动调用。它也结合了发布-订阅模式(用于解耦)和策略模式(用于灵活切换扩展点行为),是多种设计模式的协同产物。

  • 发布-订阅:通过事件总线中转,插件与主程序之间不直接通信,而是通过事件中心转发;
  • 策略模式:每个生命周期 hook 可以视为一个策略点,插件注入的不同实现策略不同。

🍊 四套查找规则

  • 作用域:静态关系。与代码结构和闭包有关,决定了作用域链(全局、函数、块作用域);
    • 查变量名、闭包访问、箭头函数 this 都依赖它。
  • 调用栈:动态时产物。在运行时,每执行一个函数,就会创建函数执行上下文,放入栈中;
    • 影响执行顺序、错误堆栈、上下文环境等。
  • this:调用者决定。每个函数执行上下文都有一个 this。func.call(this, …args)
    • 与调用方式有关:new、显式(apply, call, bind)、隐式(obj.fn())、默认绑定(fn())
  • 原型链:通过 [[prototype]] 连接起来的对象,构成了原型链;
    • 对象内属性的调用从原型链中查;

JS 的单线程

  • 初衷:JavaScript 是为浏览器交互而生的。职责是操作 DOM、响应用户事件、控制页面弹窗等行为。如果是多线程,可能存在多个线程改同一个 DOM,浏览器需要实现线程锁机制。这增加了开发成本,设计过重。
  • DOM 是非线程安全的。浏览器的渲染引擎(Blink)和 JS 引擎都可以共享这块儿资源。如果 JS 多线程,无法和渲染引擎进行协调。所以规定 DOM 操作只能在主线程完成。

闭包

问题一:什么是闭包?

  1. 变量的集合:堆内存中的闭包

    在 JavaScript 中,根据词法作用域中作用域嵌套的规则,如果外部函数内嵌套了一个内部函数,那么内部函数总是可以访问外部函数中声明的变量。

    当通过调用一个外部函数返回一个内部函数后,即使这个外部函数已经执行结束了,但是若内部函数引用了外部函数的变量,那么这些被引用的变量会依然保存在内存中,我们就把这些变量的集合称为闭包。

    即时删除了当前执行上下文,其生成的闭包依然被其他环境持有,所以不会被销毁。

  2. 执行上下文的精简:

    在即将销毁一个执行上下文时,如果该执行上下文内部有被其他作用域引用的变量,则会把这些变量进行打包,只销毁用不到的用不到变量和其他结构,就这样形成了一个闭包 Closure。

  3. 作用域链的一环:内存管理(垃圾回收)

    查找顺序:当前词法环境(块作用域)、当前变量环境(函数作用域)、外部环境 outer(闭包、执行上下文。

问题二:为什么要放在堆内存中?

涉及到 js 的 内存管理垃圾回收 机制。

问题三:闭包的应用场景

原理:内部函数,使用外部函数的变量,就形成了闭包。 目的:私有化数据,把变量放在私有化函数里,通过私有化来保持数据。

  1. 节流、防抖:保持对回调函数、timer 的引用。🔗

    • 通过 timer 记录最近一次触发事件的时机。使用 timer 私有化定时器,保持对定时器时间的引用,保持数据。确定触发时机后, 会调用回调函数。

      节流,降低事件的触发的频率。防抖,确保事件必须大于延迟事件后才触发。

  2. 柯里化(高阶函数):🔗单一职责原则,把多步骤逻辑进行拆分,达到复用。闭包保存了每个步骤的处理结果,以便调用下一个逻辑时使用,达到了复用效果。

    • 柯里化的使用场景:log 日志打印
  3. 高阶函数:满足任意条件: 1:函数可以作为参数被传递 2:函数可以作为返回值输出

问题四:闭包的缺点

内存泄露。闭包延长了变量的生命周期,使这些原本应当销毁的变量继续留存在内存中。当这些变量不再需要时,如果依然保持引用,则变量依然没有销毁,变量多余的生命周期就是内存泄漏。

  • 解决:当闭包不再使用时,保持的引用赋值为 null 等其他值,只要解除对闭包的引用,即可在被 GC 回收。

对于一个闭包来说,只要有一个函数执行上下文保持对它的引用。这个闭包就无法被 GC 回收。

如果大量使用闭包,且有常驻调用栈的函数大量引用了闭包,这导致大量闭包长期保存在堆内存中,消耗大量内存。最终影响网页的性能。

所以,闭包的使用有可能会占用大量内存、进而造成内存泄漏。

问题五:内存泄露的情况

  • 定时器设置后没有及时销毁。setTimeout, setInterval
  • DOM 引用。用 js 进行 DOM 操作时,会使用变量缓存 DOM 节点。移除节点的时候,没有同步释放引用。
  • 使用 Map 和 Set。存储对象时,因对对象保持引用,不能销毁。而应当使用 weakMap、weakSet。
  • 绑定非常多的事件监听,但不再使用后不及时销毁。

问题六:垃圾回收机制(开头)

let、var 的区别

思路:let、var、const、function 一起讲。

提升与暂时性死区:🔗

  • 暂时性死区:标准规定,letconst 创建的变量仅允许创建提升,不允许初始化和赋值提升。在变量仅创建、尚未初始化和赋值的阶段,不允许 JS 引擎访问,此时处于暂时性死区,报错:ReferenceError
  • function 函数的创建、初始化和赋值均会被提升。
  • var 变量的创建和初始化被提升,赋值不会被提升。
  • letconst 变量的创建被提升,初始化和赋值不会被提升。它们被提升到了块作用域中。

其他:版本不同、作用域不同。

箭头函数

箭头函数用 new

  1. 讲述 new 的过程;
    • 创建一个新对象 obj = {}
    • 新对象的原型链绑定为构造函数的 prototype:obj.__proto__ = Fn.prototype
    • 绑定 this ,然后调用构造函数:Fn.call(obj, "xxxxx")
    • 返回对象 obj,如果构造函数有返回新对象,就返回新对象。
  2. 箭头函数没有自己的 this、arguments、prototype,不是构造函数,自然就不能用 new。
    1. 报错:TypeError: ArrowFn is not a constructor
  3. 所以,天生没有 [[Construct]] 构造能力

作为构造函数

和上一个问题相同:

  1. 构造函数的本质
    • 能被 new 调用的函数,必须具有内部的 [[Construct]] 方法;
    • 同时拥有 prototype 属性(用于构造出的对象的原型链)。
  2. **不能,**因为它们没有构造能力(没有 [[Construct]] 内部方法),所以不能使用 new 调用。

和普通函数的区别

特性普通函数 function箭头函数 ()=>{}
是否可构造(能否 new)✅ 可以(有 [[Construct]])❌ 不可以(报错)
是否有 this✅ 有自己的 this(动态绑定)❌ 没有自己的 this,继承外层作用域
是否有 arguments✅ 有❌ 没有(用 rest 参数代替)
是否有 super✅ 有(在 class 中)❌ 没有(继承外层)
是否有 prototype✅ 有❌ 没有
适合用作构造函数 / 回调 / 普通函数回调函数 / 内层闭包函数
this 指向调用者决定定义时所在作用域决定(词法作用域)

JS 计算

MAX_SAFE_INTEGER

Js 用双精度 64 位浮点格式 Double-precision floating-point format,表示数字。

JS 中的Number类型只能安全地表示-9007199254740991 (-(2^53-1))9007199254740991(2^53-1)之间的整数,任何超出此范围的整数值都可能失去精度。

  • Number.MAX_SAFE_INTEGER 常量,表示最大安全 整数 Number.MIN_SAFE_INTEGER 常量,表示最小安全整数
  • Number.MAX_VALUE 常量,表示最大数,属性值接近于 1.79E+308。大于 MAX_VALUE 的值代表 "Infinity"。

javascript 中的数都是用 IEEE754 标准的双精度浮点数来表示的:

img[60]

sign 为 0 表示正数,为 1 表示负数;exponent 表示科学计数法中的指数部分,实际存储的时候必须加上一个偏移值 1023;fraction 表示小数点后的部分,整数部分永远为 1,计算机不存储,但是运算的时候会加上。

// 最大数:
(Math.pow(2, 53) - 1) *
Math.pow(
2,
971
)(
// 1.7976931348623157e+308
Math.pow(2, 53) - 1
) *
Math.pow(2, 971) ===
Number.MAX_VALUE; // true

// 最大安全整数:
Math.pow(2, 53) - 1 === Number.MAX_SAFE_INTEGER; // true
2 ** 53 - 1 === Number.MAX_SAFE_INTEGER; // true

0.1 + 0.2 == 0.3 的判断,使用最小精度值:

Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON;

js 中怎么表示浮点数?

  • parseFloat(number/string):解析一个参数(必要时先转换为字符串)并返回一个浮点数。

  • parseInt(string, radix) :解析一个字符串并返回指定基数的十进制整数。

    • radix 是 2-36 之间的整数,表示被解析字符串的基数。

    • 默认为 10, 例如 16 是十六进制数,尽量指定。

parseFloat(3.14);
parseFloat("3.14");
parseFloat(" 3.14 ");
parseFloat("314e-2");
parseFloat("0.0314E+2");
parseFloat("3.14some non-digit characters");
parseFloat({
toString: function () {
return "3.14";
},
});
// 均为 3.14

JS 为什么有浮点精度问题

原因:JavaScript 使用 IEEE 754 双精度浮点数(64 位)表示所有数字,小数在二进制中无法精确表示,从而造成精度误差。

  • 并不是所有的十进制小数,都能被精确地转化为二进制小数。浮点数只能保留有限位,所以 JS 中实际存储的是一个近似值。

🤔 那么如何解决呢?

  • 转换成整数计算,必要时可以用 bigInt 计算整数;
  • 使用 toFixed() 或四舍五入处理显示,但返回值变成了字符串;
  • 使用第三方计算库(如 Big.js、Decimal.js、bignumber.js);

0.1+0.2=?0.6-0.4=?

JavaScript浮点数精度问题示例图 [30]

解决 0.1 + 0.2 的问题,用 toFixed(num)

  • num.toFixed(num):**截断。**保留 num 位小数,格式化一个数值。
  • 入参:默认为 0;出参:string 返回字符串。
// 解决
(0.1 + 0.2).toFixed(2).; // '0.30' string
parseFloat((0.1 + 0.2).toFixed(2)) // 0.3


// 常用
function financial(x) {
return Number.parseFloat(x).toFixed(2);
}

New

Symbol/BigInt 不能 new

Symbol 和 BigInt 虽然没有对应的「包装构造函数」可通过 new 使用,但它们依然拥有包装对象类型,并可调用方法。只是 new 语法被禁止。

  • 但是他们和下边的 Number 等一样,也有包装类型,只是不能 new 一个包装类型出来。

在引用类型中,有三种原始值包装类型:String、Number、Boolean。 他们既可以用 new 创建,也可以不使用 new 创建。

  • 原始值:基本数据类型。直接创建,"abc"、123 等不是对象;
  • 原始值包装类型 :包装类型。用 new 创建,原始值包装成包装类型。
const num = new Number(123); // 包装类型
Object.prototype.toString.call(num); // '[object Number]' 包装类型,是一个对象
num == 123; // true
num === 123; // false

包装类型的主要目的,是可以让基本数据类型调用包装方法,自动装箱/拆箱

// 自动装箱/拆箱
"abc".length(
// 3 让不能具有方法的 string,可以使用 length 属性
123
).toString; // '123'

Fn 是否通过 new 调用

  1. (ES6)在调用 Fn 时,内部可通过 new.target === true 判断。只在构造函数执行时存在

    function Fn() {
    new.target ? console.log("是通过 new 调用") : console.log("不是通过 new 调用");
    }
    Fn(); // 不是通过 new 调用
    new Fn(); // 是通过 new 调用
  2. this instanceof Fn,new 执行时,构造函数的原型链,绑定在 Fn.prototype 上。

    • 如果是 new Fn(),则 this.__proto__ === Fn.prototype 成立;
    • 如果调用 Fn 时用 call, apply 改变 this 则回不准确。
  3. 直接判断 this ,如果 this 是 window,undefined 则直接调,不是则通过 new。

类型判断

区分 Array 和 Object

  • Array.isArray(value) 区分数组;
  • Object.prototype.toString.call(val)"[object Array]""[object Object]"
  • 构造函数:[].constructor === Array({}).constructor === Object
    • instanceof 也是:[] instanceof Array // true
  • 不能用 typeof,这个是判断基本数据类型的方法。

类型判断的方式

  • typeof 操作符。可以判断基本数据类型值。

    • 可以判断基本数据类型 + function,其余都是 object
  • instance1 instenceof Father 操作符。

    • 判断引用数据类型
    • 本质:判断 instance1 的原型链上是否有 Father.prototype 原型对象。
  • Father.protoype.isProtoypeOf(instance1)。是 instanceof 的替代品,表意更明确

  • Object.getPrototypeOf(instance1)。获取 Instance1 的构造函数的原型对象

    • 相当于:instance1.__proto__
  • Object.prototype.toString.call(instance1)函数。获取 instance1 的引用类型名称,替代 instanceof 操作符。

    • 最准确用法,防止 Object.create(null) 创建,或者修改原型链,导致无法追踪
  • 更多.

// toString 返回更适合观察的打印值  ==> string 类型
[1, 2, 3].toString(); // "1,2,3"
[].toString(); // ""
[10].toString(); // "10"
new Date().toString(); // "Sun Apr 18 2021 21:23:13 GMT+0800 (中国标准时间)"

// valueOf 返回原始值,或该对象本身 ==> 自身类型
[1, 2, 3].valueOf(); // (3) [1, 2, 3] 返回自身
new Date().valueOf(); // 1618753134122 毫秒数

js 为什么要有引用类型?

本质上是讨论:为什么不能所有值都用“基本类型”(如 Number、String、Boolean)表示,非要引入对象(Object)、数组(Array)、函数(Function)等“引用类型”呢?

  • 基本类型:变量保存的是具体的值,无法两个变量无法共同指向同一个值;
  • 引用类型:变量保存的是一个堆内存中的引用地址,多个变量可以指向同一个对象;

原因:

  1. 继承和引用。所有函数、类、实例、闭包,这些需要继承机制的东西,都需要引用类型。
  2. 复杂结构。这样设计,就可以利用对象构建出更复杂的数据结构,Js 一切类型皆对象。
  3. 共享和传递。多处代码引用同一个对象,实现状态共享、数据同步。

遍历对象的所有属性

  1. for...in...:es3 操作符。遍历自身的 + 继承的,可枚举属性,不包含 symbol;
  2. Object.keys():es6 操作符:遍历自身的,可枚举属性值,不包含 symbol;

可枚举 / 不可枚举

对象是否有某个属性

const a = {test:123}

// in 操作符
"test" in a // true ⚠️原型链上的也包含

// 获得属性的方法
a.hasOwnProperty('test'); // true

// 转数组+判断
Object.keys(a).indexOf("test"); // 0
Object.keys(a).includes("test"); // true
// 转 map set + 判断
new Map(Object.entries(a)).has("test"); // true
new Set(Object.keys(a)).has("test"); // true

// 不考虑原型链,只看自身
// - 可枚举:Symbol.iterator
for...of
Object.keys(object);
Object.values(object);
Object.entries(object);
// - 不区分枚举
Object.getOwnPropertyNames() // 属性
Object.getOwnPropertySymbols() // Symbol属性
// - Reflect
Reflect.ownKeys() // 全部属性名:属性 + Symbol属性

WeakSet/Map 使用场景

weak 特点:对象约束(成员只能是对象)、弱引用(方便 GC 垃圾回收)、无法遍历、无法清空。

  • 注意:WeakMap 只支持 object 作为成员的 key。

目的:避免内存泄漏

  • 关联某个对象的元数据,但不想影响该对象的垃圾回收时,用 WeakMap。

使用场景:用来收集和统计数据

  • 例如缓存 DOM 元素的状态、绑定事件处理器数据,避免因为缓存导致内存无法释放;
  • 这样做,可以省去每次销毁对象时,还需要在绑定它的 map 上去做引用解除;
  • 典型的 Vue3 响应式,就使用 WeakMap 去跟踪对象的依赖关系的。

什么是栈空间、堆空间

在 JavaScript 的运行时, 主要有三种类型内存空间:代码空间、栈空间、堆空间。

栈内存:存放基本类型的变量,对象的引用和方法调用,遵循先入后出的原则。

  • 体积小,速度快。

堆内存:存放所有引用类型、闭包 (是一个集合) 变量。

  • 在运行时,Js 会在堆中 动态地 申请某个大小的内存空间。堆内存实际上指的就是(满足堆内存性质的)优先队列的一种数据结构,即更靠前的元素有更高的优先权。
  • 操作系统中:空闲内存通过链表登记保存。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的分区,切分申请的大小,将剩余的分区再链接到链表上。
    • 根据链表的形式,有:
      • (使用)首次适应算法:链表按地址递增串联。 查表顺序从 0 开始;
      • 临近适应算法:链表首位相连,查表则从上一次查找位置开始;
      • 最佳适应算法:链表按空间递增串联。查表也从小到大开始;
      • 最差适应算法:链表按空间递减串联。查表也从大到小开始,先用大的。
  • 有闭包的存在,容易有内存泄露产生。

Symbol 的作用

在 ES6 前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突:

  • 如,在某个外来对象中添加一个新属性,但是我们在不确定它原来内部有什么内容的情况下,很容易造成命名冲突,从而覆盖掉它内部的某个属性;
  • 如,手写 js 中的 apply、call、bind 实现时,要添加一个 fn 属性,如果它内部原来已经有了 fn 属性就会发生覆盖;
  • 如,开发中使用混入,如果混入的多个对象间出现了同名属性,必然有一个会被覆盖掉;

Symbol 用来生成一个 独一无二的值

分页拉取-数据重复

解决有三个思路:

  • 前端一次获得完整的数据集,自行分割
  • 固定数据集范围
    • 后端解决:比如 15 分钟间隔,更新一次数据集,日志和时间戳绑定。
  • 无限滚动的页面,前端拼接内容时,过滤掉重复的数据集。