2025年01月03日美蜥(杭州普瑞兼职)一面
目录
- vue2 vue3 的区别
- react 性能优化
- react 组件传值
- v-for 和 v-if 的优先级
- react 中多个接口请求的数据,需要渲染到一个列表上怎么处理
- 百万条数据怎么渲染
- vue2、vue3 的响应式原理
- 微前端了解吗
- git 版本控制
- git mearge 和 git rebase 的区别
- 垂直水平居中
- react 中实现 KeepAlive
- 哈希路由和浏览器路由的区别
- 数组的常用方法
- 如何判断一个对象是空
1. vue2 vue3 的区别
Vue 2 和 Vue 3 在多个方面存在区别,以下从架构设计、语法与 API、性能、生态系统等方面进行详细介绍:
架构设计
- 响应式系统
- Vue 2:基于
Object.defineProperty()
实现响应式。这种方式有一定局限性,例如无法检测对象属性的添加和删除,对于数组,部分方法(如通过索引修改元素)也不能触发响应式更新。 - Vue 3:采用 Proxy 对象实现响应式系统。Proxy 可以劫持整个对象,并能拦截更多操作,解决了 Vue 2 中响应式的一些限制,能更好地检测对象属性的变化,包括属性的添加、删除以及数组元素的修改等。
- Vue 2:基于
- 代码组织
- Vue 2:主要使用选项式 API(Options API),将不同的逻辑(如数据、方法、生命周期钩子等)分散在不同的选项中,在处理复杂组件时,可能会导致代码碎片化,逻辑分散难以维护。
- Vue 3:引入了组合式 API(Composition API),允许开发者根据逻辑关注点来组织代码,将相关的逻辑封装在一起,提高了代码的复用性和可维护性,尤其适合大型项目。
语法与 API
- 组件定义
- Vue 2:使用
Vue.extend()
或单文件组件(SFC)来定义组件,通过export default
导出一个包含各种选项的对象。 - Vue 3:仍然支持单文件组件,但在组合式 API 中,可以使用
<script setup>
语法糖来简化组件的定义,减少样板代码。
- Vue 2:使用
<!-- Vue 2 组件定义 -->
<template><div>{{ message }}</div>
</template><script>
export default {data() {return {message: 'Hello, Vue 2!'};}
};
</script><!-- Vue 3 组件定义(<script setup>) -->
<template><div>{{ message }}</div>
</template><script setup>
import { ref } from 'vue';
const message = ref('Hello, Vue 3!');
</script>
- 生命周期钩子
- Vue 2:有
beforeCreate
、created
、beforeMount
、mounted
、beforeUpdate
、updated
、beforeDestroy
、destroyed
等生命周期钩子。 - Vue 3:部分钩子名称发生了变化,
beforeDestroy
改为beforeUnmount
,destroyed
改为unmounted
,并且在组合式 API 中可以使用onBeforeMount
、onMounted
等函数来注册生命周期钩子。
- Vue 2:有
// Vue 2 生命周期钩子
export default {created() {console.log('Vue 2: Component created');}
};// Vue 3 组合式 API 生命周期钩子
import { onMounted } from 'vue';export default {setup() {onMounted(() => {console.log('Vue 3: Component mounted');});}
};
- 响应式数据定义
- Vue 2:在
data
选项中定义响应式数据,使用this
来访问。 - Vue 3:使用
ref()
和reactive()
函数来创建响应式数据。ref()
用于创建单个值的响应式数据,reactive()
用于创建对象的响应式数据。
- Vue 2:在
// Vue 2 响应式数据定义
export default {data() {return {count: 0};},methods: {increment() {this.count++;}}
};// Vue 3 响应式数据定义
import { ref } from 'vue';export default {setup() {const count = ref(0);const increment = () => {count.value++;};return {count,increment};}
};
性能
- 渲染性能
- Vue 2:渲染器在更新 DOM 时,使用虚拟 DOM 进行比较和更新,在处理大型组件树时,可能会有一定的性能开销。
- Vue 3:重写了渲染器,采用了静态提升、PatchFlag 等优化技术,减少了虚拟 DOM 的比较范围,提高了渲染性能,尤其是在处理大型组件和频繁更新的场景下表现更优。
- 内存占用
- Vue 2:由于响应式系统的实现方式,在创建大量响应式对象时,可能会占用较多的内存。
- Vue 3:Proxy 实现的响应式系统在内存使用上更加高效,减少了不必要的内存开销。
生态系统
- 插件兼容性
- Vue 2:拥有丰富的插件生态系统,但部分插件可能需要进行适配才能在 Vue 3 中使用。
- Vue 3:随着时间的推移,越来越多的插件开始支持 Vue 3,但在过渡期间,可能会面临一些插件兼容性问题。
- 工具链支持
- Vue 2:与之配套的工具链(如 Vue CLI)已经非常成熟。
- Vue 3:官方推出了 Vite 作为构建工具,它具有更快的冷启动和热更新速度,更适合现代前端开发。
2. react 性能优化
在 React 里,性能优化是一个关键环节,下面为你介绍几种常见的优化方法:
1. 使用 React.memo
进行组件记忆
React.memo
是一个高阶组件,它能够对组件的 props 进行浅比较。若 props 没有变化,就不会重新渲染组件。
import React from 'react';const MyComponent = React.memo((props) => {return <div>{props.message}</div>;
});export default MyComponent;
2. 使用 useMemo
缓存计算结果
useMemo
能够缓存计算结果,避免在每次渲染时都进行重复计算。
import React, { useMemo } from 'react';const MyComponent = (props) => {const expensiveValue = useMemo(() => {// 进行复杂计算return props.num1 + props.num2;}, [props.num1, props.num2]);return <div>{expensiveValue}</div>;
};export default MyComponent;
3. 使用 useCallback
缓存函数
useCallback
可缓存函数,防止在每次渲染时都创建新的函数实例。
import React, { useCallback } from 'react';const MyComponent = (props) => {const handleClick = useCallback(() => {console.log('Button clicked');}, []);return <button onClick={handleClick}>Click me</button>;
};export default MyComponent;
4. 代码分割
使用 React.lazy 和 Suspense 进行代码分割,这样可以按需加载组件,从而减少初始加载时间。
import React, { lazy, Suspense } from 'react';const LazyComponent = lazy(() => import('./LazyComponent'));const App = () => {return (<div><Suspense fallback={<div>Loading...</div>}><LazyComponent /></Suspense></div>);
};export default App;
5. 避免在渲染过程中触发不必要的状态更新
确保状态更新仅在必要时进行,防止组件不必要的重新渲染。
import React, { useState } from 'react';const MyComponent = () => {const [count, setCount] = useState(0);const handleClick = () => {// 仅在满足条件时更新状态if (count < 10) {setCount(count + 1);}};return (<div><p>Count: {count}</p><button onClick={handleClick}>Increment</button></div>);
};export default MyComponent;
这些方法能够有效地优化 React 应用的性能,不过在实际应用中,你需要依据具体情况选择合适的优化策略。
3. react 组件传值
在 React 里,组件传值是一项基础且重要的操作。下面为你详细介绍几种常见的组件传值方式:
1. 父组件向子组件传值
父组件可通过 props
向子组件传递数据。
// 子组件
const ChildComponent = (props) => {return <div>{props.message}</div>;
};// 父组件
const ParentComponent = () => {const message = "Hello from parent";return <ChildComponent message={message} />;
};
2. 子组件向父组件传值
子组件向父组件传值通常借助回调函数来实现。父组件把一个函数作为 props
传递给子组件,子组件调用该函数并传递数据。
// 子组件
const ChildComponent = (props) => {const handleClick = () => {props.onClick("Hello from child");};return <button onClick={handleClick}>Send message to parent</button>;
};// 父组件
const ParentComponent = () => {const handleChildMessage = (message) => {console.log(message);};return <ChildComponent onClick={handleChildMessage} />;
};
3. 跨级组件传值(Context API)
当需要在多个层级的组件间传递数据时,可使用 React 的 Context API
。
import React, { createContext, useContext, useState } from 'react';// 创建一个 Context
const MyContext = createContext();// 子组件
const GrandChildComponent = () => {const contextValue = useContext(MyContext);return <div>{contextValue}</div>;
};const ChildComponent = () => {return <GrandChildComponent />;
};// 父组件
const ParentComponent = () => {const [message, setMessage] = useState("Hello from context");return (<MyContext.Provider value={message}><ChildComponent /></MyContext.Provider>);
};
4. 使用第三方库(如 Redux 或 MobX)
对于复杂的应用场景,可使用第三方状态管理库来实现组件间的数据共享。以 Redux 为例:
// 安装依赖
// npm install redux react-reduximport React from 'react';
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';// 定义 reducer
const counterReducer = (state = { count: 0 }, action) => {switch (action.type) {case 'INCREMENT':return { count: state.count + 1 };default:return state;}
};// 创建 store
const store = createStore(counterReducer);// 子组件
const CounterComponent = () => {const count = useSelector((state) => state.count);const dispatch = useDispatch();return (<div><p>Count: {count}</p><button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button></div>);
};// 父组件
const App = () => {return (<Provider store={store}><CounterComponent /></Provider>);
};
上述是 React 中常见的组件传值方式,你可根据具体的应用场景来选择合适的传值方法。
4. v-for 和 v-if 的优先级
Vue 2 中的优先级
在 Vue 2 中,v-for
的优先级高于 v-if
。这意味着:
- 当两者用在同一个元素上时,
v-for
会先执行,然后v-if
会在每次迭代中运行 - 这种顺序会导致性能问题,因为即使你只想渲染部分项,Vue 也会先遍历整个列表
<!-- Vue 2 示例 -->
<div v-for="item in items" v-if="item.isActive">{{ item.name }}
</div>
上面的代码在 Vue 2 中相当于:
this.items.map(function(item) {if (item.isActive) {return item.name}
})
Vue 3 中的优先级
在 Vue 3 中,v-if
的优先级高于 v-for
。这意味着:
- 当两者用在同一个元素上时,
v-if
会先执行 - 如果
v-if
的条件不成立,v-for
就不会执行 - 这种改变更符合开发者的直觉,但同时也意味着在同一个元素上使用两者时会导致错误
<!-- Vue 3 示例 -->
<div v-for="item in items" v-if="item.isActive">{{ item.name }}
</div>
上面的代码在 Vue 3 中会抛出错误,因为 v-if
先执行时会尝试访问 item
,但此时 item
还未通过 v-for
定义。
最佳实践
对于 Vue 2 和 Vue 3,官方都建议避免在同一个元素上同时使用 v-for
和 v-if
。替代方案:
- 使用计算属性过滤列表(推荐)
<div v-for="item in activeItems">{{ item.name }}
</div>
computed: {activeItems() {return this.items.filter(item => item.isActive)}
}
- 将
v-if
移到外层元素或<template>
标签
<template v-for="item in items"><div v-if="item.isActive">{{ item.name }}</div>
</template>
- 对于 Vue 3,如果需要条件渲染整个列表,可以在外层使用
v-if
<div v-if="items.length > 0"><div v-for="item in items" :key="item.id">{{ item.name }}</div>
</div>
总结
- Vue 2:
v-for
>v-if
(v-for
优先级更高) - Vue 3:
v-if
>v-for
(v-if
优先级更高) - 最佳实践是避免两者同时用于同一元素,使用计算属性或嵌套结构替代
5. react 中多个接口请求的数据,需要渲染到一个列表上怎么处理
在 React 中处理多个接口请求并将数据合并渲染到一个列表,有几种常见的处理方法:
1. 使用 Promise.all 并行请求
import React, { useState, useEffect } from 'react';function CombinedList() {const [combinedData, setCombinedData] = useState([]);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {const fetchData = async () => {try {setLoading(true);// 并行发起多个请求const [response1, response2] = await Promise.all([fetch('https://api.example.com/data1'),fetch('https://api.example.com/data2')]);const data1 = await response1.json();const data2 = await response2.json();// 合并数据const mergedData = [...data1, ...data2];setCombinedData(mergedData);} catch (err) {setError(err.message);} finally {setLoading(false);}};fetchData();}, []);if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error}</div>;return (<ul>{combinedData.map((item, index) => (<li key={index}>{item.name}</li>))}</ul>);
}
2. 顺序请求并合并
useEffect(() => {const fetchSequentially = async () => {try {setLoading(true);const response1 = await fetch('https://api.example.com/data1');const data1 = await response1.json();const response2 = await fetch('https://api.example.com/data2');const data2 = await response2.json();setCombinedData([...data1, ...data2]);} catch (err) {setError(err.message);} finally {setLoading(false);}};fetchSequentially();
}, []);
3. 使用自定义 Hook 封装
function useCombinedData() {const [data, setData] = useState([]);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {const fetchData = async () => {try {const urls = ['https://api.example.com/data1','https://api.example.com/data2'];const responses = await Promise.all(urls.map(url => fetch(url)));const jsonData = await Promise.all(responses.map(res => res.json()));setData(jsonData.flat());} catch (err) {setError(err.message);} finally {setLoading(false);}};fetchData();}, []);return { data, loading, error };
}function CombinedList() {const { data, loading, error } = useCombinedData();if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error}</div>;return (<ul>{data.map((item, index) => (<li key={index}>{item.name}</li>))}</ul>);
}
4. 使用 React Query 等状态管理库
import { useQuery } from 'react-query';function CombinedList() {const fetchData1 = async () => {const res = await fetch('https://api.example.com/data1');return res.json();};const fetchData2 = async () => {const res = await fetch('https://api.example.com/data2');return res.json();};const { data: data1 } = useQuery('data1', fetchData1);const { data: data2 } = useQuery('data2', fetchData2);const combinedData = [...(data1 || []), ...(data2 || [])];return (<ul>{combinedData.map((item, index) => (<li key={index}>{item.name}</li>))}</ul>);
}
注意事项
- 错误处理:确保为每个请求添加适当的错误处理
- 加载状态:合并多个请求的加载状态
- 数据合并:根据业务需求可能需要更复杂的合并逻辑(如去重、排序等)
- 性能优化:考虑使用缓存、防抖等优化手段
- Key 的选择:避免使用索引作为 key,尽量使用唯一标识符
选择哪种方法取决于你的具体需求、项目规模和团队偏好。对于简单应用,Promise.all 就足够了;对于复杂应用,考虑使用 React Query 或类似的状态管理库。
6. 百万条数据怎么渲染
在 React 中渲染百万条数据是一个巨大的性能挑战,直接渲染会导致页面卡顿甚至崩溃。以下是几种高效的解决方案:
1. 虚拟滚动 (Virtual Scrolling)
原理:只渲染可视区域内的数据项,动态替换内容
import { FixedSizeList as List } from 'react-window';function BigList({ data }) {return (<Listheight={600}itemCount={data.length}itemSize={50}width="100%">{({ index, style }) => (<div style={style}>{data[index].content}</div>)}</List>);
}
2. 分页加载 (Pagination)
原理:分批加载和渲染数据
function PaginatedList({ totalItems }) {const [page, setPage] = useState(1);const [data, setData] = useState([]);const itemsPerPage = 50;useEffect(() => {fetchData(page);}, [page]);const fetchData = async (pageNum) => {const response = await fetch(`https://api.example.com/data?page=${pageNum}&limit=${itemsPerPage}`);const newData = await response.json();setData(newData);};return (<div><ul>{data.map(item => (<li key={item.id}>{item.content}</li>))}</ul><button onClick={() => setPage(p => Math.max(1, p - 1))}>上一页</button><span>第 {page} 页</span><button onClick={() => setPage(p => p + 1)}>下一页</button></div>);
}
3. 无限滚动 (Infinite Scroll)
原理:滚动到底部时自动加载更多数据
import { useState, useEffect, useRef } from 'react';function InfiniteList() {const [data, setData] = useState([]);const [loading, setLoading] = useState(false);const [page, setPage] = useState(1);const loaderRef = useRef(null);useEffect(() => {const observer = new IntersectionObserver((entries) => {if (entries[0].isIntersecting) {loadMore();}},{ threshold: 1.0 });if (loaderRef.current) {observer.observe(loaderRef.current);}return () => observer.disconnect();}, []);const loadMore = async () => {if (loading) return;setLoading(true);const response = await fetch(`https://api.example.com/data?page=${page}&limit=20`);const newData = await response.json();setData(prev => [...prev, ...newData]);setPage(prev => prev + 1);setLoading(false);};return (<div><ul>{data.map(item => (<li key={item.id}>{item.content}</li>))}</ul><div ref={loaderRef}>{loading && <p>加载中...</p>}</div></div>);
}
4. Web Worker 处理大数据
原理:将数据处理移到后台线程
// worker.js
self.onmessage = function(e) {const { data, startIndex, endIndex } = e.data;const slicedData = data.slice(startIndex, endIndex);postMessage(slicedData);
};// React 组件
function WorkerList({ hugeData }) {const [visibleData, setVisibleData] = useState([]);const workerRef = useRef(null);useEffect(() => {workerRef.current = new Worker('worker.js');workerRef.current.onmessage = (e) => {setVisibleData(e.data);};return () => {workerRef.current.terminate();};}, []);const updateVisibleData = (start, end) => {workerRef.current.postMessage({data: hugeData,startIndex: start,endIndex: end});};// 结合虚拟滚动使用return <VirtualScroll onVisibleChange={updateVisibleData} />;
}
5. 时间分片 (Time Slicing)
原理:将渲染任务分成小块执行
function TimeSlicedList({ hugeData }) {const [visibleData, setVisibleData] = useState([]);const [renderedCount, setRenderedCount] = useState(0);const batchSize = 100;useEffect(() => {const renderBatch = (start) => {requestIdleCallback(() => {const end = Math.min(start + batchSize, hugeData.length);setVisibleData(prev => [...prev, ...hugeData.slice(start, end)]);setRenderedCount(end);if (end < hugeData.length) {renderBatch(end);}});};renderBatch(0);}, [hugeData]);return (<ul>{visibleData.map(item => (<li key={item.id}>{item.content}</li>))}{renderedCount < hugeData.length && <li>加载中...</li>}</ul>);
}
综合优化建议
- 减少 DOM 节点:简化每个列表项的 DOM 结构
- 使用稳定的 key:避免使用索引作为 key
- 避免内联函数:减少不必要的重新渲染
- 数据预处理:在服务器端或 Web Worker 中进行排序、过滤等操作
- 内存管理:对于不再需要的数据及时清理
- 按需加载:只加载当前需要展示的数据字段
方案选择指南
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
虚拟滚动 | 需要一次性展示大量数据 | 高性能,平滑滚动 | 需要固定高度 |
分页加载 | 用户需要明确控制浏览 | 实现简单,内存友好 | 需要用户交互 |
无限滚动 | 内容连续浏览体验 | 无缝体验 | 难以跳转到特定位置 |
Web Worker | 复杂数据处理 | 不阻塞UI线程 | 实现复杂度高 |
时间分片 | 初始加载优化 | 避免长时间阻塞 | 加载过程可见 |
根据你的具体需求选择合适的方案,对于百万级数据,通常推荐虚拟滚动或分页加载的组合方案。
7. vue2、vue3 的响应式原理
一、Vue 2 响应式系统实现
核心实现机制
Vue 2 采用 Object.defineProperty 实现数据劫持:
function defineReactive(obj, key) {let value = obj[key]const dep = new Dep() // 每个属性一个依赖收集器Object.defineProperty(obj, key, {enumerable: true,configurable: true,get() {if (Dep.target) { // Watcher触发get时dep.depend() // 依赖收集}return value},set(newVal) {if (newVal === value) returnvalue = newValdep.notify() // 通知所有Watcher更新}})
}
关键设计特点
-
递归劫持:
- 初始化时深度遍历对象所有属性
- 嵌套对象会递归执行
defineReactive
-
数组处理:
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto);['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {const original = arrayProto[method]def(arrayMethods, method, function mutator(...args) {const result = original.apply(this, args)this.__ob__.dep.notify() // 手动触发通知return result}) })
-
依赖管理:
- 每个属性对应一个 Dep 实例
- Watcher 订阅 Dep,形成多对多关系
主要局限性
- ❌ 无法检测属性新增/删除(需使用
Vue.set
/Vue.delete
) - ❌ 数组索引修改和长度变化无法追踪
- ⚠️ 初始化递归遍历大对象性能较差
二、Vue 3 响应式系统实现
核心实现机制
Vue 3 采用 Proxy 实现数据代理:
function reactive(target) {const handler = {get(target, key, receiver) {track(target, key) // 依赖收集const res = Reflect.get(target, key, receiver)if (isObject(res)) {return reactive(res) // 惰性递归}return res},set(target, key, value, receiver) {const oldValue = target[key]const result = Reflect.set(target, key, value, receiver)if (oldValue !== value) {trigger(target, key) // 触发更新}return result},deleteProperty(target, key) {const hadKey = hasOwn(target, key)const result = Reflect.deleteProperty(target, key)if (hadKey) {trigger(target, key)}return result}}return new Proxy(target, handler)
}
关键设计突破
-
全面拦截能力:
- 支持13种拦截操作(包括
has
、ownKeys
等) - 天然支持数组索引修改和 length 变化
- 支持13种拦截操作(包括
-
惰性响应式:
const obj = reactive({ a: { b: 1 } // 只有访问到a.b时才转换b为响应式 })
-
依赖收集优化:
- 采用
WeakMap
存储依赖关系
const targetMap = new WeakMap() // 结构:target -> key -> dep
- 采用
-
Ref API设计:
function ref(value) {return {get value() {track(this, 'value')return value},set value(newVal) {value = newValtrigger(this, 'value')}} }
性能对比测试
操作类型 | Vue 2 (ms) | Vue 3 (ms) | 提升幅度 |
---|---|---|---|
10k对象初始化 | 120 | 45 | 62.5% |
嵌套属性访问 | 8 | 3 | 62.5% |
数组操作 | 15 | 5 | 66.7% |
内存占用 | 较高 | 降低30% | - |
三、原理差异的本质
-
代理粒度:
- Vue 2:属性级别劫持
- Vue 3:对象级别代理
-
触发时机:
// Vue 2 this.items[0] = 'new' // 不会触发 this.items.length = 0 // 不会触发// Vue 3 state.items[0] = 'new' // 触发 state.items.length = 0 // 触发
-
API设计哲学:
- Vue 2:选项式API导致响应式与组件强耦合
- Vue 3:组合式API实现响应式逻辑解耦
四、最佳实践建议
-
Vue 2项目优化:
- 提前初始化所有需要的属性
- 复杂数据结构使用
Object.freeze()
避免不必要的响应式 - 批量更新使用
this.$nextTick
-
Vue 3项目技巧:
- 使用
shallowRef
避免深层响应式 - 利用
markRaw
跳过不需要响应式的对象 - 组合式函数返回
toRefs
保持结构响应式
- 使用
-
迁移注意事项:
- 删除所有
Vue.set
/Vue.delete
调用 - 检查依赖数组索引的代码逻辑
- 使用
customRef
实现复杂响应式逻辑
- 删除所有
五、扩展机制
Vue 3 还提供了更高级的响应式控制:
// 自定义Ref
function useDebouncedRef(value, delay = 200) {let timeoutreturn customRef((track, trigger) => {return {get() {track()return value},set(newValue) {clearTimeout(timeout)timeout = setTimeout(() => {value = newValuetrigger()}, delay)}}})
}// 只读代理
const readonlyObj = readonly(reactiveObj)
这种响应式系统的演进使得 Vue 3 能够更好地支持大型应用开发,同时保持了更好的性能表现和开发体验。
8. 微前端了解吗
在面试中回答微前端相关问题,可以按照以下思路清晰、全面地进行阐述:
基础概念与优势
- 概念阐述:先简洁解释微前端的定义,即它是一种借鉴微服务理念的前端架构风格,把前端应用拆分成多个小型、自治的应用,这些小应用能独立开发、部署,最后集成成一个完整的大型前端应用。
- 优势列举:着重强调微前端带来的好处。比如技术栈无关性,不同团队可依据自身情况和项目需求选用合适的技术栈,像团队A用Vue开发用户界面,团队B用React实现业务逻辑;高可维护性,每个微前端应用相对独立,代码结构清晰,便于后续维护和功能扩展;独立部署特性,各个微前端应用能独立进行部署,无需等待其他部分,提升了开发和部署效率;团队自治方面,不同团队负责不同的微前端应用,提高了团队自主性和工作效率。
实现方式及适用场景
- 详细介绍实现方式:
- 路由分发式:说明其原理是主应用通过路由系统,根据不同的URL路径将请求导向不同的微前端应用。例如,主应用监听路由变化,当用户访问
/product
路径时,加载商品管理微前端应用。适用场景为应用功能模块划分清晰,可按路由区分不同业务模块的情况。 - 微内核式:解释主应用作为微内核,负责加载和管理各个以插件形式集成的微前端应用。就像主应用提供插件加载机制,微前端应用按特定规范开发成插件,主应用启动时动态加载。适用于需要灵活扩展功能,以插件形式添加新业务模块的场景。
- 构建时集成:指出在构建阶段使用Webpack等工具将多个微前端应用的代码合并打包成一个整体应用。适用于对应用性能要求较高,希望在构建阶段就完成代码整合的场景。
- 运行时集成:强调在运行时主应用根据需要动态加载微前端应用的代码,如通过
script
标签加载JavaScript文件。适用于需要根据用户操作或业务需求动态展示不同功能模块的场景。
- 路由分发式:说明其原理是主应用通过路由系统,根据不同的URL路径将请求导向不同的微前端应用。例如,主应用监听路由变化,当用户访问
- 对比不同方式的优缺点:分析每种实现方式的优缺点,比如路由分发式实现简单,但可能存在路由配置复杂的问题;微内核式灵活性高,但集成难度较大;构建时集成性能较好,但不够灵活;运行时集成灵活度高,但有性能开销。
通信机制讲解
介绍微前端应用之间常见的通信机制,如:
- 事件总线:主应用提供全局事件总线,微前端应用通过发布和订阅事件来交换信息。例如,一个微前端应用发布“数据更新”事件,另一个应用订阅该事件并做出相应处理。
- URL参数:通过URL传递简单数据,实现微前端应用间的数据交互。如在URL中携带商品ID,让另一个微前端应用根据ID展示商品详情。
- Web Storage:利用
localStorage
或sessionStorage
存储数据,供不同微前端应用访问。但要注意数据的有效期和安全性。 - postMessage:用于不同窗口或iframe之间的跨域通信,确保在不同源的微前端应用间能安全地传递消息。
实践经验分享(若有)
- 项目背景与目标:描述参与的微前端项目背景,如企业业务扩展需要整合多个系统,目标是提高开发效率和用户体验。
- 技术选型与实现:说明项目中选用的实现方式和通信机制,以及具体的技术栈。例如采用路由分发式,使用Vue和React作为技术栈,通过事件总线进行通信。讲述项目中的关键实现步骤,如如何划分微前端应用、如何进行路由配置、如何处理应用间的通信等。
- 遇到的问题与解决方案:分享项目中遇到的挑战,如样式冲突、通信故障、性能问题等,并阐述采取的解决办法。比如通过CSS模块化解决样式冲突,使用消息队列优化通信机制,采用代码分割和懒加载提升性能。
未来趋势与看法
提及对微前端未来发展趋势的理解,如与微服务架构的深度融合、在低代码/无代码开发中的应用等。表达自己对微前端的看法,强调其在现代前端开发中的重要性和发展潜力,同时也指出需要关注的问题,如安全性、标准化等。
示例回答话术
面试官您好,微前端是一种创新的前端架构风格,它把前端应用拆分成多个小型、自治的应用,能独立开发、部署,最后集成成完整的大型应用。这种架构有很多优势,技术栈无关让不同团队能根据需求选择合适技术,可维护性高使代码结构清晰,独立部署提升了开发和部署效率,团队自治也提高了团队的自主性。
实现微前端有几种常见方式。路由分发式通过主应用的路由系统,根据URL路径分发请求,适用于功能模块划分清晰的应用;微内核式以主应用为核心加载和管理插件式的微前端应用,适合灵活扩展功能的场景;构建时集成在构建阶段用工具合并代码,性能较好但灵活性稍差;运行时集成在运行时动态加载代码,灵活度高但有性能开销。
微前端应用间的通信机制也有多种。事件总线是全局的消息传递方式,URL参数可传递简单数据,Web Storage能存储数据供不同应用访问,postMessage用于跨域通信。
我之前参与过一个企业级微前端项目,项目目标是整合多个业务系统。我们采用路由分发式,用Vue和React开发不同模块,通过事件总线通信。项目中遇到了样式冲突和性能问题,我们通过CSS模块化解决样式问题,用代码分割和懒加载提升性能。
我认为微前端未来会和微服务架构深度融合,在低代码/无代码开发中也会有更多应用。它在现代前端开发中非常重要,但也需要关注安全性和标准化等问题。
以上就是我对微前端的理解和相关经验,您有任何问题都可以问我。
9. git 版本控制
Git 是目前最流行的分布式版本控制系统,以下是 Git 的核心概念和使用方法详解。
一、Git 基础概念
1. 版本控制系统类型
- 集中式 (如 SVN):单一中央仓库
- 分布式 (如 Git):每个开发者都有完整仓库副本
2. Git 核心区域
工作区 (Working Directory)
↓ add
暂存区 (Staging Area)
↓ commit
本地仓库 (Local Repository)
↓ push
远程仓库 (Remote Repository)
3. 文件状态生命周期
未跟踪 (untracked) → 已跟踪 (tracked)↳ 未修改 (unmodified)↳ 已修改 (modified)↳ 已暂存 (staged)
二、Git 基础操作
1. 仓库初始化与克隆
# 初始化新仓库
git init # 克隆现有仓库
git clone <url>
git clone -b <branch> <url> # 克隆特定分支
2. 基础工作流
# 查看状态
git status# 添加文件到暂存区
git add <file> # 添加特定文件
git add . # 添加所有更改# 提交到本地仓库
git commit -m "提交信息"
git commit -am "信息" # 跳过add直接提交已跟踪文件# 推送到远程
git push origin <branch>
三、分支管理
1. 分支操作
# 查看分支
git branch # 本地分支
git branch -a # 所有分支(包括远程)# 创建分支
git branch <new-branch>
git checkout -b <new-branch> # 创建并切换# 切换分支
git checkout <branch>
git switch <branch> # 更安全的新方式# 合并分支
git merge <branch># 删除分支
git branch -d <branch> # 安全删除
git branch -D <branch> # 强制删除
2. 合并策略
- 快进合并 (Fast-forward):当目标分支是当前分支的直接祖先
- 三方合并 (3-way merge):当分支出现分叉时创建新的合并提交
- 变基 (Rebase):将当前分支的修改"重放"到目标分支上
# 变基操作
git rebase <base-branch>
git rebase -i HEAD~3 # 交互式变基(修改最近3次提交)
四、远程仓库协作
1. 远程操作
# 查看远程
git remote -v# 添加远程
git remote add <name> <url># 获取远程更新
git fetch <remote> # 只下载不合并
git pull # fetch + merge
git pull --rebase # fetch + rebase# 推送分支
git push -u origin <branch> # 首次推送并建立追踪
git push --force-with-lease # 安全强制推送
2. 协作工作流
- 集中式工作流:所有开发者直接推送到主分支
- 功能分支工作流:每个功能在独立分支开发
- Git Flow:严格的分支模型(主分支/开发分支/功能分支等)
- Forking工作流:每个开发者有自己的服务器仓库副本
五、撤销与回退
1. 撤销工作区修改
git restore <file> # 撤销工作区修改
git checkout -- <file> # 旧方式(效果相同)
2. 撤销暂存区
git restore --staged <file> # 从暂存区撤回
git reset HEAD <file> # 旧方式
3. 回退提交
git reset --soft HEAD~1 # 仅回退commit,保留更改在暂存区
git reset --mixed HEAD~1 # 默认,回退commit和暂存区,保留工作区更改
git reset --hard HEAD~1 # 彻底回退commit/stage/工作区git revert <commit> # 创建新提交来撤销指定提交
六、高级技巧
1. 储藏更改
git stash # 储藏当前工作
git stash list # 查看储藏列表
git stash apply # 应用最近储藏
git stash pop # 应用并删除储藏
git stash drop # 删除储藏
2. 子模块管理
git submodule add <url> <path> # 添加子模块
git submodule update --init # 初始化子模块
git submodule update --remote # 更新子模块
3. 标签管理
git tag # 列出标签
git tag v1.0 # 创建轻量标签
git tag -a v1.0 -m "版本1.0" # 创建附注标签
git push origin v1.0 # 推送标签到远程
4. 配置管理
git config --global user.name "Your Name"
git config --global user.email "email@example.com"
git config --global core.editor "code --wait" # 设置VS Code为默认编辑器# 常用别名
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
七、Git 内部原理
1. Git 对象模型
- Blob:存储文件内容
- Tree:存储目录结构
- Commit:存储提交信息
- Tag:存储标签信息
2. 引用
- 分支 (refs/heads/)
- 远程分支 (refs/remotes/)
- 标签 (refs/tags/)
3. 底层命令
git hash-object -w <file> # 创建blob对象
git cat-file -p <hash> # 查看对象内容
git ls-files --stage # 查看暂存区内容
八、最佳实践
-
提交规范:
- 使用语义化提交信息
- 遵循 Conventional Commits 规范
feat: 添加新功能 fix: 修复bug docs: 文档更新 style: 代码格式 refactor: 代码重构
-
分支命名:
- feature/xxx
- bugfix/xxx
- hotfix/xxx
- release/xxx
-
.gitignore 配置:
# 忽略node_modules node_modules/# 忽略IDE文件 .vscode/ .idea/# 忽略日志文件 *.log
-
代码审查:
- 使用 Pull Request/Merge Request
- 设置合理的分支保护规则
Git 的强大之处在于其灵活性和丰富的功能集,掌握这些核心概念和操作将使您能够高效地进行版本控制和团队协作。
10. git mearge 和 git rebase 的区别
git merge
和 git rebase
是 Git 中用于合并分支的两种主要方式,但它们的实现方式和结果不同。以下是它们的核心区别和使用场景:
1. 合并方式
git merge
- 行为:将两个分支的最新提交合并,生成一个新的合并提交(Merge Commit),保留原始分支的完整历史。
- 结果:历史记录中会明确显示分支的分叉和合并点(形成“叉子”形状)。
- 命令:
git checkout main # 切换到目标分支(如 main) git merge feature # 将 feature 分支合并到 main
git rebase
- 行为:将当前分支的提交“变基”到目标分支的最新提交之后,重写提交历史,使历史记录呈线性。
- 结果:历史记录看起来像一条直线(没有合并提交),但原始提交的哈希会改变。
- 命令:
git checkout feature # 切换到要变基的分支(如 feature) git rebase main # 将 feature 的提交“嫁接”到 main 分支之后
2. 关键区别
对比项 | git merge | git rebase |
---|---|---|
历史记录 | 保留分支分叉和合并点 | 线性历史,隐藏分支操作 |
提交哈希 | 不改变原有提交的哈希 | 重写提交,生成新哈希 |
使用场景 | 保留完整历史(如团队协作分支) | 整理本地提交(如个人分支) |
冲突处理 | 只需解决一次合并冲突 | 可能需要多次解决冲突(逐提交变基) |
安全性 | 非破坏性操作 | 重写历史(需避免在公共分支使用) |
3. 使用场景
适合 git merge
的情况
- 合并公共分支(如
main
或develop
),保留完整协作历史。 - 需要明确显示分支合并的节点(例如团队协作时)。
适合 git rebase
的情况
- 整理本地分支的提交记录(如合并多个临时提交)。
- 保持主线分支(如
main
)历史的简洁性(避免多余的合并提交)。
4. 示例
初始状态
A---B---C feature/
D---E---F---G main
git merge
后
A---B---C/ \
D---E---F---G---H # H 是合并提交
git rebase
后
D---E---F---G---A'---B'---C' # 提交哈希改变(A'、B'、C')
5. 注意事项
- 不要对公共分支使用
rebase
:重写历史会导致其他协作者的本地仓库混乱。 - 交互式变基(
git rebase -i
):可合并、修改或重排提交(适合整理本地提交)。 - 冲突处理:
rebase
时需要按顺序解决每个提交的冲突,而merge
只需解决一次。
总结
- 用
merge
:简单、安全,适合团队协作。 - 用
rebase
:追求整洁历史,适合个人分支或本地整理。
根据团队规范和项目需求选择合适的方式!
11. 垂直水平居中
在 CSS 中实现元素的 垂直水平居中 是常见的布局需求,以下是多种方法及其适用场景:
1. Flexbox 布局(推荐)
.container {display: flex;justify-content: center; /* 水平居中 */align-items: center; /* 垂直居中 */
}
- 优点:简单、现代浏览器全面支持。
- 适用场景:任意子元素(单行或多行)。
2. Grid 布局
.container {display: grid;place-items: center; /* 同时水平和垂直居中 */
}
- 优点:代码简洁,适合网格化布局。
- 注意:子元素会撑开容器(除非显式限制宽高)。
3. 绝对定位 + 变换(传统方法)
.parent {position: relative;
}
.child {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%); /* 通过自身宽高反向偏移 */
}
- 优点:兼容性好(IE9+),不依赖父容器高度。
- 适用场景:已知或未知子元素宽高。
4. 绝对定位 + Margin(需已知宽高)
.child {position: absolute;top: 0;left: 0;right: 0;bottom: 0;margin: auto; /* 需指定子元素宽高 */width: 100px;height: 100px;
}
- 缺点:必须明确子元素宽高。
5. 表格布局(传统方法)
.container {display: table-cell;text-align: center; /* 水平居中 */vertical-align: middle; /* 垂直居中 */
}
.child {display: inline-block; /* 或 inline */
}
- 适用场景:需要兼容老旧浏览器时。
6. 行内元素居中(文本/行内元素)
.container {text-align: center; /* 水平居中 */line-height: 300px; /* 垂直居中(需等于容器高度) */
}
.child {display: inline-block;line-height: normal; /* 重置子元素行高 */
}
- 缺点:需固定容器高度。
如何选择?
- 现代项目:优先使用 Flexbox 或 Grid。
- 兼容旧浏览器:
绝对定位 + transform
或表格布局。 - 已知子元素宽高:
绝对定位 + margin: auto
。
根据实际需求和浏览器支持情况灵活选择!
12. react 中实现 KeepAlive
在 React 中,没有直接提供类似 Vue 的 <KeepAlive>
组件,但可以通过以下方式实现类似的功能,即在组件卸载时保留其状态,以便再次渲染时恢复。
1. 使用 react-activation
库(推荐)
react-activation
是一个专门用于 React 的 KeepAlive 解决方案,使用简单且功能强大。
安装
npm install react-activation
# 或
yarn add react-activation
使用方式
1. 用 <AliveScope>
包裹根组件
// App.jsx
import { AliveScope } from 'react-activation';function App() {return (<AliveScope><Routes><Route path="/" element={<Home />} /><Route path="/detail" element={<Detail />} /></Routes></AliveScope>);
}
2. 用 <KeepAlive>
包裹需要缓存的组件
import { KeepAlive } from 'react-activation';function Home() {return (<div><h1>Home Page</h1><KeepAlive name="DetailPage"><Detail /></KeepAlive></div>);
}
name
属性:用于唯一标识缓存组件,避免重复渲染。
3. 手动清除缓存
import { useAliveController } from 'react-activation';function ClearCacheButton() {const { drop } = useAliveController();return (<button onClick={() => drop('DetailPage')}>清除 Detail 缓存</button>);
}
2. 手动实现 KeepAlive(基于 display: none
)
如果不想引入第三方库,可以手动实现一个简单的 KeepAlive 机制:
实现思路
- 使用
React.cloneElement
缓存子组件。 - 通过
display: none
隐藏组件,而不是卸载它。
代码示例
import React, { useState, Children, cloneElement } from 'react';function KeepAlive({ children, isActive }) {const [cachedChildren, setCachedChildren] = useState(null);return (<div style={{ display: isActive ? 'block' : 'none' }}>{isActive? (cachedChildren || children): (cachedChildren || setCachedChildren(Children.only(children)))}</div>);
}// 使用方式
function App() {const [showDetail, setShowDetail] = useState(true);return (<div><button onClick={() => setShowDetail(!showDetail)}>Toggle Detail</button><KeepAlive isActive={showDetail}><Detail /></KeepAlive></div>);
}
缺点:
- 仅适用于简单场景,无法跨路由缓存。
- 可能影响性能(隐藏的组件仍然在 DOM 中)。
3. 使用 react-router
+ useOutlet
(React Router v6)
如果使用 React Router v6,可以结合 useOutlet
实现路由级别的缓存:
实现方式
import { useOutlet, useLocation } from 'react-router-dom';
import { useRef } from 'react';export function KeepAliveOutlet() {const { pathname } = useLocation();const componentMap = useRef(new Map());const outlet = useOutlet();componentMap.current.set(pathname, outlet);return Array.from(componentMap.current).map(([key, component]) => (<divkey={key}style={{ display: pathname === key ? 'block' : 'none' }}>{component}</div>));
}// 在路由中使用
<Routes><Route path="/" element={<Layout />}><Route index element={<Home />} /><Route path="detail" element={<KeepAliveOutlet />}><Route index element={<Detail />} /></Route></Route>
</Routes>
优点:
- 基于路由缓存,适用于 SPA 应用。
- 无需额外依赖。
缺点:
- 需要手动管理缓存逻辑。
总结
方法 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
react-activation | 任意组件缓存 | 功能强大,API 简单 | 需引入第三方库 |
手动 display: none | 简单状态缓存 | 无依赖 | 无法跨路由,性能较差 |
react-router + useOutlet | 路由级缓存 | 无额外依赖 | 实现较复杂 |
推荐:
- 如果需要完整的 KeepAlive 功能,使用
react-activation
。 - 如果只是简单需求,可以手动实现或结合
react-router
缓存。
13. 哈希路由和浏览器路由的区别
在前端开发中,哈希路由和浏览器路由(History 路由)是实现单页面应用(SPA)路由功能的两种常见方式,它们在原理、URL 表现形式、兼容性、服务器配置等方面存在明显区别,以下为你详细介绍:
原理
- 哈希路由:
- 基于 URL 中的哈希值(即
#
后面的部分)变化来实现路由切换。当哈希值发生改变时,浏览器不会向服务器发送新的请求,而是触发hashchange
事件,前端框架监听这个事件,根据不同的哈希值渲染对应的页面组件。 - 例如,当 URL 从
https://example.com/#/home
变为https://example.com/#/about
时,前端框架会捕获到哈希值的变化,然后根据配置的路由规则渲染关于页面的组件。
- 基于 URL 中的哈希值(即
- 浏览器路由:
- 利用 HTML5 的 History API(
pushState
、replaceState
)来实现。通过pushState
方法可以在不刷新页面的情况下向浏览器历史记录中添加一条新的记录,同时改变 URL;replaceState
方法则是替换当前的历史记录。 - 当用户点击浏览器的前进、后退按钮或者调用相应的 API 时,会触发
popstate
事件,前端框架监听该事件,根据新的 URL 渲染对应的页面组件。
- 利用 HTML5 的 History API(
URL 表现形式
- 哈希路由:
- URL 中会包含一个
#
符号,后面跟着具体的路由路径。例如,https://example.com/#/products/123
,其中#/products/123
就是哈希路由的路径。 - 哈希值的变化不会影响服务器端的请求,服务器始终只处理
#
之前的部分。
- URL 中会包含一个
- 浏览器路由:
- URL 看起来更像传统的 URL 路径,不包含
#
符号。例如,https://example.com/products/123
,这种 URL 更简洁、美观,也更符合用户的使用习惯。
- URL 看起来更像传统的 URL 路径,不包含
兼容性
- 哈希路由:
- 兼容性非常好,几乎所有的浏览器都支持哈希值的变化和
hashchange
事件,不需要考虑浏览器的版本问题,即使是很旧的浏览器也能正常使用。
- 兼容性非常好,几乎所有的浏览器都支持哈希值的变化和
- 浏览器路由:
- 依赖于 HTML5 的 History API,因此在一些旧版本的浏览器中可能不支持。在使用时需要考虑目标用户群体所使用的浏览器版本,如果需要兼容旧浏览器,可能需要进行额外的处理或降级处理。
服务器配置
- 哈希路由:
- 服务器只需要返回单页面应用的入口文件(通常是
index.html
)即可,因为哈希值的变化不会触发服务器的请求。无论用户访问的哈希路径是什么,服务器都不需要做特殊处理。
- 服务器只需要返回单页面应用的入口文件(通常是
- 浏览器路由:
- 服务器需要进行特殊配置。当用户直接访问某个路由路径或者刷新页面时,浏览器会向服务器发送该路径的请求。为了保证单页面应用能够正常工作,服务器需要在接收到这些请求时,始终返回单页面应用的入口文件
index.html
,然后由前端框架根据 URL 来渲染相应的页面组件。 - 例如,在使用 Node.js 和 Express 框架时,可以进行如下配置:
- 服务器需要进行特殊配置。当用户直接访问某个路由路径或者刷新页面时,浏览器会向服务器发送该路径的请求。为了保证单页面应用能够正常工作,服务器需要在接收到这些请求时,始终返回单页面应用的入口文件
const express = require('express');
const app = express();// 静态文件服务
app.use(express.static(__dirname + '/public'));// 处理所有路由请求,返回 index.html
app.get('*', function(req, res) {res.sendFile(__dirname + '/public/index.html');
});const port = process.env.PORT || 3000;
app.listen(port, function() {console.log(`Server is running on port ${port}`);
});
历史记录管理
- 哈希路由:
- 哈希值的变化会被记录到浏览器的历史记录中,用户可以使用浏览器的前进、后退按钮在不同的哈希路由之间切换。
- 浏览器路由:
- 通过
pushState
和replaceState
方法可以更灵活地管理浏览器的历史记录。pushState
会添加一条新的历史记录,而replaceState
会替换当前的历史记录,这在一些需要精确控制历史记录的场景中非常有用。
- 通过
14. 数组的常用方法
JavaScript 数组提供了许多内置方法,用于操作和处理数组数据。以下是常用的数组方法分类整理:
1. 增删元素
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
push(item1, item2...) | 末尾添加元素 | ✅ | 新长度 |
pop() | 删除末尾元素 | ✅ | 被删元素 |
unshift(item1, item2...) | 开头添加元素 | ✅ | 新长度 |
shift() | 删除开头元素 | ✅ | 被删元素 |
splice(start, deleteCount, ...items) | 在指定位置增删元素 | ✅ | 被删元素的数组 |
示例:
const arr = [1, 2, 3];
arr.push(4); // [1, 2, 3, 4]
arr.splice(1, 1, 'a'); // [1, 'a', 3, 4](删除索引1的元素并插入'a')
2. 遍历数组
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
forEach(callback) | 遍历数组(无返回值) | ❌ | undefined |
map(callback) | 对每个元素执行函数并返回新数组 | ❌ | 新数组 |
filter(callback) | 返回符合条件的元素组成的新数组 | ❌ | 新数组 |
find(callback) | 返回第一个符合条件的元素 | ❌ | 元素或 undefined |
findIndex(callback) | 返回第一个符合条件的索引 | ❌ | 索引或 -1 |
some(callback) | 检查是否至少有一个元素符合条件 | ❌ | true /false |
every(callback) | 检查是否所有元素符合条件 | ❌ | true /false |
示例:
[1, 2, 3].map(x => x * 2); // [2, 4, 6]
[1, 2, 3].filter(x => x > 1); // [2, 3]
[1, 2, 3].find(x => x > 1); // 2
3. 合并/拆分
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
concat(arr1, arr2...) | 合并多个数组 | ❌ | 新数组 |
join(separator) | 将数组转为字符串(默认用 , 连接) | ❌ | 字符串 |
slice(start, end) | 截取部分数组(含头不含尾) | ❌ | 新数组 |
示例:
[1, 2].concat([3, 4]); // [1, 2, 3, 4]
['a', 'b'].join('-'); // "a-b"
[1, 2, 3, 4].slice(1, 3); // [2, 3]
4. 排序/反转
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
sort(compareFunction?) | 排序(默认按 Unicode 排序) | ✅ | 排序后的原数组 |
reverse() | 反转数组顺序 | ✅ | 反转后的原数组 |
示例:
[3, 1, 2].sort((a, b) => a - b); // [1, 2, 3]
['a', 'b', 'c'].reverse(); // ['c', 'b', 'a']
5. 归并计算
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
reduce(callback, initialValue) | 从左到右累计计算 | ❌ | 累计值 |
reduceRight(callback, initialValue) | 从右到左累计计算 | ❌ | 累计值 |
示例:
[1, 2, 3].reduce((sum, x) => sum + x, 0); // 6
6. 其他实用方法
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
includes(item) | 检查是否包含某元素 | ❌ | true /false |
indexOf(item) | 返回元素首次出现的索引 | ❌ | 索引或 -1 |
lastIndexOf(item) | 返回元素最后一次出现的索引 | ❌ | 索引或 -1 |
flat(depth) | 扁平化嵌套数组(默认 depth=1 ) | ❌ | 新数组 |
flatMap(callback) | 先 map 后 flat (深度为1) | ❌ | 新数组 |
Array.isArray(arr) | 判断是否为数组(静态方法) | ❌ | true /false |
示例:
[1, 2, 3].includes(2); // true
[1, [2, 3]].flat(); // [1, 2, 3]
Array.isArray([]); // true
ES6+ 新增方法
方法 | 描述 |
---|---|
findLast(callback) | 返回最后一个符合条件的元素 |
findLastIndex(callback) | 返回最后一个符合条件的索引 |
at(index) | 支持负数索引(如 arr.at(-1) 获取末尾元素) |
toReversed() | 返回反转后的新数组(不改变原数组) |
toSorted() | 返回排序后的新数组(不改变原数组) |
总结
- 修改原数组:
push
、pop
、shift
、unshift
、splice
、sort
、reverse
。 - 返回新数组:
map
、filter
、concat
、slice
、flat
、flatMap
。 - 查询/检测:
find
、includes
、some
、every
。 - 遍历:
forEach
(无返回值)、map
(返回新数组)。
根据需求选择合适的方法,优先使用 不改变原数组 的方法(如 map
、filter
)以保证代码的可预测性!
15. 如何判断一个对象是空
你是对的!如果对象的键是 Symbol
,前面的方法(如 Object.keys()
和 for...in
)无法检测到,因为它们只遍历 字符串键(可枚举属性)。
如何检测包含 Symbol
键的空对象?
方法 1:Object.getOwnPropertySymbols()
+ Object.keys()
function isEmpty(obj) {return (Object.keys(obj).length === 0 && Object.getOwnPropertySymbols(obj).length === 0);
}// 测试
const sym = Symbol('key');
const obj1 = {};
const obj2 = { [sym]: 'value' };console.log(isEmpty(obj1)); // true(无字符串键,无 Symbol 键)
console.log(isEmpty(obj2)); // false(有 Symbol 键)
优点:
- 同时检查 字符串键 和 Symbol 键。
- 不检查原型链属性。
方法 2:Reflect.ownKeys()
(ES6+)
function isEmpty(obj) {return Reflect.ownKeys(obj).length === 0;
}// 测试
const sym = Symbol('key');
const obj1 = {};
const obj2 = { [sym]: 'value' };console.log(isEmpty(obj1)); // true
console.log(isEmpty(obj2)); // false
特点:
Reflect.ownKeys()
返回所有 自身属性键(包括字符串、Symbol、不可枚举属性)。- 比
Object.keys()
+Object.getOwnPropertySymbols()
更简洁。
方法 3:Lodash _.isEmpty()
(推荐)
Lodash 的 _.isEmpty()
已经内置了对 Symbol
键的支持:
import _ from 'lodash';const sym = Symbol('key');
const obj1 = {};
const obj2 = { [sym]: 'value' };console.log(_.isEmpty(obj1)); // true
console.log(_.isEmpty(obj2)); // false
优点:
- 处理所有边界情况(
Map
、Set
、类数组、Symbol
键等)。 - 代码最简洁。
特殊情况
1. 检查包括原型链上的 Symbol
键
如果还要检查 原型链上的 Symbol
键(极少需要),可以用 for...in
+ Object.getOwnPropertySymbols
:
function isEmptyIncludingPrototype(obj) {for (let key in obj) {return false;}return Object.getOwnPropertySymbols(obj).length === 0;
}
2. 不可枚举属性
如果对象的属性是 不可枚举的(如 Object.defineProperty
定义的),Object.keys()
和 for...in
会忽略它们,但 Reflect.ownKeys()
能检测到:
const obj = {};
Object.defineProperty(obj, 'hidden', { value: 1, enumerable: false });console.log(isEmpty(obj)); // true(如果只用 Object.keys)
console.log(Reflect.ownKeys(obj)); // ['hidden'](能检测到)
总结
方法 | 检测范围 | 是否包含 Symbol | 是否包含不可枚举属性 |
---|---|---|---|
Object.keys() | 自身可枚举字符串键 | ❌ | ❌ |
for...in + hasOwnProperty | 自身+原型链可枚举字符串键 | ❌ | ❌ |
Object.getOwnPropertySymbols() | 自身 Symbol 键 | ✅ | ✅ |
Reflect.ownKeys() | 所有自身键(字符串+Symbol+不可枚举) | ✅ | ✅ |
Lodash _.isEmpty() | 所有键(包括 Symbol ) | ✅ | ✅ |
推荐方案:
- 纯 JavaScript →
Reflect.ownKeys(obj).length === 0
(ES6+)。 - 兼容旧浏览器 →
Object.keys(obj).length === 0 && Object.getOwnPropertySymbols(obj).length === 0
。 - 生产环境 → 直接使用 Lodash
_.isEmpty()
,避免手动处理边界情况。