前端5月28日 03:36
some、every、find、filter、map、forEach 有什么区别?这 6 个方法是 JavaScript 数组最常用的迭代方法,面试几乎必考。核心区别在于**返回值类型**和**是否短路**,按返回值分三类记忆最清晰。
## 一、遍历类(无返回值)
### forEach
纯遍历,对每个元素执行回调,返回值永远是 `undefined`。
- **不能中断**:`return` 只跳过当前回调,`break` 语法不支持,想中途退出只能用 `try/catch` 抛异常(不推荐)
- **不支持异步**:回调里写 `async/await` 不会等待 Promise,因为 `forEach` 不关心返回值
```javascript
const list = [1, 2, 3];
list.forEach(item => console.log(item)); // 1, 2, 3
// return 只跳过当次,不会中断循环
```
## 二、返回新数组
### map
每个元素经回调映射后返回**等长新数组**,不改变原数组。
```javascript
const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2); // [2, 4, 6]
```
### filter
返回**满足条件的元素**组成的新数组,长度可能小于原数组,不改变原数组。
```javascript
const nums = [1, 2, 3, 4, 5];
const big = nums.filter(n => n > 3); // [4, 5]
```
## 三、返回布尔值或单个元素
### find
返回**第一个**满足条件的元素,找到即停止遍历(短路)。找不到返回 `undefined`。
```javascript
const users = [{id: 1, name: 'A'}, {id: 2, name: 'B'}];
users.find(u => u.id === 2); // {id: 2, name: 'B'}
```
### some
有**任意一个**满足条件就返回 `true`,找到即短路。全不满足返回 `false`。**空数组返回 `false`**。
```javascript
[1, 2, 3].some(n => n > 2); // true
[1, 2, 3].some(n => n > 5); // false
[].some(n => n > 0); // false
```
### every
**所有元素**都满足条件才返回 `true`,遇到不满足即短路。**空数组返回 `true`**(空真逻辑 vacuous truth)。
```javascript
[1, 2, 3].every(n => n > 0); // true
[1, 2, 3].every(n => n > 1); // false
[].every(n => n > 0); // true(空真)
```
## 四、对比速查表
| 方法 | 返回值 | 是否短路 | 空数组返回 | 链式调用 | 修改原数组 |
|------|--------|----------|-----------|---------|-----------|
| forEach | undefined | 否 | undefined | 否 | 否 |
| map | 新数组 | 否 | [] | 是 | 否 |
| filter | 新数组 | 否 | [] | 是 | 否 |
| find | 单个元素/undefined | 是 | undefined | 否 | 否 |
| some | boolean | 是 | false | 否 | 否 |
| every | boolean | 是 | true | 否 | 否 |
## 五、高频追问
### map 和 forEach 怎么选?
需要返回新数组用 `map`,纯副作用(如 console.log、DOM 操作)用 `forEach`。关键区别:`map` 可链式调用,`forEach` 返回 `undefined` 不可链式。
### some 和 includes 有什么区别?
- `includes(val)` 判断数组是否包含某个**具体值**,用严格相等(`===`)比较
- `some(fn)` 判断是否有元素满足**自定义条件**
- `includes` 只能判断值存在性,`some` 可以写任意判断逻辑
```javascript
[1, 2, 3].includes(2); // true
[1, 2, 3].some(n => n > 2); // true
[{a: 1}].includes({a: 1}); // false(引用不同)
[{a: 1}].some(o => o.a === 1); // true
```
### 这些方法支持异步回调吗?
都不原生支持。`forEach` 里写 `async/await` 不会等待 Promise resolve。需要异步迭代用 `for...of` + `await` 或 `Promise.all` + `map`。
```javascript
// 错误:forEach 不会等待 async
ids.forEach(async id => {
const data = await fetch(id); // 并发执行,不会依次等待
});
// 正确方式1:for...of
for (const id of ids) {
const data = await fetch(id);
}
// 正确方式2:Promise.all + map(并行)
const results = await Promise.all(ids.map(id => fetch(id)));
```
### find 和 filter 怎么选?
只需第一个匹配用 `find`(性能更好,短路),需要所有匹配用 `filter`。
### reduce 为什么没列进来?
`reduce` 是这 6 个方法的基础——`map`、`filter`、`some`、`every`、`find` 都可以用 `reduce` 实现。面试中常追问 reduce 的用法,但 reduce 更偏向"累加器"模式,功能更强大也更复杂,属于另一个考点的范畴。标签
ES6
2015年版的ECMAScript规范,现在是一个标准(ECMAScript 2015)。

前端5月28日 03:36
ES5 和 ES6 有什么区别?ES6(ES2015)是 JavaScript 历史上最大的一次版本更新,面试中这道题考查的是你对 JS 语言演进的理解深度。回答的关键不是罗列特性,而是讲清楚**每个变化解决了什么问题**。
## 变量声明:从 var 到 let/const
ES5 只有 `var`,存在两大问题:
```js
// 问题1:变量提升
console.log(a); // undefined(不会报错,但容易出 bug)
var a = 1;
// 问题2:无块级作用域
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}
```
ES6 用 `let`/`const` 解决了这两个问题:
```js
// let 有块级作用域 + 暂时性死区
console.log(b); // ReferenceError(声明前访问直接报错)
let b = 1;
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}
// const 不可重新赋值(但对象属性仍可修改)
const obj = { a: 1 };
obj.a = 2; // OK
obj = { a: 2 }; // TypeError
```
**面试要点**:`const` 保证的是绑定不可变,不是值不可变。想冻结对象用 `Object.freeze()`。
## 函数:箭头函数与 this 绑定
ES5 中 `this` 指向取决于调用方式,经常需要 `var self = this` 或 `.bind(this)`:
```js
// ES5
var obj = {
name: 'ES5',
say: function() {
var self = this;
setTimeout(function() {
console.log(self.name); // 必须用 self/cache
}, 0);
}
};
// ES6 — 箭头函数继承外层 this
const obj2 = {
name: 'ES6',
say() {
setTimeout(() => {
console.log(this.name); // 直接用 this
}, 0);
}
};
```
**注意**:箭头函数没有自己的 `arguments`、`super`、`new.target`,不能用作构造函数。
## 字符串:模板字符串
```js
// ES5
var greeting = 'Hello, ' + name + '! You are ' + age + ' years old.';
// ES6
const greeting = `Hello, ${name}! You are ${age} years old.`;
```
模板字符串支持多行、变量插值、标签模板,彻底告别字符串拼接。
## 解构赋值与展开运算符
```js
// 对象解构
const { name, age } = user;
// 数组解构
const [first, ...rest] = [1, 2, 3, 4]; // first=1, rest=[2,3,4]
// 展开运算符 — 浅拷贝与合并
const copy = [...arr];
const merged = { ...defaults, ...config };
```
解构让数据提取更简洁,展开运算符替代了 `Object.assign` 和 `concat` 的大多数场景。
## 类与继承:class 语法
```js
// ES5 — 构造函数 + 原型链
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return this.name + ' makes a sound';
};
// ES6 — class 语法
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}
```
`class` 本质是原型继承的语法糖,但有行为差异:内部默认严格模式、方法不可枚举、必须用 `new` 调用。
## 模块系统:import/export
```js
// ES5 — CommonJS(Node.js)
const module = require('./module');
module.exports = { foo };
// ES6 — ES Modules
import { foo } from './module';
export const bar = 1;
export default function() {}
```
ES Modules 是静态的,支持 Tree Shaking;CommonJS 是动态的,运行时加载。现代项目(Vite/Webpack)均以 ESM 为优先。
## 异步编程:Promise 与 async/await
```js
// ES5 — 回调地狱
getData(function(a) {
getMore(a, function(b) {
getEvenMore(b, function(c) {
console.log(c);
});
});
});
// ES6 — Promise 链式调用
getData()
.then(a => getMore(a))
.then(b => getEvenMore(b))
.then(c => console.log(c));
// ES8 — async/await(同步写法)
const a = await getData();
const b = await getMore(a);
const c = await getEvenMore(b);
```
Promise 解决了回调地狱,async/await 让异步代码看起来像同步,是面试高频追问点。
## 新数据结构与 API
| 特性 | 用途 |
|------|------|
| `Map` | 键值对集合,键可以是任意类型(Object 的键只能是字符串/Symbol) |
| `Set` | 去重数组:`[...new Set(arr)]` |
| `WeakMap/WeakSet` | 键是弱引用,适合缓存和关联私有数据,不阻止 GC |
| `Symbol` | 创建唯一标识符,用于私有属性和内置协议 |
| `Proxy/Reflect` | 拦截对象操作(Vue 3 响应式核心) |
| `Generator/Iterator` | 可暂停函数,`for...of` 遍历统一接口 |
## 追问
### ES6 之后还有什么重要的新特性?
| 版本 | 关键特性 |
|------|----------|
| ES7 | `Array.prototype.includes`、指数运算符 `**` |
| ES8 | `async/await`、`Object.values/entries` |
| ES9 | `Promise.finally`、异步迭代 `for await...of` |
| ES10 | `flat/flatMap`、`Object.fromEntries` |
| ES11 | `??`(空值合并)、`?.`(可选链)、`Promise.allSettled` |
| ES12 | `replaceAll`、逻辑赋值 `||=` `&&=` `??=` |
| ES13 | `at()`、`Object.hasOwn`、Top-level await |
### let/const 和 var 最大的实际区别?
1. **块级作用域** — 解决 for 循环闭包问题
2. **暂时性死区** — 声明前访问报 ReferenceError,var 是 undefined
3. **不可重复声明** — 同一作用域内 let/const 不能重复声明同名变量
4. **const 不可重新赋值** — 但对象/数组内容仍可修改
### class 只是语法糖吗?
基本是。`class` 编译后就是原型链模式(构造函数 + prototype + Object.create)。但有几个行为差异:
- class 内部默认严格模式
- class 方法不可枚举(`for...in` 遍历不到)
- 只能用 `new` 调用(有 `new.target` 检查,直接调用报错)
- `extends` 内部用 `Object.create` 设置原型链,比 ES5 手动写更规范
### 面试回答策略
面试官问这道题,不是让你背特性列表。推荐的回答结构:
1. **一句话概括**:ES6 让 JS 从脚本语言变成工程化语言
2. **按类别讲 3-4 个重点**,每个说清楚"ES5 什么问题 → ES6 怎么解决"
3. **追问时深入**:挑一个你最熟悉的特性展开(如 class 的原型链原理、Promise 的微任务机制)前端5月28日 03:35
ES6 中的 Map 和原生的 Object 有什么区别?Map 和 Object 都能存键值对,但 Map 是专门为"字典"场景设计的,解决了 Object 做字典时的几个硬伤。
**键的类型**:Object 的 key 只能是字符串或 Symbol,数字 1 和字符串 "1" 是同一个 key。Map 的 key 可以是任意类型——对象、函数、NaN 都行,用 SameValueZero 算法比较(NaN 等于 NaN)。
**原型链污染**:Object 有原型链,`obj.__proto__`、`obj.toString` 这类属性名会冲突。`Object.create(null)` 能规避,但写法不直觉。Map 天然没有这个问题。
**大小**:Map 有 `size` 属性直接取。Object 要 `Object.keys(obj).length`。
**顺序**:Map 严格按插入顺序迭代。Object 在 ES6 后基本也按插入顺序,但整数 key 会被提前排列,容易踩坑。
**遍历**:Map 直接 `for...of` 或 `forEach`。Object 要先转数组(`Object.entries()`)或用 `for...in`(还会遍历原型链)。
**性能**:频繁增删键值对时 Map 更快。Object 在 V8 中对连续整数 key 有快属性优化,但这种优化对字典场景没帮助。
**序列化**:`JSON.stringify` 能直接处理 Object。Map 不行,需要先转成数组或对象。
```js
const m = new Map();
const obj = {};
m.set(obj, 'value'); // 对象做 key,Object 做不到
m.set(1, 'num');
m.set('1', 'str'); // 1 和 '1' 是不同 key
console.log(m.size); // 3
```
一句话:需要字典数据结构时优先用 Map,需要 JSON 序列化或简单配置对象时用 Object。
## 追问
### WeakMap 和 Map 有什么区别?
WeakMap 的 key 必须是对象,值任意。key 是弱引用——被 GC 回收后对应条目自动消失。不可迭代(没有 `size`、`forEach`、`keys()`),因为条目随时可能被回收。
| | Map | WeakMap |
|---|---|---|
| key 类型 | 任意 | 仅对象 |
| 引用方式 | 强引用 | 弱引用 |
| 可迭代 | 是 | 否 |
| size | 有 | 无 |
| 典型场景 | 字典存储 | 关联私有数据 |
### 项目里 WeakMap 用在什么地方?
Vue 3 的响应式系统用 WeakMap 存对象 → 依赖关系,对象被销毁时依赖自动清理,不会内存泄漏。另一个常见场景:给 DOM 节点绑定额外数据,节点移除后数据自动释放。
### Object.create(null) 能替代 Map 吗?
能解决原型链污染问题,但解决不了键类型限制、size 获取、顺序保证、迭代便利性。Map 是更完整的方案。
### Map 的 key 用 NaN 会怎样?
Map 用 SameValueZero 算法比较键,NaN 等于 NaN,所以 NaN 可以正常作为 key,且不会重复。Object 中 NaN 作为 key 会被转成字符串 "NaN",行为一致,但 Map 的语义更明确。前端5月28日 03:29
ES6 类继承中 super 关键字的作用是什么?`super` 在 ES6 类继承中有两种用法:**作为函数调用**和**作为对象引用**。核心要点是——`super()` 调用父类构造函数,`super.method()` 调用父类原型方法,`super.staticMethod()` 在静态方法中调用父类静态方法。
## super() 作为函数调用
在子类 `constructor` 中,`super()` 调用父类构造函数。ES6 的继承机制规定:父类负责创建 `this` 对象,子类负责在此基础上添加属性。因此 `super()` 必须在 `this` 之前调用,否则会抛出 `ReferenceError`。
```javascript
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 必须先调 super,否则下面用 this 会报错
this.age = age;
}
}
```
如果子类没有显式定义 `constructor`,引擎会自动插入一个默认的 `constructor(...args) { super(...args); }`。
## super.method() 作为对象引用
在子类普通方法中,`super` 指向父类的 `prototype`,可以调用父类原型上的方法:
```javascript
class Parent {
greet() {
return 'hello from Parent';
}
}
class Child extends Parent {
greet() {
return super.greet() + ' and Child';
}
}
new Child().greet(); // "hello from Parent and Child"
```
## 静态方法中的 super
在子类静态方法中,`super` 指向父类本身(而非 `prototype`),因此可以调用父类的静态方法:
```javascript
class Parent {
static create() {
return new this();
}
}
class Child extends Parent {
static create() {
return super.create(); // 调用 Parent.create()
}
}
```
## super 的内部指向总结
| 使用场景 | super 指向 |
|---------|-----------|
| `super()` 在 constructor 中 | 父类构造函数 |
| `super.method()` 在普通方法中 | `Parent.prototype` |
| `super.method()` 在静态方法中 | 父类本身(`Parent`) |
| `super.x = value` | 触发父类原型上的 setter(如果有) |
## 追问:子类 constructor 为什么必须先调 super?
ES6 类的继承与 ES5 的寄生组合继承有本质区别。ES5 中是先创建 `this`(子类自己的对象),再用 `Parent.apply(this)` 借用父类构造函数挂属性。ES6 反过来了——由父类构造函数先创建并初始化 `this`,子类再修改。这个顺序由 `new.target` 控制:当 `new Child()` 执行时,`new.target` 是 `Child`,但 `this` 的创建权在 `Parent` 那里。`super()` 执行后 `this` 才可用。
## 追问:super.x = value 有什么陷阱?
给 `super` 的属性赋值时,并不会像直觉那样去修改父类原型上的属性。实际行为是:如果父类原型上定义了该属性的 `setter`,赋值操作会触发那个 `setter`,`this` 指向当前子类实例;如果没有 `setter`,则相当于直接在 `this` 上创建属性:
```javascript
class Parent {
set x(val) {
console.log('setter called with', val);
}
}
class Child extends Parent {
setX() {
super.x = 42; // 触发 Parent.prototype 的 setter
}
}
new Child().setX(); // "setter called with 42"
```
## 追问:ES6 继承与 ES5 原型继承的区别
| | ES5 寄生组合继承 | ES6 class 继承 |
|--|----------------|---------------|
| this 创建 | 子类先创建 this,再借用父类 | 父类构造函数创建 this |
| super | 无,用 Parent.call(this) | super() 必须 |
| 原型链 | 手动 Object.create(Parent.prototype) | extends 自动建立 |
| 静态方法 | 不会继承 | 自动继承 |
| new.target | 不存在 | 控制实例化行为 |
前端5月27日 01:17
Promise 和 async/await 和 Callback 有什么区别?三个阶段的异步方案,层层递进:
**Callback**:把后续操作作为回调函数传给异步操作。问题是回调地狱——多层嵌套横向增长,错误处理每个回调都得单独处理。
**Promise**:把回调包装成对象,链式 `.then()` 解决横向嵌套,`.catch()` 统一处理错误。但长链仍不够直观,且 `.then()` 里不能直接用 `try-catch`。
**async/await**:Promise 的语法糖。`async` 函数返回 Promise,`await` 暂停执行等结果。写法就是同步代码的样子,错误用 `try-catch`。本质还是 Promise——`await` 的值就是 `.then()` 回调的参数。
```javascript
// 三个方案的同一操作
// Callback
getData((err, data) => { if (err) return; process(data); });
// Promise
getData().then(process).catch(handleError);
// async/await
try { const data = await getData(); process(data); } catch { handleError(); }
```
## 追问
### async/await 怎么处理并发请求?
`Promise.all([fetch1, fetch2])` 配合 `await`。不要写成 `await fetch1(); await fetch2()`——这样是串行的,第二个请求等第一个完成才发。
### async 函数返回的 Promise 和普通 Promise 有区别吗?
没有本质区别。async 函数内部抛错等于 reject,return 值等于 resolve。唯一注意的是:async 函数返回的 Promise 是原生 Promise,即使你 return 的是一个 thenable 对象,也会自动包裹成 Promise。
前端5月27日 01:16
module.exports 和 exports 的区别是什么?export 和 export default 的区别是什么?两对概念,一个在 CommonJS,一个在 ESModule。
**module.exports vs exports(CommonJS)**:
- `module.exports` 是真正的导出对象。`exports` 只是 `module.exports` 的引用(`const exports = module.exports`)
- 给 `exports` 赋新值会断开引用,导出失败;`module.exports` 赋新值可以
- 安全做法:只添加属性用 `exports.foo = bar`,需要替换整个导出用 `module.exports = foo`
```javascript
// 正确
module.exports = { a: 1 };
exports.b = 2;
// 错误 — exports 被重新赋值,断开引用
exports = { a: 1 }; // module.exports 还是 {}
```
**export vs export default(ESModule)**:
- `export` 是命名导出,可以有多个。导入时用 `{ name }` 且名字必须匹配
- `export default` 是默认导出,每个模块只有一个。导入时可以取任意名字
- 一个模块可以同时有命名导出和默认导出
```javascript
// 导入区别
import { foo } from './a'; // 命名导出
import foo from './a'; // 默认导出
import foo, { bar } from './a'; // 两者都有
```
## 追问
### 为什么 export default 导入可以随意命名?
因为默认导出本质上导出的是 `{ default: value }` 这个特殊 key。`import x from` 就是取 `default` key 的值。因此也叫 default import。
### 项目中应该优先用命名导出还是默认导出?
命名导出更好——IDE 自动补全、refactor 改名时更安全、Tree-Shaking 友好。默认导出适合"这个模块只有一个主要导出"(如一个组件、一个工具函数)。但争议是社区级的,没有绝对的优劣。
前端5月27日 01:16
var、let、const 之间的区别是什么?三个维度的区别:
**作用域**:var 是函数作用域,let/const 是块级作用域。`{ }` 内部用 let 声明的变量,括号外访问不到。
**变量提升**:var 有提升且初始化为 undefined(声明前访问得到 undefined)。let/const 也有提升但存在暂时性死区(TDZ)——声明前访问直接 ReferenceError。
**重复声明**:var 可重复声明(后覆盖前),let/const 在同一作用域不能重复声明。
**const 额外特性**:声明时必须初始化,且不能重新赋值。但对象和数组的属性可以修改(const 锁的是绑定,不是值)。
```javascript
// var:函数作用域,讨厌的经典 bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // 3 3 3
}
// let:块级作用域,每次迭代创建新绑定
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // 0 1 2
}
```
## 追问
### 为什么 let 能解决 for 循环的回调/闭包问题?
var 是整个 for 循环共享一个变量,循环结束后 i 是最终值。let 每次循环迭代都会创建一个新的绑定,每个 setTimeout 捕获的 i 是不同的绑定。即使循环结束后,这些绑定的值仍然保留着当时的 i。
### const 声明的对象属性为什么可以修改?
const 锁定的是变量名到值的绑定关系——"这个变量名不能指向别的值"。对象属性是变量指向的内存地址内部的变更,不改变绑定关系。
前端5月27日 01:16
WeakSet、WeakMap 和 Set、Map 之间的区别是什么?核心区别就一条:**Weak 版本的 key(或元素)是弱引用,不阻止垃圾回收。**
**Set vs WeakSet**:
- Set 元素可以是任何类型;WeakSet 元素只能是对象
- Set 可迭代(`forEach`、`size`、`keys`);WeakSet 不可迭代
- Set 中对象被引用着,即使对象其他地方不再使用也不会被 GC;WeakSet 中对象没有其他引用时会被回收
**Map vs WeakMap**:
- Map 的 key 可以是任何类型;WeakMap 的 key 只能是对象
- Map 可迭代;WeakMap 不可迭代
- WeakMap 条目会随 key 对象被 GC 而自动清除
WeakMap 典型场景:Vue 3 的响应式依赖追踪、存储 DOM 节点的关联数据、为第三方对象附加元数据而不造成内存泄漏。WeakSet 用得少——需要标记"这个对象我见过"但不想阻止它被 GC 时用。
## 追问
### 为什么 WeakMap 没有 size 属性?
因为 WeakMap 中条目可能随时被 GC 回收,size 值是瞬时的、不可靠的。如果 JS 引擎提供了 size,开发者的代码里依赖了这个值,但下一秒 GC 跑了一次值变了——这种不可预测性比没有 size 更糟糕。
### WeakMap 和 Map 在内存管理上有什么区别?
Map 的 key 被引用着,即使这个 key 对象在别处都已不使用,Map 里的引用也会阻止 GC——内存泄漏风险。WeakMap 的 key 是弱引用,如果 key 对象没有其他强引用了,GC 可以回收,对应的 WeakMap 条目自动消失。
前端5月27日 01:15
ES6 中有哪些解决异步的方法?ES6 之后异步方案演进:
**回调函数**:最原始的方式,问题是回调地狱(callback hell)——多层嵌套,错误处理困难。
**Promise**:ES6 引入。把回调的嵌套转成 `.then()` 的链式调用,用 `.catch()` 统一处理错误。解决了回调地狱,但长链 `.then()` 仍然不够直观。
**Generator + co**:通过 `yield` 暂停函数执行,配合自动执行器(如 co 库)实现类似同步的写法。现在基本被 async/await 取代。
**async/await**:ES8(ES2017)正式引入。Promise 的语法糖——async 函数返回 Promise,await 暂停执行等待 Promise 完成。写法最像同步代码:
```javascript
async function getData() {
const res = await fetch('/api');
const data = await res.json();
return data;
}
```
## 追问
### Promise.all、Promise.allSettled、Promise.race、Promise.any 的区别?
- `all`:全成功才成功,一个失败就失败
- `allSettled`:等全部完成(不管成败),返回结果数组含状态标记
- `race`:第一个完成的就返回(不管成败)
- `any`:第一个成功的就成功,全失败才失败(和 race 相反)
### async/await 的错误怎么处理?
`try-catch` 包裹 await。或者用 `.catch()` 链在 `async` 函数的返回值上。也可以用 `await promise.catch(() => fallbackValue)` 的模式给错误设默认值。
前端2月7日 16:44
说一下 splice 和 slice 的功能用法`splice()` 和 `slice()` 都是 JavaScript 中用来处理数组的方法,但它们的功能和用法有所不同。
### splice()
`splice()` 方法通过删除或替换现有元素或在数组中添加新元素来改变数组的内容。其基本语法如下:
```javascript
array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
```
- **start**: 指定修改的开始位置(数组索引)。
- **deleteCount**: (可选)整数,表示要从数组中删除的元素数量。
- **item1, item2, ...**: (可选)要添加进数组的新元素。
**示例**:
```javascript
let myArray = ['a', 'b', 'c', 'd'];
myArray.splice(1, 2, 'x', 'y'); // 从索引1开始删除2个元素,并添加'x'和'y'
console.log(myArray); // 输出: ['a', 'x', 'y', 'd']
```
### slice()
`slice()` 方法则返回一个新的数组,包含从开始到结束(不包括结束)选择的数组的一部分。原始数组不会被修改。其基本语法如下:
```javascript
array.slice(begin[, end])
```
- **begin**: 提取起始处的索引(从该索引开始提取元素)。
- **end**: (可选)提取结束处的索引(到该索引之前的元素会被提取)。
**示例**:
```javascript
let myArray = ['a', 'b', 'c', 'd'];
let newArray = myArray.slice(1, 3); // 提取从索引1到索引2的元素
console.log(newArray); // 输出: ['b', 'c']
console.log(myArray); // 原数组不变,输出: ['a', 'b', 'c', 'd']
```
总结来说,`splice()` 是一个可以在任何位置添加或删除元素的方法,这会改变原数组,而 `slice()` 用于创建一个新的数组,包含原数组的一部分,原数组不会改变。前端2月7日 13:47
JS 数组有哪些方法? 讲讲它们的区别跟使用场景JavaScript数组作为核心数据结构,掌握其方法能显著提升代码效率和可维护性。本文将系统分析常用数组方法,深入探讨它们的区别、适用场景及最佳实践,帮助开发者写出更简洁、高性能的代码。所有方法基于ECMAScript标准,重点聚焦于函数式方法,避免常见陷阱。对于更详细的信息,可参考[MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array)。
## 常见数组方法分类
数组方法可大致分为以下几类,每类服务于特定需求:
* **迭代方法**:用于遍历和转换数组,如 `map`、`filter`、`reduce`、`forEach`,适合声明式编程。
* **变更方法**:直接修改原数组,如 `push`、`pop`、`shift`、`unshift`、`splice`、`sort`,适用于栈操作或原地修改。
* **生成方法**:创建新数组或字符串,如 `slice`、`concat`、`join`,常用于数据处理。
* **其他方法**:如 `fill`、`from`、`includes`、`indexOf`,提供额外功能。
> **关键提示**:函数式方法(如 `map`、`filter`)返回新数组,**不修改原数组**,而变更方法(如 `push`)直接操作原数组。选择时需权衡性能和可读性。
### 迭代方法详解
迭代方法是数组处理的核心,强调**纯函数**特性,避免副作用。
#### map
**作用**:创建新数组,其中每个元素是调用回调函数的结果。不修改原数组。
**参数**:回调函数(`item`, `index`, `array`)
**使用场景**:数据转换,如数字列表转字符串、计算倍数。**避免**在回调中修改原数组,保持函数式纯度。
**代码示例**:
```javascript
const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6]
// 原数组未被修改
console.log(numbers); // [1, 2, 3]
```
#### filter
**作用**:创建新数组,包含通过测试的元素。不修改原数组。
**参数**:回调函数(`item`, `index`, `array`)
**使用场景**:数据过滤,如筛选偶数、有效对象。**与map对比**:`map`转换所有元素,`filter`仅保留满足条件的元素。
**代码示例**:
```javascript
const numbers = [1, 2, 3, 4];
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4]
// 原数组未被修改
console.log(numbers); // [1, 2, 3, 4]
```
#### reduce
**作用**:将数组元素归约成单个值(如总和、最大值)。不修改原数组。
**参数**:回调函数(`accumulator`, `currentValue`, `index`, `array`),**初始值**可指定(如 `0`)。
**使用场景**:聚合计算、链式操作。**性能提示**:对大型数组,避免嵌套循环,`reduce` 更高效。
**代码示例**:
```javascript
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 10
const max = numbers.reduce((acc, num) => Math.max(acc, num), -Infinity);
console.log(max); // 4
```
#### forEach
**作用**:对数组每个元素执行回调,但**不返回新数组**。不修改原数组。
**参数**:回调函数(`item`, `index`, `array`)
**使用场景**:遍历操作,如DOM修改。**避免使用**:因无返回值,**不适合链式操作**,仅用于副作用。
**代码示例**:
```javascript
const items = ['a', 'b', 'c'];
items.forEach(item => {
console.log(`Item: ${item}`);
});
// 输出: Item: a
// Item: b
// Item: c
// 原数组未被修改
console.log(items); // ['a', 'b', 'c']
```
### 变更方法详解
变更方法**直接修改原数组**,适用于原地操作,但可能破坏函数式纯度。
#### push/pop
**作用**:`push`添加元素到末尾,`pop`移除末尾元素(栈操作)。
**参数**:`push`接收多个值;`pop`无参数。
**使用场景**:栈实现、队列操作。**性能提示**:对于频繁操作,避免在循环中使用,考虑`slice`等替代方案。
**代码示例**:
```javascript
const stack = [];
stack.push('item1', 'item2');
console.log(stack); // ['item1', 'item2']
const last = stack.pop();
console.log(last); // 'item2'
console.log(stack); // ['item1']
```
#### splice
**作用**:插入、删除或替换数组元素,返回被移除的元素。
**参数**:`start`索引,`deleteCount`,`items`(可选)。
**使用场景**:动态数组修改。**注意事项**:修改原数组,可能导致意外副作用。
**代码示例**:
```javascript
const arr = [1, 2, 3, 4];
const removed = arr.splice(1, 2, 'a', 'b');
console.log(removed); // [2, 3]
console.log(arr); // [1, 'a', 'b', 4]
```
#### sort
**作用**:对数组元素排序,**默认按字符串规则**(需显式指定比较函数)。
**参数**:可选比较函数(`a, b`)。
**使用场景**:数据排序。**性能提示**:对大型数组,使用`Array.prototype.sort`可能慢,优先使用`Array.from`和稳定排序。
**代码示例**:
```javascript
const nums = [3, 1, 4, 2];
nums.sort((a, b) => a - b);
console.log(nums); // [1, 2, 3, 4]
// 对字符串排序
const names = ['Alice', 'Bob', 'Charlie'];
console.log(names.sort()); // ['Alice', 'Bob', 'Charlie']
```
### 生成方法详解
生成方法**返回新数组或字符串**,不修改原数组,适合数据处理。
#### slice
**作用**:返回新数组,包含从`start`到`end`(不含)的元素。
**参数**:`start`(索引,负值表示倒数),`end`(可选,索引)。
**使用场景**:复制数组片段、避免原地修改。**关键区别**:`slice` vs `splice`——`slice`不修改原数组,`splice`会修改。
**代码示例**:
```javascript
const arr = [1, 2, 3, 4];
const sub = arr.slice(1, 3);
console.log(sub); // [2, 3]
console.log(arr); // [1, 2, 3, 4] // 原数组未被修改
```
#### concat
**作用**:连接多个数组或值,返回新数组。
**参数**:一个或多个数组/值。
**使用场景**:合并数组、拼接数据。**性能提示**:对大型数组,避免嵌套`concat`,使用`[...arr1, ...arr2]`更高效。
**代码示例**:
```javascript
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = arr1.concat(arr2);
console.log(merged); // [1, 2, 3, 4]
```
#### join
**作用**:将数组元素连接成字符串,用指定分隔符。
**参数**:分隔符(默认`','`)。
**使用场景**:生成字符串、日志输出。**注意事项**:对大型数组,可能产生内存问题,避免过度使用。
**代码示例**:
```javascript
const fruits = ['apple', 'banana', 'cherry'];
const str = fruits.join(', ');
console.log(str); // 'apple, banana, cherry'
```
## 方法选择指南
掌握方法区别后,需根据场景选择最优方案:
* **map vs filter**:`map`用于转换所有元素(如`[1,2,3]` → `[2,4,6]`),`filter`用于过滤(如`[1,2,3,4]` → `[2,4]`)。**选择建议**:数据转换用`map`,数据筛选用`filter`。
* **避免副作用**:`forEach`适合遍历副作用(如DOM操作),但不适合链式操作;`map`和`filter`返回新数组,适合纯函数式代码。
* **性能优化**:
* 对大型数组,优先使用`slice`(不修改原数组)而非`splice`(修改原数组)。
* 计算聚合时,`reduce` 比 `for` 循环更高效且可读。
* 避免在循环中使用`push`,改用`array.map().push()` 或 `array.concat()`。
* **安全实践**:
* 始终优先使用函数式方法(`map`、`filter`),避免`for`循环,提升代码可测试性。
* 对原地操作(如`splice`),确保数据副本,防止意外副作用。
> **实践建议**:在开发中,使用`console.log`验证数组行为,例如:
>
>
## 结论
JavaScript数组方法是前端开发的核心工具。本文系统分析了关键方法的区别与使用场景,强调函数式方法的优势(如`map`、`filter`)和变更方法的适用性。**最佳实践**:优先使用声明式方法,避免副作用;性能敏感场景,选择高效操作;持续学习新特性(如`Array.from`和`Array.of`)。掌握这些方法,能显著提升代码质量,使开发更高效、可维护。记住:**数组方法的正确选择是性能优化和代码健壮性的关键**。

> **延伸阅读**:在现代JavaScript中,数组方法与迭代器结合,可实现更高级的流式处理。例如,使用`Array.from`转换可迭代对象:
>
>
前端2月7日 12:48
为什么javascript ES6 Promises在resolve后继续执行?JavaScript ES6 Promises 在 `resolve` 后继续执行的原因在于它们的设计理念。Promise 旨在处理异步操作,它允许代码在等待异步操作完成的同时继续执行其他任务。当一个 Promise 被 `resolve` 时,这仅表示关联的异步操作完成并成功了。然而,这并不意味着程序的其他部分或其他异步操作会停止执行。
此外,JavaScript 运行时使用的是事件循环机制,即使在 Promise 被 `resolve` 之后,事件循环仍会继续处理其他待处理的事件或任务。因此,即便一个特定的 Promise 已经解决,JavaScript 的执行环境仍会继续运行,处理其他的代码或者事件,直到所有任务都被适当地处理完毕。这种设计确保了高效的资源利用和良好的用户体验,因为它允许多个操作几乎同时进行,而不是顺序执行。前端2024年6月24日 16:43
let 块作用域是怎么实现的?`let` 关键字在JavaScript中被引入是为了提供块作用域(block scope)的功能。块作用域意味着由 `let` 声明的变量仅在声明它们的代码块内部是可见的。代码块是被花括号 `{}` 包围的一段代码,例如在 `if` 语句、`for` 和 `while` 循环以及函数定义中都会用到代码块。
在ES6之前,JavaScript主要依赖的是函数作用域(function scope),由 `var` 关键字声明的变量要么是全局的,要么是在函数内部局部的。这种设计有时会导致意料之外的问题,特别是在循环中。
下面是一个使用 `let` 的例子来说明块作用域是如何工作的:
```javascript
function runLoop() {
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100 * i);
}
}
runLoop();
```
在这个例子中,变量 `i` 是用 `let` 在 `for` 循环的块中声明的。这意味着每次循环迭代时,变量 `i` 都是一个新的变量,并且它被限制在这个循环的块作用域中。所以当 `setTimeout` 的回调函数执行时,它能够访问到循环迭代时对应的 `i` 的值。
如果我们用 `var` 替换掉 `let`,结果将会不同:
```javascript
function runLoop() {
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100 * i);
}
}
runLoop();
```
在这个例子中,由于 `var` 声明的变量 `i` 是函数作用域的,当 `setTimeout` 的回调函数执行时,它会打印出循环结束后变量 `i` 的最终值,即5,会打印五次5,而不是0到4。
总结来说,`let` 关键字允许开发者在更细粒度的级别控制变量的作用域。这样做提高了代码的可读性和可维护性,并且减少了由于作用域导致的常见错误。前端2024年6月24日 16:43
讲一下 import 的原理,与 require 有什么不同?### `import` 的原理
在JavaScript中,`import`语句用于从模块中导入绑定(即函数、对象、原始类型等)。这是ES6规范(即ECMAScript 2015)引入的模块化特性的一部分。`import`的工作原理基于ECMAScript模块(ESM)系统。
当你使用 `import`语句时,JavaScript引擎执行以下步骤:
1. **解析模块标识符**:确定要导入的模块的位置及其文件路径。
2. **模块加载**:如果模块尚未加载,JavaScript引擎会加载模块文件。
3. **编译模块**:引擎会对模块代码进行编译,检查语法并进行优化。
4. **执行模块代码**:在私有的模块作用域内执行模块代码,以初始化导出的绑定。
5. **缓存模块**:模块的导出会被缓存,这意味着每个模块只会被执行一次,之后的导入会重用同一份导出的实例,保持状态的一致性。
### `import` 与 `require` 的不同
`import`和 `require`都是JavaScript中用于加载模块的语句,但它们之间存在几个关键差异:
1. **语法规范**:`import`是ES6中引入的模块化语法,而 `require`则来自于CommonJS规范,后者主要用于Node.js环境中。
2. **模块类型**:`import`用于加载ESM模块,而 `require`用于加载CommonJS模块。
3. **加载方式**:
- `import`声明是静态的,意味着它必须位于模块的顶部,不能动态运行或按条件导入模块。
- `require`是动态的,可以在代码的任何地方调用,支持条件加载和运行时动态计算路径。
4. **异步与同步**:
- `import`可以支持异步模块的导入,通过 `import()`函数进行动态导入,返回一个Promise对象。
- `require`的加载是同步的,当调用 `require`时,代码会停止执行,直到模块被加载和返回。
5. **性能优化**:由于 `import`是静态的,它允许JavaScript引擎进行更强大的性能优化,比如死代码消除和模块的静态分析。
6. **导出绑定的可变性**:
- 使用 `import`导入的绑定是活动的,也就是说如果导出的模块变量值发生变化,导入的绑定也会更新。
- 使用 `require`导入的值是导出值的拷贝,一旦导入,无论源模块如何变化,导入的值都不会改变。
### 例子
**使用 `import`:**
```javascript
// ES6模块导入语法
import { myFunction, myVariable } from './myModule.js';
// 使用导入的函数和变量
myFunction();
console.log(myVariable);
```
**使用 `require`:**
```javascript
// CommonJS模块导入语法
const myModule = require('./myModule.js');
// 使用模块的属性和方法
myModule.myFunction();
console.log(myModule.myVariable);
```
在处理前端项目时,我们可能更倾向于使用 `import`,因为它与现代JavaScript模块化标准一致,而在Node.js环境中,尽管现在已经支持ESM,`require`依然被广泛使用,特别是在老项目中。
前端2024年6月24日 16:43
Promise 是如何实现链式调用的?Promise 实现链式调用主要依赖于其返回一个新的 Promise 对象的特性。
在 JavaScript 中,Promise 是一个处理异步操作的对象,可以在原调用位置以同步方式处理异步操作结果。
下面是 Promise 的链式调用的基本实现:
1. Promise 构造函数接收一个执行函数,执行函数接收两个参数:resolve 和 reject,分别用于异步操作成功与失败的情况。
2. 调用 Promise 对象的 `.then` 方法提供链式调用。`.then` 方法接收两个参数(都是可选的):`onFulfilled` 和 `onRejected`,分别在 Promise 成功或失败时调用。`.then` 方法也返回一个 Promise 对象,以便进行链式调用。
3. 如果 `onFulfilled` 或 `onRejected` 返回一个值 x,运行 Promise 解决过程:[[Promise Resolution Procedure]](https://promisesaplus.com/#the-promise-resolution-procedure)。
4. 如果 `onFulfilled` 或 `onRejected` 抛出一个异常 e,`Promise.then` 的返回的 Promise 对象会被 reject 掉。
5. 如果 `onFulfilled` 不是函数且 promise1(前一个 promise) 成功执行,promise2(下一个 promise)成功处理 promise1 的 final state。
6. 如果 `onRejected` 不是函数且 promise1 失败,promise2 会拒绝 promise1 的原因。
以下是一个示例:
```javascript
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // 第一步:创建一个 Promise 并执行一个异步操作
}).then(function(result) { // 第二步:注册一个 onFulfilled 回调
console.log(result); // 打印:1
return result + 2;
}).then(function(result) { // 第三步:链式调用
console.log(result); // 打印:3
return result + 2;
}).then(function(result) {
console.log(result); // 打印:5
return result + 2;
});
```
在这个例子中,每个 `.then` 调用后都返回一个新的 Promise 对象,这个新的 Promise 对象会立即执行,并在执行完毕后调用下一个 `.then` 注册的回调。通过这种方式,我们可以以同步的方式处理异步的结果,而这就是 Promise 链式调用的本质。
前端2024年6月24日 16:43
如何基于 Promise.all 实现Ajax请求的串行和并行?### Ajax请求的串行实现
对于串行执行多个Ajax请求,我们通常需要确保一个请求完全完成后,再执行下一个请求。这可以通过链式调用`then`方法来实现,也就是在每个Promise对象的`then`方法中启动下一个Ajax请求。
```javascript
function ajaxRequest(url) {
return new Promise((resolve, reject) => {
// 这里是Ajax请求的代码,成功时调用resolve,失败时调用reject
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
});
}
const urls = ['/url1', '/url2', '/url3']; // 假设我们有多个请求需要串行处理
let promiseChain = Promise.resolve(); // 初始化一个已完成的Promise
urls.forEach(url => {
promiseChain = promiseChain.then(() => ajaxRequest(url)).then(response => {
console.log('请求完成:', response);
// 这里可以处理每个请求的响应
});
});
// 最后可以在所有请求都完成后执行一些操作
promiseChain.then(() => {
console.log('所有请求都已串行完成。');
});
```
在这个例子中,每个请求仅在前一个请求的`then`方法中被调用,这确保了请求的串行执行。
### Ajax请求的并行实现
要并行执行多个Ajax请求,可以使用`Promise.all`方法。`Promise.all`接收一个Promise对象数组,等待所有的Promise对象都成功完成后,它将返回一个新的Promise,这个新Promise将解析为一个结果数组,数组中的每个结果对应于原Promise数组中的每个请求。
```javascript
function ajaxRequest(url) {
return new Promise((resolve, reject) => {
// 这里是Ajax请求的代码,成功时调用resolve,失败时调用reject
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
});
}
const urls = ['/url1', '/url2', '/url3']; // 假设我们有多个请求需要并行处理
const promises = urls.map(ajaxRequest); // 创建一个包含所有请求的Promise数组
Promise.all(promises).then(responses => {
console.log('所有请求都已并行完成。');
responses.forEach(response => {
console.log('请求完成:', response);
// 这里可以处理每个请求的响应
});
}).catch(error => {
// 如果任何一个请求失败,这里会捕获到错误
console.error('请求失败:', error);
});
```
在这个例子中,`Promise.all`并行地处理所有的Ajax请求,并在所有请求成功完成后,按照请求的顺序输出响应结果。如果任何一个请求失败,`Promise.all`会立即拒绝,并返回第一个遇到的错误。
这两种方法是处理多个Ajax请求时常用的串行和并行模式。根据实际需求选择合适的方式。在实际面试中,可以根据面试官的要求提供更详细的代码实例或解释。前端2024年6月24日 16:43
ES6是如何实现迭代器的?ES6通过提供一个新的协议,即*迭代器协议*来实现迭代器。迭代器协议定义了一种统一的方式,使得任何对象只要遵循这个协议,都可以被迭代。迭代器协议要求实现两个方法:`next` 和 `Symbol.iterator`。
以下是实现迭代器协议的两个主要方面:
1. **迭代器协议**:该协议要求任何对象的 `next()` 方法都返回一个对象,该对象包含两个属性:`value` 和 `done`。其中,`value` 属性表示下一个迭代的值,`done` 是一个布尔值,如果迭代已经完成,则值为 `true`;如果迭代尚未完成,则值为 `false`。
例如,实现一个简单的迭代器可以如下所示:
```javascript
function createCounter(start, end) {
let current = start;
// 这里返回的对象符合迭代器协议
return {
next() {
if (current <= end) {
return { value: current++, done: false };
} else {
return { done: true };
}
}
};
}
const counter = createCounter(1, 3);
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { done: true }
```
2. **可迭代协议**:该协议要求对象具有一个 `Symbol.iterator` 方法。这个方法必须返回一个符合迭代器协议的对象。这意味着这个方法返回一个迭代器,可用于获取对象的连续值。
当使用像 `for...of` 这样的循环语句时,会自动寻找对象的 `Symbol.iterator` 方法来获取迭代器,然后通过这个迭代器进行迭代。
下面是一个实现可迭代协议的例子:
```javascript
class RangeIterator {
constructor(start, end) {
this.current = start;
this.end = end;
}
[Symbol.iterator]() {
return this;
}
next() {
if (this.current <= this.end) {
return { value: this.current++, done: false };
} else {
return { done: true };
}
}
}
for (const num of new RangeIterator(1, 3)) {
console.log(num); // 依次打印出 1, 2, 3
}
```
在上述的 `RangeIterator` 类中,我们实现了 `Symbol.iterator` 方法并且让它返回 `this`,即它自身是一个迭代器。此外,我们也实现了 `next()` 方法来满足迭代器协议。
通过这样的机制,ES6 不仅让内置对象如数组和字符串成为可迭代对象,也允许开发者自定义迭代行为,这在处理自定义数据结构时非常有用。