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) 是指函数可以访问其外部作用域的变量,即使外部函数已经执行结束。
特点:
- 可以访问外部函数的变量,即使外部函数执行完毕。
- 变量不会被垃圾回收(可能导致内存泄露)。
- 适用于模块化开发,模拟私有变量。
示例:
function outer() {let count = 0;return function inner() {count++;console.log(count);};
}
const counter = outer();
counter(); // 1
counter(); // 2
闭包(Closure)是指一个函数能够访问其外部作用域中的变量,即使在外部函数执行结束后,仍然可以保留对外部变量的访问权限。闭包的主要使用场景如下:
1. 数据私有化(模拟私有变量)
闭包可以创建私有变量,防止外部直接访问或修改数据。
示例:模拟私有变量
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. 事件监听器 & 回调
闭包常用于事件监听器或回调函数,使得事件处理函数能够访问外部作用域中的变量。
示例:按钮点击计数
function setupButton() {let count = 0;document.getElementById("myButton").addEventListener("click", function() {count++;console.log(`Button clicked ${count} times`);});
}
setupButton();
🔹 应用场景:
- 在
addEventListener
回调中保留数据(如点击次数、鼠标移动距离等)。
3. 函数柯里化(参数预处理)
柯里化(Currying)是指将一个接收多个参数的函数,转换为多个接收单一参数的函数。
示例:实现加法柯里化
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. 延迟执行(定时器)
闭包可以用于在定时器或异步操作中保留执行上下文。
示例:定时器
function delayedMessage(msg, delay) {setTimeout(function() {console.log(msg);}, delay);
}delayedMessage("Hello, Closure!", 2000);
🔹 应用场景:
setTimeout
/setInterval
相关的任务管理(如轮询、延迟加载)。
5. 模拟块级作用域(ES5 以前)
在 ES6 之前,JavaScript 没有 let
和 const
关键字,使用闭包可以创建局部作用域,避免变量污染。
示例:IIFE(立即执行函数表达式)
(function() {var secret = "I am private";console.log(secret);
})();
console.log(typeof secret); // undefined(无法访问)
🔹 应用场景:
- 在 ES5 及以前,使用 IIFE 防止变量污染全局作用域。
- 现代 JavaScript 用
let
和const
取代该用法。
6. 记忆化(缓存计算结果,提高性能)
闭包可用于缓存计算结果,避免重复计算,提升性能。
示例:缓存计算结果
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 生成器
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
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) 是将事件监听器绑定在父级元素上,利用事件冒泡机制处理子元素事件,提高性能。
示例:
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
。
示例:
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:基本用法
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:继承构造函数
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:基本用法
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:获取数组中的最大/最小值
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:基本用法
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:延迟执行
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
指向原对象。
- 因为
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
继承父类的方法和属性。
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
关键字更直观地实现继承。
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. 全局作用域
console.log(this); // 在浏览器中:window,在 Node.js 中:global
2. 对象方法
const obj = {name: "Tom",sayHello() {console.log(this.name); // this 指向 obj}
};
obj.sayHello(); // Tom
3. 构造函数
function Person(name) {this.name = name;
}
const p = new Person("Tom");
console.log(p.name); // Tom(this 指向实例对象)
4. call
/ apply
/ bind
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. 箭头函数
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
脚本的执行顺序不确定,取决于哪个脚本先下载完。
📌 示例
<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 中的顺序执行。 - 适合多个脚本有执行顺序要求的情况。
📌 示例
<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
:
<script async src="analytics.js"></script>
<script async src="ads.js"></script>
✅ 如果脚本依赖 DOM 或多个脚本有执行顺序,可用 defer
:
<script defer src="jquery.js"></script>
<script defer src="main.js"></script>
✅ 如果脚本必须立即执行(阻塞执行),则不加 async
或 defer
:
<script src="important.js"></script>
7. 结论
async | defer |
---|---|
并行下载,下载完立刻执行 | 并行下载,HTML 解析完后按顺序执行 |
执行顺序不确定(谁先下载完谁先执行) | 按 HTML 书写顺序执行 |
适用于独立、不依赖 DOM 的脚本 | 适用于需要等待 DOM 解析完成的脚本 |
✅ 结论:
async
适合独立的、无依赖的脚本(如统计、广告)。defer
适合依赖 DOM 或者需要按顺序执行的脚本(如框架、主逻辑)。defer
是加载多个脚本的最佳选择,不会阻塞页面解析,又能保证执行顺序。 🚀
16. setTimeout 最小执行时间是多少?
在大多数浏览器中,setTimeout
的最小延迟时间是 4ms(如果时间小于 4ms,实际延迟仍为 4ms)。
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
变量声明let a = 10; // 块级作用域 const b = 20; // 常量
- 模板字符串
let name = "Tom"; console.log(`Hello, ${name}!`);
- 箭头函数
const add = (x, y) => x + y;
- 解构赋值
let { name, age } = { name: "Alice", age: 25 };
- 扩展运算符
let arr = [1, 2, 3]; let newArr = [...arr, 4, 5];
class
语法class Person {constructor(name) {this.name = name;} }
- 模块化
// 导出 export function sayHi() { console.log("Hi!"); } // 导入 import { sayHi } from "./module.js";
18.高阶函数和闭包的区别
在 JavaScript 中,高阶函数和闭包都是重要的概念,虽然有一定的关联,但它们的核心作用和使用方式不同。下面详细分析它们的概念、特点、区别、应用场景。
1. 高阶函数(Higher-Order Function)
定义:
高阶函数是接收一个函数作为参数或者返回一个函数的函数。
特点
- 参数可以是函数(回调函数)
- 返回值可以是函数(返回新的函数)
- 使代码更具可复用性和可组合性
示例
1. 作为参数传递的高阶函数
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. 返回一个函数的高阶函数
function multiplier(factor) {return function (number) {return number * factor;};
}const double = multiplier(2);
console.log(double(5)); // 10
✅ multiplier
是一个高阶函数,因为它返回了一个新函数。
2. 闭包(Closure)
定义:
闭包是一个可以访问其外部作用域变量的函数,即使在外部作用域执行结束后,函数仍然可以访问这些变量。
特点
- 函数内部引用了外部作用域的变量
- 外部作用域被销毁后,闭包仍然可以访问变量
- 变量不会被垃圾回收,可能会造成内存泄漏(如果不正确释放)
示例
1. 典型闭包示例
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. 闭包用于模拟私有变量
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. 结合高阶函数和闭包
实际上,闭包和高阶函数可以结合使用。例如,返回函数的高阶函数通常会创建闭包:
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)
宏任务通常包含:
setTimeout
setInterval
setImmediate
(Node.js)I/O 任务
UI 渲染(Rendering)
MessageChannel
requestAnimationFrame
每次 事件循环 执行时,只会从 宏任务队列 取出一个任务执行,执行完后会检查 微任务队列。
🔹 微任务(Micro Task)
微任务通常包含:
Promise.then
、catch
、finally
MutationObserver
process.nextTick
(Node.js 专属)
微任务的特点:
- 在当前事件循环的 最后,也就是 当前任务执行完后立即执行。
- 微任务优先级高于宏任务,会在每次事件循环结束后立即执行所有微任务。
4. 事件循环的执行流程
- 执行同步代码(全局代码),将函数调用入栈。
- 遇到异步任务(如
setTimeout
、Promise
):setTimeout
等 宏任务 进入 宏任务队列。Promise.then
等 微任务 进入 微任务队列。
- 同步代码执行完毕后,执行所有微任务。
- 执行一个宏任务(如
setTimeout
回调)。 - 执行所有微任务。
- 重复步骤 4 和 5,直到所有任务执行完毕。
5. 代码示例
🔹 示例 1:同步、宏任务、微任务的执行顺序
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:多个微任务与宏任务
console.log("A");setTimeout(() => {console.log("B");
}, 0);Promise.resolve().then(() => {console.log("C");
}).then(() => {console.log("D");
});console.log("E");
✅ 执行结果:
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
关键字会 暂停代码执行,并将后续代码作为微任务放入微任务队列。
示例:
async function test() {console.log("A");await Promise.resolve();console.log("B");
}console.log("C");
test();
console.log("D");
✅ 执行结果:
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) 之前 |
示例:
setImmediate(() => console.log("setImmediate"));
setTimeout(() => console.log("setTimeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("nextTick"));
✅ Node.js 结果:
nextTick
Promise
setImmediate
setTimeout
✅ 浏览器结果:
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) 错误。
- 示例(错误示范):
解决方案:function infiniteRecursion() {infiniteRecursion(); // 无限递归,导致栈溢出 } infiniteRecursion();
- 使用尾递归优化(Tail Recursion)(ES6 支持)
- 改用循环(如 for/while) 代替递归
- 设定递归终止条件
2. 递归效率低,可能导致性能问题
-
递归可能导致 重复计算,特别是在计算斐波那契数列等问题时。
-
示例(低效的递归):
function fibonacci(n) {if (n <= 1) return n;return fibonacci(n - 1) + fibonacci(n - 2); } console.log(fibonacci(40)); // 计算量非常大
解决方案:
- 使用缓存(记忆化存储,Memoization)
- 使用动态规划(Dynamic Programming) 代替递归
function 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. 递归终止条件错误,导致无限循环
- 示例:
function countdown(n) {if (n === 0) return; // 终止条件console.log(n);countdown(n - 1); } countdown(5); // 正确
- 终止条件错误可能导致 无限递归,从而引发栈溢出。
4. 递归函数的参数处理错误
- 在递归过程中,参数的状态可能变化,影响递归逻辑。
- 示例(错误的参数传递):
正确方式:function sum(arr) {if (arr.length === 0) return 0;return arr[0] + sum(arr); // 这里没有去除 arr[0],会无限递归 } console.log(sum([1, 2, 3])); // 错误
function 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 方式(简单但有局限)
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:递归深拷贝(适用于大部分场景)
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()
方法进行深拷贝:
const _ = require("lodash");
const obj = { a: 1, b: { c: 2 } };
const clone = _.cloneDeep(obj);
console.log(clone);
✅ 优点:
- 处理 大多数数据类型(对象、数组、Date、RegExp、Map、Set 等)。
❌ 缺点:
- 需要额外引入
lodash
。
方法 4:使用 structuredClone()
(推荐,现代浏览器支持)
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()
方法可以用于浅拷贝对象:
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 推荐)
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()
方法:
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()
进行数组浅拷贝
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()
:
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. 浅拷贝
浅拷贝是指创建一个新的对象,但新对象的属性仍然引用原始对象中的引用类型数据(比如数组或对象)。因此,如果原始对象中的引用类型属性被修改,拷贝后的对象也会受到影响。
示例:
const obj1 = {name: "Alice",details: { age: 25 }
};const shallowCopy = { ...obj1 }; // 浅拷贝obj1.details.age = 26; // 修改原对象的属性console.log(shallowCopy.details.age); // 输出 26,浅拷贝的对象会受到影响
使用场景:
- 当你希望快速复制一个对象,但不关心嵌套对象的引用时。
- 适用于处理浅层次的简单数据结构,如直接的基本数据类型或只包含基本数据类型的对象。
2. 深拷贝
深拷贝是指创建一个新的对象,并递归地复制原始对象的所有属性,包括对象中的引用类型数据。这样,修改原始对象中的引用类型属性不会影响拷贝后的对象。
示例:
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
。
现代方法可使用fetch
API:
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 或值。 -
错误处理:
// 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请求示例
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 区别示意
// 防抖:等待一段时间后执行,期间重新触发会重新计时
Input Events: │─A─B─C─ │────D──│──E──│
Debounced: │────────│────D──│──E──│// 节流:按照一定时间间隔执行
Input Events: │─A─B─C─D─E─F─G─│
Throttled: │─A─────D─────G─│
- 防抖(Debounce)
function debounce(fn, delay) {let timer;return (...args) => {clearTimeout(timer);timer = setTimeout(() => fn(...args), delay);};
}
// 应用场景:输入框搜索联想
- 节流(Throttle)
function throttle(fn, delay) {let last = 0;return (...args) => {const now = Date.now();if (now - last >= delay) {fn(...args);last = now;}};
}
// 应用场景:滚动事件监听
40. JSON简介
-
定义:轻量级数据交换格式(基于键值对)
-
结构:支持字符串、数字、布尔、数组、对象、
null
-
转换方法
JSON.parse('{"name":"John"}'); // 字符串 → 对象 JSON.stringify({name: 'John'}); // 对象 → 字符串
41. 数据未请求时的处理方案
-
加载状态:显示 Loading 动画或骨架屏
-
错误反馈
- 网络错误提示
- 超时自动重试(最多 3 次)
-
数据降级
- 使用本地缓存或默认值(如空列表占位)
-
重试逻辑
function fetchWithRetry(url, retries = 3) {return fetch(url).catch(err => retries > 0 ? fetchWithRetry(url, retries - 1) : Promise.reject(err)); }
42. 无感登录实现
-
Access Token 过期检测:拦截 401 状态码
-
Refresh Token 刷新
// 在响应拦截器中处理 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 存为
httpOnly
Cookie - 设置较短有效期并限制使用次数
- Refresh Token 存为
43. 大文件上传方案
-
文件分片
const chunkSize = 5 * 1024 * 1024; // 5MB/片 const chunks = []; let start = 0; while (start < file.size) {chunks.push(file.slice(start, start + chunkSize));start += chunkSize; }
-
唯一标识:计算文件哈希(如 MD5)
-
并发上传
chunks.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
响应头指定资源的过期时间,如:Expires: Wed, 22 Mar 2025 08:00:00 GMT
- 受本地时间影响,如果客户端时间不准确,可能导致缓存失效问题。
-
Cache-Control: max-age
(HTTP 1.1,优先级高于 Expires)Cache-Control: max-age=3600
表示资源在 3600 秒内有效,不需要重新请求。- 示例:
Cache-Control: max-age=86400
- 作用:
no-cache
:不使用强缓存,但会触发协商缓存。no-store
:不缓存资源,每次都重新请求。public
:所有用户都可以缓存该资源(包括代理服务器)。private
:只能被当前用户缓存,代理服务器不能缓存。
强缓存流程:
- 浏览器先检查
Cache-Control
或Expires
是否有效。 - 如果缓存有效,则直接使用缓存,不发送请求。
2. 协商缓存(Negotiated Cache)
当强缓存失效时,浏览器会向服务器发送请求,并通过 协商缓存机制 确定资源是否需要重新下载。
常见的协商缓存策略
-
Last-Modified
&If-Modified-Since
- 服务器返回资源最后修改时间:
Last-Modified: Wed, 22 Mar 2025 08:00:00 GMT
- 浏览器请求时带上
If-Modified-Since
:If-Modified-Since: Wed, 22 Mar 2025 08:00:00 GMT
- 服务器对比:
- 若资源未修改,返回
304 Not Modified
,使用缓存。 - 若资源已修改,返回新的资源
200 OK
。
- 若资源未修改,返回
- 服务器返回资源最后修改时间:
-
ETag
&If-None-Match
(优先级高于 Last-Modified)- 服务器返回资源的唯一标识:
ETag: "abc123"
- 浏览器请求时带上
If-None-Match
:If-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
。 - 示例:
Cache-Control: max-age=31536000, immutable
2. 重要数据使用协商缓存
- 方式:使用
ETag
或Last-Modified
,确保数据变更时能及时更新。 - 示例:
ETag: "abc123"
3. HTML 文件使用 no-cache
- 避免用户访问时看到旧的 HTML 页面,但仍然允许缓存 CSS、JS:
Cache-Control: no-cache
4. AJAX 接口请求
- 如果数据变化频繁,可以使用
Cache-Control: no-store
强制不缓存:Cache-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 解析。
- 示例:
<script src="script.js" defer></script>
-
async
属性(适用于外部 JS 文件)- 脚本会在 下载完成后立即执行,不会阻塞 HTML 解析,但执行顺序不确定。
- 示例:
<script src="script.js" async></script>
-
动态创建
<script>
标签- 可以在需要时动态加载 JS,适用于按需加载。
- 示例:
const script = document.createElement("script"); script.src = "script.js"; document.body.appendChild(script);
-
按需加载(懒加载)
- 通过
import()
方式进行模块化异步加载(适用于 ES6+)。 - 示例:
import("./module.js").then((module) => {module.default(); });
- 通过
-
使用 Webpack 的
code-splitting
- Webpack 提供
import()
进行代码拆分,只有在需要时才加载模块。 - 示例:
function loadModule() {import("./module.js").then((module) => {module.default();}); }
- Webpack 提供
46. JS 数据类型有哪些?
JS 具有 7 种原始类型(Number、String、Boolean、Undefined、Null、Symbol、BigInt)和 引用类型(Object)。
JavaScript 数据类型分为 原始类型(Primitive Types) 和 引用类型(Reference Types)。
(1)原始类型(基本数据类型)
-
Number
(数字类型)- 包括整数、浮点数、
NaN
(不是一个数字)、Infinity
。 - 示例:
let a = 42; let b = 3.14; let c = NaN; let d = Infinity;
- 包括整数、浮点数、
-
String
(字符串类型)- 由字符组成的文本数据,可以使用单引号、双引号或模板字符串。
- 示例:
let str1 = "Hello"; let str2 = 'World'; let str3 = `Template ${str1}`;
-
Boolean
(布尔类型)- 只有
true
和false
两个值。 - 示例:
let isTrue = true; let isFalse = false;
- 只有
-
Undefined
(未定义)- 变量声明但未赋值时的默认值。
- 示例:
let x; console.log(x); // undefined
-
Null
(空值)- 一个表示 “无值” 的特殊值,通常用于手动赋值为空。
- 示例:
let y = null;
-
Symbol
(唯一值,ES6)- 创建独一无二的标识符,常用于对象属性。
- 示例:
let sym = Symbol("unique");
-
BigInt
(大整数,ES11)- 适用于比
Number
能表示的范围更大的整数。 - 示例:
let bigInt = 123456789012345678901234567890n;
- 适用于比
(2)引用类型(复杂数据类型)
-
Object
(对象)- 由
key-value
组成的集合。 - 示例:
let obj = { name: "Alice", age: 25 };
- 由
-
Array
(数组)- 数组是一种特殊的对象,可以存储有序数据。
- 示例:
let arr = [1, 2, 3];
-
Function
(函数)- JavaScript 中函数本质上也是对象,可以赋值给变量。
- 示例:
function 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 |
判断方法:
console.log(typeof null); // "object"
console.log(typeof undefined); // "undefined"console.log(null == undefined); // true(值相等)
console.log(null === undefined); // false(类型不同)
47 . JS 数据类型考题
(1)typeof
结果
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
判断
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true
5. ==
和 ===
有什么不同?
运算符 | 是否比较类型 | 是否进行类型转换 | 例子 |
---|---|---|---|
== | 否 | 是 | "1" == 1 // true |
=== | 是 | 否 | "1" === 1 // false |
(1)==
(值相等,类型可转换)
console.log(0 == false); // true
console.log(1 == "1"); // true
console.log(null == undefined); // true
==
允许不同数据类型进行转换后再比较。
(2)===
(值和类型都必须相等)
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
(可选):结束索引(不包含)。
- 返回值:截取的新数组。
示例
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)删除元素
let arr1 = [1, 2, 3, 4, 5];// 从索引 1 开始删除 2 个元素
console.log(arr1.splice(1, 2)); // [2, 3] (返回删除的部分)
console.log(arr1); // [1, 4, 5] (原数组被修改)
(2)插入元素
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)替换元素
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
一样,可以先复制一份数组:
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 提供的一个数据结构,天然去重。
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
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
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
:
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()
+ 遍历:
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()
:
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
:
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 引入的方法,专门用于判断变量是否是数组,推荐使用。
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
运算符用于判断对象是否是某个构造函数的实例:
console.log([] instanceof Array); // true
console.log({} instanceof Array); // false
console.log(new Array() instanceof Array); // true
⚠ 缺点:
- 不同
window
或iframe
可能判断失效,因为不同iframe
可能有不同的Array
构造函数:let 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)
可以返回准确的数据类型:
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]"
可以封装成一个函数:
function isArray(value) {return Object.prototype.toString.call(value) === "[object Array]";
}
✅ 优点:
- 适用于任何数据类型,不会被
iframe
问题影响。
4. constructor
判断
可以通过 constructor
检查 Array
:
console.log([].constructor === Array); // true
console.log({}.constructor === Array); // false
console.log(new Array().constructor === Array); // true
⚠ 缺点:
constructor
可能被修改:let arr = []; arr.constructor = Object; console.log(arr.constructor === Array); // false
5. typeof
(❌ 不适用于数组)
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:递归查找(适用于任意深度)
如果数组是 不规则的多维数组,递归是最通用的方法:
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)
展开:
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
进行递归处理:
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()
(不推荐,仅供参考)
可以将数组转换为字符串,然后用正则匹配所有数值:
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
。
let obj = {}; // 创建一个空对象
2. 让新对象的 __proto__
指向构造函数的 prototype
新对象会继承构造函数的 prototype
属性:
obj.__proto__ = Constructor.prototype;
这意味着新对象可以访问构造函数的原型方法。
3. 执行构造函数,并绑定 this
到新对象
使用 call
方式调用构造函数,并将 this
绑定到新对象:
let result = Constructor.call(obj, ...args); // 传递参数,执行构造函数
如果构造函数返回的是一个对象(非 null
),那么 new
操作符最终返回该对象。否则,返回新创建的对象。
4. 返回新对象
如果构造函数显式返回一个对象,new
操作符会返回该对象,否则返回新创建的实例:
return typeof result === "object" && result !== null ? result : obj;
完整示例
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
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
统计字符频率
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
统计
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
(函数式写法)
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
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
对象,引发兼容性问题。 - 生产环境通常使用独立函数替代:
function addPrefix(str, prefix) {return prefix + str; }console.log(addPrefix("world", "hello")); // "helloworld"
- 这样做可能会影响全局
- 可使用 ES6 Class 实现(如果想避免修改原型):
class 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 码点 排序:
const arr = [10, 2, 5, 30];
console.log(arr.sort()); // [10, 2, 30, 5] (按字符串 "10"、"2"、"30"、"5" 进行排序)
所以对于数字排序,必须提供比较函数:
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()
:
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()
一般用 快排、归并排序 或 堆排序。