JavaScript 由以下三部分组成:
window
、navigator
、location
、history
、screen
等对象。JavaScript 具有以下内置对象:
Object
、Function
、Boolean
、Symbol
Number
、BigInt
、Math
String
Array
Date
RegExp
Error
、TypeError
、SyntaxError
、ReferenceError
Set
、Map
、WeakSet
、WeakMap
Promise
、AsyncFunction
数组方法可以分为几类:
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)
:返回数组的部分片段,不修改原数组。typeof
:适用于基本数据类型,但 null
误判为 "object"
。instanceof
:判断对象是否属于某个构造函数的实例。Object.prototype.toString.call(value)
:返回精准数据类型,如 "[object Array]"
。Array.isArray(value)
:判断是否为数组。闭包(Closure) 是指函数可以访问其外部作用域的变量,即使外部函数已经执行结束。
特点:
示例:
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const counter = outer();
counter(); // 1
counter(); // 2
闭包(Closure)是指一个函数能够访问其外部作用域中的变量,即使在外部函数执行结束后,仍然可以保留对外部变量的访问权限。闭包的主要使用场景如下:
闭包可以创建私有变量,防止外部直接访问或修改数据。
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(无法直接访问私有变量)
应用场景:
闭包常用于事件监听器或回调函数,使得事件处理函数能够访问外部作用域中的变量。
function setupButton() {
let count = 0;
document.getElementById("myButton").addEventListener("click", function() {
count++;
console.log(`Button clicked ${count} times`);
});
}
setupButton();
应用场景:
addEventListener
回调中保留数据(如点击次数、鼠标移动距离等)。柯里化(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
)。闭包可以用于在定时器或异步操作中保留执行上下文。
function delayedMessage(msg, delay) {
setTimeout(function() {
console.log(msg);
}, delay);
}
delayedMessage("Hello, Closure!", 2000);
应用场景:
setTimeout
/ setInterval
相关的任务管理(如轮询、延迟加载)。在 ES6 之前,JavaScript 没有 let
和 const
关键字,使用闭包可以创建局部作用域,避免变量污染。
(function() {
var secret = "I am private";
console.log(secret);
})();
console.log(typeof secret); // undefined(无法访问)
应用场景:
let
和 const
取代该用法。闭包可用于缓存计算结果,避免重复计算,提升性能。
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)); // 计算并存入缓存
应用场景:
闭包可以用来创建迭代器或唯一 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
应用场景:
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 |
面试高频问题:
闭包的本质是什么?
闭包有哪些常见应用场景?
闭包会导致内存泄漏吗?如何避免?
element.onclick = null
释放 DOM 事件闭包,或者减少不必要的闭包使用。内存泄露 是指程序不再使用某些对象,但垃圾回收机制无法释放它们,导致内存占用增加。
常见原因:
window.variable
一直存在)setInterval
没有 clearInterval
)element.addEventListener
没有 removeEventListener
)解决方案:
let
/const
限制作用域。setInterval
用完后及时 clearInterval()
。removeEventListener()
解除事件绑定。null
解除对象引用。事件委托(Event Delegation) 是将事件监听器绑定在父级元素上,利用事件冒泡机制处理子元素事件,提高性能。
示例:
document.getElementById('parent').addEventListener('click', function(event) {
if (event.target.tagName === 'BUTTON') {
console.log('Button clicked:', event.target.innerText);
}
});
这样即使新 button
动态添加到 #parent
,仍然可以触发事件。
数据类型 | 存储位置 | 赋值方式 | 比较方式 |
---|---|---|---|
基本类型 | 栈内存 | 拷贝值 | 值比较 |
引用类型 | 堆内存 | 赋引用 | 地址比较 |
__proto__
,指向其构造函数的 prototype
,形成一个链式结构。new
操作符具体做了什么?obj
。obj.__proto__
关联到构造函数的 prototype
。this
到新对象。obj
。示例:
function Person(name) {
this.name = name;
}
const p = new Person("Alice");
console.log(p.name); // Alice
call
、apply
、bind
三者有什么区别?call
、apply
和 bind
是 JavaScript 中用于更改函数 this
指向的方法,它们的主要区别如下:
方法 | 作用 | 参数 | 是否立即执行 | 返回值 |
---|---|---|---|---|
call |
绑定 this 并调用函数 |
thisArg, arg1, arg2, ... |
是 | 调用结果 |
apply |
绑定 this 并调用函数 |
thisArg, [arg1, arg2, ...] |
是 | 调用结果 |
bind |
绑定 this ,返回新函数 |
thisArg, arg1, arg2, ... |
否 | 新函数 |
call
方法call(thisArg, arg1, arg2, ...)
方法可以手动指定 this
并 立即调用 该函数,参数按顺序传递。
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"
和 "!"
依次传入。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
属性。apply
方法apply(thisArg, [arg1, arg2, ...])
也是手动指定 this
,立即调用 该函数,但参数必须是 数组。
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]
。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
允许传递数组。bind
方法bind(thisArg, arg1, arg2, ...)
与 call
、apply
的最大区别是:
this
,无论以后如何调用,它的 this
都不会改变。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
。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
。call
、apply
、bind
的主要区别方法 | 是否立即执行 | 参数传递 | 适用场景 |
---|---|---|---|
call |
是 | 依次传递参数 | 立即执行,适用于手动指定 this 的方法调用 |
apply |
是 | 以数组形式传递 | 立即执行,适用于参数数量不固定的情况(如 Math.max ) |
bind |
否 | 依次传递参数 | 返回新函数,适用于事件绑定、延迟调用等 |
场景 | 推荐方法 |
---|---|
立即调用函数,并更改 this |
call |
立即调用函数,并且参数是数组 | apply |
需要返回一个新函数,稍后执行 | bind |
继承构造函数 | call |
事件绑定,避免 this 丢失 |
bind |
setTimeout 绑定 this |
bind |
为什么 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 主要通过 原型链 和 ES6 class 语法 实现继承:
每个 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
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
关键字可以更方便地调用父类方法。JavaScript 的设计基于以下原则:
setTimeout
、Promise
。this
的指向取决于 调用方式:
console.log(this); // 在浏览器中:window,在 Node.js 中:global
const obj = {
name: "Tom",
sayHello() {
console.log(this.name); // this 指向 obj
}
};
obj.sayHello(); // Tom
function Person(name) {
this.name = name;
}
const p = new Person("Tom");
console.log(p.name); // Tom(this 指向实例对象)
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
const obj = {
name: "Tom",
sayHello: function() {
setTimeout(() => {
console.log(this.name); // this 继承自 obj
}, 1000);
}
};
obj.sayHello(); // Tom
总结:
this
由外层作用域决定。bind
可手动绑定 this
,call/apply
可立即执行。
标签中的 async
和 defer
的区别在 HTML 文档中, 标签用于加载 JavaScript 脚本,默认情况下,脚本会阻塞 HTML 解析,直到脚本加载并执行完毕。而
async
和 defer
这两个属性用于优化脚本加载方式,以提高页面性能。
async
和 defer
的基本概念属性 | 解析 HTML | 下载脚本 | 执行脚本 | 执行顺序 |
---|---|---|---|---|
默认(无 async /defer ) |
暂停 | 下载脚本 | 执行脚本 | 按HTML 书写顺序执行,阻塞渲染 |
async |
不暂停 | 并行下载 | 下载完成立即执行 | 执行顺序不一定,谁先下载完就先执行 |
defer |
不暂停 | 并行下载 | HTML 解析完后按顺序执行 | 按照 HTML 中的顺序执行 |
async
和 defer
详细区别async
(异步加载并执行)async
允许脚本 异步下载,即不会阻塞 HTML 解析。async
脚本的执行顺序不确定,取决于哪个脚本先下载完。示例
<script async src="script1.js">script>
<script async src="script2.js">script>
✅ 执行顺序:
script1.js
和 script2.js
并行下载。⚠️ 适用场景:
async
适用于 不依赖 DOM 结构 或 不依赖其他脚本 的 JavaScript 代码,如:
defer
(异步加载,但按顺序执行)defer
也允许脚本 异步下载,不会阻塞 HTML 解析。defer
脚本会等到 HTML 解析完成后,按照 HTML 中的顺序执行。示例
<script defer src="script1.js">script>
<script defer src="script2.js">script>
✅ 执行顺序:
script1.js
和 script2.js
同时下载。script1.js
→ script2.js
的顺序执行。⚠️ 适用场景:
defer
适用于 依赖 DOM 结构 或 多个脚本之间有执行顺序要求 的情况,如:
async
vs defer
vs 默认
加载方式 | HTML 解析 | JS 下载 | JS 执行 | 执行顺序 |
---|---|---|---|---|
默认
|
暂停 | 下载 | 执行 | 按HTML 书写顺序 |
async |
不暂停 | 并行下载 | 下载完成立即执行 | 下载顺序不确定 |
defer |
不暂停 | 并行下载 | HTML 解析完成后执行 | 按 HTML 书写顺序 |
async
和 defer
适用场景需求 | 适合 async |
适合 defer |
---|---|---|
独立的第三方脚本(如广告、分析工具) | ✅ | ❌ |
多个脚本之间无依赖关系 | ✅ | ❌ |
需要操作 DOM,必须等待 HTML 解析完成 | ❌ | ✅ |
多个脚本之间有顺序依赖 | ❌ | ✅ |
async
和 defer
结合使用?HTML 规范规定,不能同时使用 async
和 defer
。如果一个 标签同时有
async
和 defer
,则 async
优先,defer
被忽略。
✅ 如果脚本不依赖 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>
async |
defer |
---|---|
并行下载,下载完立刻执行 | 并行下载,HTML 解析完后按顺序执行 |
执行顺序不确定(谁先下载完谁先执行) | 按 HTML 书写顺序执行 |
适用于独立、不依赖 DOM 的脚本 | 适用于需要等待 DOM 解析完成的脚本 |
✅ 结论:
async
适合独立的、无依赖的脚本(如统计、广告)。defer
适合依赖 DOM 或者需要按顺序执行的脚本(如框架、主逻辑)。defer
是加载多个脚本的最佳选择,不会阻塞页面解析,又能保证执行顺序。 在大多数浏览器中,setTimeout
的最小延迟时间是 4ms(如果时间小于 4ms,实际延迟仍为 4ms)。
setTimeout(() => console.log("Hello"), 0); // 最早 4ms 后执行
setTimeout
超过 5 次,最小延迟变成 4ms。setTimeout(fn, 0)
也不会立即执行。特性 | ES5 | ES6 |
---|---|---|
变量声明 | var |
let / const |
作用域 | 函数作用域 | 块级作用域 |
字符串 | 字符串拼接 (+ ) |
模板字符串(`${}`) |
箭头函数 | 无 | () => {} |
类 | 基于原型 | class 语法 |
this 绑定 |
call/apply/bind |
箭头函数继承 this |
模块化 | script 标签 |
import/export |
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";
在 JavaScript 中,高阶函数和闭包都是重要的概念,虽然有一定的关联,但它们的核心作用和使用方式不同。下面详细分析它们的概念、特点、区别、应用场景。
定义:
高阶函数是接收一个函数作为参数或者返回一个函数的函数。
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
作为参数。
function multiplier(factor) {
return function (number) {
return number * factor;
};
}
const double = multiplier(2);
console.log(double(5)); // 10
✅ multiplier
是一个高阶函数,因为它返回了一个新函数。
定义:
闭包是一个可以访问其外部作用域变量的函数,即使在外部作用域执行结束后,函数仍然可以访问这些变量。
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
执行完毕。
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
访问,形成私有作用域。
特性 | 高阶函数 | 闭包 |
---|---|---|
定义 | 以函数作为参数或返回函数 | 访问外部作用域变量的函数 |
主要作用 | 代码复用、抽象、回调机制 | 数据封装、模拟私有变量 |
是否涉及作用域 | 主要关注函数传递,不强调作用域 | 强调函数保留外部作用域 |
返回值 | 可能返回一个函数 | 返回的函数可访问外部变量 |
是否影响垃圾回收 | 不一定 | 可能会导致变量不会被回收 |
常见应用 | map 、filter 、reduce 、回调函数 |
计数器、私有变量、缓存函数 |
实际上,闭包和高阶函数可以结合使用。例如,返回函数的高阶函数通常会创建闭包:
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
变量。
map
、filter
、reduce
)setTimeout
、addEventListener
)简单来说:
JavaScript 是 单线程 语言,主要用于浏览器和 Node.js 等环境中。由于 JavaScript 需要同时处理 UI 渲染、用户交互、网络请求等操作,因此采用了 事件循环(Event Loop) 机制,使其能够高效地执行任务,而不会阻塞主线程。
JavaScript 的运行机制可以概括为:
setTimeout
、Promise
、DOM 事件
等)被挂起,等待执行时机,并存入相应的 任务队列(Task Queue)。JavaScript 的任务分为 宏任务(Macro Task) 和 微任务(Micro Task)。
宏任务通常包含:
setTimeout
setInterval
setImmediate
(Node.js)I/O 任务
UI 渲染(Rendering)
MessageChannel
requestAnimationFrame
每次 事件循环 执行时,只会从 宏任务队列 取出一个任务执行,执行完后会检查 微任务队列。
微任务通常包含:
Promise.then
、catch
、finally
MutationObserver
process.nextTick
(Node.js 专属)微任务的特点:
setTimeout
、Promise
):
setTimeout
等 宏任务 进入 宏任务队列。Promise.then
等 微任务 进入 微任务队列。setTimeout
回调)。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
。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
。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
。环境 | 微任务顺序 | 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
解析:
process.nextTick
优先级最高,Promise.then
其次,setImmediate
比 setTimeout(0)
先执行。Promise.then
先执行,然后 setTimeout(0)
,最后 setImmediate
(浏览器中 setImmediate
表现与 setTimeout(0)
类似)。async/await
也是基于 Promise,await
后的代码是微任务。掌握事件循环的机制,有助于理解 JavaScript 的异步行为,编写高效的前端代码!
在使用 递归(Recursion) 时,可能会遇到以下问题:
function infiniteRecursion() {
infiniteRecursion(); // 无限递归,导致栈溢出
}
infiniteRecursion();
解决方案:
递归可能导致 重复计算,特别是在计算斐波那契数列等问题时。
示例(低效的递归):
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(40)); // 计算量非常大
解决方案:
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)); // 计算快很多
function countdown(n) {
if (n === 0) return; // 终止条件
console.log(n);
countdown(n - 1);
}
countdown(5); // 正确
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
深拷贝是指创建一个 新对象,并 完全复制原对象的所有属性值,包括嵌套对象,而不是仅仅拷贝引用(浅拷贝)。
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
等类型。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
)。lodash
库lodash
提供 _.cloneDeep()
方法进行深拷贝:
const _ = require("lodash");
const obj = { a: 1, b: { c: 2 } };
const clone = _.cloneDeep(obj);
console.log(clone);
✅ 优点:
❌ 缺点:
lodash
。structuredClone()
(推荐,现代浏览器支持)const obj = { a: 1, b: { c: 2 }, d: new Date() };
const clone = structuredClone(obj);
console.log(clone);
✅ 优点:
❌ 缺点:
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()
浅拷贝(Shallow Copy)指的是 仅拷贝对象的第一层属性,如果属性是 引用类型(如对象、数组),那么拷贝的只是引用(即指针),而不是值本身。
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(引用同一个对象)
✅ 优点:
❌ 缺点:
{ ...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(仍然指向相同的对象)
✅ 优点:
❌ 缺点:
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(仍然指向同一个对象)
✅ 优点:
❌ 缺点:
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
✅ 优点:
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()
或递归拷贝。
深浅拷贝是指在复制对象时,是否复制对象的引用还是复制对象的内容。它们的区别在于对原始对象的修改是否影响到拷贝后的对象。
浅拷贝是指创建一个新的对象,但新对象的属性仍然引用原始对象中的引用类型数据(比如数组或对象)。因此,如果原始对象中的引用类型属性被修改,拷贝后的对象也会受到影响。
const obj1 = {
name: "Alice",
details: { age: 25 }
};
const shallowCopy = { ...obj1 }; // 浅拷贝
obj1.details.age = 26; // 修改原对象的属性
console.log(shallowCopy.details.age); // 输出 26,浅拷贝的对象会受到影响
深拷贝是指创建一个新的对象,并递归地复制原始对象的所有属性,包括对象中的引用类型数据。这样,修改原始对象中的引用类型属性不会影响拷贝后的对象。
const obj1 = {
name: "Alice",
details: { age: 25 }
};
const deepCopy = JSON.parse(JSON.stringify(obj1)); // 深拷贝
obj1.details.age = 26; // 修改原对象的属性
console.log(deepCopy.details.age); // 输出 25,深拷贝的对象不会受到影响
选择何种拷贝方式取决于你对数据独立性的需求,是否需要对嵌套对象进行修改或保留原对象的状态。
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));
GET | POST |
---|---|
参数在 URL 中,长度受限(约 2048 字符) | 参数在请求体中,无长度限制 |
用于获取数据,幂等(多次请求结果相同) | 用于提交数据,非幂等 |
可缓存 | 不可缓存 |
安全性较低(URL 可见) | 相对安全 |
原理:
pending
→ fulfilled
/rejected
)。then
方法注册回调,返回新 Promise 实现链式调用。优点:
catch
)。缺点:
async/await 是 Promise 的语法糖,异步代码同步化,提高可读性。
async
函数隐式返回 Promise,await
后接 Promise 或值。
错误处理:
// Promise
fetch().catch(err => {});
// async/await
try { await fetch(); } catch (err) {}
类型 | 特点 |
---|---|
Cookie | 最大 4KB,每次请求携带,可设过期时间,同源限制。 |
localStorage | 持久存储(除非手动删除),同源,大小约 5-10MB。 |
sessionStorage | 会话级存储(标签页关闭清除),同源。 |
IndexedDB | 非关系型数据库,支持事务,存储大量数据。 |
Web Storage | (localStorage 和 sessionStorage 统称) |
Authorization
头添加 Token(如 Bearer
)。安全优化:
display: none
)。DOM 树 | 渲染树 |
---|---|
包含所有 HTML 节点(包括隐藏元素) | 仅包含需渲染的节点 |
结构完整,描述文档内容 | 结合 DOM 和 CSSOM,描述可视内容 |
CSS Sprites(精灵图) | Base64 |
---|---|
多图合并为一张,减少请求 | 图片转字符串嵌入代码 |
需通过 background-position 定位 |
增大文件体积(约 30%) |
适合多图标场景 | 适合小图标,避免额外请求 |
组成:
HS256
)。特点:无需服务端存储 Session,支持跨域。
注意:需防范 Token 盗用,避免存储敏感信息,设置短过期时间。
依赖管理:基于 package.json
描述依赖及版本(遵循语义化版本 semver
)。
模块加载:Node.js 的 CommonJS 规范。
包存储:默认从 npm 仓库下载(registry.npmjs.org)。
安装机制:
npm install
解析依赖树,扁平化安装(避免嵌套过深)。package-lock.json
锁定精确版本,确保环境一致性。“协议头”可能指的是HTTP协议的头部,即整体结构中的头部,而请求头则是请求部分的头部。
每个HTTP请求由三部分组成:请求行(请求方法、URI、HTTP版本)、请求头部(headers)、请求正文。
请求行有时也称为起始行,而请求头部则是首部字段。响应类似,有状态行、响应头部、正文。
HTTP协议中,“协议头”与“请求头”的区别可以通过以下结构化说明进行澄清:
HTTP协议中,“协议头”与“请求头”的区别可以通过以下结构化说明进行澄清:
定义:在HTTP标准中,“协议头”没有严格对应的定义。此词可能被误解为以下两种场景:
实际标准:RFC文档中未使用“协议头”一词,需结合上下文理解。
header-field
定义。一个HTTP请求由以下三部分组成(按顺序排列):
请求行(Request Line)
格式:[Method] [URI] [HTTP Version]
(如 GET /index.html HTTP/1.1
)
关键字段:
请求头(Request Headers)
格式:键值对的集合,每行一个字段。
(如 Host: example.com
,User-Agent: Chrome/123
)
常见类型:
Cache-Control
)Accept
、Authorization
)Content-Type
)。请求正文(Body)
对比维度 | 协议头(可能场景) | 请求头(Request Headers) |
---|---|---|
**组成内容 | 若指请求行:包含方法、URI、HTTP版本。 若指整个头部:包含请求行+首部字段。 |
仅包含键值对的头部字段(如Host , Accept )。 |
功能定位 | 若为请求行:定义操作类型、资源路径和协议规则。 | 提供请求的附加信息和控制参数。 |
标准化术语 | 非RFC标准术语,需结合上下文。 | RFC 7230明确定义的header-field 。 |
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标准术语,避免混淆。
同源策略(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
→ 不同源(子域名不同)同源策略针对以下操作进行限制:
数据访问
iframe
嵌入并操作B网站的DOM(除非明确同源)。网络请求
其他资源限制
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
响应头授权特定域访问资源。OPTIONS
请求确认权限。JSONP(过时技术)
标签不受同源策略限制的特性,通过回调函数获取跨域数据。document.domain(仅限同主域)
document.domain = 'example.com'
,使同主域不同子域的页面可以互操作。postMessage API
iframe
父子页面)。前端解决方案:
后端设置:
Access-Control-Allow-Origin: *
。安全权衡:
将同源策略想象成酒店房卡系统:
同源策略是浏览器安全的基石,平衡了功能性与风险。掌握其规则与跨域解决方案(如CORS),是开发现代Web应用的关键技能。
详细查看链接
// 防抖:等待一段时间后执行,期间重新触发会重新计时
Input Events: │─A─B─C─ │────D──│──E──│
Debounced: │────────│────D──│──E──│
// 节流:按照一定时间间隔执行
Input Events: │─A─B─C─D─E─F─G─│
Throttled: │─A─────D─────G─│
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// 应用场景:输入框搜索联想
function throttle(fn, delay) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= delay) {
fn(...args);
last = now;
}
};
}
// 应用场景:滚动事件监听
定义:轻量级数据交换格式(基于键值对)
结构:支持字符串、数字、布尔、数组、对象、null
转换方法
JSON.parse('{"name":"John"}'); // 字符串 → 对象
JSON.stringify({name: 'John'}); // 对象 → 字符串
加载状态:显示 Loading 动画或骨架屏
错误反馈
数据降级
重试逻辑
function fetchWithRetry(url, retries = 3) {
return fetch(url).catch(err =>
retries > 0 ? fetchWithRetry(url, retries - 1) : Promise.reject(err)
);
}
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);
}
);
安全性
httpOnly
Cookie文件分片
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);
});
断点续传:根据已上传分片列表跳过已传部分
合并分片:服务端接收到所有分片后合并成完整文件
浏览器的缓存策略主要分为 强缓存(Strong Cache) 和 协商缓存(Negotiated 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
是否有效。当强缓存失效时,浏览器会向服务器发送请求,并通过 协商缓存机制 确定资源是否需要重新下载。
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"
304 Not Modified
,使用缓存。200 OK
。协商缓存流程:
If-Modified-Since
或 If-None-Match
。304
,使用缓存。200
并提供新资源。缓存策略 | 适用情况 | 是否向服务器请求 | 响应状态码 | 主要控制字段 |
---|---|---|---|---|
强缓存 | 资源未过期 | 否 | 200(from cache) | Cache-Control 、Expires |
协商缓存 | 资源已过期 | 是 | 304(Not Modified) | ETag 、Last-Modified |
Cache-Control: max-age=31536000
,并使用 文件名哈希 处理更新,如 app.123abc.js
。Cache-Control: max-age=31536000, immutable
ETag
或 Last-Modified
,确保数据变更时能及时更新。ETag: "abc123"
no-cache
Cache-Control: no-cache
Cache-Control: no-store
强制不缓存:Cache-Control: no-store
Expires
、Cache-Control: max-age
)优先,避免请求。Last-Modified
、ETag
)减少数据传输。no-cache
或 no-store
确保数据最新。ETag
优先级高于 Last-Modified
,适用于精确缓存控制。延迟加载(Lazy Loading)JS 主要有以下方式:
defer
属性(适用于外部 JS 文件)
<script src="script.js" defer>script>
async
属性(适用于外部 JS 文件)
<script src="script.js" async>script>
动态创建 标签
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
import()
进行代码拆分,只有在需要时才加载模块。function loadModule() {
import("./module.js").then((module) => {
module.default();
});
}
JS 具有 7 种原始类型(Number、String、Boolean、Undefined、Null、Symbol、BigInt)和 引用类型(Object)。
JavaScript 数据类型分为 原始类型(Primitive Types) 和 引用类型(Reference Types)。
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;
Object
(对象)
key-value
组成的集合。let obj = { name: "Alice", age: 25 };
Array
(数组)
let arr = [1, 2, 3];
Function
(函数)
function greet() {
return "Hello";
}
Date
、RegExp
、Map
、Set
也是常见的引用类型。
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(类型不同)
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"
instanceof
判断console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true
==
和 ===
有什么不同?运算符 | 是否比较类型 | 是否进行类型转换 | 例子 |
---|---|---|---|
== |
否 | 是 | "1" == 1 // true |
=== |
是 | 否 | "1" === 1 // false |
==
(值相等,类型可转换)console.log(0 == false); // true
console.log(1 == "1"); // true
console.log(null == undefined); // true
==
允许不同数据类型进行转换后再比较。===
(值和类型都必须相等)console.log(0 === false); // false
console.log(1 === "1"); // false
console.log(null === undefined); // false
===
需要严格相等,避免了类型转换导致的潜在错误。==
会进行类型转换,===
需要类型和值都相等。===
,避免隐式转换导致的错误。slice
和 splice
的区别slice
和 splice
都是 JavaScript 数组方法,但它们的作用、影响和返回值不同:
方法 | 作用 | 是否修改原数组 | 返回值 |
---|---|---|---|
slice |
截取数组的一部分 | ❌ 不会修改原数组 | 返回截取的新数组 |
splice |
删除/替换/插入数组元素 | ✅ 会修改原数组 | 返回被删除的元素组成的数组 |
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] (原数组不变)
splice(start, deleteCount, ...items)
start
(必填):起始索引(从该索引开始操作)。deleteCount
(可选):删除的元素个数,若为 0
则不删除。items
(可选):要插入的元素(可变参数)。let arr1 = [1, 2, 3, 4, 5];
// 从索引 1 开始删除 2 个元素
console.log(arr1.splice(1, 2)); // [2, 3] (返回删除的部分)
console.log(arr1); // [1, 4, 5] (原数组被修改)
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]
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]
slice
vs splice
总结方法 | 修改原数组 | 返回值 | 用途 |
---|---|---|---|
slice |
❌ 不修改 | 新数组 | 截取 一部分数组 |
splice |
✅ 修改 | 被删除的元素数组 | 删除、替换、插入 |
slice
splice
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] (原数组未变)
在 JavaScript 中,数组去重有多种方式,以下是几种常见的方法:
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
等)。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²) 复杂度。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
略低。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
覆盖前面的,最终去重。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]
_.uniq()
如果项目中使用 Lodash,可以用 _.uniq()
:
const _ = require("lodash");
const arr = [1, 2, 2, 3, 4, 4, 5];
console.log(_.uniq(arr)); // [1, 2, 3, 4, 5]
如果对象去重的标准不只是 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
等。在 JavaScript 中,可以用多种方法来判断变量是否为数组,下面是常见的几种方法:
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
等不同的执行环境。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 造成的问题)
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
问题影响。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
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]"
在 JavaScript 中,要找出 多维数组(嵌套数组)中的最大值,可以使用递归、flat(Infinity)
或者 reduce
来实现。下面介绍几种方法:
如果数组是 不规则的多维数组,递归是最通用的方法:
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
✅ 优点:
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
),需要预处理。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
实现,代码更函数式。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
⚠ 缺点:
null
、undefined
)。方法 | 适用情况 | 代码简洁性 | 性能 |
---|---|---|---|
递归 | 适用于不规则多维数组 | 一般 | ⭐⭐⭐⭐ |
flat(Infinity) + Math.max |
适用于规则多维数组 | 最简洁 | ⭐⭐⭐ |
reduce + 递归 |
适用于所有多维数组 | 结构清晰 | ⭐⭐⭐⭐ |
JSON.stringify() + 正则 |
仅适用于简单数组 | 不推荐 | ⭐ |
Math.max(...arr.flat(Infinity))
(简单高效)。findMax(arr)
(通用性最强)。reduce
+ 递归。new
操作符在 JavaScript 中用于创建一个实例对象,它会执行以下四个步骤:
首先,new
操作符会创建一个新的空对象,这个对象会继承构造函数的 prototype
。
let obj = {}; // 创建一个空对象
__proto__
指向构造函数的 prototype
新对象会继承构造函数的 prototype
属性:
obj.__proto__ = Constructor.prototype;
这意味着新对象可以访问构造函数的原型方法。
this
到新对象使用 call
方式调用构造函数,并将 this
绑定到新对象:
let result = Constructor.call(obj, ...args); // 传递参数,执行构造函数
如果构造函数返回的是一个对象(非 null
),那么 new
操作符最终返回该对象。否则,返回新创建的对象。
如果构造函数显式返回一个对象,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
绑定到新对象。要找出字符串中出现最多的字符及其出现次数,可以使用 Map
或 Object
进行统计。
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) 读写效率,性能好。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 }
✅ 优点:
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
在大数据量时更优。要在 JavaScript 的 String 原型(prototype) 上定义 addPrefix
方法,使其能够为字符串添加前缀,可以这样实现:
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"
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
(简单易懂,适合面试展示)Array.prototype.sort()
的背后原理主要涉及 排序算法 和 稳定性,不同的 JavaScript 引擎可能采用不同的排序实现。下面是详细解析:
sort
实现V8(Chrome 和 Node.js 使用的 JavaScript 引擎)对 sort()
进行了优化,主要采用 双轴快速排序(Dual-Pivot Quicksort) 和 插入排序(Insertion Sort),不同情况下采用不同算法:
V8 在 sort()
里使用 双轴快排,它是 改进版的快速排序,相比传统单轴快排效率更高:
pivot1
pivot1
和 pivot2
之间pivot2
特点:
适用于小数组:
sort()
的稳定性V8 的 sort()
默认是不稳定的,因为 双轴快排是不稳定排序,但在某些情况下(如大量相等元素时)可能会使用 TimSort,它是 稳定排序。
sort
使用时的注意事项如果 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]
sort((a, b) => a - b)
里的回调函数必须返回:
a
排在 b
之前)b
排在 a
之前)a
和 b
位置不变)如果返回非数字值,可能会导致 排序行为不确定。
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]
sort()
在 V8 引擎中采用 双轴快速排序(大数组)+ 插入排序(小数组)。sort()
默认按 字符串 Unicode 码点 排序,排序数字时需提供 比较函数。sort()
不稳定(快排不稳定)。sort()
一般用 快排、归并排序 或 堆排序。