JS(面试)
1.js有哪些数据类型,怎么判断这些数据类型?
基本数据类型:number(范围:正负2的53次方-1)、string、boolean、undifined、null、symbol(表示独一无二的不可变值,一般来作为对象属性的键)、Bigint(没有范围)
引用数据类型:普通对象{}、数组[]、函数function(){}
基本数据类型用typeof判断,会返回对应的类型字符串(“number”)
typeof
对所有对象(包括数组、null
、日期、正则等) 返回 "object"
。
唯一的例外是函数,typeof
返回 "function"
注意:typeof null 为object,这个为历史遗留问题
引用数据类型用inctanceof判断
[] instanceof Array // true
{} instanceof Object // true
function(){} instanceof Function // true
最准确:Object.prototype.toString.call(),返回[object Number]
2.var、let、const的区别?
特性维度 |
|
|
|
作用域 | 函数作用域(function scope) | 块级作用域(block scope) | 块级作用域(block scope) |
提升(Hoisting) | 提升且初始化为 | 提升但不初始化(TDZ) | 提升但不初始化(TDZ) |
重复声明 | ✅ 允许在同一作用域重复声明 | ❌ 不允许重复声明 | ❌ 不允许重复声明 |
值是否可变 | ✅ 可以重新赋值 | ✅ 可以重新赋值 | ❌ 不可以重新赋值(常量) |
必须初始值 | ❌ 可以不赋初始值 | ❌ 可以不赋初始值 | ✅ 声明时必须赋初始值 |
let,const在声明语句执行之前,访问该变量会抛出 ReferenceError
。
3.什么是变量提升?
变量提升是 JavaScript 在执行代码前,将变量和函数声明“提前”到其作用域顶端的机制(只提升声明,不提升赋值)。
故let,const没有定义的时候会有暂时性死区
var a = 1;
function test() {console.log(a); // 输出:undefined(不是 1!)var a = 2;
}
test();
// 原因:函数作用域内的 var a 会覆盖全局的 a,且提升后初始值为 undefined
4.null和undifined的区别?
对比维度 |
|
|
类型 |
|
|
含义 | “无对象”(主动赋值) | “未定义”(默认值) |
产生场景 | 手动赋值: | 未赋值: |
与数值运算 |
|
|
严格相等 |
|
|
典型用途 | 清空对象、占位 | 判断变量是否声明未赋值 |
5.==和===的区别?
==会先进行类型转换后比较值,===会同时比较值和类型
6.说一下对闭包的理解?
闭包 = 函数 + 对其词法作用域的引用,即使外部函数已经出栈,内部函数仍持有父作用域的变量,从而“记忆”状态。
- 在 JS 引擎眼里,只要一个函数引用了外部变量,就会为这个函数创建Closure对象,把被引用的变量放进去。
- 因此闭包不是语法糖,而是作用域链 + 垃圾回收策略共同作用的结果。
换句话说,内部函数可以访问外部函数的变量并且这些变量在外部函数执行完后也不会被垃圾回收器回收
function counter() {let n = 0;return function () {return ++n; // 内部函数引用了外部 n};
}
const inc = counter();
console.log(inc()); // 1
console.log(inc()); // 2
实际项目遇到的坑:
for (var i = 0; i < 5; i++) {setTimeout(function () {console.log(i); // 都输出 5}, 100);
}
原因:
var 是函数作用域,不是块级作用域。
所有的 setTimeout 回调都共享同一个 i,等它们执行时,i 已经变成 5。
优点:
function Counter() {let count = 0;return {increment: function () {count++;},getCount: function () {return count;}};
}const counter = Counter();
counter.increment();
console.log(counter.getCount()); // 输出 1
console.log(counter.count); // 输出 undefined
外部无法直接访问 count,只能通过暴露的方法操作它
模块化
防抖节流
缺点:
function createHeavyObject() {const bigData = new Array(1000000).fill('leak');return function () {console.log('Still holding bigData');};
}const leakFn = createHeavyObject();
// 即使createHeavyObject执行完了,bigData依然存在
现代 JS 引擎(V8、SpiderMonkey 等)采用标记-清除算法:
从根对象(全局对象、当前调用栈中的函数、活跃 DOM 节点…)出发做可达性遍历。
如果闭包变量所在的 Scope 对象仍被某个可达函数引用,则标记为存活,不会被清除。
7.讲解一下js垃圾回收机制?
JavaScript 引擎通过 “可达性分析 + 标记-清除(Mark-and-Sweep)” 的垃圾回收机制,自动释放不再被任何活跃代码引用的内存
垃圾回收算法:
1.引用计数:每个对象都有一个引用计数器,当有新的引用指向该对象时,计数器加一;当某个引用不再指向该对象时,计数器减一。一旦某个对象的引用计数变为0,则该对象就会被立即回收
缺点:无法处理循环引用问题
2.标记-清除:从一组根对象开始遍历所有可达的对象,并标记这些对象。然后进行一次扫描,清除那些没有被标记的对象
缺点:可能会造成内存碎片化。
3.分代回收:基于“大部分对象生命周期都很短”的观察,将对象分为新生代和老年代。新创建的对象首先放在新生代中,经过几次垃圾回收后仍然存活的对象则晋升到老年代。不同的代采用不同的垃圾回收策略。
8.作用域和作用域链?
作用域决定变量的可见范围;作用域链决定当变量在当前作用域找不到时,去哪里继续找
类型 | 关键字/场景 | 生效范围 | 备注 |
全局作用域 | 顶层 | 整个 | 尽量少用 |
函数作用域 |
| 函数体内部 |
|
块级作用域 |
| 最近的一对大括号 |
|
- 函数在定义(而非调用)时,会保存一份外层作用域的引用,形成一条链。
- 查找变量时,从当前作用域开始,沿链向上直到全局作用域;找不到就抛
ReferenceError
。
9.原型和原型链?
在 JavaScript 中,每个函数都有一个 prototype
属性,它是一个对象,也被称为原型对象
当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性,JavaScript 引擎就会去它的原型对象中查找;如果原型对象也没有,就继续向上查找,直到找到或者查到原型链的终点为止。
function Foo() {}
const f = new Foo();console.log(f.__proto__ === Foo.prototype); // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true 终点
10.js继承有几种方式,都是怎么实现的?
1. 原型链继承
做法: 把子类的原型(prototype
)设置为父类的一个实例
function Parent() { this.name = 'Tom'; }
Parent.prototype.say = function() { console.log(this.name); };function Child() {}
Child.prototype = new Parent(); // 关键一行
// 修复 constructor 指向
Child.prototype.constructor = Child;const c = new Child();
c.say(); // Tom
缺点:所有子类实例共享一个父实例,引用属性会相互污染。
2. 借用构造函数(经典伪造)
做法:在子类构造函数中调用父类构造函数,并使用 call
或 apply
将 this
绑定到子类实例
function Child() {Parent.call(this); // 把 Parent 的 this 指向 Child 新实例
}
缺点:只能继承实例属性,拿不到原型方法。
3. 组合继承(上面 1+2)
做法:原型链拿方法 + 构造函数拿属性
function Child() {Parent.call(this); // 第二次调用 Parent
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child;
缺点:父构造函数调用了两次。
4. 原型式继承(Object.create())
做法:直接以父对象做原型
const parent = { name: 'Tom' };
const child = Object.create(parent);
缺点:同原型链继承,引用属性共享。
5. 寄生式继承
做法:在原型式基础上再给子对象额外加方法
function createChild(parent) {const clone = Object.create(parent);clone.sayHi = () => console.log('hi');return clone;
}
缺点:同 4,且方法无法复用。
6. 寄生组合继承(ES5 时代最佳实践)
做法:用寄生方式给子类原型挂一个“干净的父原型副本”,避免两次调用父构造函数
function inherit(Child, Parent) {const prototype = Object.create(Parent.prototype);prototype.constructor = Child;Child.prototype = prototype;
}function Child() { Parent.call(this); }
inherit(Child, Parent);
优点:只调用一次 Parent 构造函数,原型链干净。
Babel 把 ES6 extends 编译后就是这一套。
7. ES6 class
+ extends
(语法糖)
class Parent {constructor(name) { this.name = name; }say() { console.log(this.name); }
}class Child extends Parent {constructor(name, age) {super(name); // 必须先 superthis.age = age;}
}
底层仍是寄生组合继承 + 语法糖,解决了所有历史坑。
名称 | 关键字/方法 | 核心套路一句话 | 最大坑点 |
原型链继承 |
| 父实例当原型 | 引用属性共享 |
借用构造函数 |
| 父构造函数借给子用 | 拿不到原型方法 |
组合继承 | 1+2 | 双保险 | 两次调父构造 |
原型式继承 |
| 把对象当原型 | 共享问题同1 |
寄生式继承 | 在原型式上加方法 | 克隆后再增强 | 方法不可复用 |
寄生组合继承 |
| 只拷贝父原型,不调用父构造 | 手写略繁琐 |
ES6 extends |
| 语法糖,底层寄生组合 | 无坑,推荐 |
11.常见的数组方法?哪些会改变原数组,哪些不会?
会改变原数组(7 个) | 不会改变原数组(7 个) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12.说一下js的执行上下文?
执行上下文(Execution Context)是 JavaScript 代码运行时的“环境快照”,决定了变量、作用域链、this 指向以及代码的执行顺序
类型 | 触发场景 | 举例 |
全局上下文 | 首次加载脚本 |
|
函数上下文 | 每次函数调用 |
|
Eval 上下文 |
| 极少用,忽略 |
执行上下文的生命周期:
- 创建阶段(Creation Phase)
创建变量对象(VO):
函数声明会被存储到 VO 中。
变量声明(var)会被初始化为 undefined(变量提升)。
函数参数也会被加入 VO。
建立作用域链(Scope Chain)
确定 this 的值 - 执行阶段(Execution Phase)
变量赋值
函数被调用
代码逐行执行
注意:
JS 是单线程语言,同一时间只能做一件事。引擎通过一个执行栈来管理所有执行上下文。
- 全局上下文总是在栈底。
- 每当函数被调用时,它的执行上下文会被推入栈顶。
- 函数执行完毕后,该上下文从栈中弹出。
13.this指向?
默认 window,对象调用指向对象,call/apply/bind 指你传,new 指向实例,箭头继承外层,DOM 事件指向元素。
对比维度 | call | apply | bind |
功能 | 立即调用函数并显式指定 this | 同 call,但参数用数组 | 返回新函数,并不立即调用 |
参数形式 |
|
|
|
执行时机 | 立即执行 | 立即执行 | 稍后手动执行 |
返回值 | 函数执行结果 | 函数执行结果 | 返回已绑定 this 的新函数 |
多次调用 | 每次都要重新传 this | 每次都要重新传 this | 一次绑定,多次复用 |
典型场景 | 一次性借用方法 | 一次性借用方法,参数已数组化 | 回调、事件监听、偏函数 |
14.说说es6中的class?
1. 定义类
在 ES6 中,你可以使用 class
关键字定义一个类。类的定义包括类名、构造函数(constructor
)和方法。
class Person {constructor(name, age) {this.name = name;this.age = age;}sayHello() {console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);}
}
2. 创建实例
使用 new
关键字创建类的实例。
const person1 = new Person('Alice', 30);
person1.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.
3. 继承
ES6 的 class
支持继承,使用 extends
关键字来实现。
class Student extends Person {constructor(name, age, grade) {super(name, age); // 调用父类的构造函数this.grade = grade;}sayHello() {super.sayHello(); // 调用父类的方法console.log(`I am in grade ${this.grade}.`);}
}const student1 = new Student('Bob', 15, 9);
student1.sayHello();
// 输出:
// Hello, my name is Bob and I am 15 years old.
// I am in grade 9.
4. 静态方法和属性
ES6 允许在类中定义静态方法和属性。静态方法和属性不会被实例继承,而是直接属于类本身。
class MathUtils {static add(a, b) {return a + b;}static subtract(a, b) {return a - b;}
}console.log(MathUtils.add(5, 3)); // 输出: 8
console.log(MathUtils.subtract(5, 3)); // 输出: 2
5. Getter 和 Setter
ES6 的类支持定义 getter 和 setter 方法,用于访问和修改类的属性。
class Person {constructor(name, age) {this._name = name;this._age = age;}get name() {return this._name;}set name(value) {this._name = value;}get age() {return this._age;}set age(value) {this._age = value;}
}const person1 = new Person('Alice', 30);
console.log(person1.name); // 输出: Alice
person1.name = 'Bob';
console.log(person1.name); // 输出: Bob
15.讲一下JSONP的原理?
主要用于解决浏览器的同源策略限制,允许前端从不同域名的服务器获取数据
核心思想:利用 <script>
标签的 src
属性没有跨域限制的特性。<script>
标签可以加载任意域名下的 JavaScript 文件,因此可以通过动态创建 <script>
标签来加载跨域服务器返回的 JavaScript 代码
步骤 1:前端动态创建 <script>
标签
function handleResponse(data) {console.log('收到的数据:', data);
}let script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.body.appendChild(script);
步骤 2:后端接收请求并返回函数调用形式的数据
当后端收到请求:
GET /data?callback=handleResponse
后端返回的内容是一个 JavaScript 函数调用,例如:
handleResponse({"name": "Tom", "age": 25});
步骤 3:浏览器执行返回的脚本
因为是 <script>
加载进来的,浏览器会自动执行这段 JS 代码,从而触发 handleResponse
函数,并传入 JSON 数据作为参数。
优点:兼容性好,实现简单
缺点:只能get,容易受xss攻击,现在一般用CORS
16.说一下new的过程?
1.创建一个新的空对象
new
操作符首先会创建一个全新的空对象。这个对象在初始状态下没有任何属性和方法。
2.设置新对象的原型
新创建的对象会将其原型指向构造函数的 prototype
属性。这意味着新对象的 __proto__
属性会被设置为构造函数的 prototype
属性。
- 原型链的作用:通过原型链,新对象可以访问构造函数原型上的方法和属性。
3 执行构造函数
new
操作符会将构造函数的 this
绑定到新创建的对象上,并执行构造函数。构造函数可以初始化对象的属性和方法。
this
的绑定:在构造函数中,this
指向新创建的对象。构造函数可以通过this
来设置对象的属性和方法。
4 返回新对象
如果构造函数没有返回其他对象,则 new
操作符会返回新创建的对象。如果构造函数返回了一个对象,则返回该对象。
5.手动实现
自己手动实现new可以用Object.create()创建对象,然后通过apply调用构造函数来实现
function myNew(Constructor, ...args) {// 步骤1: 创建一个新对象// 使用 Object.create() 是最标准的方式,它直接将新对象的 [[Prototype]] 指向指定对象const obj = Object.create(Constructor.prototype);// 步骤2 & 3: 绑定 this 并执行构造函数// 使用 apply 将 Constructor 的 this 绑定到 obj,并传入参数// 注意:这里 Constructor 可能有返回值const result = Constructor.apply(obj, args);// 步骤4: 返回对象// 如果构造函数返回了一个对象,则返回这个对象// 否则,返回我们创建的新对象 obj// 注意:typeof null 也是 "object",但 null 通常不被视为有效的返回对象if (result !== null && (typeof result === 'object' || typeof result === 'function')) {return result;}return obj;
}
const person1 = {}; // 创建一个空对象
person1.__proto__ = Person.prototype; // 设置原型
Person.call(person1, 'Alice', 25); // 调用构造函数,并将 this 绑定到 person1
console.log(person1); // Person { name: 'Alice', age: 25 }
17.对象和数组是如何在内存中存储的?
对象存储
- 对象在内存中通常以哈希表的形式存储。
- 每个键值对通过哈希函数映射到内存地址。
- 哈希表通过冲突解决机制(如链地址法或开放寻址法)处理键冲突。
链地址法:将冲突的键值对存储在同一个地址的链表中
开放寻址法:寻找下一个空闲的内存地址
数组存储
- 数组是一种特殊的对象,其键是数字索引。
- 数组通常会被优化为连续的内存块,以提高访问速度。
- 稀疏数组可能会使用哈希表来存储元素,以节省内存。
内存优化
- JavaScript 引擎会根据数组和对象的使用情况动态优化内存存储。
- 密集数组和稀疏数组的存储方式可能会有所不同。
18.Object和map的区别?
1 Object 的特点
- 键是字符串或符号类型。
- 可以通过点符号或方括号访问属性。
- 有原型链,可能会导致意外的属性访问。
- 使用
for...in
遍历键,但会包括原型链上的属性。 - 提供了
Object.keys()
、Object.values()
、Object.entries()
等方法。
2 Map 的特点
- 键可以是任意类型。
- 不依赖原型链,不会受到原型链的影响。
- 提供了
forEach
方法,也可以通过for...of
遍历键值对。 - 提供了
set()
、get()
、has()
、delete()
等方法。 - 按照插入顺序保存键值对。
3 相互转换
- Object 转 Map:使用
Object.entries()
提取键值对,然后通过Map
的构造函数进行转换。 - Map 转 Object:使用
Array.from()
提取键值对,然后通过Object.fromEntries()
进行转换。
18.讲一下js获取和修改元素的基础方法?
4.1 获取元素
document.getElementById
:通过id
获取单个元素。document.getElementsByClassName
:通过class
获取一组元素。document.getElementsByTagName
:通过标签名获取一组元素。document.querySelector
:通过 CSS 选择器获取第一个匹配的元素。document.querySelectorAll
:通过 CSS 选择器获取所有匹配的元素。
4.2 修改元素
- 内容:
-
textContent
:设置或获取文本内容。innerHTML
:设置或获取 HTML 内容。
- 样式:
-
style
:通过style
属性修改样式。
- 属性:
-
setAttribute
:设置属性。getAttribute
:获取属性。removeAttribute
:移除属性。
- 添加和移除:
-
appendChild
:将节点添加到子节点列表末尾。insertBefore
:将节点插入到指定子节点之前。removeChild
:移除子节点。
19.讲一下事件冒泡和事件委托?
4.1 事件冒泡
- 定义:事件从最具体的元素开始,逐级向上传播到较为不具体的节点。
- 默认行为:大部分事件默认会冒泡,但有些事件(如
focus
、blur
)不会冒泡。 - 应用场景:理解事件冒泡机制有助于调试和优化事件处理逻辑。
- 阻止冒泡:stopPropagation()
4.2 事件委托
- 定义:利用事件冒泡机制,将事件监听器绑定到父元素上,通过
event.target
判断目标元素。 - 优势:
-
- 减少事件监听器数量,节省内存和性能。
- 自动处理动态添加的子元素,无需重新绑定事件。
- 实现方式:
-
- 将事件监听器绑定到父元素。
- 在事件处理函数中,通过
event.target
判断目标元素。 - 根据条件执行逻辑。
- 应用场景:
-
- 动态元素:处理动态生成的元素。
- 性能优化:减少事件监听器数量。
- 简化代码:减少重复代码。
20.异步加载 CSS 是否会阻塞页面渲染?
2.1 同步加载 CSS
- 定义:默认情况下,CSS 是同步加载的。浏览器会暂停 DOM 构建,直到 CSS 文件加载完成并解析完毕。
- 影响:同步加载 CSS 会阻塞页面的渲染,因为浏览器需要在渲染页面之前知道所有样式信息。
2.2 异步加载 CSS
- 定义:通过
link
标签的rel="stylesheet"
属性可以将 CSS 文件标记为异步加载。HTML预览复制
<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
或者使用 JavaScript 动态加载:JavaScript复制
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';
document.head.appendChild(link);
- 影响:异步加载 CSS 不会阻塞页面的 DOM 构建和渲染。浏览器会继续构建 DOM 树并渲染页面,即使 CSS 文件尚未加载完成。这可能会导致页面在 CSS 加载完成之前以无样式或部分样式显示。
21.js时间循环机制(Event Loop)?
https://juejin.cn/post/7379960023407853580?searchId=2025081510075537A1853657AF7799B062
5.1 单线程模型
JavaScript 是单线程语言,所有任务都在一个线程上按顺序执行。
5.2 事件循环的作用
事件循环确保程序能够响应异步事件,而不会因为单线程的限制而卡住。
5.3 调用栈和任务队列
- 调用栈:用于跟踪当前正在执行的函数。
- 任务队列:用于存储等待执行的任务。
5.4 事件循环的工作原理
事件循环会不断检查调用栈是否为空,如果为空,就从任务队列中取出任务执行。
5.5 宏任务和微任务
- 宏任务:包括整体代码块、
setTimeout
、setInterval
、setImmediate
、I/O 操作、UI 渲染等。 - 微任务:包括
Promise
、MutationObserver
、process.nextTick
等。 - 执行顺序:事件循环会先执行宏任务,然后在每次宏任务执行完毕后,清空微任务队列。
正确的一 次 Event loop 顺 序 是 :
1. 执行 同 步 代 码, 这属 于 宏 任务
2. 执行栈为 空 , 查询 是 否有微任务需 要 执行
3. 执行 所 有微任务
4. 必要 的话渲染 UI
5. 然 后 开始 下一轮 Event loop, 执行宏 任务
中的 异 步 代 码
22.ES6新特性?
- let 和 const:块级作用域的变量声明,避免了
var
的作用域提升问题。 - 箭头函数:简化了函数的写法,没有自己的
this
。 - 模板字符串:支持多行字符串和嵌入表达式。
- 解构赋值:从数组或对象中提取值并赋值给变量。
- 默认参数:函数参数可以有默认值。
- Promise:用于异步编程,解决了回调地狱问题。
- 类(Class):提供了更简洁的语法来创建对象和继承。
- 模块(Module):支持将代码分割成模块,便于代码复用和管理。
- Map 和 Set:提供了新的数据结构。
- 扩展运算符:用于展开数组或对象。
23.箭头函数和普通函数的区别?
this
绑定:
-
- 普通函数有自己的
this
,取决于调用方式。 - 箭头函数没有自己的
this
,捕获定义时的上下文。
- 普通函数有自己的
arguments
对象:
-
- 普通函数有
arguments
对象。 - 箭头函数没有
arguments
对象,只能通过参数列表访问。
- 普通函数有
- 语法:
-
- 普通函数语法较为冗长。
- 箭头函数语法更为简洁。
- 作为构造函数:
-
- 普通函数可以作为构造函数。
- 箭头函数不能作为构造函数。
- 原型:
-
- 普通函数有
prototype
属性。 - 箭头函数没有
prototype
属性。
- 普通函数有
24.什么是ajax?
ajax是一种技术组合,通过浏览器内置的 XMLHttpRequest
对象,实现异步请求,从而在不刷新页面的情况下更新数据。
// 1. 创建 XMLHttpRequest 对象
let xhr = new XMLHttpRequest();// 2. 配置请求:方法、地址、是否异步
xhr.open('GET', '/api/user?id=123', true);// 3. 监听状态变化
xhr.onreadystatechange = function () {if (xhr.readyState === 4 && xhr.status === 200) {// 4. 处理返回的数据let data = JSON.parse(xhr.responseText);document.getElementById('username').innerText = data.name;}
};// 5. 发送请求
xhr.send();
25.什么是fetch?
fetch 是浏览器原生提供的现代网络请求 API,基于 Promise 实现,语法更简洁,替代了传统的 XMLHttpRequest。
基本语法:
fetch(url, options).then(response => response.json()).then(data => console.log(data)).catch(error => console.error(error));实现post
fetch('https://jsonplaceholder.typicode.com/posts', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({title: 'foo',body: 'bar',userId: 1})
}).then(res => res.json()).then(data => console.log('创建成功:', data));使用async/await
async function getUser() {try {const res = await fetch('https://api.github.com/users/octocat');const data = await res.json();console.log(data);} catch (err) {console.error('请求失败:', err);}
}
getUser();错误判断:
fetch('/api/data').then(res => {if (!res.ok) {throw new Error(`HTTP 错误!状态码:${res.status}`);}return res.json();}).catch(err => console.error(err));中断请求:
const controller = new AbortController();fetch('/api/data', { signal: controller.signal }).then(res => res.json()).catch(err => {if (err.name === 'AbortError') {console.log('请求被中断');}});// 5秒后中断请求
setTimeout(() => controller.abort(), 5000);上传文件:
const form = document.querySelector('form');
const formData = new FormData(form);fetch('/upload', {method: 'POST',body: formData
}).then(res => res.json()).then(data => console.log('上传成功:', data));
26.同步和异步是什么?
- 同步任务:指的是在主线程上排队执行的任务,只有前一个任务执行完毕后,才会开始下一个任务。这种模式下,每个任务必须等待上一个任务结束才能开始,这可能会导致阻塞,特别是在处理耗时操作(如网络请求、文件读写等)时。
- 异步任务:允许在主程序流程之外执行某些操作,不会阻塞后续代码的执行。当发起一个异步任务(例如 AJAX 请求),它会在后台执行,而JavaScript继续执行后面的代码。一旦异步任务完成,会通过回调函数、Promise 或者 async/await 来处理结果
回调函数实现异步:settimeout,读取文件内容
27.说一下 async 和 await 的原理,generator 用来做什么?
Async 和 Await 的原理
async
和 await
是 ES2017 引入的用于简化异步编程的新特性,它们基于 Promise 实现。以下是它们的工作原理:
- Async 函数:当你在一个函数前加上
async
关键字时,这个函数会自动返回一个 Promise。如果该函数返回一个值,那么这个 Promise 会被解决(resolved)为该值;如果函数抛出异常,则 Promise 会被拒绝(rejected)。 - Await 操作符:只能在
async
函数内使用,用来暂停函数的执行直到等待的 Promise 被解决或被拒绝。一旦 Promise 被解决,await
表达式将返回该结果。如果 Promise 被拒绝,await
将抛出错误,可以通过 try...catch 结构来捕获处理。
async function fetchExample() {try {let response = await fetch('https://api.example.com/data');let data = await response.json();console.log(data);} catch (error) {console.error('Error fetching data:', error);}
}
在这个例子中,fetchExample
函数是异步的,并且它使用 await
来等待 fetch
请求完成以及 JSON 数据解析完成。
Generator 用来做什么
Generators(生成器)是 ES6 引入的一个特殊类型的函数,允许你暂停和恢复函数的执行。通过 function*
定义,并使用 yield
关键字来暂停执行,这使得它可以逐步地产生一系列值,而不是一次性返回所有结果。
使用场景:
- 迭代器:Generators 提供了一种简单的方法来定义迭代器对象,可以用来遍历复杂的数据结构。
function* idMaker() {let index = 0;while(true)yield index++;
}let gen = idMaker();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
- 异步流程控制:尽管现在有了
async
/await
,但在之前,Generators 可以与 Promises 结合使用来管理复杂的异步操作流,提供一种类似同步代码的写法来处理异步逻辑。
function request(url) {// 返回一个模拟的异步请求return new Promise((resolve, reject) => {setTimeout(() => resolve(`Data from ${url}`), 1000);});
}function* asyncOperation() {const result1 = yield request('http://example.com/page1');console.log(result1);const result2 = yield request('http://example.com/page2');console.log(result2);
}// 运行生成器函数并处理异步调用
function run(generatorFunc) {const generatorObject = generatorFunc();function handle(yielded) {if (!yielded.done) {yielded.value.then(function(value) {return handle(generatorObject.next(value));});}}handle(generatorObject.next());
}run(asyncOperation);
28.浏览器实现多线程?
28.什么情况下会造成内存泄露?
1.未声明、意外的全局变量
var a = '这是一个全局变量';
function test(){b = '变量b'; //b 成为一个全局变量,不会被回收
}
2.遗忘的定时器
3.事件绑定
4.闭包
29.Promise全解
基础手写:https://juejin.cn/post/6945319439772434469#heading-1
promise.all:https://juejin.cn/post/7475266130786877477?searchId=2025081115295560CF6110EB736E3FFF33
https://juejin.cn/post/7533150906670841906#heading-8
实现并发控制:
async function concurrencyControl(tasks, concurrency = 2) {const ret = Array(tasks.length); // 预分配,保证顺序const executing = [];for (let i = 0; i < tasks.length; i++) {const task = tasks[i];const promise = task().then(res => (ret[i] = res)) // 按索引写结果.catch(err => (ret[i] = { error: err })); // 容错:把错误也存起来executing.push(promise);// 如果池子满了,等一个任务先跑完if (executing.length >= concurrency) {await Promise.race(executing);}}// 等最后一个批次await Promise.all(executing);return ret;
}
30.new Map()和new Set()
🌟 一、Map
和 Set
的核心区别
特性 |
|
|
存储形式 | 键值对(key-value) | 只存唯一值(value) |
是否有键 | 有键(key)和值(value) | 没有键,只有值 |
是否允许重复 | key 不能重复,value 可重复 | 值不能重复 |
主要用途 | 存储键值映射关系 | 存储去重的值集合 |
🧩 二、Map
详解
✅ 1. 创建 Map
const map = new Map();
✅ 2. 常用方法
方法 | 说明 |
| 添加或更新一个键值对 |
| 获取指定 key 的值,不存在返回 |
| 判断是否有该 key,返回 |
| 删除指定 key 的键值对 |
| 清空所有内容 |
| 获取键值对的数量 |
✅ 3. 遍历方法
方法 | 说明 |
| 返回所有 key 的迭代器 |
| 返回所有 value 的迭代器 |
| 返回所有 的迭代器 |
| 遍历每个键值对 |
✅ 4. 示例
const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);console.log(map.get('name')); // "Alice"
console.log(map.has('age')); // true
map.delete('age');for (let [key, value] of map) {console.log(key, value); // name Alice
}
💡 Map
支持任意类型作为 key(包括对象、函数),而普通对象的 key 只能是字符串或 Symbol。
🧩 三、Set
详解
✅ 1. 创建 Set
const set = new Set();
✅ 2. 常用方法
方法 | 说明 |
| 添加一个值 |
| 判断是否有该值,返回 |
| 删除指定值 |
| 清空所有内容 |
| 获取值的数量 |
✅ 3. 遍历方法
方法 | 说明 |
或 | 返回所有值的迭代器(Set 中 keys 和 values 相同) |
| 返回 的迭代器 |
| 遍历每个值 |
✅ 4. 示例
const set = new Set();
set.add(1);
set.add(2);
set.add(1); // 重复,不会添加console.log(set.has(1)); // true
console.log(set.size); // 2for (let value of set) {console.log(value); // 1, 2
}// 常见用途:数组去重
const arr = [1, 2, 2, 3, 3, 4];
const unique = [...new Set(arr)]; // [1, 2, 3, 4]
🆚 四、对比总结
场景 | 用 还是 ? |
存储用户名 → 用户信息 | ✅ (key: 用户名, value: 信息) |
存储一组不重复的标签 | ✅ (如:[‘JavaScript’, ‘React’, ‘Vue’]) |
判断某个值是否存在 | ✅ 更合适( 方法) |
需要键值映射关系 | ✅ |
数组去重 | ✅ 是最佳选择 |
🔁 五、Map 和 Set 的转换
Map
转数组
Array.from(map.keys())
Array.from(map.values())
Array.from(map.entries())
Set
转数组
Array.from(set)
// 或
[...set]
✅ 总结
|
| |
本质 | 键值对集合 | 唯一值集合 |
核心方法 |
, , , |
, , |
遍历 |
得到 |
得到 |
典型用途 | 缓存、字典、计数器 | 去重、成员判断、集合运算 |
✅ 简单记:
- 要 存 key-value?用
Map
- 要 存唯一值?用
Set
希望这个讲解清晰明了!如果你有具体使用场景,也可以告诉我,我来帮你判断该用哪个。
31.{}和new Map()的区别
✅ 一、核心区别总结
特性 |
(普通对象) |
|
键的类型 | 只能是字符串或 | 可以是任意类型(字符串、数字、对象、函数等) |
性能 | 大量键值操作时较慢 | 专为频繁增删改优化,性能更好 |
可迭代性 | 需要额外方法(如 )遍历 | 原生支持 遍历 |
获取大小 | 需手动计算 | 直接用 |
继承属性干扰 | 可能与原型链上的属性冲突 | 不受原型影响,纯净 |
内存管理 | 普通引用 | 支持 (弱引用,避免内存泄漏) |
✅ 二、详细对比
1. 键的类型不同 ✅(最重要区别)
// ❌ {}:键只能是字符串或 Symbol
const obj = {};
obj[{}] = 'hello';
console.log(obj);
// 输出:{'[object Object]': 'hello'} → 对象被转成字符串 "[object Object]"// ✅ Map:键可以是任意类型
const map = new Map();
map.set({}, 'hello');
map.set(function(){}, 'world');
console.log(map.size); // 2,正确保存了两个不同的键
💡 所以如果你要用对象作为键,必须用 Map
。
2. 遍历方式不同
const map = new Map([['a', 1], ['b', 2]]);
const obj = { a: 1, b: 2 };// ✅ Map 原生可迭代
for (let [k, v] of map) {console.log(k, v); // a 1, b 2
}// ❌ {} 不是可迭代对象,不能直接 for...of
// for (let [k, v] of obj) {} // 报错!// 需要这样遍历:
for (let k in obj) {if (obj.hasOwnProperty(k)) {console.log(k, obj[k]);}
}
3. 性能差异
Map
是专门为频繁增删改查设计的,性能更优。{}
在键很多时,查找、删除性能下降明显。
📊 场景:如果你要存储成千上万个键值对,Map
更快更高效。
4. 大小获取
const map = new Map();
map.set('a', 1);
map.set('b', 2);
console.log(map.size); // 2 ✅ 直接获取const obj = { a: 1, b: 2 };
console.log(Object.keys(obj).length); // 2 ❌ 需要转换再计算
5. 原型链干扰问题
const obj = {};
obj.constructor = 'hacked';
console.log(obj.hasOwnProperty('toString')); // false,但其实有!
// 因为 toString 是继承来的// ❌ 如果你从用户输入中读取键名,可能意外覆盖或冲突const map = new Map();
map.set('constructor', 'safe');
// 不受影响,Map 不依赖原型
6. 初始化方式
// Map 可以用数组初始化
const map = new Map([['name', 'Alice'], ['age', 25]]);// 对象也可以
const obj = { name: 'Alice', age: 25 };
✅ 三、什么时候用哪个?
场景 | 推荐 |
存配置、JSON 数据、简单字典 | ✅ |
键是字符串,且数量少 | ✅ |
键是对象、函数、数字等 | ✅ |
频繁增删键值对 | ✅ |
需要按插入顺序遍历 | ✅ (保持插入顺序) |
大量数据存储 | ✅ |
需要 属性 | ✅ |
✅ 四、小结
问题 | 回答 |
和 能互换吗? | 小项目可以,但有局限 |
哪个更强大? |
功能更强,更现代 |
日常开发用哪个? | 简单场景用 ,复杂用 |
面试推荐用哪个? | 涉及“计数”“缓存”“大量键值”时,优先 |
💡 一句话总结:
🔹 {}
是“数据对象”,适合表示一个实体(如用户、配置)。
🔹 Map
是“集合容器”,适合存储键值映射关系,尤其是键不是字符串时。
如果你在写算法题(比如 groupAnagrams
),涉及“字符计数”“字符串映射”,强烈推荐使用 Map
,它更清晰、高效、不易出错。
32.for...of和for...in?
下面用一句话先给出“口诀”——
- 遍历可迭代对象(数组、Map、Set、字符串、NodeList、Generator …)→ 用
for...of
- 遍历普通对象的键(或需要知道可枚举属性名 / 原型链属性名)→ 用
for...in
再展开聊一下为什么,以及容易踩的坑。
一、for...of
的适用场景
适用对象 | 例子 |
数组 |
|
字符串 |
|
Set / Map |
|
NodeList / 类数组 |
|
共同点:这些对象都实现了 可迭代协议(有 Symbol.iterator
)。
拿到的值就是“元素本身”,而不是“索引”或“键”。
二、for...in
的适用场景
适用对象 | 例子 |
普通对象 |
|
需要枚举原型链属性 |
|
注意:
- 遍历出来的是“键”(字符串),而不是值;
- 会遍历原型链(除非手动
hasOwnProperty
过滤); - 顺序不确定,并且会漏掉
Symbol
键。
三、数组到底用谁?
- 要拿 值 →
for...of
- 要拿 索引 →
for (let i = 0; i < arr.length; i++)
或for (const idx in arr)
(后者会遍历到原型链属性,不推荐) - 既要值又要索引 →
arr.forEach((v, i) => { ... })
或者for (const [i, v] of arr.entries())
四、一张速查表
场景 | for…of | for…in |
数组取值 | ✅ | ❌ |
数组取索引 | ❌ | ⚠️(会枚举原型) |
普通对象取值 | ❌(对象不可迭代) | ✅(遍历键) |
字符串拆字符 | ✅ | ❌ |
Map/Set 遍历 | ✅ | ❌ |
五、小结一句话
“能看见 length
或 Symbol.iterator
就用 for…of;只想枚举键就用 for…in,并且别忘了 hasOwnProperty
。”
33.AMD,esMOdule,Commenjs的区别?
AMD、CommonJS 与 ES6 Module 的全面对比
这三种是 JavaScript 不同历史阶段的模块化规范。理解它们的区别对于掌握 JavaScript 生态至关重要。
一、核心概览
特性 | AMD (Asynchronous Module Definition) | CommonJS | ES6 Module (ESM) |
诞生时间 | 约 2009-2010 年 | 约 2009 年 | ES6 (2015) 标准,2017+ 浏览器支持 |
主要目标环境 | 浏览器 (前端) | 服务器 (Node.js, 后端) | 通用 (浏览器 & 服务器) |
加载方式 | 异步 (Asynchronous) | 同步 (Synchronous) | 静态 (Static) / 动态 ( ) |
典型实现/使用 | RequireJS, curl.js | Node.js 的 / | 浏览器原生支持, Node.js, Webpack, Vite |
模块执行 | 异步加载后执行 | 同步加载后执行 | 静态分析,依赖关系在编译时确定 |
现状 | 已过时,被 ESM 和打包工具取代 | Node.js 主流,前端打包中支持 | 现代标准,未来方向 |
二、详细对比
1. 设计哲学与加载机制
- AMD (为浏览器而生):
-
- 问题:浏览器通过网络加载脚本,延迟高。同步加载会阻塞页面渲染。
- 解决方案:异步加载。模块及其依赖可以并行下载,不阻塞主线程。
- 方式:使用回调函数 (
callback
) 或Promise
(现代实现) 来处理加载完成后的逻辑。 - 优点:优化用户体验,避免页面卡顿。
- 缺点:语法相对复杂,需要额外的加载器(如 RequireJS)。
- CommonJS (为服务器而生):
-
- 问题:服务器环境(Node.js)中,模块文件在本地磁盘,读取速度快。
- 解决方案:同步加载。执行到
require
时,必须立即拿到模块的值,保证代码执行顺序。 - 方式:
require()
函数同步返回模块的exports
对象。 - 优点:语法简单直观,符合传统编程习惯。
- 缺点:在浏览器中同步加载网络资源会严重阻塞,不可行。
- ES6 Module (现代通用方案):
-
- 问题:需要一个原生、通用、高效的模块系统。
- 解决方案:静态模块系统。
-
-
- 静态分析:
import
和export
是静态声明,在代码编译/解析阶段就能确定依赖关系,便于Tree Shaking(摇树优化,移除未使用代码)。 - 动态导入:通过
import('module-path')
返回Promise
,实现异步、按需加载。 - 绑定而非拷贝:导入的是对导出值的只读引用(活绑定),导出模块修改值,导入模块能感知到。
- 静态分析:
-
-
- 优点:原生支持,语法简洁,支持 Tree Shaking,兼顾静态分析和动态加载。
- 缺点:早期浏览器支持需要转译或打包。
2. 语法对比
操作 | AMD | CommonJS | ES6 Module |
定义模块 |
|
|
|
导入模块 |
|
|
|
默认导出 | 无原生概念,通常 一个对象 |
|
|
命名导出 | 无原生概念,通常 |
|
|
动态加载 |
|
(同步) |
(异步 ) |
3. 依赖处理
- AMD:前置声明。依赖在
define
的依赖数组中明确列出,加载器据此提前异步加载。 - CommonJS:使用时声明。依赖在代码中通过
require
语句引入,位置灵活。 - ES6 Module:静态声明。依赖在
import
语句中声明,必须在模块顶层,编译时即可分析。
4. 循环依赖
- AMD:处理较复杂,RequireJS 有特定机制。
- CommonJS:Node.js 有明确规则(返回
module.exports
的占位对象),相对可预测。 - ES6 Module:由于是活绑定,处理循环依赖更自然。导入的模块即使未执行完,也能获取到其导出的引用(值可能是
undefined
或初始值)。
5. 作用域与提升
- AMD:模块作用域(由加载器管理)。
- CommonJS:模块作用域(每个文件是一个模块)。
- ES6 Module:模块作用域,且
import
声明会被提升到模块顶部(但绑定是活的)。
6. 生态系统与工具链
- AMD:需要 RequireJS 等加载器。现代打包工具(Webpack, Rollup)可以处理 AMD 模块,但已非主流。
- CommonJS:Node.js 原生支持。打包工具能将其转换为适合浏览器的格式。
- ES6 Module:现代打包工具(Webpack, Vite, Rollup)的首选输入格式。支持
import
/export
的库更受现代工具链欢迎。Node.js 也已支持。
三、总结与推荐
方面 | AMD | CommonJS | ES6 Module |
核心 | 浏览器异步加载 | 服务器同步加载 | 通用静态模块 |
加载 | 异步 | 同步 | 静态分析 / 动态 |
环境 | 前端 (历史) | 后端 (Node.js) | 通用 (现代) |
语法 | 回调式 | 函数式 | 声明式 |
Tree Shaking | ❌ 困难 | ❌ 困难 (需额外处理) | ✅ 天然支持 |
现状 | 过时 | Node.js 主流 | 未来标准 |
结论与建议:
- 学习和新项目:优先学习和使用 ES6 Module (
import
/export
)。它是 JavaScript 的官方标准,被现代浏览器和工具链原生支持,是未来的方向。 - Node.js 开发:虽然 ESM 支持良好,但 CommonJS 依然非常普遍,尤其是在 NPM 生态中。根据项目需求和团队习惯选择,但了解 ESM 是必要的。
- 维护旧项目:如果遇到使用 RequireJS (AMD) 的老项目,需要理解其语法和机制。
- 工具链:现代构建工具(Vite, Webpack)能无缝处理这三种模块格式的转换和打包,让开发者可以专注于使用 ESM。
简单记忆:
require
/module.exports
-> CommonJS (Node.js)define
/require
(回调) -> AMD (老式前端)import
/export
-> ES6 Module (现代标准)
34.匿名函数的作用?
匿名函数就是没有名字的函数,类似箭头函数
一般用途:
- 回调函数 (Callbacks):
-
setTimeout
,setInterval
- 事件监听 (
addEventListener
) - 数组方法 (
map
,filter
,reduce
,forEach
)
- 立即执行 (IIFE):
-
- 创建私有作用域,避免全局污染。
- 模块化模式的基础。
- 闭包 (Closures):
-
- 匿名函数常用于创建闭包,封装私有数据。
function createCounter() {let count = 0; // 私有变量return function() { // 匿名函数形成闭包return ++count;};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
- 高阶函数 (Higher-Order Functions):
-
- 接受函数作为参数或返回函数的函数。
- 简化代码 (箭头函数):
-
- 在简单逻辑中,箭头函数提供更简洁的语法