本文主要是介绍真实前端面试题(蚂蚁外包),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1.闭包定义应用场景
闭包(Closure) 是指一个函数包含了对其外部作用域中变量的引用,即使在该函数外部作用域执行完毕后仍然可以访问这些变量。闭包允许你在一个函数内部访问另一个函数的变量,这在许多编程语言中是一种强大的特性。
- 保护私有变量:闭包允许你创建一个包含私有数据的函数,这些数据对外部是不可见的。这在模块化编程中非常有用,可以防止外部代码直接访问和修改内部状态。例如:
javascript
复制代码
function counter() { let count = 0; return function() { count++; console.log(count); }; } const increment = counter(); increment(); // 输出 1 increment(); // 输出 2
- 实现数据封装:闭包可以用于创建类似于面向对象编程中的对象实例。你可以定义一个包含内部状态和方法的函数,然后通过闭包来访问和操作这些数据。这种方式被称为 "模块模式":
javascript
复制代码
function createPerson(name) { let age = 0; return { getName: function() { return name; }, getAge: function() { return age; }, setAge: function(newAge) { if (newAge >= 0) { age = newAge; } } }; } const person = createPerson("Alice"); console.log(person.getName()); // 输出 "Alice" console.log(person.getAge()); // 输出 0 person.setAge(30); console.log(person.getAge()); // 输出 30
- 实现回调函数:闭包经常用于创建回调函数,将函数作为参数传递给其他函数。这些回调函数可以访问外部函数的局部变量,以便在异步操作完成后执行特定的逻辑。
javascript
复制代码
function fetchData(url, callback) { // 模拟异步请求 setTimeout(function() { const data = "Some data from " + url; callback(data); }, 1000); } fetchData("https://example.com/api", function(response) { console.log("Received data: " + response); });
- 实现函数工厂:闭包可以用于创建定制的函数,这些函数可以生成特定的行为或配置。这在某些库和框架中很常见。
javascript
复制代码
function createMultiplier(factor) { return function(x) { return x * factor; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 输出 10 console.log(triple(5)); // 输出 15
总之,闭包是一种强大的编程工具,它可以用于许多不同的应用场景,包括数据封装、模块化编程、回调函数等。它使得JavaScript等语言更加灵活和功能强大。
2.异步流程
异步流程是指在程序中执行的一系列操作不是按照顺序同步执行的,而是按照事件发生或者异步操作的完成情况来执行的一种流程。这种方式允许程序在等待某些操作完成时继续执行其他任务,而不会被阻塞。异步流程在处理网络请求、文件读写、用户输入等需要等待的情况下非常有用。
以下是异步流程的一般步骤和一些常见的异步编程模式:
-
发起异步操作:首先,你会发起一个异步操作,比如发起一个网络请求、读取一个文件、等待用户输入等。这些操作可能需要一段时间来完成。
-
注册回调函数:一旦异步操作被触发,你通常会注册一个回调函数,这个函数将在操作完成后被调用。回调函数是异步流程中的关键,因为它定义了在异步操作完成时要执行的逻辑。
-
继续执行:在注册回调函数后,程序通常会继续执行其他任务,而不会等待异步操作完成。这样可以提高程序的响应性,不会让程序在等待I/O操作时被阻塞。
-
异步操作完成:当异步操作完成(比如网络请求返回数据、文件读取完成、用户输入就绪等),注册的回调函数将被调用,执行与异步操作相关的逻辑。
以下是一些常见的异步编程模式和技术:
-
回调函数:最基本的异步编程模式是使用回调函数。你将一个函数作为参数传递给异步操作,当操作完成时,回调函数将被执行。
-
Promise:Promise是一种更高级的异步编程模式,它提供了一种更结构化的方式来处理异步操作。通过Promise,你可以更容易地处理异步操作的成功和失败情况。
-
async/await:async/await是JavaScript中的异步编程语法糖,它基于Promise构建,使异步代码看起来更像同步代码,提高了可读性。
-
事件驱动编程:在事件驱动编程中,你将事件处理程序注册到特定事件上,当事件发生时,处理程序将被调用。这在前端开发和Node.js等环境中常见。
-
生成器函数:生成器函数允许你在迭代中暂停和恢复执行,这对于处理异步操作的结果很有用。
异步流程允许程序在执行过程中非阻塞地处理多个任务,提高了程序的效率和用户体验。然而,它也需要更复杂的控制流程,因此需要小心处理回调地狱和异步错误处理等问题。
3.微任务宏任务
微任务(Microtask)和宏任务(Macrotask)是与事件循环(Event Loop)相关的两个重要概念,用于管理异步操作和任务执行的顺序。
微任务(Microtask):
- 微任务是一种异步任务,它的执行顺序在宏任务之后,在每个事件循环迭代中立即执行。
- 微任务通常比宏任务更高优先级,因此它们会在当前宏任务执行完毕后立即执行,而不会等待下一个宏任务。
- 常见的微任务包括Promise的
then
和catch
方法、process.nextTick
(Node.js中的微任务)等。 - 微任务通常用于执行一些高优先级的任务,比如更新UI,处理Promise的结果等。
示例代码(JavaScript):
javascript
复制代码
console.log('Start'); Promise.resolve().then(() => { console.log('Microtask 1'); }); console.log('End');
输出:
sql
复制代码
Start End Microtask 1
宏任务(Macrotask):
- 宏任务是一种异步任务,它的执行顺序在微任务之后,通常在每个事件循环迭代中只执行一个。
- 宏任务包括整体的JavaScript代码、setTimeout、setInterval、requestAnimationFrame(浏览器中的动画帧任务)、I/O操作等。
- 宏任务的执行顺序会等待当前的微任务队列执行完毕后,然后从宏任务队列中选择下一个宏任务执行。
示例代码(JavaScript):
javascript
复制代码
console.log('Start'); setTimeout(() => { console.log('Timeout'); }, 0); console.log('End');
输出:
sql
复制代码
Start End Timeout
总结:
微任务和宏任务是异步编程中用于控制任务执行顺序的两个关键概念。微任务通常比宏任务具有更高的优先级,因此它们会在当前宏任务执行完毕后立即执行。了解它们的执行顺序对于理解事件循环和编写可预测的异步代码非常重要。在浏览器环境中,宏任务和微任务的常见示例包括setTimeout、Promise、MutationObserver等。在Node.js环境中,宏任务和微任务的示例包括setImmediate、process.nextTick等。
4.类继承的方式,缺点
在面向对象编程中,类继承是一种重要的机制,它允许一个类(子类)继承另一个类(父类)的属性和方法。类继承可以通过不同的方式实现,其中最常见的方式有以下几种:
-
单继承:每个子类只能继承一个父类。这是一种简单且常见的继承方式,用于构建类层次结构。
-
多继承:一个子类可以继承多个父类的属性和方法。多继承在某些编程语言中支持,但容易引发复杂性和歧义,因此在一些语言中不被推荐或禁止使用。
-
接口继承:子类可以实现多个接口,从而继承接口中定义的方法签名,而不是具体实现。
-
混入(Mixin):混入是一种通过将多个类的功能组合到一个类中来实现多继承的技术。它通常使用组合而非继承来实现复用。
不过,类继承也存在一些缺点和问题:
-
紧耦合(Tight Coupling):类继承会在子类和父类之间创建紧耦合的关系,子类通常依赖于父类的内部实现细节。这可能导致代码的脆弱性,因为对父类的修改可能会影响到子类的行为。
-
继承层次复杂性:随着继承层次的增加,代码的复杂性也会增加,导致难以维护和理解。深层次的继承层次可能会导致"钻石问题",即多个子类继承自同一个父类,而这些子类又被用于创建新的子类,形成一个钻石形状的继承层次。
-
限制了代码重用:类继承通常采用单一继承模式,这意味着子类只能从一个父类继承功能。这可能会限制代码的重用性,因为某个类可能需要继承多个不同的功能。
-
不利于单元测试:继承链中的每个类都可能有自己的状态和行为,这使得单元测试变得复杂,因为需要考虑多层继承中的各种情况和依赖关系。
-
不够灵活:继承是一种静态的关系,一旦定义了继承关系,就难以在运行时动态地改变。这可能限制了代码的灵活性和可扩展性。
为了克服这些缺点,一些编程语言和编程范式引入了其他方式来实现代码重用和抽象,如组合、接口、混入等。在选择使用类继承时,需要慎重考虑设计和维护的复杂性,以确保它适合特定的问题和场景。
5.promise,async await
Promise 和 async/await 是 JavaScript 中用于处理异步操作的两种不同的机制,它们都旨在让异步代码更加可读和易于管理。
Promise:
-
Promise 是一种异步编程模式,它提供了一种更结构化的方式来处理异步操作。一个 Promise 表示一个异步操作的最终完成或失败,以及它的结果值或失败原因。
-
Promise 有三个状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。一旦 Promise 进入 fulfilled 或 rejected 状态,它就不会再改变状态。
-
Promise 使用
.then()
方法来注册回调函数,当异步操作成功时执行then()
方法的第一个回调函数,当异步操作失败时执行第二个回调函数。 -
Promise 链(Promise chaining)允许你按顺序执行一系列的异步操作,每个操作都返回一个 Promise,这样可以更好地控制异步代码的流程。
示例代码(使用 Promise):
javascript
复制代码
function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("Data fetched successfully"); }, 1000); }); } fetchData() .then((result) => { console.log(result); }) .catch((error) => { console.error(error); });
async/await:
-
async/await 是基于 Promise 的语法糖,它提供了一种更像同步代码的方式来处理异步操作。async 函数返回一个 Promise,而 await 关键字用于暂停函数的执行,等待一个 Promise 的解决。
-
async 函数内部可以使用 await 关键字来等待异步操作的结果,这样可以在代码中像编写同步代码一样使用异步操作。
-
async 函数的执行会在遇到第一个 await 表达式时暂停,然后等待该表达式的 Promise 解决。之后,async 函数会继续执行,直到遇到下一个 await 表达式或函数结束。
示例代码(使用 async/await):
javascript
复制代码
async function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("Data fetched successfully"); }, 1000); }); } async function fetchDataAndLog() { try { const result = await fetchData(); console.log(result); } catch (error) { console.error(error); } } fetchDataAndLog();
总结:
Promise 是一种更底层的异步处理机制,适用于处理单个异步操作,而 async/await 是基于 Promise 的更高级、更易读的语法糖,适用于编写更具可读性的异步代码。选择使用哪种方式取决于你的项目需求和个人偏好,但在现代 JavaScript 中,async/await 成为了处理异步操作的首选方式,因为它更容易理解和维护。
6.es6模块化
ES6(ECMAScript 2015)模块化是 JavaScript 中一种用于组织和管理代码的现代模块系统。它使得代码模块化、可重用、可维护,并允许你将代码拆分成多个文件,以便更好地管理复杂性和依赖关系。下面是 ES6 模块化的一些关键特点和用法:
-
导出模块成员(Export):
使用
export
关键字可以将变量、函数、类等从一个模块导出,使其在其他模块中可用。可以导出多个成员,并且可以使用命名导出和默认导出两种方式。javascript
复制代码
// 导出单个成员(命名导出) export const name = "John"; // 导出多个成员(命名导出) export function greet() { return "Hello, world!"; } // 默认导出 export default function() { console.log("Default export"); }
-
导入模块成员(Import):
使用
import
关键字可以在其他模块中导入已经导出的成员。可以使用具名导入和默认导入两种方式。javascript
复制代码
// 导入单个成员(具名导入) import { name, greet } from "./module"; // 导入默认成员(默认导入) import defaultFunction from "./module";
-
模块之间的依赖关系:
模块可以明确指定它们之间的依赖关系。如果一个模块依赖于另一个模块,当导入的模块发生变化时,导入的模块会自动重新加载。
-
模块的执行顺序:
模块的执行顺序是静态的,它们在运行时只会加载一次。这有助于避免全局污染和循环依赖问题。
-
循环依赖:
尽量避免模块之间的循环依赖,这可能导致难以理解的行为。
-
模块化加载:
在浏览器中,可以使用
<script type="module">
标签来加载 ES6 模块,或者使用工具(如Webpack、Rollup)将模块打包为一个文件,然后在浏览器中引入打包后的文件。
ES6 模块化提供了一种现代、可维护和可重用的方式来组织 JavaScript 代码,它已经成为了前端开发和Node.js开发中的标准实践。它有助于减少全局命名空间污染、提高代码可维护性、更好地管理依赖关系等。
7.react组件通信
在 React 中,组件通信是非常重要的,因为一个复杂的应用程序通常由多个组件组成。以下是一些用于在 React 中实现组件通信的常见方法:
-
Props(属性):
这是 React 中最基本的组件通信方式。通过将数据作为属性传递给子组件,可以实现从父组件向子组件传递数据。这是一种单向传递的方式,父组件向子组件传递数据,子组件只能读取这些数据。
javascript
复制代码
// 父组件 function ParentComponent() { const data = "Hello from parent!"; return <ChildComponent message={data} />; } // 子组件 function ChildComponent(props) { return <p>{props.message}</p>; }
-
回调函数:
通过将回调函数传递给子组件,父组件可以与子组件进行通信。子组件可以调用这些回调函数来触发父组件中的操作。
javascript
复制代码
// 父组件 function ParentComponent() { const handleChildClick = () => { console.log("Child clicked!"); }; return <ChildComponent onClick={handleChildClick} />; } // 子组件 function ChildComponent(props) { return <button onClick={props.onClick}>Click me</button>; }
-
Context(上下文):
React Context 是一种用于在组件树中共享数据的高级机制。它允许您在不必一级一级传递属性的情况下共享数据。
javascript
复制代码
// 创建上下文 const MyContext = React.createContext(); // 父组件 function ParentComponent() { return ( <MyContext.Provider value="Hello from context"> <ChildComponent /> </MyContext.Provider> ); } // 子组件 function ChildComponent() { const data = useContext(MyContext); return <p>{data}</p>; }
-
Redux(状态管理库):
Redux 是一种用于管理应用程序状态的库,它允许组件之间通过一个全局存储来进行通信。Redux 的核心概念包括 Store、Action 和 Reducer。
-
事件总线(Event Bus):
事件总线是一种发布-订阅模式的实现,允许不直接相关的组件通过中央事件总线进行通信。
-
第三方库和工具:
可以使用一些第三方库和工具来实现更高级的组件通信,例如 Mobx、GraphQL、Apollo Client 等。
选择适合您的应用程序需求的通信方式取决于您的项目复杂性和结构。对于简单的通信,Props 和回调函数通常足够;而对于大型应用程序,可能需要使用 Context、Redux 等状态管理工具来更好地管理和组织数据。
8.hooks 、useEffect、 uselazyEffect
React Hooks 是 React 16.8 引入的一组函数,用于让函数组件拥有类组件的状态管理和生命周期方法的能力。其中,useEffect
和 useLayoutEffect
是两个用于处理副作用(如数据获取、订阅、手动 DOM 操作等)的钩子,而 useMemo
和 useCallback
则用于优化性能。目前,React 并没有官方提供名为 uselazyEffect
的 Hook。
下面我将介绍 useEffect
和 useLayoutEffect
这两个常用的副作用 Hook。
useEffect
useEffect
是 React 提供的用于在函数组件中处理副作用的 Hook。它在组件渲染后执行,可以用于处理诸如数据获取、订阅、DOM 操作等副作用任务。useEffect
接受两个参数:一个函数和一个依赖数组。
javascript
复制代码
import React, { useEffect, useState } from 'react'; function Example() { const [count, setCount] = useState(0); useEffect(() => { // 在组件渲染后执行的副作用代码 document.title = `Count: ${count}`; }, [count]); // 仅在 count 改变时执行 return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } export default Example;
上述示例中,useEffect
在 count
改变时会更新文档标题。依赖数组 [count]
指定了只有 count
发生变化时,useEffect
才会运行。
useLayoutEffect
useLayoutEffect
与 useEffect
类似,但它会在 DOM 更新之后、浏览器 layout 之前执行。这使得它更适合一些需要立即操作 DOM 的情况。useLayoutEffect
的使用方式与 useEffect
相同,只是它会更快地执行副作用代码。
javascript
复制代码
import React, { useLayoutEffect, useState } from 'react'; function Example() { const [width, setWidth] = useState(0); useLayoutEffect(() => { function updateWidth() { setWidth(window.innerWidth); } window.addEventListener('resize', updateWidth); updateWidth(); // 确保在渲染前更新宽度 return () => { window.removeEventListener('resize', updateWidth); }; }, []); return <p>Window width: {width}px</p>; } export default Example;
在这个示例中,useLayoutEffect
用于监听窗口大小的变化,并实时更新 width
变量。
需要注意的是,由于 useLayoutEffect
可能会对性能造成影响,因此建议仅在确实需要同步 DOM 操作的情况下使用它,否则使用 useEffect
更合适。 React 文档建议,如果不需要立即执行的 DOM 操作,应首选使用 useEffect
。
9.react组件优化
React 组件优化是一项重要的任务,可以提高应用程序的性能和用户体验。以下是一些常见的 React 组件优化技巧:
-
使用函数组件和 React Hooks:
- 函数组件通常比类组件具有更好的性能,因为它们更轻量化。
- 使用 React Hooks(如useState、useEffect、useMemo)来管理组件的状态和副作用,以减少不必要的渲染。
-
避免不必要的渲染:
- 使用
shouldComponentUpdate
方法或React.memo
高阶组件来防止不必要的组件渲染。 - 使用
PureComponent
或React.memo
来避免在 props 或 state 没有变化时触发的渲染。
- 使用
-
使用React的异步更新机制:
- 使用
setState
的回调函数或者useEffect
来确保在组件状态更新后进行相应的操作,而不会触发额外的渲染。
- 使用
-
避免内联函数:
- 避免在渲染方法中创建内联函数,因为它们可能会导致不必要的函数重复创建。可以将这些函数提升到组件的构造函数或使用
useCallback
来进行优化。
- 避免在渲染方法中创建内联函数,因为它们可能会导致不必要的函数重复创建。可以将这些函数提升到组件的构造函数或使用
-
分割大型组件:
- 如果一个组件变得过于复杂,可以将其拆分成多个小型组件,每个组件专注于特定的任务。
-
使用列表的
key
属性:- 在渲染列表时,为每个列表项提供唯一的
key
属性,以帮助 React 更有效地管理列表项的更新。
- 在渲染列表时,为每个列表项提供唯一的
-
懒加载组件:
- 使用React的懒加载功能(如
React.lazy
和Suspense
)来按需加载组件,以减少初始加载时的资源占用。
- 使用React的懒加载功能(如
-
使用Memoization:
- 使用
useMemo
或React.memo
来记忆组件的渲染结果,以避免不必要的计算。
- 使用
-
性能分析工具:
- 使用 React DevTools 或其他性能分析工具来识别组件渲染的瓶颈,并找出性能问题的根本原因。
-
服务器端渲染(SSR):
- 对于需要更快的初始加载时间的应用程序,考虑使用服务器端渲染来提供更快的首次渲染。
-
使用组件级别的代码拆分:
- 使用动态导入(Dynamic Import)来按需加载组件,以减少初始包大小。
-
使用Memo组件:
- 如果组件有昂贵的计算成本或者渲染成本,可以考虑使用
React.memo
或useMemo
来缓存组件的输出。
- 如果组件有昂贵的计算成本或者渲染成本,可以考虑使用
-
避免不必要的全局状态:
- 避免在全局状态中存储不必要的数据,只在需要的组件中共享状态。
-
组件的拆分和组合:
- 将复杂的组件拆分为多个小组件,并使用组件的组合来构建界面。
-
使用React Profiler:
- React Profiler 是 React DevTools 的一部分,可用于分析组件的渲染性能,找出哪些组件渲染时间较长。
React 组件优化是一个持续改进的过程,需要根据具体的应用和性能需求来调整和优化。始终使用性能分析工具来帮助确定优化的地方,并在实际测试中验证性能的改善。
10.高阶组件
高阶组件(Higher-Order Component,HOC)是一种在 React 中用于复用组件逻辑的高级技术。它本质上是一个函数,接受一个组件作为参数并返回一个新的组件。高阶组件在 React 应用中非常有用,因为它可以帮助你在不修改原始组件的情况下添加或修改功能。
以下是高阶组件的一些常见用法和示例:
-
添加新的 Props: 高阶组件可以接受一些数据,并将其作为 props 传递给包装的组件。这可以用于向组件注入一些共享的数据或配置信息。
jsx
复制代码
const withUserData = (WrappedComponent) => { const userData = { name: 'John', age: 30 }; return (props) => <WrappedComponent {...props} userData={userData} />; }; const UserComponent = ({ userData }) => ( <div> <p>Name: {userData.name}</p> <p>Age: {userData.age}</p> </div> ); const UserComponentWithUserData = withUserData(UserComponent);
-
条件渲染: 高阶组件可以根据一些条件来决定是否渲染包装组件。这可以用于创建权限控制的组件。
jsx
复制代码
const withAuthorization = (WrappedComponent, allowedRoles) => { // Check user's role here const userRole = 'admin'; return userRole === allowedRoles ? <WrappedComponent /> : <div>Access Denied</div>; }; const AdminDashboard = () => <div>Admin Dashboard</div>; const ProtectedAdminDashboard = withAuthorization(AdminDashboard, 'admin');
-
包装组件生命周期方法: 高阶组件可以包装组件的生命周期方法,以添加额外的行为或副作用。
jsx
复制代码
const withLogger = (WrappedComponent) => { return class extends React.Component { componentDidMount() { console.log('Component did mount'); } render() { return <WrappedComponent {...this.props} />; } }; }; const MyComponent = () => <div>Hello, World!</div>; const MyComponentWithLogger = withLogger(MyComponent);
-
状态管理: 高阶组件可以用于管理某些状态,将状态传递给包装的组件或在需要时进行更新。
jsx
复制代码
const withCounter = (WrappedComponent) => { return class extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } increment = () => { this.setState({ count: this.state.count + 1 }); } render() { return <WrappedComponent {...this.props} count={this.state.count} increment={this.increment} />; } }; }; const CounterComponent = ({ count, increment }) => ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); const CounterComponentWithCounter = withCounter(CounterComponent);
高阶组件是一种强大的工具,可以帮助你在 React 中实现复杂的功能和逻辑复用。但要小心不要滥用它们,以免导致代码变得复杂难以维护。通常,建议在需要复用组件逻辑时使用高阶组件,而在其他情况下优先考虑使用 React Hooks。
作者:我代码真的菜
链接:https://juejin.cn/post/7278299358910201897
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这篇关于真实前端面试题(蚂蚁外包)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!