总结-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()
)
- 与调用方式有关:new、显式(apply, call, bind)、隐式(
- 原型链:通过
[[prototype]]
连接起来的对象,构成了原型链;- 对象内属性的调用从原型链中查;
JS 的单线程
- 初衷:JavaScript 是为浏览器交互而生的。职责是操作 DOM、响应用户事件、控制页面弹窗等行为。如果是多线程,可能存在多个线程改同一个 DOM,浏览器需要实现线程锁机制。这增加了开发成本,设计过重。
- DOM 是非线程安全的。浏览器的渲染引擎(Blink)和 JS 引擎都可以共享这块儿资源。如果 JS 多线程,无法和渲染引擎进行协调。所以规定 DOM 操作只能在主线程完成。
闭包
问题一:什么是闭包?
-
变量的集合:堆内存中的闭包
在 JavaScript 中,根据词法作用域中作用域嵌套的规则,如果外部函数内嵌套了一个内部函数,那么内部函数总是可以访问外部函数中声明的变量。
当通过调用一个外部函数返回一个内部函数后,即使这个外部函数已经执行结束了,但是若内部函数引用了外部函数的变量,那么这些被引用的变量会依然保存在内存中,我们就把这些变量的集合称为闭包。
即时删除了当前执行上下文,其生成的闭包依然被其他环境持有,所以不会被销毁。
-
执行上下文的精简:
在即将销毁一个执行上下文时,如果该执行上下文内部有被其他作用域引用的变量,则会把这些变量进行打包,只销毁用不到的用不到变量和其他结构,就这样形成了一个闭包 Closure。
-
作用域链的一环:内存管理(垃圾回收)
查找顺序:当前词法环境(块作用域)、当前变量环境(函数作用域)、外部环境 outer(闭包、执行上下文。
问题二:为什么要放在堆内存中?
涉及到 js 的 内存管理 和 垃圾回收 机制。
问题三:闭包的应用场景
原理:内部函数,使用外部函数的变量,就形成了闭包。 目的:私有化数据,把变量放在私有化函数里,通过私有化来保持数据。
-
节流、防抖:保持对回调函数、timer 的引用。🔗
-
通过 timer 记录最近一次触发事件的时机。使用 timer 私有化定时器,保持对定时器时间的引用,保持数据。确定触发时机后, 会调用回调函数。
节流,降低事件的触发的频率。防抖,确保事件必须大于延迟事件后才触发。
-
-
柯里化(高阶函数):🔗单一职责原则,把多步骤逻辑进行拆分,达到复用。闭包保存了每个步骤的处理结果,以便调用下一个逻辑时使用,达到了复用效果。
- 柯里化的使用场景:log 日志打印
-
高阶函数:满足任意条件: 1:函数可以作为参数被传递 2:函数可以作为返回值输出
问题四:闭包的缺点
内存泄露。闭包延长了变量的生命周期,使这些原本应当销毁的变量继续留存在内存中。当这些变量不再需要时,如果依然保持引用,则变量依然没有销毁,变量多余的生命周期就是内存泄漏。
- 解决:当闭包不再使用时,保持的引用赋值为
null
等其他值,只要解除对闭包的引用,即可在被 GC 回收。
对于一个 闭包来说,只要有一个函数执行上下文保持对它的引用。这个闭包就无法被 GC 回收。
如果大量使用闭包,且有常驻调用栈的函数大量引用了闭包,这导致大量闭包长期保存在堆内存中,消耗大量内存。最终影响网页的性能。
所以,闭包的使用有可能会占用大量内存、进而造成内存泄漏。
问题五:内存泄露的情况
- 定时器设置后没有及时销毁。
setTimeout
,setInterval
- DOM 引用。用 js 进行 DOM 操作时,会使用变量缓存 DOM 节点。移除节点的时候,没有同步释放引用。
- 使用 Map 和 Set。存储对象时,因对对象保持引用,不能销毁。而应当使用 weakMap、weakSet。
- 绑定非常多的事件监听,但不再使用后不及时销毁。
问题六:垃圾回收机制(开头)
let、var 的区别
思路:let、var、const、function 一起讲。
提升与暂时性死区:🔗
- 暂时性死区:标准规定,
let
和const
创建的变量仅允许创建提升,不允许初始化和赋值提升。在变量仅创建、尚未初始化和赋值的阶段,不允许 JS 引擎访问,此时处于暂时性死区,报错:ReferenceError
。 function
函数的创建、初始化和赋值均会被提升。var
变量的创建和初始化被提升,赋值不会被提升。let
和const
变量的创建被提升,初始化和赋值不会被提升。它们被提升到了块作用域中。
其他:版本不同、作用域不同。
箭头函数
箭头函数用 new
- 讲述 new 的过程;
- 创建一个新对象
obj = {}
; - 新对象的原型链绑定为构造函数的 prototype:
obj.__proto__ = Fn.prototype
- 绑定
this
,然后调用构造函数:Fn.call(obj, "xxxxx")
; - 返回对象
obj
,如果构造函数有返回新对象,就返回新对象。
- 创建一个新对象
- 箭头函数没有自己的 this、arguments、prototype,不是构造函数,自然就不能用 new。
- 报错:
TypeError: ArrowFn is not a constructor
- 报错:
- 所以,天生没有 [[Construct]] 构造能力
作为构造函数
和上一个问题相同:
- 构造函数的本质
- 能被 new 调用的函数,必须具有内部的 [[Construct]] 方法;
- 同时拥有 prototype 属性(用于构造出的对象的原型链)。
- **不能,**因为它们没有构造能力(没有 [[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 标准的双精度浮点数来表示的:
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=?
解决 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 调用
-
(ES6)在调用 Fn 时,内部可通过
new.target === true
判断。只在构造函数执行时存在function Fn() {
new.target ? console.log("是通过 new 调用") : console.log("不是通过 new 调用");
}
Fn(); // 不是通过 new 调用
new Fn(); // 是通过 new 调用 -
this instanceof Fn
,new 执行时,构造函数的原型链,绑定在 Fn.prototype 上。- 如果是 new Fn(),则
this.__proto__ === Fn.prototype
成立; - 如果调用 Fn 时用 call, apply 改变 this 则回不准确。
- 如果是 new Fn(),则
-
直接判断
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
- instanceof 也是:
- 不能用
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)等“引用类型”呢?
- 基本类型:变量保存的是具体的值,无法两个变量无法共同指向同一个值;
- 引用类型:变量保存的是一个堆内存中的引用地址,多个变量可以指向同一个对象;
原因:
- 继承和引用。所有函数、类、实例、闭包,这些需要继承机制的东西,都需要引用类型。
- 复杂结构。这样设计,就可以利用对象构建出更复杂的数据结构,Js 一切类型皆对象。
- 共享和传递。多处代码引用同一个对象,实现状态共享、数据同步。
遍历对象的所有属性
for...in...
:es3 操作符。遍历自身的 + 继承的,可枚举属性,不包含 symbol;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 分钟间隔,更新一次数据集,日志和时间戳绑定。
- 无限滚动的页面,前端拼接内容时,过滤掉重复的数据集。