Javascript八股

Javascript八股
Milo节流与防抖
防抖:就是指连续触发事件但是在设定的一段时间内中只执行最后一次
应用场景:搜索框搜索输入 文本编辑器实时保存
1 | let timerId = null; |
节流:就是指连续触发事件但是在设定的一段时间内中只执行一次函数。
例如:设定1000毫秒执行,那你在1000毫秒触发在多次,也只在1000毫秒后执行一次
记忆方法:不要打断我
应用场景:
高频事件例如 快速点击 鼠标滑动、resize 事件、scroll 事件
下拉加载
视频播放记录时间等
1 | let timerId = null; |
- 防抖:单位时间内,频繁触发事件,只执行最后一次
典型场景:搜索框搜索输入
代码思路是利用定时器,每次触发先清掉以前的定时器(从新开始) - 节流:单位时间内,频繁触发事件,只执行一次
典型场景:高频事件快速点击、鼠标滑动、resize 事件、scroll 事件
代码思路也是利用定时器,等定时器执行完毕,才开启定时器(不要打断)
说一下JS的数据类型
JS是一种弱类型语言,包含以下几种数据类型:
1.基本类型/原始类型
- Number:表示整数浮点数
- String:表示文本数据
- Boolean:true和false,用于逻辑判断
- Undedined:表示变量已声明但未初始化
- Null:表示故意赋予变量空值,常用于表示不存在的对象
- Symbol:用于创建唯一的不可变的代号,常用于属性名
- BigInt:用于表示大于2^53 - 1的整数
2.复杂类型/引用类型 - 对象(Object):由大括号包围的一组属性的集合。
- 数组(Array):由方括号【】包围的一组有序的值的集合。
- 函数(Function):可以执行特定操作的可重复使用的代码块。
- 日期(Date):表示日期和时间的对象。
- 正则表达式(RegExp):用于匹配和操作字符串的对象。
- Map:一种可迭代的键值对集合。
- Set:一种不重复值的集合。
数据类型检测的方法有哪些
1.typeof操作符
1 | typeof 123 // "number" |
2.instanceof操作符instanceof 是 JavaScript 的二元运算符,用来判断某个对象是否是某个构造函数的实例(或原型链上的后代)
沿着 object.__proto__ 不断向上找,只要遇到 Constructor.prototype 就返回 true,否则到顶返回 false。
1 | [] instanceof Array // true |
3.Object.prototype.toString.call()
- 更为精准的类型检测方法,可以用来区分null和undefined以及获取对象的准确类型
4.constructor属性 - 某些内置类型有constructor属性,可以用来检测类型,但是可以被改写,所以不是很可靠
5.Array.isArray() - 专门判断一个对象是否是数组,比typeof更为精准
‘ == ‘ 和 ‘ === ‘区别是什么
- == 宽松相等,允许隐式类型转换。=== 严格相等,不转换,类型不同直接false
隐式类型转换流程 ECMA 规定固定阶梯,无“优先级表”,记住 5 步即可:
类型相同 → 直接比
null == undefined→true(特殊豁免)string vs number → string 转 number
boolean vs 任何 → boolean 转 number(
true→1false→0)对象 vs 原始 → 把对象转为原始值再比较,对象 ToPrimitive(number) 再从头比
其余 →
false
null和undefined的区别是什么
null和undefined都用来表示没有值,但:
- undefined表示一个变量声明了但还没有赋值。在调用函数的时候,应该返回值但是没有返回值的话,就会返回undefined,所以undefined表示一个“缺少值”的原始值
- null表示一个变量当前没有指向任何对象,即显式的表示一个空值。也就是null就是一个表示“空值”的对象
- JSON.stringify ->null会输出null,undefined会被忽略
Number(null)→0Number(undefined)→NaN
解释下什么是变量提升
在JS中,变量提升是指变量声明和函数声明在代码执行前被提升到当前作用域的顶部的过程,有以下要点
- 只有变量的声明被提升,赋值不会,var定义变量在声明前访问是undefined
- 函数的声明,包括函数体内的变量和函数都会被提升
- let和const与var不同,变量提升后不会直接赋值undefined,而是会进入一个称为暂时性死区的状态,在初始化前访问会报错
简单说一下JS的作用域和作用域链
1.作用域:
在JS中,作用域决定了变量和函数的可见性和可访问性。它限定了变量和函数的生命周期
- 全局作用域:在这声明的变量和函数整个脚本都可以访问
- 函数作用域:只在该函数内部可以访问
- 块级作用域:ES6引入的let和const,在块级作用域定义只在其中有效(如 if 语句,循环,{}代码块)
2.作用域链
作用域链是一种机制,用来确定在哪个作用域中查找变量。当访问一个变量时,JS引擎会从当前作用域开始查找,然后逐级向上查找直到全局作用域。其中闭包是作用域链的一个应用,允许函数访问并操作外部函数作用域中的变量
说一说对执行上下文的理解
执行上下文是JS代码执行的环境,它定义了变量和函数的访问方式
- 全局执行上下文:当JS代码开始运行时创建,全局上下文中的变量是全局变量,所有代码都共享这个全局上下文
- 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文,其中的变量是局部变量,只有函数内部可以访问
- 块级执行上下文(ES6新增):让let和const关键字具有块级作用域,变量只在{}内部有效
- 评估阶段:在代码执行前,会进行变量和函数声明的评估
- 执行阶段:从上到下执行代码,遇到函数调用就创建新的执行上下文
- 作用域链:每个执行上下文都有自己的作用域链,用于确定变量和函数的访问权限。作用域链顶端是当前上下文的变量对象,然后逐级向上查找,直到全局上下文
简单说一下JS的闭包
闭包,简单来说就是一个函数和它外部环境的组合。相当于在函数内定义一个函数,用外层函数来创造一个相对封闭的环境来保护内部的函数与变量,内层可以访问外层的函数和变量。
(在一个函数的环境中,闭包 = 函数 + 词法环境)
闭包造成内存泄露的情况:
- 持有本该被销毁的函数,造成其关联的词法环境无法销毁。
- 当有多个函数共享词法环境时,可能导致词法环境膨胀,从而发生无法访问但是无法销毁的数据。
闭包的能力: - 可以访问创建它的外部函数中定义的变量
- 闭包常用于创建带状态的函数,比如计时器
- 闭包可以延长变量的生命周期
如何形成闭包: - 当一个函数从包含它的外部函数中被返回,或者作为参数传递给其他函数时,就形成了闭包
使用场景: - 模块化,实现私有变量和函数,通过闭包暴露共有接口
- 函数柯里化,即把多参数函数拆成单参数链
- 事件处理器,保存事件处理函数的状态。
讲一讲JS垃圾回收机制
JS使用自动垃圾回收机制来管理内存,主要通过标记清除算法来实现
其要点如下:
- 引用类型:JS中的对象如数组,函数,对象等,都是通过引用来访问的
- 标记阶段:垃圾回收器会从全局对象开始,查找所有从根对象可达的引用。被标记的对象就是正在使用中的对象
- 清除阶段:完成标记后,垃圾回收器会遍历内存,清除未被标记的对象。
解释一下什么是原型和原型链
在JS中,原型是一种面向对象的特征,用于实现继承和共享方法
原型(prototype):
每个函数在创建时,都会有一个prototype属性,指向一个对象,这个对象包含了可以由通过该函数构造的所有实例共享的属性和方法
原型链:
当我们尝试访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,它会根据__proto__属性沿着原型链向上查找,直到找到属性或方法,最顶层找不到则返回null
Object.prototype是原型链的最顶端,所有的原型链最终都会在这里结束
![[Pasted image 20250920132244.png]]
手写instanceof
在Javascirpt中instanceof是一个二元运算符,用来判断某个对象是不是某个构造函数的实例。换句话说,它可以检查对象的原型链上是否存在指定构造函数prototype。
1 | function myInstanceof(left, right) { |
new过程中发生了什么
- 创建一个新的对象,这个对象是{},它将作为新对象的实例
- 这个新对象的原型
__proto__会被指向构造函数的prototype属性 - 构造函数内部的this被赋值为这个新对象,这样构造函数中的属性和方法就可以被添加到新对象上
- 然后执行构造函数中的代码,通常包括给对象添加属性和方法
- 如果构造函数中没有返回一个对象类型的值,那么默认返回新创建的对象
遍历对象属性的方法有哪些
for...in循环:就是“把对象 + 原型链上所有可枚举的字符串键扫一遍”——记得加hasOwnProperty,数组别用它。
1 | function Foo() { this.x = 1; } |
Object.keys():仅返回对象自身的可枚举属性的名称数组,不包括原型链中的属性
1 | const obj = { b: 2, a: 1, 3: '三' }; |
Object.getOwnPropertyNames():返回对象自身所有属性(包括不可枚举的属性)的名称数组
1 | const obj = {}; |
Object.entries():返回一个给定对象自身可枚举属性的键值对数组,每个键值对是数组的形式
1 | const obj = { a: 1, b: 2 }; |
for...of循环+Object.values():通过Object.values()获取对象的所有可枚举属性值的数值,然后用for…of循环遍历
1 | // 1. 拿到对象 |
Reflect.ownKeys():返回对象自身的所有键(包括不可枚举的属性和Symbol属性)的数组
1 | const s = Symbol('s'); |
call/bind/apply有什么区别
- call方法:调用时需要逐个传递参数,立即执行
- apply方法:调用时传递一个参数数组,立即执行
- bind方法:用于创建一个新的函数,其this被永久绑定到提供的值。不会立即执行,而是返回一个新的函数
区别:
call和apply都是立即执行的,区别在于参数的传递方式。bind是用来创建一个新的函数,可以立即执行,也可以稍后执行。
1 | // 1. 一个简单的对象 |
this绑定的规则
在JS中,this的值取决于函数的调用方式,而不是在哪里定义的
- 默认绑定:如果函数是独立调用的,this默认指向全局对象(浏览器是window)
- 隐式绑定:如果函数作为某个对象的方法被调用,this绑定到调用它的对象上
- 显示绑定:使用call,apply或bind方法可以显式地设置this指向
- 新对象绑定(构造函数):如果一个函数作为构造函数(new调用)this会绑定到新创建的对象上
- 箭头函数不会创建自己的this,它会捕获其所在上下文的this值作为自己的this
继承有哪些实现的方式
在JS中由于它本身不支持传统的类继承,我们通常通过一些技巧来模拟继承行为:
- 原型链继承:让一个构造函数的实例成为另一个构造函数的实例,缺点是父构造函数被执行 1 次,无法向 Parent 传参。
1 | function Parent() { this.p = 'parent'; } |
- 借用构造函数(经典继承):使用call或apply将父类构造函数的上下文绑定到子类构造函数,缺点是访问不到 Parent.prototype 上的方法。
1 | function Parent(name) { this.name = name; this.colors = ['red']; } |
- 组合继承(原型链+借用构造函数):可以继承原型上的方法,也可以避免父类构造函数的多次调用。缺点是父类构造函数被调用了两次(一次原型挂接,一次子类构造)。
1 | function Parent(name) { this.name = name; } |
- 原型式继承:使用
Object.create()创建一个新对象,使用一个对象来作为另一个对象的原型,缺点:引用属性共用一份,一改全变。
1 | const parent = { |
- 寄生式继承:创建一个增强对象的函数,然后返回这个对象。适合只给少量实例增加方法,避免改动原型。
1 | function createStudent(base) { |
- 寄生组合继承:组合继承的优化,只调用一次父类构造函数。
1 | function Parent(name) { this.name = name; } |
- ES6类继承:使用class关键字和extends实现继承(底层仍是寄生组合)简洁、无重复调用、无手动修正
prototype,现代开发首选。
1 | class Parent { |
浅拷贝与深拷贝的实现方式
1.浅拷贝只复制对象的第一层属性,对于嵌套的对象,复制的是引用,而不是对象本身
- 扩展运算符(…)
Object.assign():const 返回的对象 = Object.assign(目标对象, 源对象1, 源对象2, …)Array.prototype.slice()针对数组
2.深拷贝会递归复制对象的所有层级,确保新对象与原对象完全独立JSON.stringify()将对象序列化为字符串,再通过JSON.parse()将字符串反序列化为新对象。缺点是不能处理函数,undefined,NaN,Infinity,循环引用等特殊值,也不能处理Date,RegExp等- 库函数,比如lodash的_.cloneDeep()方法
- 手写:
1 | function deepClone(obj,hash = new WeakMap()){ |
let和var有啥区别
- let有块级作用域,它只在声明它的代码块内部可见,而var是函数作用域或全局作用域,可能会在无意间造成变量泄露
- let声明的变量不会在代码块开始时被初始化,而是在声明时初始化,在声明前访问变量会导致错误。而var声明变量会被提升并初始化为undefined
- 在同一作用域中,不能用let重复说明同一个变量。而var可以重复声明同一个变量,只有最后一次声明会被使用
- let声明的变量不会成为全局对象的属性,var会成为全局对象(window)的属性
什么是暂时性死区
- 在代码块的开始到let或const声明变量的位置之间的区域,这个区域内访问这些变量会导致一个ReferenceError错误
- 这意味着在声明前,这些变量是不可见的,形成一个死区,在死区内任何对这些变量的访问尝试都不会成功
ES6中有哪些新特性,你常用哪些
- let和const:引入了块级作用域的变量声明
- 箭头函数:提供了更简洁的函数写法,并且this的绑定更加一致
- 模板字符串:允许在字符串中嵌入表达式,使拼接更方便
- 默认参数:可以在函数参数中设置默认值
- 解构赋值:允许从数组和对象中提取数据并赋值给新的变量
- 模块导入导出:使得代码模块化更加方便
- Promises:用于异步计算
- 类:引入了基于类的面向对象编程
- 生成器和迭代器:允许逐个产生值,而不是一次性计算并返回所有值
- 新的数组方法:如
Array.from(), find(), filter(), map(), reduce()等 - 新的数值和数学方法:如
Number.isInteger(), Math.trunc() - Symbol:一种新的原始数据类型,用于创建唯一的属性名
- Proxy和Reflect:提供一种在操作对象时自定义行为的能力
箭头函数与普通函数区别是什么
箭头函数是ES6引入的新特性,它提供了一种更简洁的方式来写函数,并且有一些独特的行为
- 语法简洁:箭头函数没有自己的arguments对象,可以使用剩余参数(…)和拓展运算符(…)来处理参数
1 | const nums = [1, 2, 3]; |
- this绑定:箭头函数不绑定自己的this,它会捕获其所在上下文的this值作为自己的this
- 不可以使用new:箭头函数不能作为构造函数,使用new会报错
- 没有原型:箭头函数没有自己的原型属性,prototype是不可枚举的
- 不支持call,apply,bind:不能使用这些方法来改变this指向
什么是同步和异步,JS异步解决方案有哪些
- 同步:同步操作是阻塞的,在一个任务完成前,会阻塞代码的执行,不进行下一个任务
- 异步:异步操作是非阻塞的,任务执行不会等待结果,代码继续执行,任务结束后通过回调或其他方式获取结果
异步解决方法:
- 回调函数:最早的异步解决方案,容易形成回调地狱
- Promises:提供了更好的异步操作管理方式,可以链式调用,避免回调地狱
- async/await:建立在promises上,可以用同步代码的形式来写异步代码,更简洁易读
- 事件监听器:通过注册事件和监听器来处理异步事件
描述下对事件循环的理解
事件循环是JavaScript的一种特殊运行机制,它允许JS引擎在单线程环境中执行异步操作
事件循环要点:
- 调用栈:所有同步任务都在调用栈中执行,它们是阻塞的,必须等待当前任务完成后才能执行下一个任务
- 事件队列:异步任务(比如回调函数,Promises,setTimeout等)会被放入事件队列中,等待被调用
- 事件循环检查:事件循环会不断检查调用栈是否清空。一旦清空,事件循环就会从事件队列中取出任务,放入调用栈中执行
- 宏任务与微任务:事件队列分为宏任务(如setTimeout,setInterval,I/O,UI渲染)和微任务(如Promises,MutationObserver)。微任务在当前执行栈执行完毕之后,宏任务之前执行
宏任务和微任务的区别
在JS的异步编程中,任务被分为两类:
- 宏任务:这些事比较重的任务,通过会导致JS允许环境让出控制权
- 微任务:这些事较轻的任务,会在当前执行栈情况之后,下一次事件循环开始之前执行
区别:
- 执行时机:当一个宏任务执行完毕之后,会检查微任务队列,所有微任务执行完才会执行下一个宏任务
- 执行顺序:微任务和宏任务执行顺序都是先进先出,但宏任务有多个队列可能会被浏览器重新调度优先级,微任务一次清到底
什么是Promise,Promise的常用方法有哪些
Promise是JS中用于异步编程的一种解决方案,它代表了一个异步操作的最终完成或失败。Promise提供了一种更加优雅的方式来处理异步操作,避免了回调地狱
Promise的状态:
- pending(进行中),fulfilled(已成功),rejected(已失败)
Promise的常用方法:
Promise.prototype.then():用于指定promise成功后的处理函数Promise.prototype.catch():用于指定失败后的处理函数Promise.prototype.finally():无论失败成功都会执行的函数Promise.all():用于将多个promise实例合成一个promise,只有所有都成功才解析Promise.race():多个promise合成,谁最先落定就采用谁Promise.resolve():返回一个已解析的promisePromise.reject():返回一个已拒绝的promise
async关键字和await关键字的作用
这两个关键字是ES2017引入的,它建立在Promises之上,使得异步代码看起来更像是同步代码,从而简化异步编程
async:用于声明一个异步函数,这意味着这个函数总是返回一个Promise对象。如果函数正常执行结束,则Promise被成功解析(resolved)如果函数中抛出错误,则Promise被拒绝(rejected)await:它只能在async函数内使用,用于等待一个Promise完成,并且在完成之前暂停async函数的执行。await后面通常跟着一个异步操作,它会暂停代码执行直到Promise解析完成
proxy和Object.defineProperty的区别
Object.defineProperty:它用于在对象上定义或修改属性,可以控制属性的描述符,如是否可写,可枚举,可配置等。它只能定义或修改对象的单个属性Proxy:用于创建一个对象的代理,从而在访问对象的属性或方法时可以拦截和自定义操作,包括属性访问,赋值,枚举,函数调用等。它可以针对整个对象,不仅仅是单个属性
区别:
- 一个用于定义或修改属性的特性,另一个用于创建一个对象的代理,可以拦截和自定义对象操作
- Object.defineProperty无法拦截对属性的操作。Proxy几乎可以拦截所有类型的操作
- 当需要定义或修改属性的描述符时,可以使用Object.defineProperty。当需要对对象的操作进行更复杂的拦截和自定义时,可以使用Proxy
Map和普通对象,WeakMap的区别
- 键的类型:Map可以有任何类型的键;普通对象键只能是字符串或符号;WeakMap的键只能是对象引用
- 内存占用:Map和普通对象都会阻止垃圾回收其键和值;WeakMap的键是弱引用,键的对象可以被垃圾回收
- 迭代能力:Map是可迭代的,可以被
for...of循环遍历;普通对象可以通过Object.values(), Object.keys(), Object.entries()进行迭代;WeakMap不可以进行迭代
for,forEach,map的区别/ for…in,for…of的区别
| 语法 | 遍历目标 | 能否中断 | 返回值/副作用 | 极简示例 |
|---|---|---|---|---|
for |
数字索引或条件 | break/continue |
手动管理索引 | for (let i = 0; i < 3; i++) console.log(i); |
forEach |
数组元素 | ❌ 不可中断 | 无返回值(纯副作用) | [1,2,3].forEach(v => console.log(v)); |
map |
数组元素 | ❌ 不可中断 | 返回新数组 | [1,2,3].map(v => v * 2); // [2,4,6] |
for...in |
可枚举字符串键(含原型) | break/continue |
得到键名 | for (const k in {a:1,b:2}) console.log(k); // a b |
for...of |
可迭代值序列(数组/Map/Set/String) | break/continue |
得到元素值 | for (const [k, v] of map) {console.log(k, v); // a 1 → b 2 → c 3} |
简单来说,for 循环最通用但需要手动设置迭代,forEach 用于执行数组元素的函数,map 用于生成新数组;for...in 用于遍历对象属性,for...of 用于遍历可迭代对象的值。 |
说一说对CommonJS和ES6模块化的理解
在JS中,模块化是一种代码组织方式,它允许我们将代码分割成独立,可重用的模块。
- CommonJS模块:是Nodejs使用的模块系统,基于同步加载,每个模块都是一个单独的作用域;使用require方法加载模块,使用module.exports来导出模块的接口
- ES6模块:是JS语言标准的模块系统;基于静态加载,编译时就确定了模块的依赖关系;使用import语句导入模块成员,使用export语句来导出模块成员
区别:
- 加载方式:前者是运行时加载,可以有条件地加载模块;后者是编译时加载,静态分析依赖关系
- 运行环境:前者用于Nodejs服务器环境;后者即可以用于浏览器环境,也可用于Nodejs
- 语法不同:前者使用require和module.exports;后者使用import和export
事件流的过程
在Web开发中,事件流是一个描述DOM事件在页面中传播过程的概念。主要有两种事件流模型:冒泡和捕获
- 事件冒泡:事件开始时由最具体的元素接收,任何逐级向上传播到较为不具体的节点(如文档根节点)
- 事件捕获:与冒泡相反,事件从最不具体的节点(文档根节点)开始捕获,直到达到最具体的节点(事件的目标)
事件处理的三个阶段:
- 捕获阶段:事件从文档根节点开始向下传播到目标元素
- 目标阶段:事件到达目标元素,触发该元素的事件处理程序
- 冒泡阶段:事件从目标元素回传到文档根节点
事件流的控制:
- 冒泡:可以通过
event.stopPropagation()方法阻止事件进一步冒泡 - 事件捕获:可以通过设置
addEventListener的第三个参数为true来启用捕获阶段的事件处理 - 大多数现代浏览器的默认行为是先冒泡,再捕获,但可以通过
addEventListener方法的第三个参数来指定。
什么是事件委托
事件委托是一种在父元素上设置事件监听器,来管理多个子元素或未来添加的元素的事件处理方式
要点:
- 利用冒泡原理:事件会从触发他的元素开始冒泡,通过祖先元素传递到根;在父元素上设置监听器可以捕获到在子元素上触发的事件
- 性能优化:相比于在每个子元素上分别设置监听器,事件委托可以减少内存消耗和提高性能
- 动态元素管理:对于动态添加的元素,即使它们在设置监听器后被添加到DOM中,事件委托依然有效
e.target和e.currentTarget的区别
- 触发事件的元素:e.target指的是触发事件的那个元素
- 绑定事件的元素:e.currentTarget是指事件处理器绑定的那个元素
简单来说,e.target是事件的实际触发点,而e.currentTarget是事件监听被设置的地方。在没有事件冒泡的情况下,这两者是相同的,但如果有冒泡,它们就可能不同了。
简单说下axios,为啥用axios,和fetch区别
- axios是一个基于Promise的HTTP客户端,用于浏览器和nodejs,它提供了一个简单的API来执行HTTP请求
- 为什么用:
易用性:API直观,使用方便
兼容性:支持所有现代浏览器
功能丰富:支持请求和响应的拦截,转换数据格式,自动转换JSON数据等
基于Promise:使得异步处理更加优雅 - 区别
- 返回值:axios返回的是一个Promise对象;fetch返回的是一个Response对象,需要再次调用.json()之类方法来手动解析 JSON数据
- 错误处理:axios在请求失败时会抛出一个错误;fetch不会,它只有在网络故障时才会失败,其他如404或500错误都会解析为成功的响应
- 使用方便:axios可以直接设置请求头,Axios 自动将 JavaScript 对象序列化为 JSON请求体等;fetch需要手动设置
- 拦截器:axios有强大的拦截器功能,可以拦截请求和响应,fetch则需要手动实现拦截罗姐
总的来说,Axios 和 Fetch 是解决同一问题的两种不同方案,并非简单的升级关系。它们的关系好比智能手机相机与专业单反相机——Fetch 如同智能手机相机,是浏览器内置的“原生应用”,开箱即用、轻量便捷,能满足日常基础需求;而 Axios 则如同专业单反,是需要额外安装的“专业设备”,提供了更丰富的功能、更便捷的自动化操作和更优的开发体验。









