一、数据类型
- 基本类型 VS 引用类型
基本类型:
String/Number/Boolean/Null/Undefined/Symbol/BigInt
存在栈中,值传递,赋值是深拷贝,互不影响
引用类型:
Object(包含对象 {}、数组 []、函数 function、Date、RegExp)
栈存地址、堆存数据,引用传递,修改会相互影响,赋值是浅拷贝
堆VS栈
栈:存放基本类型、函数执行上下文,空间小、自动回收
堆:存放引用类型,空间大、垃圾回收
强类型语言VS弱类型语言强类型语言禁止隐式类型转换,类型严格、变量类型固定、更健壮,如Java、C、TypeScript;弱类型语言允许自动隐式转换,类型随时可变、易出 BUG,如JS 。
解释性语言VS编译型语言编译型语言:整体编译、生成可执行文件.exe、运行快、平台绑定、编译期报错。C
解释型语言:逐行翻译、跨平台、开发灵活、运行慢、运行时报错。JS / TS
包装类型包装类型就是 JS 为基本类型(String/Number/Boolean)临时创建的对象,使其能调用属性和方法,用完即销毁;因此基本类型无法添加属性,而包装对象可以。
内置对象(自带、不用自己定义就能直接用的对象,分为三类:标准内置对象、包装对象、全局对象。)基础:Object Array Function
包装:String Number Boolean Symbol BigInt
工具:Math JSON Date RegExp
高级:Promise Map Set WeakMap WeakSet Error
检测方式
typeof:检测基本类型。null 会误判为 object,数组、对象、正则等都返回 object,无法区分
instanceof:基于原型链,检测引用类型(原理:instanceof 用于判断某个对象是否属于某个构造函数的实例,递归查找实例的 _proto _ 原型链,判断是否能找到构造函数的 prototype。
实现:循环获取实例的隐式原型,与构造函数显式原型对比,找到返回 true,到原型链顶端返回 false)
Object.prototype.toString.call ():最精准,可判断所有类型,返回[object Type]
constructor :容易被修改原型,不稳定
Array.isArray():专门判断数组
-
undefined VS null
1.undefined 表示变量已声明但未赋值;null 表示赋值为空,代表 "空对象"。
2.typeof undefined 为 undefined,typeof null 为 object(原因是 JS 最初实现中会用类型标签标识类型,null 在内存中表示为空指针(二进制全 0),而对象的类型标签恰好也是 000,导致 typeof 误判,这是一个历史遗留 bug);
3.转为数字时 undefined 是 NaN,null 是 0
4.安全获取 undefined 推荐使用 void 0,因为 undefined 不是关键字,可能被局部变量覆盖
-
隐式类型转换
运算:+ 遇到字符串转拼接,否则转数字; - * / %一律转数字
比较:== 优先转数字;
条件判断 :if /while 转布尔;
对象先 valueOf 再 toString 转原始值。
-
BigInt
因为Number 类型数值有精度上限,无法安全表示和运算超大整数,因此提出 BigInt 提案,用于表示任意精度的大整数。
-
NaN
typeof NaN 的结果是 number,NaN 是 JavaScript 中唯一一个不等于自身的值
isNaN()先把值转为Number,再判断是不是 NaN
Number.isNaN() 先判断是不是Number,再判断是不是 NaN,更准确
-
Object.is() 与===、== 的区别?
1.== 会隐式转换,比较值
2.=== 比较类型+值,但对 NaN、±0 判断怪异
NaN === NaN → false
+0 === -0 → true
3.Object.is 同 ===,但修复了 两个怪异点
Object.is(NaN, NaN) → true
Object.is(+0, -0) → false
-
|| 和 && 的返回值
|| 返回第一个真值,全假返回最后一个;&& 返回第一个假值,全真返回最后一个。两者都返回操作数本身,而非布尔值。
-
精度缺失
原因:JavaScript 采用 IEEE 双精度浮点数标准,0.1 和 0.2 在二进制中是无限循环小数,存储时会被截断,产生精度丢失,导致相加不等于 0.3。
解决方法:
1.转成整数运算后再转回小数(先乘倍数变成整数,运算后再除回去)
2.使用 toFixed 四舍五入
-
模板字符串
用反引号,支持直接换行、${} 嵌入变量 / 表达式
新增方法
includes(str):是否包含指定字符串
startsWith(str):是否以指定字符串开头
endsWith(str):是否以指定字符串结尾
repeat(n):重复 n 次
padStart(len, str):前面填充
padEnd(len, str):后面填充
trimStart() / trimEnd():去除开头 / 结尾空格
trim():去除首尾空格
js
const str = "hello world";
str.includes("world"); // true
str.startsWith("hello"); // true
str.endsWith("d"); // true
"*".repeat(3); // "***"
"123".padStart(5, "0"); // "00123"
"123".padEnd(5, "0"); // "12300"
- 数组
1.增删改(会改变原数组)
push:末尾添加 → 返回新长度
pop:末尾删除 → 返回被删元素
unshift:开头添加 → 返回新长度
shift:开头删除 → 返回被删元素
splice(start, count, ...items):删除 / 插入 / 替换
slice(start, end):截取
sort:排序
reverse:反转
fill:填充
concat:拼接多个数组
join:转字符串
2.遍历(不改变原数组)
for...in:遍历索引 key
for...of:遍历元素值
forEach:只遍历,不能链式,无返回
map:遍历并返回一个新数组,可以链式调用,常用于数据加工
filter:过滤,返回符合条件的新数组
find:找第一个符合条件元素,找不到返回 undefined
findIndex:找第一个符合条件索引
findLast / findLastIndex:从后找
reduce :累计 arr.reduce((prev,cur)=> prev+cur, 0)
some:有一个满足就 true
every:全部满足才 true
indexOf:从前找索引
includes:判断是否包含某一项,返回布尔值
keys / values / entries:for (const [i,val] of arr.entries()){}
3.静态
Array.isArray:判断是否数组
Array.from:类数组 → 真数组
- for...in VS for...of
for...in 遍历键名/ 索引(key/index),能遍历对象,会遍历原型链,适合对象;
for...of 遍历值(value),不能遍历普通对象,不遍历原型链,适合数组 / Set / Map / 类数组 / 字符串(可迭代对象)。
普通对象不可迭代,不能直接用 for...of;
借助 Object.keys / values / entries 转为数组,即可遍历;
for (const [key, value] of Object.entries(obj)) {}
- Map VS WeakMap VS Object
| 特性 | Object | Map | WeakMap |
|---|---|---|---|
| 键的类型 | 字符串 或 Symbol | 任意类型 | 对象 |
| 键的顺序 | 部分有序 | 按插入顺序遍历 | -- |
| 长度 | Object.keys(obj).length | map.size | 无size |
| 原型 | 有 | 无 | 无 |
| 遍历 | 不可直接迭代,需先获取键 | 原生可迭代(for...of),支持 forEach | 不可遍历 |
| 性能 | 频繁增删性能一般 | 频繁增删性能更优 | -- |
| 引用方式 | 强引用,内存泄漏风险较高 | 弱引用,可 GC 回收,内存泄漏风险极低 |
Map 键类型任意、有序、可直接遍历、高频操作性能更好,强引用,容易内存泄漏,适合动态、键为复杂类型,且需要频繁增删 / 维护顺序的键值对场景。
Object 键仅限String 或 Symbol、有原型属性、可直接 JSON.stringify,适合存储静态的结构化数据
WeakMap 键只能是对象,是弱引用,会被垃圾回收,不可遍历、无 size,主要用于避免内存泄漏
JSON 是一种轻量级、纯文本的数据交换格式,是纯字符串,键必须双引号
常见用途:前后端接口数据传输;配置文件;本地存储 localStorage;深拷贝(JSON.parse(JSON.stringify(obj)))
二、深拷贝 vs 浅拷贝
浅拷贝
定义:只拷贝第一层属性,引用类型共享地址
实现:Object.assign 和扩展运算符都是浅拷贝。
const newObj = {...obj}语法简洁,只能用于对象字面量;
Object.assign(target, obj1, obj2)会修改第一个目标对象,并支持一次性合并多个源对象。
深拷贝
定义:完全拷贝所有层级,互不影响
实现:JSON.parse(JSON.stringify(obj))、递归
扩展运算符
1.数组 / 对象浅拷贝与合并
2.函数传参(将数组直接作为函数的多个参数传入)如求max、min
3.将类数组转为真数组(Array.from()/...)
const nodeList = document.querySelectorAll('div')
const nodeArr = [...nodeList]
4.解构,快速获取剩余元素
- 解构
解构赋值 = 快速从数组 / 对象中提取值,赋值给变量
数组解构按位置顺序取值,对象解构按属性名匹配
都支持默认值、嵌套、剩余参数
//默认值
const [a, b = 20] = [10] // a = 10, b = 20
//嵌套
const { user: { name } } = { user: { name: 'zs' } } // name = 'zs'
//rest 参数( ...变量名)
用于获取函数的多余参数,将其整合为一个数组,必须放在参数末尾,参数个数不确定时使用
| 特性 | arguments | rest 参数 |
|---|---|---|
| 类型 | 类数组对象(不是真数组) | 真正的数组(可直接用 map/filter) |
| 获取 | 获取函数所有传入的参数 | 获取形参之外的剩余参数 |
| 写法 | 无需声明 | 需要通过 ... 显式声明 |
类数组对象
定义:可以通过下标访问、有 length 属性,但不能直接调用数组方法。常见如 arguments、DOM 集合。
转换:Array.from(arrayLike) / [...arrayLike]
原因:arguments 是设计时,只需要索引和 length,所以是轻量级类数组对象;遍历可用 for 循环、for...of、转数组
三、原型链
每个函数有 prototype ,用于挂载公共属性方法,节省内存
每个对象都有 _proto _ ,指向构造函数的 prototype
对象访问属性会通过 proto 逐层向上查找,这条链式结构就是原型链,原型链最终指向 Object.prototype,终点为 null,JS 借助原型与原型链实现继承和方法复用。

原型修改:在原 prototype 上追加 / 修改方法,不破坏原型关系,新旧实例通用,constructor 正常;
原型重写:直接覆盖整个 prototype 为新对象,旧实例失效、constructor 丢失、原原型方法清空,需手动挂载 constructor
开发中优先使用原型追加修改
-
原型链指向:
1.实例 _proto _ ->自身构造函数的 prototype
2.所有引用类型的原型->Object.prototype
Person.prototype._proto _ → Object.prototype
Function.prototype._proto _ → Object.prototype
3.Object.prototype._proto _ = null
4.构造函数的 _proto _ -> Function.prototype
Function._proto _ → Function.prototype
Object.proto → Function.prototype
-
获得对象非原型链上的属性
Object.keys (obj) 返回可枚举的属性名数组
需包含不可枚举属性用 Object.getOwnPropertyNames (obj)
若还需包含 Symbol 键用 Reflect.ownKeys (obj)。
-
new
实现原理:
创建空对象、关联原型(将实例的 proto 指向构造函数的 prototype)、将构造函数的 this 指向实例,执行函数并返回新对象或返回函数返回的对象。
返回值规则:
构造函数无返回值 → 返回新对象
构造函数返回原始值 → 返回新对象
构造函数返回对象 → 返回该对象(覆盖新对象)
- 箭头函数VS普通函数
箭头函数没有 prototype、没有自己的 this,继承外层 this,不能修改 this,也没有内部构造方法,不能被 new 实例化,否则会抛出类型错误。
普通函数 this 动态绑定,指向调用它的对象,用 call/apply/bind 可以改变 this。可做构造函数,new 生成实例,有 prototype。
四、变量提升
原因:变量提升是为了解决函数相互调用和 JS 编译执行阶段的设计,会导致变量未赋值就可访问并得到 undefined,容易出错。
解决:var 有提升,默认 undefined、无块级作用域、可重复、挂全局;let 有块级作用域、存在暂时性死区、不重复;const 同 let,但必须初始化赋值且栈内容不可改(基础类型不能修改,引用类型可以修改内部属性,但不能改指向);函数提升只提升声明
尾调用就是函数最后一步是返回另一个函数的调用结果,后面不能再有其他操作;
尾递归是特殊尾调用。引擎可做尾调用优化,复用栈帧,无论递归多少次,调用栈深度始终为 1,不会溢出。
CommonJS:require/module.exports、同步加载,运行时解析,先执行完模块代码,再拿到导出结果、浅拷贝、不支持树摇;ESM:import/export、异步加载,编译时解析,适合前端。引用绑定,外部导入的值自动更新、支持静态分析和 Tree-Shaking,打包可删除未使用代码,优化体积。
同:都实现模块化、作用域隔离、代码复用;
use strict 是严格模式;限制未声明变量、修正 this 指向this === undefined、禁止怪异语法、强制代码规范,减少隐性 bug,提升代码安全性与引擎优化能力
五、 闭包
内层函数 + 外层函数变量= 闭包
函数嵌套,内层函数引用外层变量且被外部引用,外层执行完毕变量不销毁
作用:实现变量私有化;外部可访问内部变量;缓存、柯里化、防抖、节流
缺点:内存泄漏(局部变量仍存在),需手动释放引用
六、作用域 & 作用域链
作用域:变量可访问范围。分为全局(页面关闭时销毁)、函数、块级作用域(GC)
作用域链:变量的查找规则。先在当前作用域查找;找不到就逐级向外层父作用域查找;一直查到全局作用域。这种层层向上查找变量的链式结构,就是作用域链,在函数定义时就已确定
var 函数作用域;
let/const 有块级作用域
执行上下文是 JS 引擎为代码运行创建的抽象环境,代表代码当前正在执行的环境,包含了 This 指向、变量存储的变量对象以及访问上级的作用域链。它分为创建(确定 This、构建 VO、建立作用域链)和执行(代码运行)两个阶段。会创建全局、函数执行上下文。一个程序只有 一个 全局上下文,页面关闭时销毁。可以有 无数个函数执行上下文,但同一时间只有一个在运行,函数执行完毕后,上下文销毁,内部变量被垃圾回收。
多个执行上下文通过 调用栈 (Call Stack) 来管理,遵循后进先出 (LIFO) 原则。
this 由函数调用方式决定,而非定义位置;1.箭头函数无自身 this,继承外层作用域的 this
2.new 绑定:this 指向实例对象
3.call/apply/bind 硬绑定:
fn.call(thisArg, 参数1, 参数2)、fn.apply(thisArg, [参数数组])
同:立即执行,this 绑定为传入对象,返回结果和新函数
异:传参形式不同(call 接收零散参数,apply 接收数组参数)
fn.bind(obj):不执行,返回新函数,永久绑定 this,不会被修改
4.谁调用,指向谁
5.函数:
非严格模式:独立函数直接调用,this → window / 全局对象
严格模式:独立调用,this → undefined
七、 防抖、节流
防抖:触发后 n 秒内不再触发才执行/多次触发只执行最后一次(搜索框输入)等你不动了,我再执行
节流:固定时间内只执行一次/多次触发只执行一次(滚动、拖拽)不管点多快,我按固定节奏来
js
// 防抖 :设置定时器,频繁触发就清空重计时;
function debounce(fn, delay) {
let timer = null;
return function(...args) {
// 每次触发都清空定时器,重新计时
if (timer) clearTimeout(timer);//有则清除
timer = setTimeout(() => { //没有就添加
fn.apply(this, args);
timer = null; //使用完清除
}, delay);
};
}
//Lodash 实现
npm i lodash
const debounceSearch = _.debounce(函数, 延迟毫秒, [配置])
//常用配置
//leading: true 立即执行,之后等待间隔
//trailing: true 默认,结束后延迟执行
// 取消
debounceSearch.cancel();
// 节流:记录时间戳,间隔到了才允许执行。
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}
//Lodash 实现
const throttleScroll = _.throttle(函数, 300);
// 绑定滚动事件
window.addEventListener('scroll', throttleScroll);
React 中必须用 useRef 存防抖 / 节流函数,函数组件每次渲染都会重新创建函数实例,不加 useRef 防抖节流会失效
js
//搜索框防抖
import { useState, useRef } from 'react';
import _ from 'lodash';
export default function SearchInput() {
const [keyword, setKeyword] = useState('');
// useRef 缓存防抖函数实例
const debounceFn = useRef(
_.debounce((val) => {
// 发起接口请求
console.log('请求搜索:', val);
}, 500)
).current;
const handleChange = (e) => {
const val = e.target.value;
setKeyword(val);
debounceFn(val);
};
return <input value={keyword} onChange={handleChange} placeholder="输入搜索" />;
}
}
js
滚动节流
import { useEffect, useRef } from 'react';
import _ from 'lodash';
export default function ScrollBox() {
const throttleFn = useRef(
_.throttle(() => {
console.log('滚动位置:', window.scrollY);
}, 200)
).current;
useEffect(() => {
window.addEventListener('scroll', throttleFn);
return () => {
// 组件卸载取消监听 + 取消节流
window.removeEventListener('scroll', throttleFn);
throttleFn.cancel();
};
}, []);
return <div style={{height: '200vh'}}>滚动页面测试</div>;
}
八、垃圾回收机制
1.主要回收算法
引用计数:每个对象维护一个引用计数器,计数器为 0 立即回收。互相引用,内存泄漏
标记清除:从根节点标记可达对象,清除不可达,现代浏览器主流
2.分代回收
新生代:存放短期使用、生命周期短的对象,空间小、回收频繁、速度快,算法:复制回收,采用两个等大空间From / To,存活对象复制转移,清空原空间
老生代:长期存活、常驻对象,空间大、回收频率低,算法:标记清除 + 标记压缩,避免内存碎片
3.内存泄漏
意外全局变量(未声明变量挂载 window)
闭包滥用:内层长期引用外层变量,无法释放
定时器 / 事件监听 未销毁
DOM 元素脱离文档,仍被 JS 引用
循环引用(旧引用计数缺陷,现代标记清除已解决)
全局缓存无限累加
第三方组件未销毁
4.如何避免内存泄漏
合理使用 let/const,禁用隐式全局
定时器、监听事件页面销毁时手动清除
闭包合理使用,不用的引用手动置空 obj = null
DOM 操作后及时解绑引用
九、转换
普通字符串 ↔ 数组 split() / join()
对象/数组 ↔ JSON字符串 JSON.stringify/ JSON.parse
对象 -> 数组 keys()/values()/entries()