学习资料:学习JavaScript这一篇就够了-CSDN博客
一、JS 基础语法
1. JS的数据类型有哪些?怎么分类?
通俗标准答案
一共7种,分为两大类:
-
基本数据类型(简单类型/值类型):string、number、boolean、undefined、null、symbol
-
引用数据类型(复杂类型):object(数组Array、函数Function、普通对象都属于Object)
核心区别:
-
基本类型:存在栈内存,赋值是拷贝本身的值,互不影响
-
引用类型:变量存内存地址,真实数据存在堆内存;赋值只拷贝地址,两个变量会互相影响
2. null 和 undefined 的区别?
通俗直白答案
-
undefined :系统自动赋值,定义了变量但是没给值、函数无返回值、对象不存在的属性,默认都是undefined
-
null :开发者手动赋值,代表空对象、手动清空变量;
面试一句话总结:undefined是不知道是什么,null是空对象;typeof null 结果是object(JS历史bug)
3. == 和 === 的区别?
必考标准答案
-
== 弱相等 :只比较值,会自动隐式类型转换,转换成相同类型再对比
-
=== 强相等(严格相等) :先对比数据类型,类型不一样直接false;类型相同再对比值,不做类型转换
经典面试特例:null == undefined 结果为true;null === undefined 为false
4. JS 隐式类型转换规则?
大白话总结:运算、== 判断的时候,JS自动帮你转类型
-
字符串+数字:数字转字符串,做拼接
-
减乘除、大小比较:字符串转数字
-
布尔判断:0、''、null、undefined、NaN 为 false;其余全是true
5. typeof 能判断哪些类型?缺点是什么?
typeof 判断结果:string/number/boolean/undefined/symbol/function/object
二、数组高频八股
6. splice、slice、split 三个方法区别?
小白最简单背诵版
-
splice() :修改原数组;删除/新增/替换数组元素,参数:(起始下标,删除个数,新增元素)
-
slice() :不修改原数组;截取数组/字符串,参数(开始下标,结束下标),包左不包右
-
split() :字符串方法;把字符串切割成数组,不修改原字符串
7. Array.sort() 排序坑点?
-
不传回调函数:默认按字符串字典ASCII排序,数字排序错乱(11排在2前面)
-
数字升序:arr.sort((a,b)=>a-b)
-
数字降序:arr.sort((a,b)=>b-a)
-
会直接修改原数组
8. 数组遍历方法有哪些?区别?
-
forEach:普通遍历,不能break终止
-
map:遍历并返回新数组,多用于数据加工
-
filter:筛选数组,返回符合条件的新数组
-
find:查找第一个符合条件的元素
-
every/some:判断数组全部/部分元素是否符合条件
9. 数组哪些方法修改原数组?
修改:push、pop、shift、unshift、splice、sort、reverse
不修改:slice、map、filter、concat
三、作用域&变量提升
10. var、let、const 的区别?
-
作用域 :var 没有块级作用域;let、const 有块级作用域(大括号内部失效)
-
变量提升:var 会变量提升;let、const 不会
-
重复声明:var可以重复定义;let、const不能重复声明
-
常量:const 不能修改栈内存的值;引用类型内部属性可以修改
11. 什么是变量提升?
JS代码执行前,浏览器会把 var定义的变量、函数声明,提前提升到当前作用域最顶部;
缺点:容易出现变量泄露、逻辑混乱;所以现在全部禁用var,用let/const
12. 什么是暂时性死区?
let/const专属:块级作用域内,变量声明之前,不能使用这个变量,这段区间就是暂时性死区
13. 闭包是什么?作用和缺点
内部函数调用外部函数的变量,就形成闭包
作用:1. 实现私有变量;2. 延长局部变量生命周期
缺点:变量常驻堆内存,容易造成内存泄漏
四、执行上下文&事件循环
14. JS 执行机制是什么?
JS是单线程:同一时间只能做一件事
设计初衷:JS 要操作 DOM。 如果多线程同时改页面元素,会出现冲突:一个线程删按钮、一个线程改按钮,浏览器不知道以谁为准。
所以规定:所有 DOM 操作、JS 代码都交给唯一主线程执行,同一时刻只能执行一段代码。
JS 靠什么实现 "看起来同时执行"?事件循环(并发实现原理)
核心:把任务分成三类,排队轮流执行,快速切换,视觉上并行。
- 同步代码(主线程立刻执行,优先全部做完)
- 微任务(同步完一次性清空所有微任务)
- 宏任务(一次只取一个,再回去处理微任务)
JS 真正能做到并行的方案
主线程依旧单线程,但是浏览器提供额外线程,实现真正并行:
- Web Worker 单独开辟一条独立线程,和主线程并行运行,用来处理大量计算、大数据循环,不会阻塞页面 DOM 渲染。 限制:Worker 不能操作 DOM,只能和主线程通过消息通信。
- 浏览器底层线程(浏览器多线程,JS 主线程只是其中一条)
- GUI 渲染线程:绘制页面
- HTTP 请求线程:发 ajax、接口请求(并行发起多个请求)
- 定时器线程:管理 setTimeout 这些线程是浏览器提供的,不属于 JS 主线程,所以多个网络请求可以并行发送。
15. 宏任务和微任务区别?事件循环执行顺序
-
先执行同步代码
-
再执行全部微任务(promise.then、async/await、queueMicrotask)
-
最后执行宏任务(setTimeout、setInterval、ajax)
-
一轮循环结束,重复执行
16. async 和 promise 了解吗?
通俗讲:都是解决回调地狱,异步编程方案;async/await 是promise的语法糖,写法更简单
五、this指向
17. this 的指向规则
谁调用,this就指向谁
-
普通函数直接调用:非严格模式指向window;严格模式指向undefined
-
对象调用函数:指向调用函数的对象
-
箭头函数:没有自己的this,继承上层作用域的this
-
dom事件绑定:指向绑定事件的dom元素
18. call、apply、bind 区别?
-
共同点:修改函数this指向
-
call:参数逐个传入,立即执行
-
apply:参数数组传入,立即执行
-
bind:修改this,返回新函数,不会立即执行
六、原型&继承
19. 原型和原型链是什么?
JS所有对象都有__proto__原型属性,指向公共原型对象;
一层层往上查找原型,形成的链条就是原型链;用来实现方法复用,节省内存
20. constructor、prototype、proto 关系?
构造函数有prototype;实例对象有__proto__;两个互相指向;constructor指向构造函数
七、拷贝问题
21. 浅拷贝和深拷贝区别?
-
浅拷贝:只拷贝第一层属性;内层引用类型还是共用堆内存,修改互相影响
-
深拷贝:拷贝全部层级数据,开辟全新堆内存,两个数据完全互不影响
简单深拷贝方案:JSON.parse(JSON.stringify(obj))
八、浏览器&内存相关
22. 什么是内存泄漏?JS常见场景
JS 有垃圾回收机制 GC:会自动找出没有任何变量引用、再也用不到 的数据,把堆内存释放掉。 内存泄漏指:一段代码逻辑已经彻底用完、不会再使用的数据,却依然被变量持有引用,GC 识别不到,无法回收这块堆内存。 随着页面长时间运行,泄漏的数据越堆越多,堆内存持续上涨,出现页面卡顿、动画掉帧、切换页面变卡,严重时浏览器标签页崩溃。
内存泄漏:无用数据占用堆内存,浏览器无法回收,页面卡顿崩溃
常见场景:全局变量过多、闭包、定时器未清除、dom引用未清空
23. 栈溢出和堆溢出区别?
栈溢出:
- StackOverflow,递归死循环、函数嵌套过多,栈内存空间不足
- 栈容量是固定有限的,函数调用层级过多,栈帧堆满。
- 无限递归,一直调用自身,不断创建栈帧,没有终止条件;
- 几百上千层函数嵌套调用。
- 报错:
RangeError: Maximum call stack size exceeded - 和堆内存无关,只是栈空间耗尽;
- 程序直接卡死抛出错误,不会缓慢卡顿,一运行直接报错
堆溢出:
- OOM,大量对象不销毁,堆内存占满
- 堆中创建大量对象、大数据,并且存在引用无法 GC 回收,堆内存被占满。
- 来源一般是持续内存泄漏:全局变量、闭包、定时器、未释放 DOM 不断堆积数据。
- 不会立刻报错,是缓慢恶化:页面越用越卡;
- 内存占用持续走高,浏览器任务管理器内存只涨不跌;
- 内存耗尽后浏览器直接崩溃、标签页闪退。
九、前端手撕简答
1. 手写数组去重最简最优方案(ES6,代码最短)
javascript
// 原理:Set集合不允许重复元素
function uniqueArr(arr) {
return [...new Set(arr)]
}
// 测试
console.log(uniqueArr([1,2,2,3,3,'1']))
// [1,2,3,'1']
面试口述讲解:利用ES6 Set数据结构天然去重,扩展运算符转为数组;代码最少、性能最好,日常开发和面试首选;缺点无法去除引用类型重复。
2. 手写简单深拷贝
适配普通对象、数组,满足90%面试场景,不用递归复杂边界
javascript
// 简单版深拷贝
function deepClone(obj) {
if(typeof obj !== 'object' || obj === null){
return obj
}
// 判断数组/普通对象
let newObj = Array.isArray(obj) ? [] : {}
// 遍历赋值所有属性
for(let key in obj){
if(obj.hasOwnProperty(key)){
// 不拷贝原型上的属性
newObj[key] = deepClone(obj[key])
}
}
return newObj
}
面试口述讲解:递归遍历对象;基本类型直接返回,引用类型新建内存空间;逐层拷贝所有层级属性;新旧数据完全隔离互不影响。
深拷贝:
javascript
JSON.parse(JSON.stringify(obj))
缺点:无法拷贝函数、undefined
3. 手写节流、防抖
先背概念
-
防抖:频繁触发事件,停止触发后 n 秒才执行一次;输入框搜索、弹窗关闭
-
节流:频繁触发事件,固定间隔 n 秒只能执行一次;滚动页面、窗口缩放、按钮频繁点击
javascript
### ① 手写防抖
// 防抖函数
function debounce(fn, delay) {
let timer = null
return function(...args) {
clearTimeout(timer)
// 频繁触发清空定时器
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
### ② 手写节流(时间戳版最简)
// 节流函数
function throttle(fn, delay) {
let lastTime = 0
return function(...args) {
let now = Date.now()
// 间隔达标才执行函数
if (now - lastTime >= delay) {
fn.apply(this, args)
lastTime = now
}
}
}
4. 解释 LRU 缓存前端应用
1、定义:LRU是最近最少使用缓存淘汰策略;缓存容量满了之后,自动剔除最久没有访问的数据,保留高频访问数据。
2、底层实现:哈希表 + 双向链表;哈希表快速查询,双向链表维护访问顺序;头部存最近访问数据,尾部存最少访问数据。
3、前端实际工程应用:
-
浏览器本地缓存、页面内存缓存管理
-
前端接口结果缓存,重复请求直接读缓存,减少服务器压力
-
大模型前端上下文缓存,限制上下文长度淘汰老旧无用对话
-
框架内部组件缓存(Vue keep-alive 底层就是LRU算法)