1. JS 由哪三部分组成?
JavaScript 由以下三部分组成:
- ECMAScript(ES):JavaScript 的核心语法,如变量、作用域、数据类型、函数、对象等。
- DOM(文档对象模型):用于操作 HTML 和 XML 文档的 API,可以动态修改网页内容、结构和样式。
- BOM(浏览器对象模型) :用于操作浏览器窗口和页面,例如
window、navigator、location、history、screen等对象。
2. JS 有哪些内置对象?
JavaScript 具有以下内置对象:
- 基本对象 :
Object、Function、Boolean、Symbol - 数值对象 :
Number、BigInt、Math - 字符串对象 :
String - 数组对象 :
Array - 日期对象 :
Date - 正则对象 :
RegExp - 错误对象 :
Error、TypeError、SyntaxError、ReferenceError - 集合对象 :
Set、Map、WeakSet、WeakMap - 异步对象 :
Promise、AsyncFunction
3. 操作数组的方法有哪些?
数组方法可以分为几类:
① 增加元素
push(value):在数组末尾添加元素,返回新长度。unshift(value):在数组头部添加元素,返回新长度。splice(index, 0, value):在指定位置插入元素。
② 删除元素
pop():删除数组最后一个元素,并返回该元素。shift():删除数组第一个元素,并返回该元素。splice(index, count):删除指定位置的count个元素。
③ 查找元素
indexOf(value):返回元素第一次出现的位置,找不到返回-1。find(callback):返回符合条件的第一个元素,没有符合条件的返回undefined。findIndex(callback):返回符合条件的元素索引,找不到返回-1。includes(value):判断数组是否包含某个元素,返回true/false。
④ 其他常用方法
map(callback):返回一个新数组,每个元素由回调函数处理。filter(callback):筛选符合条件的元素,返回新数组。reduce(callback, initialValue):累加数组值,常用于计算总和、扁平化数组。sort(callback):对数组进行排序(默认按 Unicode 编码排序)。reverse():反转数组元素顺序。concat(arr):合并数组,返回新数组。slice(start, end):返回数组的部分片段,不修改原数组。
4. JS 对数据类型的检测方式有哪些?
typeof:适用于基本数据类型,但null误判为"object"。instanceof:判断对象是否属于某个构造函数的实例。Object.prototype.toString.call(value):返回精准数据类型,如"[object Array]"。Array.isArray(value):判断是否为数组。
5. 说一下闭包,闭包有什么特点?
闭包(Closure) 是指函数可以访问其外部作用域的变量,即使外部函数已经执行结束。
特点:
- 可以访问外部函数的变量,即使外部函数执行完毕。
- 变量不会被垃圾回收(可能导致内存泄露)。
- 适用于模块化开发,模拟私有变量。
示例:
js
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const counter = outer();
counter(); // 1
counter(); // 2
闭包(Closure)是指一个函数能够访问其外部作用域中的变量,即使在外部函数执行结束后,仍然可以保留对外部变量的访问权限。闭包的主要使用场景如下:
1. 数据私有化(模拟私有变量)
闭包可以创建私有变量,防止外部直接访问或修改数据。
示例:模拟私有变量
js
function createCounter() {
let count = 0; // 作为私有变量
return {
increment: function() {
count++;
console.log(count);
},
decrement: function() {
count--;
console.log(count);
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined(无法直接访问私有变量)
🔹 应用场景:
- 需要封装变量,避免外部随意修改(如计数器、缓存管理、权限控制)。
2. 事件监听器 & 回调
闭包常用于事件监听器或回调函数,使得事件处理函数能够访问外部作用域中的变量。
示例:按钮点击计数
js
function setupButton() {
let count = 0;
document.getElementById("myButton").addEventListener("click", function() {
count++;
console.log(`Button clicked ${count} times`);
});
}
setupButton();
🔹 应用场景:
- 在
addEventListener回调中保留数据(如点击次数、鼠标移动距离等)。
3. 函数柯里化(参数预处理)
柯里化(Currying)是指将一个接收多个参数的函数,转换为多个接收单一参数的函数。
示例:实现加法柯里化
js
function add(x) {
return function(y) {
return x + y;
};
}
const addFive = add(5);
console.log(addFive(3)); // 8
console.log(addFive(10)); // 15
🔹 应用场景:
- 预设部分参数,提高代码复用性(如
bind预设this)。 - 在 Redux、Lodash 等库中广泛应用。
4. 延迟执行(定时器)
闭包可以用于在定时器或异步操作中保留执行上下文。
示例:定时器
js
function delayedMessage(msg, delay) {
setTimeout(function() {
console.log(msg);
}, delay);
}
delayedMessage("Hello, Closure!", 2000);
🔹 应用场景:
setTimeout/setInterval相关的任务管理(如轮询、延迟加载)。
5. 模拟块级作用域(ES5 以前)
在 ES6 之前,JavaScript 没有 let 和 const 关键字,使用闭包可以创建局部作用域,避免变量污染。
示例:IIFE(立即执行函数表达式)
js
(function() {
var secret = "I am private";
console.log(secret);
})();
console.log(typeof secret); // undefined(无法访问)
🔹 应用场景:
- 在 ES5 及以前,使用 IIFE 防止变量污染全局作用域。
- 现代 JavaScript 用
let和const取代该用法。
6. 记忆化(缓存计算结果,提高性能)
闭包可用于缓存计算结果,避免重复计算,提升性能。
示例:缓存计算结果
js
function memoize(fn) {
let cache = {};
return function(arg) {
if (cache[arg]) {
console.log("Fetching from cache:", arg);
return cache[arg];
}
console.log("Calculating result for:", arg);
cache[arg] = fn(arg);
return cache[arg];
};
}
const square = memoize(x => x * x);
console.log(square(4)); // 计算并存入缓存
console.log(square(4)); // 直接从缓存获取
console.log(square(5)); // 计算并存入缓存
🔹 应用场景:
- 计算密集型任务的优化(如斐波那契数列、递归)。
- 缓存 API 请求结果,减少重复网络请求。
7. 迭代器 & 生成唯一 ID
闭包可以用来创建迭代器或唯一 ID 生成器。
示例:唯一 ID 生成器
js
function createIdGenerator() {
let id = 0;
return function() {
return id++;
};
}
const getId = createIdGenerator();
console.log(getId()); // 0
console.log(getId()); // 1
console.log(getId()); // 2
🔹 应用场景:
- 生成唯一标识符(如任务 ID、DOM 元素 ID)。
8. 绑定 this(模拟 bind 方法)
闭包可以用于创建绑定 this 的新函数。
示例:手写 bind
js
function myBind(fn, context) {
return function(...args) {
return fn.apply(context, args);
};
}
const obj = { name: "Alice" };
function sayName(greeting) {
console.log(greeting + ", " + this.name);
}
const boundSayName = myBind(sayName, obj);
boundSayName("Hello"); // Hello, Alice
🔹 应用场景:
- 事件处理时确保
this绑定正确。
总结
| 使用场景 | 说明 | 示例 |
|---|---|---|
| 数据私有化 | 模拟私有变量,防止外部访问 | 计数器、权限管理 |
| 事件监听器 | 保留外部变量的数据 | 统计按钮点击次数 |
| 函数柯里化 | 预处理参数,提升复用性 | add(5)(10) |
| 定时器 | 异步任务执行 | setTimeout 回调 |
| 模拟块级作用域 | 防止变量污染 | IIFE |
| 缓存优化 | 记忆化函数,减少重复计算 | Fibonacci、API 请求缓存 |
| 生成唯一 ID | 生成不重复的 ID | 任务队列管理 |
绑定 this |
确保回调 this 指向正确 |
myBind |
面试高频问题:
-
闭包的本质是什么?
- 一个函数可以访问其外部作用域的变量,即使外部函数执行结束后,变量依然可用。
-
闭包有哪些常见应用场景?
- 数据私有化、事件监听、定时器、柯里化、缓存优化、唯一 ID 生成等。
-
闭包会导致内存泄漏吗?如何避免?
- 是的,闭包可能导致变量无法被垃圾回收。
- 解决方案:手动解除引用 ,如
element.onclick = null释放 DOM 事件闭包,或者减少不必要的闭包使用。
闭包是 JavaScript 重要的特性之一,熟练掌握它的应用能提升代码质量和性能!🚀
6. 前端的内存泄露怎么理解?
内存泄露 是指程序不再使用某些对象,但垃圾回收机制无法释放它们,导致内存占用增加。
常见原因:
- 全局变量未释放 (
window.variable一直存在) - 未清理的定时器 (
setInterval没有clearInterval) - 闭包未正确释放(函数执行后仍然引用外部变量)
- 未移除的 DOM 事件监听 (
element.addEventListener没有removeEventListener)
解决方案:
- 避免全局变量,使用
let/const限制作用域。 setInterval用完后及时clearInterval()。- 及时
removeEventListener()解除事件绑定。 - 手动置
null解除对象引用。
7. 事件委托是什么?
事件委托(Event Delegation) 是将事件监听器绑定在父级元素上,利用事件冒泡机制处理子元素事件,提高性能。
示例:
js
document.getElementById('parent').addEventListener('click', function(event) {
if (event.target.tagName === 'BUTTON') {
console.log('Button clicked:', event.target.innerText);
}
});
这样即使新 button 动态添加到 #parent,仍然可以触发事件。
8. 基本数据类型和引用数据类型的区别?
| 数据类型 | 存储位置 | 赋值方式 | 比较方式 |
|---|---|---|---|
| 基本类型 | 栈内存 | 拷贝值 | 值比较 |
| 引用类型 | 堆内存 | 赋引用 | 地址比较 |
9. 说一下原型链。
原型链(Prototype Chain) 是 JavaScript 继承机制的核心。
每个对象都有 __proto__,指向其构造函数的 prototype,形成一个链式结构。
详细参照链接:js原型与原型链
10. new 操作符具体做了什么?
- 创建一个新对象
obj。 - 将
obj.__proto__关联到构造函数的prototype。 - 执行构造函数,并绑定
this到新对象。 - 如果构造函数返回对象,则返回该对象,否则返回
obj。
示例:
js
function Person(name) {
this.name = name;
}
const p = new Person("Alice");
console.log(p.name); // Alice
11. call、apply、bind 三者有什么区别?
call、apply 和 bind 是 JavaScript 中用于更改函数 this 指向的方法,它们的主要区别如下:
| 方法 | 作用 | 参数 | 是否立即执行 | 返回值 |
|---|---|---|---|---|
call |
绑定 this 并调用函数 |
thisArg, arg1, arg2, ... |
是 | 调用结果 |
apply |
绑定 this 并调用函数 |
thisArg, [arg1, arg2, ...] |
是 | 调用结果 |
bind |
绑定 this,返回新函数 |
thisArg, arg1, arg2, ... |
否 | 新函数 |
1. call 方法
call(thisArg, arg1, arg2, ...) 方法可以手动指定 this 并 立即调用 该函数,参数按顺序传递。
示例 1:基本用法
js
function greet(greeting, punctuation) {
console.log(greeting + ", " + this.name + punctuation);
}
const person = { name: "Alice" };
greet.call(person, "Hello", "!"); // Hello, Alice!
this被绑定到person,输出"Hello, Alice!"。- 参数
"Hello"和"!"依次传入。
示例 2:继承构造函数
js
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name); // 调用 Parent 构造函数
this.age = age;
}
const child = new Child("Bob", 10);
console.log(child.name, child.age); // Bob 10
Child通过call调用了Parent,继承了name属性。
2. apply 方法
apply(thisArg, [arg1, arg2, ...]) 也是手动指定 this,立即调用 该函数,但参数必须是 数组。
示例 1:基本用法
js
function sum(a, b, c) {
return a + b + c;
}
console.log(sum.apply(null, [1, 2, 3])); // 6
null代表this,因为sum本身不依赖this。apply传入参数数组[1, 2, 3]。
示例 2:获取数组中的最大/最小值
js
const numbers = [3, 8, 2, 7, 4];
console.log(Math.max.apply(null, numbers)); // 8
console.log(Math.min.apply(null, numbers)); // 2
Math.max和Math.min只能接收多个单独的参数,而apply允许传递数组。
3. bind 方法
bind(thisArg, arg1, arg2, ...) 与 call、apply 的最大区别是:
- 不会立即调用函数,而是返回一个新的函数。
- 新函数永久绑定
this,无论以后如何调用,它的this都不会改变。
示例 1:基本用法
js
function greet(greeting, punctuation) {
console.log(greeting + ", " + this.name + punctuation);
}
const person = { name: "Alice" };
const boundGreet = greet.bind(person, "Hello");
boundGreet("!"); // Hello, Alice!
bind生成的新函数boundGreet绑定了this为person。"Hello"作为greeting预设进去,调用时只需提供punctuation。
示例 2:延迟执行
js
const obj = {
name: "Charlie",
sayName: function() {
console.log(this.name);
}
};
const say = obj.sayName.bind(obj);
setTimeout(say, 1000); // 1 秒后打印 "Charlie"
bind绑定this,即使setTimeout在全局环境中执行,this仍然指向obj。
4. call、apply、bind 的主要区别
| 方法 | 是否立即执行 | 参数传递 | 适用场景 |
|---|---|---|---|
call |
是 | 依次传递参数 | 立即执行,适用于手动指定 this 的方法调用 |
apply |
是 | 以数组形式传递 | 立即执行,适用于参数数量不固定的情况(如 Math.max) |
bind |
否 | 依次传递参数 | 返回新函数,适用于事件绑定、延迟调用等 |
5. 适用场景总结
| 场景 | 推荐方法 |
|---|---|
立即调用函数,并更改 this |
call |
| 立即调用函数,并且参数是数组 | apply |
| 需要返回一个新函数,稍后执行 | bind |
| 继承构造函数 | call |
事件绑定,避免 this 丢失 |
bind |
setTimeout 绑定 this |
bind |
6. 面试高频考点
-
为什么
bind返回的是新函数?- 因为
bind不会立即执行,而是返回一个永久绑定this的新函数,适用于回调和事件处理。
- 因为
-
call和apply什么时候使用?- 当参数已知,使用
call(传递参数更直观)。 - 当参数是动态数组,使用
apply(例如Math.max.apply(null, array))。
- 当参数已知,使用
-
为什么
bind在setTimeout中很重要?- 因为
setTimeout内部的this默认指向window,使用bind可以确保this指向原对象。
- 因为
js
const obj = { name: "Bob" };
setTimeout(function() {
console.log(this.name); // undefined
}, 1000);
setTimeout(function() {
console.log(this.name); // Bob
}.bind(obj), 1000);
这三个方法是 JavaScript 面试的高频考点,掌握它们的区别和应用场景能帮助你更高效地编写代码!🚀
12.. JS 是如何实现继承的?**
JavaScript 主要通过 原型链 和 ES6 class 语法 实现继承:
1. 原型链继承(Prototype Inheritance)
每个 JavaScript 对象都有一个 __proto__ 指向它的原型对象,子类可以通过 prototype 继承父类的方法和属性。
js
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function() {
console.log("Hello, " + this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承父类的属性
this.age = age;
}
// 继承父类方法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const child = new Child("Tom", 10);
child.sayHello(); // Hello, Tom
2. ES6 class 继承
ES6 引入 class 语法,使用 extends 关键字更直观地实现继承。
js
class Parent {
constructor(name) {
this.name = name;
}
sayHello() {
console.log("Hello, " + this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 继承父类构造函数
this.age = age;
}
}
const child = new Child("Tom", 10);
child.sayHello(); // Hello, Tom
🔹 区别:
prototype继承是基于原型链,而class继承是 ES6 语法糖,底层仍然依赖原型。super关键字可以更方便地调用父类方法。
13. JS 的设计原理是什么?
JavaScript 的设计基于以下原则:
- 单线程:JavaScript 主要用于 Web 页面,设计为单线程避免 UI 渲染冲突。
- 事件驱动 :依赖 事件循环(Event Loop) 执行异步任务,如
setTimeout、Promise。 - 动态类型:变量类型动态变化,无需声明类型。
- 原型继承 :JS 采用 原型链 作为继承机制,而非类继承。
- 函数式编程:JS 允许高阶函数、闭包等,支持函数式编程风格。
14. JS 中关于 this 指向的问题
this 的指向取决于 调用方式:
1. 全局作用域
js
console.log(this); // 在浏览器中:window,在 Node.js 中:global
2. 对象方法
js
const obj = {
name: "Tom",
sayHello() {
console.log(this.name); // this 指向 obj
}
};
obj.sayHello(); // Tom
3. 构造函数
js
function Person(name) {
this.name = name;
}
const p = new Person("Tom");
console.log(p.name); // Tom(this 指向实例对象)
4. call / apply / bind
js
function sayHi() {
console.log(this.name);
}
const user = { name: "Alice" };
sayHi.call(user); // Alice
sayHi.apply(user); // Alice
const boundSayHi = sayHi.bind(user);
boundSayHi(); // Alice
5. 箭头函数
js
const obj = {
name: "Tom",
sayHello: function() {
setTimeout(() => {
console.log(this.name); // this 继承自 obj
}, 1000);
}
};
obj.sayHello(); // Tom
🔹 总结:
- 箭头函数的
this由外层作用域决定。 bind可手动绑定this,call/apply可立即执行。
15. script 标签里的 async 和 defer 有什么区别?
<script> 标签中的 async 和 defer 的区别
在 HTML 文档中,<script> 标签用于加载 JavaScript 脚本,默认情况下,脚本会阻塞 HTML 解析,直到脚本加载并执行完毕。而 async 和 defer 这两个属性用于优化脚本加载方式,以提高页面性能。
1. async 和 defer 的基本概念
| 属性 | 解析 HTML | 下载脚本 | 执行脚本 | 执行顺序 |
|---|---|---|---|---|
默认(无 async/defer) |
暂停 | 下载脚本 | 执行脚本 | 按HTML 书写顺序 执行,阻塞渲染 |
async |
不暂停 | 并行下载 | 下载完成立即执行 | 执行顺序不一定,谁先下载完就先执行 |
defer |
不暂停 | 并行下载 | HTML 解析完后按顺序执行 | 按照 HTML 中的顺序执行 |
2. async 和 defer 详细区别
1️⃣ async(异步加载并执行)
async允许脚本 异步下载,即不会阻塞 HTML 解析。- 一旦下载完成,就立即执行,不会等待 HTML 解析结束。
- 多个
async脚本的执行顺序不确定,取决于哪个脚本先下载完。
📌 示例
html
<script async src="script1.js"></script>
<script async src="script2.js"></script>
✅ 执行顺序:
script1.js和script2.js并行下载。- 哪个先下载完,哪个就先执行,与 HTML 书写顺序无关。
⚠️ 适用场景:
async适用于 不依赖 DOM 结构 或 不依赖其他脚本 的 JavaScript 代码,如:- 广告脚本
- 统计分析脚本
- 第三方 SDK(如 Google Analytics)
2️⃣ defer(异步加载,但按顺序执行)
defer也允许脚本 异步下载,不会阻塞 HTML 解析。- 所有
defer脚本会等到 HTML 解析完成后,按照 HTML 中的顺序执行。 - 适合多个脚本有执行顺序要求的情况。
📌 示例
html
<script defer src="script1.js"></script>
<script defer src="script2.js"></script>
✅ 执行顺序:
script1.js和script2.js同时下载。- 等 HTML 完全解析完毕后,按照
script1.js→script2.js的顺序执行。
⚠️ 适用场景:
defer适用于 依赖 DOM 结构 或 多个脚本之间有执行顺序要求 的情况,如:- DOM 操作脚本
- 框架初始化脚本
- 多个依赖关系的 JavaScript 文件
3. async vs defer vs 默认 <script>
| 加载方式 | HTML 解析 | JS 下载 | JS 执行 | 执行顺序 |
|---|---|---|---|---|
默认 <script> |
暂停 | 下载 | 执行 | 按HTML 书写顺序 |
async |
不暂停 | 并行下载 | 下载完成立即执行 | 下载顺序不确定 |
defer |
不暂停 | 并行下载 | HTML 解析完成后执行 | 按 HTML 书写顺序 |
4. async 和 defer 适用场景
| 需求 | 适合 async |
适合 defer |
|---|---|---|
| 独立的第三方脚本(如广告、分析工具) | ✅ | ❌ |
| 多个脚本之间无依赖关系 | ✅ | ❌ |
| 需要操作 DOM,必须等待 HTML 解析完成 | ❌ | ✅ |
| 多个脚本之间有顺序依赖 | ❌ | ✅ |
5. async 和 defer 结合使用?
HTML 规范规定,不能同时使用 async 和 defer 。如果一个 <script> 标签同时有 async 和 defer,则 async 优先,defer 被忽略。
6. 最佳实践
✅ 如果脚本不依赖 DOM,可用 async:
html
<script async src="analytics.js"></script>
<script async src="ads.js"></script>
✅ 如果脚本依赖 DOM 或多个脚本有执行顺序,可用 defer:
html
<script defer src="jquery.js"></script>
<script defer src="main.js"></script>
✅ 如果脚本必须立即执行(阻塞执行),则不加 async 或 defer:
html
<script src="important.js"></script>
7. 结论
async |
defer |
|---|---|
| 并行下载,下载完立刻执行 | 并行下载,HTML 解析完后按顺序执行 |
| 执行顺序不确定(谁先下载完谁先执行) | 按 HTML 书写顺序执行 |
| 适用于独立、不依赖 DOM 的脚本 | 适用于需要等待 DOM 解析完成的脚本 |
✅ 结论:
async适合独立的、无依赖的脚本(如统计、广告)。defer适合依赖 DOM 或者需要按顺序执行的脚本(如框架、主逻辑)。defer是加载多个脚本的最佳选择,不会阻塞页面解析,又能保证执行顺序。 🚀
16. setTimeout 最小执行时间是多少?
在大多数浏览器中,setTimeout 的最小延迟时间是 4ms(如果时间小于 4ms,实际延迟仍为 4ms)。
js
setTimeout(() => console.log("Hello"), 0); // 最早 4ms 后执行
- 浏览器为了节能,嵌套
setTimeout超过 5 次,最小延迟变成 4ms。 - 受 事件循环(Event Loop) 影响,
setTimeout(fn, 0)也不会立即执行。
17. ES6 和 ES5 有什么区别?
| 特性 | ES5 | ES6 |
|---|---|---|
| 变量声明 | var |
let / const |
| 作用域 | 函数作用域 | 块级作用域 |
| 字符串 | 字符串拼接 (+) |
模板字符串(`${}`) |
| 箭头函数 | 无 | () => {} |
| 类 | 基于原型 | class 语法 |
this 绑定 |
call/apply/bind |
箭头函数继承 this |
| 模块化 | script 标签 |
import/export |
18. ES6 的新特性有哪些?
ES6 带来了许多新特性,包括:
-
let和const变量声明jslet a = 10; // 块级作用域 const b = 20; // 常量 -
模板字符串
jslet name = "Tom"; console.log(`Hello, ${name}!`); -
箭头函数
jsconst add = (x, y) => x + y; -
解构赋值
jslet { name, age } = { name: "Alice", age: 25 }; -
扩展运算符
jslet arr = [1, 2, 3]; let newArr = [...arr, 4, 5]; -
class语法jsclass Person { constructor(name) { this.name = name; } } -
模块化
js// 导出 export function sayHi() { console.log("Hi!"); } // 导入 import { sayHi } from "./module.js";
18.高阶函数和闭包的区别
在 JavaScript 中,高阶函数和闭包都是重要的概念,虽然有一定的关联,但它们的核心作用和使用方式不同。下面详细分析它们的概念、特点、区别、应用场景。
1. 高阶函数(Higher-Order Function)
定义 :
高阶函数 是接收一个函数作为参数 或者返回一个函数的函数。
特点
- 参数可以是函数(回调函数)
- 返回值可以是函数(返回新的函数)
- 使代码更具可复用性和可组合性
示例
1. 作为参数传递的高阶函数
js
function operate(a, b, fn) {
return fn(a, b);
}
function add(x, y) {
return x + y;
}
console.log(operate(3, 5, add)); // 8
✅ operate 是一个高阶函数,因为它接受 fn 作为参数。
2. 返回一个函数的高阶函数
js
function multiplier(factor) {
return function (number) {
return number * factor;
};
}
const double = multiplier(2);
console.log(double(5)); // 10
✅ multiplier 是一个高阶函数,因为它返回了一个新函数。
2. 闭包(Closure)
定义 :
闭包 是一个可以访问其外部作用域变量的函数,即使在外部作用域执行结束后,函数仍然可以访问这些变量。
特点
- 函数内部引用了外部作用域的变量
- 外部作用域被销毁后,闭包仍然可以访问变量
- 变量不会被垃圾回收,可能会造成内存泄漏(如果不正确释放)
示例
1. 典型闭包示例
js
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3
✅ inner 仍然可以访问 outer 作用域中的 count 变量,即使 outer 执行完毕。
2. 闭包用于模拟私有变量
js
function createCounter() {
let count = 0;
return {
increment: function () {
count++;
console.log(count);
},
decrement: function () {
count--;
console.log(count);
}
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
✅ 变量 count 只能被 increment 和 decrement 访问,形成私有作用域。
3. 高阶函数 vs. 闭包
| 特性 | 高阶函数 | 闭包 |
|---|---|---|
| 定义 | 以函数作为参数 或返回函数 | 访问外部作用域变量的函数 |
| 主要作用 | 代码复用 、抽象 、回调机制 | 数据封装 、模拟私有变量 |
| 是否涉及作用域 | 主要关注函数传递,不强调作用域 | 强调函数保留外部作用域 |
| 返回值 | 可能返回一个函数 | 返回的函数可访问外部变量 |
| 是否影响垃圾回收 | 不一定 | 可能会导致变量不会被回收 |
| 常见应用 | map、filter、reduce、回调函数 |
计数器、私有变量、缓存函数 |
4. 结合高阶函数和闭包
实际上,闭包和高阶函数可以结合使用。例如,返回函数的高阶函数通常会创建闭包:
js
function createMultiplier(factor) {
return function (number) {
return number * factor;
};
}
const double = createMultiplier(2);
console.log(double(5)); // 10
console.log(double(10)); // 20
✅ createMultiplier 是高阶函数,因为它返回了一个新函数,而返回的函数是闭包,因为它引用了 factor 变量。
5. 适用场景
高阶函数适用场景
- 数组操作 (
map、filter、reduce) - 回调函数 (
setTimeout、addEventListener) - 函数柯里化
- 动态函数生成
闭包适用场景
- 创建私有变量
- 缓存计算结果
- 延迟执行
- 封装业务逻辑
6. 结论
- 高阶函数 主要用于 函数的传递和返回,提高代码复用性。
- 闭包 主要用于 保持状态和封装作用域,防止变量污染。
🔹 简单来说:
- 高阶函数 = 操作函数的函数
- 闭包 = 访问外部变量的函数
- 高阶函数和闭包可以结合使用,返回的函数往往会形成闭包 🚀
19. 谈谈js的事件循环(Event Loop)
JavaScript 事件循环(Event Loop)详解
1. 什么是事件循环(Event Loop)?
JavaScript 是 单线程 语言,主要用于浏览器和 Node.js 等环境中。由于 JavaScript 需要同时处理 UI 渲染、用户交互、网络请求等操作,因此采用了 事件循环(Event Loop) 机制,使其能够高效地执行任务,而不会阻塞主线程。
2. 事件循环的核心机制
JavaScript 的运行机制可以概括为:
- 所有同步任务 (同步代码、函数调用)都在主线程上执行,形成一个 执行栈(Call Stack)。
- 异步任务 (如
setTimeout、Promise、DOM 事件等)被挂起,等待执行时机,并存入相应的 任务队列(Task Queue)。 - 事件循环不断检查 执行栈是否为空,如果为空,就会从任务队列中取出任务放入执行栈执行。
- 这个过程循环往复,形成 事件循环(Event Loop)。
3. 任务队列(宏任务与微任务)
JavaScript 的任务分为 宏任务(Macro Task) 和 微任务(Micro Task)。
🔹 宏任务(Macro Task)
宏任务通常包含:
setTimeoutsetIntervalsetImmediate(Node.js)I/O 任务UI 渲染(Rendering)MessageChannelrequestAnimationFrame
每次 事件循环 执行时,只会从 宏任务队列 取出一个任务执行,执行完后会检查 微任务队列。
🔹 微任务(Micro Task)
微任务通常包含:
Promise.then、catch、finallyMutationObserverprocess.nextTick(Node.js 专属)
微任务的特点:
- 在当前事件循环的 最后 ,也就是 当前任务执行完后立即执行。
- 微任务优先级高于宏任务,会在每次事件循环结束后立即执行所有微任务。
4. 事件循环的执行流程
- 执行同步代码(全局代码),将函数调用入栈。
- 遇到异步任务(如
setTimeout、Promise) :setTimeout等 宏任务 进入 宏任务队列。Promise.then等 微任务 进入 微任务队列。
- 同步代码执行完毕后,执行所有微任务。
- 执行一个宏任务 (如
setTimeout回调)。 - 执行所有微任务。
- 重复步骤 4 和 5,直到所有任务执行完毕。
5. 代码示例
🔹 示例 1:同步、宏任务、微任务的执行顺序
js
console.log("1"); // 同步任务
setTimeout(() => {
console.log("2"); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log("3"); // 微任务
});
console.log("4"); // 同步任务
✅ 执行结果:
1
4
3
2
📌 解析:
- 先执行同步代码,输出
1。 setTimeout进入宏任务队列。Promise.then进入微任务队列。- 执行同步代码
console.log(4),输出4。 - 执行微任务
console.log(3),输出3。 - 执行宏任务
console.log(2),输出2。
🔹 示例 2:多个微任务与宏任务
js
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
Promise.resolve().then(() => {
console.log("C");
}).then(() => {
console.log("D");
});
console.log("E");
✅ 执行结果:
css
A
E
C
D
B
📌 解析:
- 先执行同步代码,输出
A和E。 setTimeout进入 宏任务队列。Promise.then进入 微任务队列 ,执行console.log("C"),输出C。Promise.then产生的第二个微任务console.log("D")执行,输出D。- 事件循环进入宏任务阶段,执行
setTimeout,输出B。
6. 常见问题
🔹 setTimeout(fn, 0) 真的会立即执行吗?
不会。即使 setTimeout 设为 0,它依然是 宏任务 ,必须等到当前执行栈和所有 微任务执行完毕后,才会执行。
🔹 async/await 和事件循环
async/await 其实是 Promise 的语法糖 ,其 await 关键字会 暂停代码执行,并将后续代码作为微任务放入微任务队列。
示例:
js
async function test() {
console.log("A");
await Promise.resolve();
console.log("B");
}
console.log("C");
test();
console.log("D");
✅ 执行结果:
css
C
A
D
B
📌 解析:
console.log("C")先执行,输出C。test()执行,输出A,遇到await,暂停。console.log("D")执行,输出D。await后的代码作为微任务console.log("B")进入微任务队列,随后执行,输出B。
7. 浏览器和 Node.js 的 Event Loop 区别
| 环境 | 微任务顺序 | setImmediate |
|---|---|---|
| 浏览器 | Promise.then 优先于 setTimeout |
setImmediate 在 setTimeout(0) 之后 |
| Node.js | process.nextTick 优先于 Promise.then |
setImmediate 在 setTimeout(0) 之前 |
示例:
js
setImmediate(() => console.log("setImmediate"));
setTimeout(() => console.log("setTimeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("nextTick"));
✅ Node.js 结果:
javascript
nextTick
Promise
setImmediate
setTimeout
✅ 浏览器结果:
javascript
Promise
setTimeout
setImmediate
📌 解析:
- Node.js 中
process.nextTick优先级最高,Promise.then其次,setImmediate比setTimeout(0)先执行。 - 浏览器 中
Promise.then先执行,然后setTimeout(0),最后setImmediate(浏览器中setImmediate表现与setTimeout(0)类似)。
8. 总结
- JavaScript 是单线程的,依赖事件循环(Event Loop)处理异步任务。
- 任务分为 :
- 同步任务(同步执行)
- 微任务(Promise.then, MutationObserver, process.nextTick)
- 宏任务(setTimeout, setInterval, setImmediate, UI渲染等)
- 事件循环流程 :
- 执行同步任务
- 执行所有微任务
- 执行下一个宏任务(然后再执行微任务)
async/await也是基于 Promise,await后的代码是微任务。
💡 掌握事件循环的机制,有助于理解 JavaScript 的异步行为,编写高效的前端代码!🚀
20. 用递归的时候有没有遇到什么问题?**
在使用 递归(Recursion) 时,可能会遇到以下问题:
1. 递归深度过大,导致栈溢出(Stack Overflow)
-
递归会不断调用自身,每次调用都会占用 调用栈(Call Stack) 的空间。如果递归深度过大,可能会导致 堆栈溢出(Maximum call stack size exceeded) 错误。
-
示例 (错误示范):
jsfunction infiniteRecursion() { infiniteRecursion(); // 无限递归,导致栈溢出 } infiniteRecursion();解决方案 :
- 使用尾递归优化(Tail Recursion)(ES6 支持)
- 改用循环(如 for/while) 代替递归
- 设定递归终止条件
2. 递归效率低,可能导致性能问题
-
递归可能导致 重复计算,特别是在计算斐波那契数列等问题时。
-
示例(低效的递归):
jsfunction fibonacci(n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); } console.log(fibonacci(40)); // 计算量非常大解决方案:
- 使用缓存(记忆化存储,Memoization)
- 使用动态规划(Dynamic Programming) 代替递归
jsfunction fibonacciMemo(n, memo = {}) { if (n in memo) return memo[n]; if (n <= 1) return n; return memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo); } console.log(fibonacciMemo(40)); // 计算快很多
3. 递归终止条件错误,导致无限循环
-
示例 :
jsfunction countdown(n) { if (n === 0) return; // 终止条件 console.log(n); countdown(n - 1); } countdown(5); // 正确- 终止条件错误可能导致 无限递归,从而引发栈溢出。
4. 递归函数的参数处理错误
-
在递归过程中,参数的状态可能变化,影响递归逻辑。
-
示例 (错误的参数传递):
jsfunction sum(arr) { if (arr.length === 0) return 0; return arr[0] + sum(arr); // 这里没有去除 arr[0],会无限递归 } console.log(sum([1, 2, 3])); // 错误正确方式 :
jsfunction sum(arr) { if (arr.length === 0) return 0; return arr[0] + sum(arr.slice(1)); // 传入去掉首元素的数组 } console.log(sum([1, 2, 3])); // 6
21. 如何实现一个深拷贝(Deep Clone)?
深拷贝是指创建一个 新对象 ,并 完全复制原对象的所有属性值,包括嵌套对象,而不是仅仅拷贝引用(浅拷贝)。
方法 1:JSON 方式(简单但有局限)
js
const obj = { a: 1, b: { c: 2 } };
const clone = JSON.parse(JSON.stringify(obj));
console.log(clone); // { a: 1, b: { c: 2 } }
✅ 优点:
- 适用于 简单对象(无函数、循环引用等)。
❌ 缺点:
- 无法处理 函数、
undefined、Date、RegExp、Map、Set等类型。 - 会移除 原型链(Prototype)。
方法 2:递归深拷贝(适用于大部分场景)
js
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
if (hash.has(obj)) return hash.get(obj); // 处理循环引用
let cloneObj = Array.isArray(obj) ? [] : {};
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const clone = deepClone(obj);
console.log(clone); // 深拷贝成功
✅ 优点:
- 适用于 普通对象、数组。
- 解决循环引用 问题。
❌ 缺点:
- 无法拷贝函数、特殊对象(如
Map,Set,RegExp)。
方法 3:基于 lodash 库
lodash 提供 _.cloneDeep() 方法进行深拷贝:
js
const _ = require("lodash");
const obj = { a: 1, b: { c: 2 } };
const clone = _.cloneDeep(obj);
console.log(clone);
✅ 优点:
- 处理 大多数数据类型(对象、数组、Date、RegExp、Map、Set 等)。
❌ 缺点:
- 需要额外引入
lodash。
方法 4:使用 structuredClone()(推荐,现代浏览器支持)
js
const obj = { a: 1, b: { c: 2 }, d: new Date() };
const clone = structuredClone(obj);
console.log(clone);
✅ 优点:
- 支持大多数数据类型(对象、数组、Date、Blob、Map、Set等)。
- 性能比递归方式更优。
❌ 缺点:
- 不支持函数、
undefined、DOM元素。
总结
| 方法 | 优点 | 缺点 |
|---|---|---|
JSON.parse(JSON.stringify()) |
适用于 简单对象,性能较优 | 无法处理 function、undefined、RegExp、Date、循环引用 |
| 递归深拷贝 | 适用于大部分对象,支持循环引用 | 无法拷贝 Map, Set, RegExp, Date |
lodash _.cloneDeep() |
功能强大,支持 Date, Map, Set |
需要引入第三方库 |
structuredClone()(推荐) |
原生方法,支持多数数据类型,性能优秀 | 不支持函数、undefined |
🚀 推荐方案:
- 普通对象 :用
JSON.parse(JSON.stringify()) - 复杂对象(包含
Map、Set、循环引用) :用structuredClone()或_.cloneDeep() - 需要最大兼容性 :用 递归方法
22.如何实现一个浅拷贝(Shallow Copy)?
浅拷贝(Shallow Copy)指的是 仅拷贝对象的第一层属性 ,如果属性是 引用类型(如对象、数组),那么拷贝的只是引用(即指针),而不是值本身。
1. 使用 Object.assign()
Object.assign() 方法可以用于浅拷贝对象:
js
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);
console.log(shallowCopy); // { a: 1, b: { c: 2 } }
console.log(shallowCopy.b === obj.b); // true(引用同一个对象)
✅ 优点:
- 简单直观,适用于浅拷贝。
❌ 缺点:
- 无法拷贝原型链(Prototype)。
- 对于嵌套对象,只复制引用(不是深拷贝)。
2. 使用展开运算符 { ...obj }(ES6 推荐)
js
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };
console.log(shallowCopy); // { a: 1, b: { c: 2 } }
console.log(shallowCopy.b === obj.b); // true(仍然指向相同的对象)
✅ 优点:
- 语法更简洁。
- 适用于 对象 的浅拷贝。
❌ 缺点:
- 仅拷贝 第一层,内部对象仍然是引用。
3. 使用 Array.prototype.slice() 进行数组浅拷贝
如果是数组,可以使用 slice() 方法:
js
const arr = [1, 2, { a: 3 }];
const shallowCopy = arr.slice();
console.log(shallowCopy); // [1, 2, { a: 3 }]
console.log(shallowCopy[2] === arr[2]); // true(仍然指向同一个对象)
✅ 优点:
- 适用于数组的浅拷贝。
❌ 缺点:
- 数组中的 对象引用不会被复制,仍然指向原对象。
4. 使用 Array.prototype.concat() 进行数组浅拷贝
js
const arr = [1, 2, { a: 3 }];
const shallowCopy = [].concat(arr);
console.log(shallowCopy); // [1, 2, { a: 3 }]
console.log(shallowCopy[2] === arr[2]); // true
✅ 优点:
- 适用于数组浅拷贝。
5. 使用 Object.create()
如果希望复制 原型链 ,可以使用 Object.create():
js
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
console.log(shallowCopy); // { a: 1, b: { c: 2 } }
console.log(shallowCopy.b === obj.b); // true(仍然指向同一个对象)
✅ 优点:
- 复制 原型链 ,比
Object.assign()更完整。
❌ 缺点:
- 仍然是浅拷贝。
📌 总结
| 方法 | 适用类型 | 是否拷贝原型 | 是否深拷贝 | 优缺点 |
|---|---|---|---|---|
Object.assign() |
对象 | ❌ 否 | ❌ 否 | 简单易用,但不能拷贝嵌套对象 |
{ ...obj }(展开运算符) |
对象 | ❌ 否 | ❌ 否 | 语法更简洁,适用于浅拷贝 |
slice() |
数组 | ❌ 否 | ❌ 否 | 适用于数组,但嵌套对象仍然是引用 |
concat() |
数组 | ❌ 否 | ❌ 否 | 适用于数组浅拷贝 |
Object.create() |
对象 | ✅ 是 | ❌ 否 | 复制原型链,但仍然是浅拷贝 |
🚀 推荐方案
- 拷贝对象 :
{ ...obj }(ES6) - 拷贝数组 :
slice()或concat() - 需要保留原型 :
Object.create()
⚠️ 注意 :如果对象中有 嵌套对象(深层引用) ,则需要使用 深拷贝 ,如 structuredClone() 或递归拷贝。
23.深浅拷贝的区别和使用场景
深浅拷贝是指在复制对象时,是否复制对象的引用还是复制对象的内容。它们的区别在于对原始对象的修改是否影响到拷贝后的对象。
1. 浅拷贝
浅拷贝是指创建一个新的对象,但新对象的属性仍然引用原始对象中的引用类型数据(比如数组或对象)。因此,如果原始对象中的引用类型属性被修改,拷贝后的对象也会受到影响。
示例:
javascript
const obj1 = {
name: "Alice",
details: { age: 25 }
};
const shallowCopy = { ...obj1 }; // 浅拷贝
obj1.details.age = 26; // 修改原对象的属性
console.log(shallowCopy.details.age); // 输出 26,浅拷贝的对象会受到影响
使用场景:
- 当你希望快速复制一个对象,但不关心嵌套对象的引用时。
- 适用于处理浅层次的简单数据结构,如直接的基本数据类型或只包含基本数据类型的对象。
2. 深拷贝
深拷贝是指创建一个新的对象,并递归地复制原始对象的所有属性,包括对象中的引用类型数据。这样,修改原始对象中的引用类型属性不会影响拷贝后的对象。
示例:
javascript
const obj1 = {
name: "Alice",
details: { age: 25 }
};
const deepCopy = JSON.parse(JSON.stringify(obj1)); // 深拷贝
obj1.details.age = 26; // 修改原对象的属性
console.log(deepCopy.details.age); // 输出 25,深拷贝的对象不会受到影响
使用场景:
- 当你需要完全独立的对象,确保修改原始对象的引用类型数据时不会影响到拷贝后的对象。
- 适用于复杂数据结构,包含嵌套对象和数组的情况,尤其是当你需要保证每个对象是独立的副本时。
总结:
- 浅拷贝:仅复制对象的一级属性,对引用类型的属性(如数组、对象)进行的是引用拷贝。
- 深拷贝:递归地复制对象及其所有嵌套的引用类型属性,生成一个完全独立的副本。
选择何种拷贝方式取决于你对数据独立性的需求,是否需要对嵌套对象进行修改或保留原对象的状态。
24. AJAX 是什么?怎么实现的?
AJAX (Asynchronous JavaScript and XML)是一种异步通信技术,允许网页在不刷新的情况下与服务器交换数据。
实现方式:
- 创建
XMLHttpRequest对象。 - 定义回调函数处理响应(
onreadystatechange或onload)。 - 使用
open()设置请求方法(GET/POST)和 URL。 - 发送请求(
send()),如 POST 需设置请求头Content-Type。
现代方法可使用fetchAPI:
ini
fetch(url)
.then(response => response.json())
.then(data => console.log(data));
25. GET 和 POST 有什么区别?
| GET | POST |
|---|---|
| 参数在 URL 中,长度受限(约 2048 字符) | 参数在请求体中,无长度限制 |
| 用于获取数据,幂等(多次请求结果相同) | 用于提交数据,非幂等 |
| 可缓存 | 不可缓存 |
| 安全性较低(URL 可见) | 相对安全 |
26. Promise 的内部原理是什么?优缺点?
原理:
- 状态机模型(
pending→fulfilled/rejected)。 - 通过
then方法注册回调,返回新 Promise 实现链式调用。 - 微任务机制(回调放入微任务队列,优先级高于宏任务)。
优点:
- 解决回调地狱,代码更线性。
- 统一错误处理(
catch)。
缺点:
- 无法取消正在执行的 Promise。
- 未正确处理错误可能导致静默失败。
27. Promise 和 async/await 的区别?
-
async/await 是 Promise 的语法糖,异步代码同步化,提高可读性。
-
async函数隐式返回 Promise,await后接 Promise 或值。 -
错误处理:
scss// Promise fetch().catch(err => {}); // async/await try { await fetch(); } catch (err) {}
28. 浏览器的存储方式有哪些?
| 类型 | 特点 |
|---|---|
| Cookie | 最大 4KB,每次请求携带,可设过期时间,同源限制。 |
| localStorage | 持久存储(除非手动删除),同源,大小约 5-10MB。 |
| sessionStorage | 会话级存储(标签页关闭清除),同源。 |
| IndexedDB | 非关系型数据库,支持事务,存储大量数据。 |
| Web Storage | (localStorage 和 sessionStorage 统称) |
29. Token 存在 sessionStorage 还是 localStorage?
- sessionStorage:会话结束(标签页关闭)自动清除,降低 XSS 攻击后 Token 泄漏风险。
- localStorage :持久存储,适合需要长期登录的场景,但需防范 XSS。
建议:敏感数据优先存 sessionStorage,结合服务端设置较短 Token 有效期。
30. Token 的登录流程
- 用户输入账号密码提交登录。
- 服务端验证通过,生成 Token(如 JWT)返回。
- 前端存储 Token(如 sessionStorage)。
- 后续请求在
Authorization头添加 Token(如Bearer <token>)。 - 服务端校验 Token 有效性,返回数据。
安全优化:
- Token 设置较短有效期,搭配 Refresh Token 用于续期。
- 启用 HTTPS,防范中间人攻击。
31. 页面渲染的过程
- 构建 DOM 树:解析 HTML 生成节点树。
- 构建 CSSOM:解析 CSS 生成样式树。
- 合成渲染树 :合并 DOM 和 CSSOM,排除不可见元素(如
display: none)。 - 布局(Layout) :计算节点几何信息(尺寸、位置)。
- 绘制(Paint) :将布局信息转换为像素。
- 合成(Composite) :分层绘制,GPU 加速渲染。
32. DOM 树和渲染树的区别
| DOM 树 | 渲染树 |
|---|---|
| 包含所有 HTML 节点(包括隐藏元素) | 仅包含需渲染的节点 |
| 结构完整,描述文档内容 | 结合 DOM 和 CSSOM,描述可视内容 |
33. 精灵图和 Base64 的区别
| CSS Sprites(精灵图) | Base64 |
|---|---|
| 多图合并为一张,减少请求 | 图片转字符串嵌入代码 |
需通过 background-position 定位 |
增大文件体积(约 30%) |
| 适合多图标场景 | 适合小图标,避免额外请求 |
34. SVG 格式的特点
- 矢量图形:无损缩放,适用于高分辨率屏幕。
- XML 格式:可通过 CSS/JS 动态修改,支持动画和交互。
- SEO 友好:文本内容可被搜索引擎读取。
- 缺点:复杂图形可能性能不如 Canvas。
35. JWT 的组成与特点
组成:
- Header :算法和类型(如
HS256)。 - Payload:自定义数据(如用户 ID)。
- Signature:签名(防篡改)。
特点 :无需服务端存储 Session,支持跨域。
注意:需防范 Token 盗用,避免存储敏感信息,设置短过期时间。
36. npm 的底层环境
-
依赖管理 :基于
package.json描述依赖及版本(遵循语义化版本semver)。 -
模块加载:Node.js 的 CommonJS 规范。
-
包存储:默认从 npm 仓库下载(registry.npmjs.org)。
-
安装机制:
npm install解析依赖树,扁平化安装(避免嵌套过深)。package-lock.json锁定精确版本,确保环境一致性。
37.HTTP协议规定的协议头和请求头的区别**
"协议头"可能指的是HTTP协议的头部,即整体结构中的头部,而请求头则是请求部分的头部。 每个HTTP请求由三部分组成:请求行(请求方法、URI、HTTP版本)、请求头部(headers)、请求正文。 请求行有时也称为起始行,而请求头部则是首部字段。响应类似,有状态行、响应头部、正文。 HTTP协议中,"协议头"与"请求头"的区别可以通过以下结构化说明进行澄清: HTTP协议中,"协议头"与"请求头"的区别可以通过以下结构化说明进行澄清:
一、明确术语定义
1. 协议头(HTTP Message Headers)
-
定义:在HTTP标准中,"协议头"没有严格对应的定义。此词可能被误解为以下两种场景:
- 广义:指整个HTTP消息的头部(包含起始行和首部字段)。
- 狭义:可能指请求行或响应状态行(即描述协议版本的部分)。
-
实际标准:RFC文档中未使用"协议头"一词,需结合上下文理解。
2. 请求头(Request Headers)
- 明确定义 :严格属于HTTP请求中的首部字段部分。
- 作用:包含客户端发送的附加信息(如身份验证、内容类型、缓存策略等)。
- 标准依据 :由RFC 7230等规范中的
header-field定义。
二、HTTP请求的结构分解
一个HTTP请求由以下三部分组成(按顺序排列):
-
请求行(Request Line)
-
格式:
[Method] [URI] [HTTP Version](如
GET /index.html HTTP/1.1) -
关键字段:
- HTTP方法:GET、POST等操作类型。
- URI:请求的资源路径。
- 协议版本(如HTTP/1.1):规定通信规则。
-
-
请求头(Request Headers)
-
格式:键值对的集合,每行一个字段。
(如
Host: example.com,User-Agent: Chrome/123) -
常见类型:
- 通用头(如
Cache-Control) - 请求头(如
Accept、Authorization) - 实体头(如
Content-Type)。
- 通用头(如
-
-
请求正文(Body)
- 可选部分,主要用于POST/PUT等包含数据的请求。
三、关键对比
| 对比维度 | 协议头(可能场景) | 请求头(Request Headers) |
|---|---|---|
| **组成内容 | 若指请求行 :包含方法、URI、HTTP版本。 若指整个头部:包含请求行+首部字段。 | 仅包含键值对的头部字段(如Host, Accept)。 |
| 功能定位 | 若为请求行:定义操作类型、资源路径和协议规则。 | 提供请求的附加信息和控制参数。 |
| 标准化术语 | 非RFC标准术语,需结合上下文。 | RFC 7230明确定义的header-field。 |
四、示例说明
HTTP请求示例
vbnet
GET /search?q=test HTTP/1.1 ← 请求行(请求方法、URI、HTTP版本)
Host: www.example.com ← 请求头(首部字段)
User-Agent: Mozilla/5.0
Accept: text/html
- 协议头(若指请求行) :
GET /search?q=test HTTP/1.1 - 请求头 :从
Host到Accept的所有键值对字段。
五、总结
-
请求头是HTTP请求中明确的首部字段部分,用于传递附加信息。
-
协议头需要根据上下文判断:
- 若指请求行,表示操作和协议版本;
- 若指整个头部范畴,则包含请求行+请求头,但此用法不符合标准术语。
-
实际开发中应严格遵循RFC标准术语,避免混淆。
**38. 同源策略是什么你怎么理解
同源策略(Same-Origin Policy)是浏览器为防止恶意攻击而实施的核心安全机制。以下为系统化的解析:
一、定义
同源策略 规定:浏览器仅允许网页脚本(如JavaScript)访问与其同源的资源,跨源访问会被默认禁止。
-
同源的三要素 :要求 协议 、域名 、端口 三者完全相同。
示例:https://www.example.com/page与https://www.example.com/api→ 同源(路径不同不影响)http://www.example.com与https://www.example.com→ 不同源(协议不同)www.example.com与api.example.com→ 不同源(子域名不同)
二、作用目标
同源策略针对以下操作进行限制:
-
数据访问
- Cookie/LocalStorage:A网站的脚本无法读取B网站的存储数据。
- DOM操作 :A网站的页面无法通过
iframe嵌入并操作B网站的DOM(除非明确同源)。
-
网络请求
- XMLHttpRequest、Fetch API默认禁止跨域请求(需CORS或代理支持)。
-
其他资源限制
script、img等标签可跨域加载资源,但脚本无法直接读取内容(如跨域图片的像素数据需许可)。
三、核心逻辑
浏览器在以下场景中执行同源检查:
| 场景 | 是否允许 | 示例说明 |
|---|---|---|
| AJAX请求 | 默认禁止跨域,除非服务器返回CORS头 | fetch('https://api.site.com/data')被拦截 |
| 操作跨域iframe内容 | 禁止读写DOM/调用函数 | iframe.contentWindow.document.body会报错 |
| Web存储访问 | 本地存储数据仅允许同源脚本访问 | localStorage不同源页面无法共享数据 |
| Web Workers脚本 | 需同源或明确启用跨域 | 加载跨域Worker脚本需服务器支持CORS |
四、例外机制
以下场景可绕过同源策略(但需显式配置):
-
CORS(跨域资源共享)
- 服务器通过
Access-Control-Allow-Origin响应头授权特定域访问资源。 - 预检请求 :复杂请求(如带自定义头的POST)需先发送
OPTIONS请求确认权限。
- 服务器通过
-
JSONP(过时技术)
- 利用
<script>标签不受同源策略限制的特性,通过回调函数获取跨域数据。
- 利用
-
document.domain(仅限同主域)
- 设置
document.domain = 'example.com',使同主域不同子域的页面可以互操作。
- 设置
-
postMessage API
- 允许不同源的窗口间通过消息传递安全通信(如
iframe父子页面)。
- 允许不同源的窗口间通过消息传递安全通信(如
五、开发实践建议
-
前端解决方案:
- 开发环境通过代理(如Webpack DevServer)转发跨域请求。
-
后端设置:
- 配置CORS头(精确控制允许的源、方法、头字段),避免使用
Access-Control-Allow-Origin: *。
- 配置CORS头(精确控制允许的源、方法、头字段),避免使用
-
安全权衡:
- 确保动态开放的跨域资源(如公共API)不会泄露敏感信息或暴露未授权操作。
六、类比理解
将同源策略想象成酒店房卡系统:
- 每个房间(源)的房卡(脚本权限)仅能打开自己的房门(同源资源)。
- 若需访问其他房间(跨域),需前台(服务器)授权临时通行证(CORS头)。
总结
同源策略是浏览器安全的基石,平衡了功能性与风险。掌握其规则与跨域解决方案(如CORS),是开发现代Web应用的关键技能。
39. 防抖与节流
1. 概念对比
1.1 防抖(Debounce)
- 定义: 在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时
- 场景 :
- 搜索框输入联想
- 窗口大小调整
- 表单验证
- 按钮提交事件
1.2 节流(Throttle)
- 定义: 规定在一个单位时间内,只能触发一次函数,如果这个单位时间内触发多次函数,只有一次生效
- 场景 :
- 滚动事件处理
- 页面resize事件
- 射击游戏中的子弹发射
- 表单快速提交
1.3 区别示意
javascript
// 防抖:等待一段时间后执行,期间重新触发会重新计时
Input Events: │─A─B─C─ │────D──│──E──│
Debounced: │────────│────D──│──E──│
// 节流:按照一定时间间隔执行
Input Events: │─A─B─C─D─E─F─G─│
Throttled: │─A─────D─────G─│
- 防抖(Debounce)
javascript
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// 应用场景:输入框搜索联想
- 节流(Throttle)
ini
function throttle(fn, delay) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= delay) {
fn(...args);
last = now;
}
};
}
// 应用场景:滚动事件监听
40. JSON简介
-
定义:轻量级数据交换格式(基于键值对)
-
结构 :支持字符串、数字、布尔、数组、对象、
null -
转换方法
javascriptJSON.parse('{"name":"John"}'); // 字符串 → 对象 JSON.stringify({name: 'John'}); // 对象 → 字符串
41. 数据未请求时的处理方案
-
加载状态:显示 Loading 动画或骨架屏
-
错误反馈
- 网络错误提示
- 超时自动重试(最多 3 次)
-
数据降级
- 使用本地缓存或默认值(如空列表占位)
-
重试逻辑
scssfunction fetchWithRetry(url, retries = 3) { return fetch(url).catch(err => retries > 0 ? fetchWithRetry(url, retries - 1) : Promise.reject(err) ); }
42. 无感登录实现
-
Access Token 过期检测:拦截 401 状态码
-
Refresh Token 刷新
ini// 在响应拦截器中处理 axios.interceptors.response.use( response => response, async error => { if (error.response.status === 401) { const newToken = await refreshToken(); error.config.headers.Authorization = `Bearer ${newToken}`; return axios.request(error.config); // 重发原请求 } return Promise.reject(error); } ); -
安全性
- Refresh Token 存为
httpOnlyCookie - 设置较短有效期并限制使用次数
- Refresh Token 存为
43. 大文件上传方案
-
文件分片
iniconst chunkSize = 5 * 1024 * 1024; // 5MB/片 const chunks = []; let start = 0; while (start < file.size) { chunks.push(file.slice(start, start + chunkSize)); start += chunkSize; } -
唯一标识:计算文件哈希(如 MD5)
-
并发上传
javascriptchunks.forEach((chunk, index) => { const formData = new FormData(); formData.append('chunk', chunk); formData.append('hash', `${fileHash}-${index}`); axios.post('/upload', formData); }); -
断点续传:根据已上传分片列表跳过已传部分
-
合并分片:服务端接收到所有分片后合并成完整文件
44. 说一下浏览器的缓存策略
浏览器的缓存策略主要分为 强缓存(Strong Cache) 和 协商缓存(Negotiated Cache),它们用于减少重复请求、提升页面加载速度。
1. 强缓存(Strong Cache)
强缓存是指浏览器在缓存有效期内 不向服务器发送请求,直接从本地缓存中获取资源,提高访问速度。
常见的强缓存策略
-
Expires(HTTP 1.0)-
Expires响应头指定资源的过期时间,如:httpExpires: Wed, 22 Mar 2025 08:00:00 GMT -
受本地时间影响,如果客户端时间不准确,可能导致缓存失效问题。
-
-
Cache-Control: max-age(HTTP 1.1,优先级高于 Expires)-
Cache-Control: max-age=3600表示资源在 3600 秒内有效,不需要重新请求。 -
示例 :
httpCache-Control: max-age=86400 -
作用:
no-cache:不使用强缓存,但会触发协商缓存。no-store:不缓存资源,每次都重新请求。public:所有用户都可以缓存该资源(包括代理服务器)。private:只能被当前用户缓存,代理服务器不能缓存。
-
强缓存流程:
- 浏览器先检查
Cache-Control或Expires是否有效。 - 如果缓存有效,则直接使用缓存,不发送请求。
2. 协商缓存(Negotiated Cache)
当强缓存失效时,浏览器会向服务器发送请求,并通过 协商缓存机制 确定资源是否需要重新下载。
常见的协商缓存策略
-
Last-Modified&If-Modified-Since-
服务器返回资源最后修改时间 :
httpLast-Modified: Wed, 22 Mar 2025 08:00:00 GMT -
浏览器请求时带上
If-Modified-Since:httpIf-Modified-Since: Wed, 22 Mar 2025 08:00:00 GMT -
服务器对比 :
- 若资源未修改,返回
304 Not Modified,使用缓存。 - 若资源已修改,返回新的资源
200 OK。
- 若资源未修改,返回
-
-
ETag&If-None-Match(优先级高于 Last-Modified)-
服务器返回资源的唯一标识 :
httpETag: "abc123" -
浏览器请求时带上
If-None-Match:httpIf-None-Match: "abc123" -
服务器对比 :
- 若 ETag 未变,返回
304 Not Modified,使用缓存。 - 若 ETag 变化,返回新的资源
200 OK。
- 若 ETag 未变,返回
-
协商缓存流程:
- 浏览器向服务器发送请求,带上
If-Modified-Since或If-None-Match。 - 服务器检查资源是否更新:
- 未更新 :返回
304,使用缓存。 - 已更新 :返回
200并提供新资源。
- 未更新 :返回
3. 缓存策略对比
| 缓存策略 | 适用情况 | 是否向服务器请求 | 响应状态码 | 主要控制字段 |
|---|---|---|---|---|
| 强缓存 | 资源未过期 | 否 | 200(from cache) | Cache-Control、Expires |
| 协商缓存 | 资源已过期 | 是 | 304(Not Modified) | ETag、Last-Modified |
4. 现实应用中的缓存优化
1. 静态资源使用强缓存
-
方式 :设置
Cache-Control: max-age=31536000,并使用 文件名哈希 处理更新,如app.123abc.js。 -
示例 :
httpCache-Control: max-age=31536000, immutable
2. 重要数据使用协商缓存
-
方式 :使用
ETag或Last-Modified,确保数据变更时能及时更新。 -
示例 :
httpETag: "abc123"
3. HTML 文件使用 no-cache
-
避免用户访问时看到旧的 HTML 页面,但仍然允许缓存 CSS、JS:
httpCache-Control: no-cache
4. AJAX 接口请求
-
如果数据变化频繁,可以使用
Cache-Control: no-store强制不缓存:httpCache-Control: no-store
5. 总结
- 强缓存 (
Expires、Cache-Control: max-age)优先,避免请求。 - 强缓存失效后,协商缓存 (
Last-Modified、ETag)减少数据传输。 - 静态资源(JS/CSS/图片)用强缓存 + 文件名哈希 方式优化。
- HTML、API 响应一般使用
no-cache或no-store确保数据最新。 ETag优先级高于Last-Modified,适用于精确缓存控制。
45 ** 延迟加载 JS 有哪些方式?**
延迟加载(Lazy Loading)JS 主要有以下方式:
-
defer属性(适用于外部 JS 文件)-
脚本会在 HTML 解析完成后 按顺序执行,不会阻塞 HTML 解析。
-
示例 :
html<script src="script.js" defer></script>
-
-
async属性(适用于外部 JS 文件)-
脚本会在 下载完成后立即执行,不会阻塞 HTML 解析,但执行顺序不确定。
-
示例 :
html<script src="script.js" async></script>
-
-
动态创建
<script>标签-
可以在需要时动态加载 JS,适用于按需加载。
-
示例 :
jsconst script = document.createElement("script"); script.src = "script.js"; document.body.appendChild(script);
-
-
按需加载(懒加载)
-
通过
import()方式进行模块化异步加载(适用于 ES6+)。 -
示例 :
jsimport("./module.js").then((module) => { module.default(); });
-
-
使用 Webpack 的
code-splitting-
Webpack 提供
import()进行代码拆分,只有在需要时才加载模块。 -
示例 :
jsfunction loadModule() { import("./module.js").then((module) => { module.default(); }); }
-
46. JS 数据类型有哪些?
JS 具有 7 种原始类型(Number、String、Boolean、Undefined、Null、Symbol、BigInt)和 引用类型(Object)。 JavaScript 数据类型分为 原始类型(Primitive Types) 和 引用类型(Reference Types)。
(1)原始类型(基本数据类型)
-
Number(数字类型)-
包括整数、浮点数、
NaN(不是一个数字)、Infinity。 -
示例 :
jslet a = 42; let b = 3.14; let c = NaN; let d = Infinity;
-
-
String(字符串类型)-
由字符组成的文本数据,可以使用单引号、双引号或模板字符串。
-
示例 :
jslet str1 = "Hello"; let str2 = 'World'; let str3 = `Template ${str1}`;
-
-
Boolean(布尔类型)-
只有
true和false两个值。 -
示例 :
jslet isTrue = true; let isFalse = false;
-
-
Undefined(未定义)-
变量声明但未赋值时的默认值。
-
示例 :
jslet x; console.log(x); // undefined
-
-
Null(空值)-
一个表示 "无值" 的特殊值,通常用于手动赋值为空。
-
示例 :
jslet y = null;
-
-
Symbol(唯一值,ES6)-
创建独一无二的标识符,常用于对象属性。
-
示例 :
jslet sym = Symbol("unique");
-
-
BigInt(大整数,ES11)-
适用于比
Number能表示的范围更大的整数。 -
示例 :
jslet bigInt = 123456789012345678901234567890n;
-
(2)引用类型(复杂数据类型)
-
Object(对象)-
由
key-value组成的集合。 -
示例 :
jslet obj = { name: "Alice", age: 25 };
-
-
Array(数组)-
数组是一种特殊的对象,可以存储有序数据。
-
示例 :
jslet arr = [1, 2, 3];
-
-
Function(函数)-
JavaScript 中函数本质上也是对象,可以赋值给变量。
-
示例 :
jsfunction greet() { return "Hello"; }
-
-
Date、RegExp、Map、Set也是常见的引用类型。
3. null 和 undefined 的区别
null 表示"空值",undefined 表示"未定义"。
| 关键点 | null |
undefined |
|---|---|---|
| 含义 | 表示 "无值",需手动赋值 | 变量未赋值时的默认值 |
| 类型 | object(JS 设计缺陷) |
undefined |
| 使用场景 | 明确赋值为空 | 变量未声明或未赋值 |
| 示例 | let a = null; |
let b; console.log(b); // undefined |
判断方法:
js
console.log(typeof null); // "object"
console.log(typeof undefined); // "undefined"
console.log(null == undefined); // true(值相等)
console.log(null === undefined); // false(类型不同)
47 . JS 数据类型考题
(1)typeof 结果
js
console.log(typeof null); // "object"(历史遗留问题)
console.log(typeof undefined); // "undefined"
console.log(typeof 42); // "number"
console.log(typeof "Hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof function(){}); // "function"
(2)instanceof 判断
js
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true
5. == 和 === 有什么不同?
| 运算符 | 是否比较类型 | 是否进行类型转换 | 例子 |
|---|---|---|---|
== |
否 | 是 | "1" == 1 // true |
=== |
是 | 否 | "1" === 1 // false |
(1)==(值相等,类型可转换)
js
console.log(0 == false); // true
console.log(1 == "1"); // true
console.log(null == undefined); // true
==允许不同数据类型进行转换后再比较。
(2)===(值和类型都必须相等)
js
console.log(0 === false); // false
console.log(1 === "1"); // false
console.log(null === undefined); // false
===需要严格相等,避免了类型转换导致的潜在错误。==会进行类型转换,===需要类型和值都相等。- JS 代码优化建议使用
===,避免隐式转换导致的错误。
48.slice和splice的区别
slice 和 splice 的区别
slice 和 splice 都是 JavaScript 数组方法,但它们的作用、影响和返回值不同:
| 方法 | 作用 | 是否修改原数组 | 返回值 |
|---|---|---|---|
slice |
截取数组的一部分 | ❌ 不会修改原数组 | 返回截取的新数组 |
splice |
删除/替换/插入数组元素 | ✅ 会修改原数组 | 返回被删除的元素组成的数组 |
1. slice(start, end)
- 作用 :从数组中 截取 指定范围的元素,不修改原数组。
- 参数 :
start(必填):起始索引(包含)。end(可选):结束索引(不包含)。
- 返回值:截取的新数组。
示例
js
let arr = [1, 2, 3, 4, 5];
console.log(arr.slice(1, 4)); // [2, 3, 4] (索引 1 ~ 3)
console.log(arr.slice(2)); // [3, 4, 5] (索引 2 到末尾)
console.log(arr.slice(-3)); // [3, 4, 5] (倒数第 3 个元素到末尾)
console.log(arr); // [1, 2, 3, 4, 5] (原数组不变)
2. splice(start, deleteCount, ...items)
- 作用 :删除、替换或插入数组中的元素,修改原数组。
- 参数 :
start(必填):起始索引(从该索引开始操作)。deleteCount(可选):删除的元素个数,若为0则不删除。items(可选):要插入的元素(可变参数)。
- 返回值 :被删除的元素组成的新数组。
示例
(1)删除元素
js
let arr1 = [1, 2, 3, 4, 5];
// 从索引 1 开始删除 2 个元素
console.log(arr1.splice(1, 2)); // [2, 3] (返回删除的部分)
console.log(arr1); // [1, 4, 5] (原数组被修改)
(2)插入元素
js
let arr2 = [1, 2, 3, 4, 5];
// 从索引 2 处插入 "a" 和 "b"
arr2.splice(2, 0, "a", "b");
console.log(arr2); // [1, 2, "a", "b", 3, 4, 5]
(3)替换元素
js
let arr3 = [1, 2, 3, 4, 5];
// 替换索引 1 处的 2 个元素(2、3 替换为 "x", "y")
arr3.splice(1, 2, "x", "y");
console.log(arr3); // [1, "x", "y", 4, 5]
3. slice vs splice 总结
| 方法 | 修改原数组 | 返回值 | 用途 |
|---|---|---|---|
slice |
❌ 不修改 | 新数组 | 截取 一部分数组 |
splice |
✅ 修改 | 被删除的元素数组 | 删除、替换、插入 |
什么时候用哪个?
- 想获取数组的部分数据,但不改变原数组 →
slice - 想删除/插入/替换元素并修改原数组 →
splice
4. 扩展:如何用 splice 模拟 slice?
虽然 splice 通常会修改原数组,但如果我们想要 splice 的返回值 和 slice 一样,可以先复制一份数组:
js
let arr = [1, 2, 3, 4, 5];
let slicedArr = arr.slice(1, 3);
let splicedArr = arr.concat().splice(1, 2);
console.log(slicedArr); // [2, 3] (slice 结果)
console.log(splicedArr); // [2, 3] (splice 结果)
console.log(arr); // [1, 2, 3, 4, 5] (原数组未变)
49.js数组去重的方法
JS 数组去重的常见方法
在 JavaScript 中,数组去重有多种方式,以下是几种常见的方法:
1. 使用 Set(最简洁高效)
Set 是 ES6 提供的一个数据结构,天然去重。
js
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3, 4, 5]
- 优点 :代码简洁,性能优秀,适用于基本数据类型(
number、string等)。 - 缺点:无法去重对象(引用类型)。
2. 使用 filter + indexOf
js
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);
console.log(uniqueArr); // [1, 2, 3, 4, 5]
- 原理 :
indexOf(item)返回当前元素首次出现的索引,只有首次出现的位置和当前索引相等时才保留。 - 缺点 :对于大数组,性能不如
Set,因为indexOf是 O(n) ,导致整体 O(n²) 复杂度。
3. 使用 reduce + includes
js
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.reduce((acc, cur) => {
if (!acc.includes(cur)) acc.push(cur);
return acc;
}, []);
console.log(uniqueArr); // [1, 2, 3, 4, 5]
- 原理 :遍历数组,若
acc(累积数组)中没有当前值,则添加进去。 - 缺点 :内部使用
includes进行查找,性能比Set略低。
4. 使用 Map(适用于对象去重)
如果数组包含对象,Set 不能去重,可以用 Map:
js
const arr = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 1, name: "Alice" }
];
const uniqueArr = [...new Map(arr.map(item => [item.id, item])).values()];
console.log(uniqueArr);
// [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
- 原理 :
Map以id作为键,后面的相同id覆盖前面的,最终去重。
5. 使用 sort + for(适用于已排序数组)
如果数组是 有序的 ,可以使用 sort() + 遍历:
js
const arr = [1, 1, 2, 3, 3, 4, 5, 5];
const uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] !== arr[i + 1]) {
uniqueArr.push(arr[i]);
}
}
console.log(uniqueArr); // [1, 2, 3, 4, 5]
- 缺点:需要先排序,不适用于未排序数组。
6. 使用 Lodash 库的 _.uniq()
如果项目中使用 Lodash ,可以用 _.uniq():
js
const _ = require("lodash");
const arr = [1, 2, 2, 3, 4, 4, 5];
console.log(_.uniq(arr)); // [1, 2, 3, 4, 5]
- 优点:库方法优化过,性能好。
- 缺点:需要引入外部库。
7. 适用于复杂去重(对象+多字段)
如果对象去重的标准不只是 id,可以使用 JSON.stringify:
js
const arr = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 1, name: "Alice" }
];
const uniqueArr = arr.filter(
(item, index, self) =>
index === self.findIndex(t => JSON.stringify(t) === JSON.stringify(item))
);
console.log(uniqueArr);
- 优点:适用于多字段的对象去重。
- 缺点 :性能不高,
JSON.stringify()可能影响排序。
方法对比
| 方法 | 可去重基本数据类型 | 可去重对象 | 是否修改原数组 | 适用场景 | 性能 |
|---|---|---|---|---|---|
Set |
✅ | ❌ | ❌ | 数字、字符串 | ⭐⭐⭐⭐⭐ |
filter + indexOf |
✅ | ❌ | ❌ | 适用于小数组 | ⭐⭐⭐ |
reduce + includes |
✅ | ❌ | ❌ | 适用于小数组 | ⭐⭐⭐ |
Map |
✅ | ✅ | ❌ | 对象去重 | ⭐⭐⭐⭐ |
sort + for |
✅ | ❌ | ✅ | 已排序数组 | ⭐⭐⭐ |
Lodash _.uniq() |
✅ | ❌ | ❌ | Lodash 用户 | ⭐⭐⭐⭐⭐ |
JSON.stringify |
✅ | ✅ | ❌ | 复杂对象去重 | ⭐⭐ |
总结
- 最推荐 :
Set(简单高效,适用于基本数据类型)。 - 对象去重 :
Map(适用于id唯一的对象数组)。 - 其他方法 :按需选择,如
filter、reduce等。
50.js判断变量是否是数组的方法有哪些
在 JavaScript 中,可以用多种方法来判断变量是否为数组,下面是常见的几种方法:
1. Array.isArray(value)(推荐,最可靠)
Array.isArray() 是 ES5 引入的方法,专门用于判断变量是否是数组,推荐使用。
js
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
console.log(Array.isArray("hello")); // false
console.log(Array.isArray(new Array())); // true
✅ 优点:
- 语义清晰,专门用于判断数组。
- 兼容
iframe、window等不同的执行环境。
2. instanceof Array
instanceof 运算符用于判断对象是否是某个构造函数的实例:
js
console.log([] instanceof Array); // true
console.log({} instanceof Array); // false
console.log(new Array() instanceof Array); // true
⚠ 缺点:
-
不同
window或iframe可能判断失效 ,因为不同iframe可能有不同的Array构造函数:jslet iframe = document.createElement('iframe'); document.body.appendChild(iframe); let iframeArray = window.frames[0].Array; let arr = new iframeArray(); console.log(arr instanceof Array); // false (不同 window 造成的问题)
3. Object.prototype.toString.call(value)
利用 Object.prototype.toString.call(value) 可以返回准确的数据类型:
js
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call("hello")); // "[object String]"
可以封装成一个函数:
js
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
✅ 优点:
- 适用于任何数据类型,不会被
iframe问题影响。
4. constructor 判断
可以通过 constructor 检查 Array:
js
console.log([].constructor === Array); // true
console.log({}.constructor === Array); // false
console.log(new Array().constructor === Array); // true
⚠ 缺点:
-
constructor可能被修改:jslet arr = []; arr.constructor = Object; console.log(arr.constructor === Array); // false
5. typeof(❌ 不适用于数组)
js
console.log(typeof []); // "object"
console.log(typeof {}); // "object"
⚠ 问题:
typeof无法区分数组和普通对象,不推荐用于判断数组。
对比总结
| 方法 | 兼容性 | 是否受 iframe 影响 |
是否可靠 | 推荐指数 |
|---|---|---|---|---|
Array.isArray(value) |
ES5+ | ❌ 不受影响 | ✅ 最推荐 | ⭐⭐⭐⭐⭐ |
instanceof Array |
ES3+ | ✅ 受 iframe 影响 |
❌ 可能失效 | ⭐⭐⭐ |
Object.prototype.toString.call(value) |
ES3+ | ❌ 不受影响 | ✅ 可靠 | ⭐⭐⭐⭐⭐ |
constructor |
ES3+ | ✅ 受影响 | ❌ 可被修改 | ⭐⭐ |
typeof |
ES3+ | ❌ 不受影响 | ❌ 无法区分数组和对象 | ⭐ |
最佳实践(推荐)
在项目中,最推荐 这两种方法:
- 首选:
Array.isArray(value) - 备选:
Object.prototype.toString.call(value) === "[object Array]"
51.js找出多维数组中的最大值
在 JavaScript 中,要找出 多维数组 (嵌套数组)中的最大值,可以使用递归、flat(Infinity) 或者 reduce 来实现。下面介绍几种方法:
方法 1:递归查找(适用于任意深度)
如果数组是 不规则的多维数组,递归是最通用的方法:
js
function findMax(arr) {
let max = -Infinity;
for (let item of arr) {
if (Array.isArray(item)) {
max = Math.max(max, findMax(item)); // 递归处理子数组
} else {
max = Math.max(max, item); // 处理当前数值
}
}
return max;
}
const arr = [1, [2, [3, 8, [10]], 5], 4, [7, 6]];
console.log(findMax(arr)); // 10
✅ 优点:
- 适用于不规则的嵌套数组。
- 递归实现,逻辑清晰。
方法 2:使用 flat(Infinity) + Math.max(最简洁,适用于规则数组)
如果数组是 规则的多维数组 (例如 [[1, 2], [3, 4]]),可以用 flat(Infinity) 展开:
js
const arr = [1, [2, [3, 8, [10]], 5], 4, [7, 6]];
const max = Math.max(...arr.flat(Infinity));
console.log(max); // 10
✅ 优点:
- 代码简洁,一行搞定。
- 适用于规则的嵌套数组。
⚠ 缺点:
flat(Infinity)可能影响性能,如果数组特别大,不建议使用。- 如果数组中包含非数值元素(如
null、undefined),需要预处理。
方法 3:reduce + 递归(适用于不规则数组)
使用 reduce 进行递归处理:
js
function findMax(arr) {
return arr.reduce((max, item) =>
Array.isArray(item) ? Math.max(max, findMax(item)) : Math.max(max, item),
-Infinity
);
}
const arr = [1, [2, [3, 8, [10]], 5], 4, [7, 6]];
console.log(findMax(arr)); // 10
✅ 优点:
- 递归 +
reduce实现,代码更函数式。
方法 4:使用 JSON.stringify()(不推荐,仅供参考)
可以将数组转换为字符串,然后用正则匹配所有数值:
js
const arr = [1, [2, [3, 8, [10]], 5], 4, [7, 6]];
const max = Math.max(...JSON.stringify(arr).match(/-?\d+/g).map(Number));
console.log(max); // 10
⚠ 缺点:
- 性能差,需要解析 JSON 并匹配正则,不适用于大数组。
- 无法处理非数值元素 (如
null、undefined)。
方法对比
| 方法 | 适用情况 | 代码简洁性 | 性能 |
|---|---|---|---|
| 递归 | 适用于不规则多维数组 | 一般 | ⭐⭐⭐⭐ |
flat(Infinity) + Math.max |
适用于规则多维数组 | 最简洁 | ⭐⭐⭐ |
reduce + 递归 |
适用于所有多维数组 | 结构清晰 | ⭐⭐⭐⭐ |
JSON.stringify() + 正则 |
仅适用于简单数组 | 不推荐 | ⭐ |
结论(最佳实践)
- 如果数组是规则的 ,推荐
Math.max(...arr.flat(Infinity))(简单高效)。 - 如果数组是嵌套的(不规则) ,推荐 递归 方法
findMax(arr)(通用性最强)。 - 如果你喜欢函数式编程 ,可以使用
reduce+ 递归。
52.js new操作符做了什事情
new 操作符在 JavaScript 中用于创建一个实例对象,它会执行以下四个步骤:
1. 创建一个新的空对象
首先,new 操作符会创建一个新的空对象 ,这个对象会继承构造函数的 prototype。
js
let obj = {}; // 创建一个空对象
2. 让新对象的 __proto__ 指向构造函数的 prototype
新对象会继承 构造函数的 prototype 属性:
js
obj.__proto__ = Constructor.prototype;
这意味着新对象可以访问构造函数的原型方法。
3. 执行构造函数,并绑定 this 到新对象
使用 call 方式调用构造函数,并将 this 绑定到新对象:
js
let result = Constructor.call(obj, ...args); // 传递参数,执行构造函数
如果构造函数返回的是一个对象 (非 null),那么 new 操作符最终返回该对象。否则,返回新创建的对象。
4. 返回新对象
如果构造函数显式返回一个对象 ,new 操作符会返回该对象,否则返回新创建的实例:
js
return typeof result === "object" && result !== null ? result : obj;
完整示例
js
function Person(name, age) {
this.name = name;
this.age = age;
}
const p = new Person("Alice", 25);
console.log(p.name); // "Alice"
console.log(p.age); // 25
这里 p 继承了 Person.prototype,是 Person 的实例。
手写实现 new
js
function myNew(constructor, ...args) {
// 1. 创建一个新的空对象
let obj = Object.create(constructor.prototype);
// 2. 绑定 this 并执行构造函数
let result = constructor.apply(obj, args);
// 3. 如果构造函数返回一个对象,则返回该对象,否则返回新创建的对象
return result instanceof Object ? result : obj;
}
// 测试
function Person(name) {
this.name = name;
}
const p = myNew(Person, "Bob");
console.log(p.name); // "Bob"
总结
new 操作符的核心作用:
- 创建一个新对象。
- 链接原型 ,让新对象的
__proto__指向构造函数的prototype。 - 执行构造函数 ,并将
this绑定到新对象。 - 返回新对象(如果构造函数返回的是对象,则返回该对象)。
53. js找出字符串中出现最多的字符及其出现次数
要找出字符串中出现最多的字符 及其出现次数 ,可以使用 Map 或 Object 进行统计。
方法 1:使用 Map 统计字符频率
js
function findMostFrequentChar(str) {
let charMap = new Map();
let maxChar = '';
let maxCount = 0;
// 统计字符出现次数
for (let char of str) {
charMap.set(char, (charMap.get(char) || 0) + 1);
// 更新最大值
if (charMap.get(char) > maxCount) {
maxCount = charMap.get(char);
maxChar = char;
}
}
return { maxChar, maxCount };
}
// 测试
const str = "abcaabbcccccddd";
console.log(findMostFrequentChar(str)); // { maxChar: 'c', maxCount: 5 }
✅ 优点:
Map具有 O(1) 读写效率,性能好。- 代码清晰,易于理解。
方法 2:使用 Object 统计
js
function findMostFrequentChar(str) {
let charCount = {};
let maxChar = '';
let maxCount = 0;
for (let char of str) {
charCount[char] = (charCount[char] || 0) + 1;
if (charCount[char] > maxCount) {
maxCount = charCount[char];
maxChar = char;
}
}
return { maxChar, maxCount };
}
console.log(findMostFrequentChar("abcaabbcccccddd")); // { maxChar: 'c', maxCount: 5 }
✅ 优点:
- 适用于简单场景,兼容性好。
方法 3:使用 reduce(函数式写法)
js
function findMostFrequentChar(str) {
let charCount = [...str].reduce((acc, char) => {
acc[char] = (acc[char] || 0) + 1;
return acc;
}, {});
let maxChar = Object.keys(charCount).reduce((a, b) =>
charCount[a] >= charCount[b] ? a : b
);
return { maxChar, maxCount: charCount[maxChar] };
}
console.log(findMostFrequentChar("abcaabbcccccddd")); // { maxChar: 'c', maxCount: 5 }
✅ 优点:
- 代码更简洁,符合函数式编程思想。
方法对比
| 方法 | 代码简洁性 | 性能 | 适用场景 |
|---|---|---|---|
| Map 统计 | ⭐⭐⭐ | O(n) 高效 | 适用于所有情况,推荐 |
| Object 统计 | ⭐⭐ | O(n) | 适用于普通字符串 |
| reduce | ⭐⭐⭐⭐ | O(n) | 代码简洁,适合喜欢函数式编程的开发者 |
结论(最佳实践)
- 如果想要高效且直观的方案,推荐
Map方案。 - 如果想要代码最短,可用
reduce。 Object适用于简单情况,但Map在大数据量时更优。
54. 给字符串新增方法实现功能
要在 JavaScript 的 String 原型(prototype) 上定义 addPrefix 方法,使其能够为字符串添加前缀,可以这样实现:
解法:扩展 String.prototype
js
String.prototype.addPrefix = function(str) {
return str + this;
};
// 测试
console.log("world".addPrefix("hello")); // 输出:"helloworld"
解释
- 扩展
String.prototype- 我们在
String.prototype上定义了addPrefix方法,使所有字符串都可以调用它。
- 我们在
- 使用
this关键字- 在
String.prototype方法内部,this指的是当前字符串 (即"world")。 str + this连接str(前缀)和this(原字符串)。
- 在
注意事项
-
不推荐修改
String.prototype- 这样做可能会影响全局
String对象,引发兼容性问题。 - 生产环境通常使用独立函数替代:
jsfunction addPrefix(str, prefix) { return prefix + str; } console.log(addPrefix("world", "hello")); // "helloworld" - 这样做可能会影响全局
-
可使用 ES6 Class 实现 (如果想避免修改原型):
jsclass CustomString { constructor(str) { this.str = str; } addPrefix(prefix) { return prefix + this.str; } } let myStr = new CustomString("world"); console.log(myStr.addPrefix("hello")); // "helloworld"
总结
✅ 面试推荐解法:
- 定义
String.prototype.addPrefix(简单易懂,适合面试展示) - 避免污染原型,可使用独立函数或类
55.sort背后的原理是什么
Array.prototype.sort() 的背后原理主要涉及 排序算法 和 稳定性,不同的 JavaScript 引擎可能采用不同的排序实现。下面是详细解析:
1. V8 引擎中的 sort 实现
V8(Chrome 和 Node.js 使用的 JavaScript 引擎)对 sort() 进行了优化,主要采用 双轴快速排序(Dual-Pivot Quicksort) 和 插入排序(Insertion Sort),不同情况下采用不同算法:
- 数组长度 ≤ 10 时,使用 插入排序
- 数组长度 > 10 时,使用 双轴快速排序
- 特殊情况(如大量相等元素) 时,可能改用 归并排序(TimSort)
2. 排序算法的核心思想
(1)双轴快速排序(Dual-Pivot Quicksort)
V8 在 sort() 里使用 双轴快排 ,它是 改进版的快速排序,相比传统单轴快排效率更高:
- 选取 两个基准值(pivot1, pivot2)
- 将数组划分成 三个区域 :
- 小于
pivot1 - 介于
pivot1和pivot2之间 - 大于
pivot2
- 小于
- 递归对子区间进行排序
特点:
- 平均时间复杂度:O(n log n)
- 最坏时间复杂度:O(n²)(若选取的 pivot 较差)
- 空间复杂度:O(log n)
- 不稳定排序(即相同值的元素排序后相对顺序可能变)
(2)插入排序(Insertion Sort)
适用于小数组:
- 依次遍历数组元素,将当前元素插入到前面已经排好序的部分
- 时间复杂度 O(n²)
- 稳定排序
3. sort() 的稳定性
V8 的 sort() 默认是不稳定的 ,因为 双轴快排是不稳定排序 ,但在某些情况下(如大量相等元素时)可能会使用 TimSort ,它是 稳定排序。
4. sort 使用时的注意事项
(1)默认按 Unicode 码点排序
如果 sort() 不提供 比较函数 ,它会把元素 转换为字符串 ,然后按 Unicode 码点 排序:
js
const arr = [10, 2, 5, 30];
console.log(arr.sort()); // [10, 2, 30, 5] (按字符串 "10"、"2"、"30"、"5" 进行排序)
所以对于数字排序,必须提供比较函数:
js
console.log(arr.sort((a, b) => a - b)); // [2, 5, 10, 30]
(2)如果比较函数返回值不规范
sort((a, b) => a - b) 里的回调函数必须返回:
- 负数(
a排在b之前) - 正数(
b排在a之前) - 0(
a和b位置不变)
如果返回非数字值,可能会导致 排序行为不确定。
5. 如何手写一个 sort?
这里用 快排 手写 sort():
js
function quickSort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter(x => x < pivot);
const middle = arr.filter(x => x === pivot);
const right = arr.filter(x => x > pivot);
return [...quickSort(left), ...middle, ...quickSort(right)];
}
console.log(quickSort([10, 2, 5, 30])); // [2, 5, 10, 30]
6. 总结
sort()在 V8 引擎中采用 双轴快速排序 (大数组)+ 插入排序(小数组)。sort()默认按 字符串 Unicode 码点 排序,排序数字时需提供 比较函数。- V8 的
sort()不稳定(快排不稳定)。 - 不同 JS 引擎可能采用不同排序算法(如 TimSort)。
- 手写
sort()一般用 快排 、归并排序 或 堆排序。