[TOC]
### 1. 什么是闭包,形成的条件,如何解决
~~~
什么是闭包
1. 闭包是指函数可以访问其词法作用域之外的变量,即使在其外部函数执行完毕后仍然可以访问这些变量
~~~
~~~
形成的条件 (函数嵌套,内部函数对变量引用,外部函数返回内部函数的引用)
1. 内部函数引用外部函数的变量:内部函数必须引用外部函数中的变量,也就是说内部函数在词法作用域中引用了外部函数的变量。
2. 外部函数执行完毕后仍然存在对内部函数的引用:在外部函数执行完毕后,如果仍然存在对内部函数的引用(例如将内部函数作为返回值返回或将其赋值给其他变量),则形成了闭包。
~~~
~~~
如何解决
对函数引用设置为空,inner = null,inner = undefined
~~~
~~~
以下是一个闭包的例子:
function outerFunction() {
var outerVariable = 'Hello';
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
var inner = outerFunction();
inner(); // 输出: Hello
在这个例子中,`outerFunction` 是外部函数,它包含一个内部函数 `innerFunction`。`outerFunction` 内部定义了一个变量 `outerVariable`,然后将 `innerFunction` 返回。我们将返回的函数赋值给变量 `inner`。当我们调用 `inner()` 时,它会访问 `outerVariable` 的值,并将其打印到控制台上。
在这个例子中,虽然 `outerFunction` 已经执行完毕,但是 `innerFunction` 仍然可以访问 `outerVariable`。这是因为 `innerFunction` 形成了一个闭包,它可以记住 `outerFunction` 的词法环境,包括其中定义的变量。
闭包可以用于许多场景,例如私有变量、模块化等。它的优点包括可以隐藏变量和函数,并且使代码更加模块化和可维护。但是需要注意的是,闭包可能导致内存泄漏,因为嵌套函数中的变量无法被垃圾回收。因此,在使用闭包时需要注意内存管理。
~~~
### 2. 什么是防抖,什么是截流
防抖和节流都是用于控制函数执行频率的技术,以提高性能和优化用户体验。
防抖(Debounce)是指在触发事件后,等待一定时间(如1000毫秒),如果在这个时间内没有再次触发相同事件,则执行该事件。如果在这个时间内再次触发了相同事件,则重新计时。
防抖常用于处理频繁触发的事件,例如输入框的输入事件。当用户连续输入时,防抖可以确保只在用户停止输入后才执行相应的操作,以避免过多的计算或请求。
以下是一个防抖的示例代码:
~~~
1function debounce(func, delay) {
2 let timeoutId;
3
4 return function() {
5 clearTimeout(timeoutId);
6 timeoutId = setTimeout(func, delay);
7 };
8}
9
10// 使用防抖函数来处理输入框的输入事件
11const input = document.querySelector('input');
12input.addEventListener('input', debounce(function() {
13 // 执行相应的操作
14}, 1000));
~~~
节流(Throttle)是指在一定时间间隔内只执行一次函数。当触发事件后,函数会立即执行,然后在设定的时间间隔内忽略后续的事件触发。
节流常用于处理高频率触发的事件,例如滚动事件和窗口大小调整事件。通过节流可以限制函数的执行频率,避免过度的计算或操作。
以下是一个节流的示例代码:
~~~
1function throttle(func, delay) {
2 let timeoutId;
3 let lastExecTime = 0;
4
5 return function() {
6 const currentTime = Date.now();
7 const remainingTime = delay - (currentTime - lastExecTime);
8
9 clearTimeout(timeoutId);
10
11 if (remainingTime <= 0) {
12 func();
13 lastExecTime = currentTime;
14 } else {
15 timeoutId = setTimeout(func, remainingTime);
16 }
17 };
18}
19
20// 使用节流函数来处理滚动事件
21window.addEventListener('scroll', throttle(function() {
22 // 执行相应的操作
23}, 1000));
~~~
在使用防抖和节流时,需要根据实际场景和需求选择合适的技术。防抖适用于需要等待一段时间后再执行的情况,而节流适用于需要限制函数执行频率的情况。
#### 总结:防抖截流都是使用setTimeout实现,区别是在一定时间内再此触发重新计算时间,一段时间内重复触发忽略不计的是截流
### useMemo、useCallback和 React.memo比较
`useMemo`、`useCallback` 和 `React.memo` 都是 React 中用于性能优化的机制,但它们的作用和用法有所不同。
1. `useMemo`: `useMemo` 是用来缓存计算结果的钩子函数。它接收一个函数和依赖数组,并返回函数的计算结果。当依赖数组中的值发生变化时,`useMemo` 会重新执行函数并返回新的结果,否则会返回上一次的缓存结果。用于避免不必要的计算和重新渲染。
~~~
1const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
~~~
2. `useCallback`: `useCallback` 是用来缓存函数引用的钩子函数。它接收一个函数和依赖数组,并返回一个经过缓存的函数引用。当依赖数组中的值发生变化时,`useCallback` 会返回新的函数引用,否则会返回上一次缓存的函数引用。用于避免不必要的函数重新创建,并在传递给子组件时优化性能。
~~~
1const memoizedCallback = useCallback(() => { /* 处理函数逻辑 */ }, [dependency1, dependency2]);
~~~
3. `React.memo`: `React.memo` 是一个高阶组件(Higher Order Component),用于包装函数组件。它会对组件的输入的 `props` 进行浅比较,如果 `props` 没有发生变化,就会直接使用上一次的渲染结果,避免不必要的重渲染。
~~~
1const MemoizedComponent = React.memo(Component);
~~~
`useMemo` 和 `useCallback` 都是在函数组件内部使用的钩子函数,用于优化计算和函数引用,而 `React.memo` 是用于优化组件的输出。它们可以结合使用,以进一步提高性能和避免不必要的计算和渲染。
### 受控组件和非受控组件
~~~
受控组件和非受控组件是React中两种不同的组件状态管理方式。
受控组件是指组件的状态(比如input的值)由React来管理。当用户输入时,React会更新组件的状态,并且通过props将新的状态传递给组件。受控组件需要在组件中定义一个事件处理函数来更新状态,并且在render方法中通过props将更新后的状态传递给组件。
非受控组件是指组件的状态由组件自身来管理。当用户输入时,组件直接更新自身的状态,而不是通过React来管理。非受控组件不需要在组件中定义事件处理函数来更新状态,而是通过ref来获取输入框的值。
受控组件的优点是可以精确控制组件的状态,可以对用户的输入进行验证和处理。非受控组件的优点是简单且易于使用。
在实际开发中,可以根据具体的需求选择使用受控组件或非受控组件。如果需要对用户输入进行复杂的验证和处理,或者需要将组件的状态在不同组件之间共享,可以使用受控组件。如果只是简单地获取用户输入的值,或者需要处理大量的输入框,可以使用非受控组件。
~~~
### Node.js的事件循环机制
~~~
Node.js的事件循环机制是其运行时环境的核心之一。它使用单线程来处理所有的I/O操作,并利用事件循环机制来实现非阻塞的异步操作。
下面是Node.js事件循环的基本流程:
1. 执行全局代码:Node.js在启动时会执行全局代码,并初始化一些内置模块和变量。
2. 执行事件循环(Event Loop):事件循环是一个无限循环,它会不断地从事件队列中取出事件并执行对应的回调函数。
3. 执行阶段划分:事件循环将事件的执行分为不同的阶段,每个阶段都有对应的任务队列。常见的阶段包括定时器(Timers)、I/O回调(I/O Callbacks)、闲置(Idle)、轮询(Polling)、检查(Check)和关闭回调(Close Callbacks)。
4. 执行任务队列:在每个阶段中,事件循环会检查对应的任务队列是否有待执行的任务,如果有,则按照顺序执行它们的回调函数。
5. 事件循环一直执行,直到没有事件可处理或达到了退出条件。
值得注意的是,事件循环机制中的每个阶段都有一个特定的顺序和执行时间,不同的阶段之间可能会存在优先级关系。在执行完一个阶段的所有任务之后,事件循环会检查是否需要触发下一个阶段,如果需要则继续执行,否则就会等待新的事件进入事件队列。
Node.js的事件循环机制使得它能够高效地处理大量的并发请求,同时也能够避免阻塞导致的性能问题。通过合理地利用回调函数和事件驱动的思想,Node.js可以实现非阻塞的异步编程模型。
~~~
### Chair 和 Koa 区别
~~~
Chair 和 Koa 是两个不同的 Node.js 框架,它们有以下区别:
1. 生态系统:Koa 是一个非常流行的 Node.js 框架,拥有庞大的社区和广泛的生态系统。它有很多可用的中间件和插件,可以方便地扩展和定制。而 Chair 是一个相对较新的框架,生态系统相对较小,可用的中间件和插件也较少。
2. 设计哲学:Koa 的设计哲学是极简主义,它提供了一套简洁而灵活的 API 集合,允许开发者自由组合中间件来构建自己想要的功能。而 Chair 是一个面向企业级应用的框架,它提供了更多的开箱即用的功能和工具,以便于开发者快速搭建和部署应用。
3. 异步支持:Koa 是基于 async/await 的异步编程模型,通过使用 async 函数来处理中间件。这使得编写异步代码变得更加简单和直观。而 Chair 目前仍然使用传统的回调函数和 Promise 链式调用来处理异步操作,不直接支持 async/await。
4. 框架大小和性能:由于 Koa 的设计理念是极简主义,它的核心代码体积较小,相对而言性能较高。而 Chair 的设计目标是提供更多的功能和工具,可能会导致相对较大的代码体积,并且在某些场景下性能可能稍逊于 Koa。
综上所述,Koa 是一个成熟、流行且灵活的 Node.js 框架,适用于构建中小型应用和个人项目。而 Chair 则更适合处理复杂的企业级应用,提供了更多的功能和工具,但生态系统相对较小。选择使用哪个框架取决于项目需求以及开发者个人偏好。
~~~
### 浏览器缓存
~~~
浏览器缓存是浏览器在本地存储和重复使用已经获取的资源的一种机制。它可以提高网页加载速度和减轻服务器负载,同时减少用户的数据使用量。
浏览器缓存的工作原理如下:
1. 当用户第一次访问一个网页时,浏览器会请求服务器获取网页的内容,并将内容存储在本地的缓存中。
2. 在用户再次访问相同网页时,浏览器会首先检查缓存,如果有缓存的副本,并且副本尚未过期,浏览器会直接从缓存中加载资源,而不是向服务器发送请求。
3. 如果缓存的副本已过期或者用户强制刷新页面,浏览器会向服务器发送请求,服务器会返回最新的资源,并更新缓存。
浏览器缓存可以分为两种类型:强缓存和协商缓存。
* 强缓存:当浏览器访问一个资源时,如果该资源的缓存策略设置了强缓存,并且缓存未过期,浏览器会直接从缓存中加载资源,而不会发送请求到服务器。常用的强缓存策略有设置`Expires`响应头和`Cache-Control`响应头(比如`max-age`)。
* 协商缓存:当浏览器访问一个资源时,如果该资源的缓存策略设置了协商缓存,浏览器会发送请求到服务器,服务器会通过比较资源的标识(比如`ETag`和`Last-Modified`响应头)来决定是否返回新的资源。如果资源未变化,服务器会返回`304 Not Modified`状态码,浏览器会从缓存中加载资源,否则服务器会返回新的资源。
开发者可以通过设置响应头来控制浏览器缓存行为,以提高网页性能和用户体验。但需要注意,应该正确地设置缓存策略,避免在资源更新时用户仍然加载旧的资源。
~~~
### 浏览器本身的缓存用什么做
~~~
浏览器本身的缓存通常使用硬盘上的文件系统来存储缓存的资源。具体来说,浏览器缓存使用以下几种方式来存储缓存资源:
1. 磁盘缓存:浏览器将资源保存在磁盘上的缓存文件夹中。这些文件可以是普通的文件,也可以是特定格式的文件,例如WebP格式的图片。
2. 内存缓存:浏览器可以将一部分资源保存在内存中的缓存中,以提高资源的读取速度。内存缓存更快,但容量较小,适用于频繁访问的资源。
3. HTTP 缓存控制:浏览器通过 HTTP 头部中的缓存控制字段(如 Cache-Control 和 Expires)来判断是否使用缓存。这些字段指示浏览器缓存资源的时间和有效期。
4. 数据库缓存:某些浏览器使用数据库存储来缓存资源。这种方式通常用于存储用户的表单数据、Cookie 信息等。
需要注意的是,浏览器缓存是有限的,当缓存达到一定大小或者存储时间过长时,浏览器可能会自动清理缓存。用户也可以手动清除浏览器缓存,以释放磁盘空间或者清除过时的缓存数据。
浏览器缓存不仅可以提高网页的加载速度,还可以减少对服务器的请求,节省网络资源和流量。同时,合理配置缓存策略也能够提高网站的性能和用户体验。
~~~
### 304原理
~~~
304状态码是HTTP协议中的一种响应状态码,表示资源未修改,客户端可以使用缓存的副本。当客户端发送一个GET请求获取某个资源时,服务器会根据请求头中的`If-None-Match`和`If-Modified-Since`字段检查资源的标识和最后修改时间,然后做出相应的处理。
下面是304状态码的工作原理:
1. 客户端发送GET请求到服务器,并在请求头中包含`If-None-Match`和`If-Modified-Since`字段,分别表示资源的标识和最后修改时间。
2. 服务器接收到请求后,会根据请求头中的`If-None-Match`和`If-Modified-Since`字段来判断资源是否未修改。如果资源未修改,则服务器会返回304状态码。
3. 客户端接收到304状态码后,表示服务器的资源未修改,客户端可以直接从本地缓存中获取资源。
4. 如果客户端本地没有缓存或者缓存已过期,客户端会发送新的请求到服务器,服务器会正常返回最新的资源。
通过使用304状态码,可以减少不必要的数据传输和服务器负载,提高网页的加载速度和用户体验。服务器可以在响应头中设置`ETag`和`Last-Modified`字段,用于标识资源和记录最后修改时间,以便客户端下次请求时进行比较。如果资源未修改,服务器可以直接返回304状态码,而不用返回整个资源内容。这样可以减少网络传输量和服务器的响应时间。
~~~
### etag 防重
~~~
ETag(实体标签)是HTTP协议中用于标识资源的一种机制,它可以用于防止重放攻击(replay attack)。
防重放攻击是指攻击者通过截获和重放合法用户的请求来进行欺骗或伪造身份。为了防止这种攻击,服务器可以使用ETag来标识每个资源,并将ETag值随着响应一起发送到客户端。
客户端在发送请求时,会在请求头中包含`If-None-Match`字段,该字段的值为上次请求中服务器返回的ETag值。服务器在接收到请求后,会将请求头中的`If-None-Match`值与当前资源的ETag值进行对比。
如果两个值相同,表示客户端请求的资源与服务器上的资源一致,服务器会返回304状态码,告诉客户端直接使用缓存的资源。
如果两个值不同,表示客户端请求的资源与服务器上的资源不一致,服务器会返回200状态码,并返回最新的资源。
通过使用ETag来进行资源比对,可以避免客户端重复请求相同的资源,节省网络带宽和服务器资源。同时,攻击者无法重放以前的ETag值来获取资源,因为只有与当前资源一致的ETag才能通过对比。这有效地防止了重放攻击。
~~~
### 登录原理
~~~
登录原理是指用户在访问应用程序或网站时,通过提供凭证(如用户名和密码)来验证身份,并获取访问权限的过程。下面是一个基本的登录原理:
1. 用户访问应用程序或网站的登录页面,并输入用户名和密码。
2. 应用程序或网站将用户提供的用户名和密码发送到服务器进行验证。
3. 服务器接收到用户名和密码后,首先会对用户名进行检查,确认用户是否存在。
4. 如果用户存在,服务器会通过加密算法将用户输入的密码与数据库中存储的密码进行比对。如果密码匹配,则验证成功。
5. 服务器会生成一个令牌(Token)或Session ID,作为用户的身份标识,并将令牌返回给客户端。
6. 客户端(如浏览器)将令牌存储在本地,通常以Cookie或者本地存储的形式保存。
7. 在用户继续访问应用程序或网站的其他页面时,客户端会将令牌作为请求头或参数发送到服务器。
8. 服务器接收到请求后,会验证令牌的有效性。如果令牌有效,则表示用户已经登录,并根据用户的权限和角色进行相应的处理。
登录原理的关键在于用户提供的凭证与服务器存储的凭证进行比对,并生成有效的身份标识。这种身份标识在后续的请求中用于验证用户的身份,以确保用户拥有相应的权限和访问权限。同时,为了保证安全性,密码通常会进行加密存储,并使用SSL/TLS等加密协议来保护通信过程中的敏感信息。
~~~
### hooks和生命周期区别
~~~
Hooks 是 React 提供的一种函数式编程方式,用于在函数组件中添加和管理状态以及其他 React 功能。而生命周期是指组件在不同阶段的运行过程中,会触发一系列的方法。
下面是 Hooks 和生命周期之间的区别:
1. 代码组织方式:生命周期方法是通过在类组件中声明和定义的,而 Hooks 是通过在函数组件中使用特定的 Hook 函数来添加和管理状态以及其他 React 功能。Hooks 支持将逻辑相关的代码进行组合,使得组件代码更加清晰、易于理解和维护。
2. 使用方式:生命周期方法是在特定的时间点被自动触发的,而 Hooks 则是在组件的顶层进行调用。Hooks 是基于函数调用的方式,可以在组件内部的任意位置调用,而不需要遵循特定的声明顺序。
3. 逻辑复用:Hooks 提供了一系列的 Hook 函数,如 useState、useEffect、useContext 等,可以方便地复用组件逻辑。而生命周期方法在类组件中是分散在不同的生命周期阶段中的,逻辑复用相对较为复杂。
4. 性能优化:Hooks 的设计可以更好地优化组件的性能。例如,使用 useEffect 可以避免在每次渲染时都重复执行副作用代码,而只在依赖项变化时执行。这样可以更精细地控制组件的更新。
总的来说,Hooks 是一种更现代、更灵活的编程方式,可以使函数组件具备类组件的状态管理和其他功能。它提供了更好的代码组织方式、逻辑复用和性能优化。生命周期方法则是在类组件中使用的一套方法,用于管理组件的生命周期。在使用 React 开发时,可以根据实际情况选择使用 Hooks 或生命周期方法
~~~
### 如何保障项目安全上线,监控
~~~
确保项目安全上线和监控是一个重要的任务,以下是一些常见的实践和建议:
1. 安全审计和代码审查:在项目上线之前,进行全面的安全审计和代码审查,以发现和修复潜在的安全漏洞和代码问题。可以使用静态代码分析工具、漏洞扫描工具等进行检查。
2. 访问控制和权限管理:确保项目的访问控制机制和权限管理系统健全有效,只有授权的用户才能访问和操作敏感数据和功能。
3. 强化身份验证:使用适当的身份验证机制,如多因素身份验证、单点登录(SSO)等,确保用户的身份安全。
4. 数据加密和保护:对敏感数据进行加密存储和传输,使用合适的加密算法和密钥管理机制,以防止数据泄露。
5. 定期更新和修补漏洞:保持项目的软件和依赖库等组件的最新版本,并及时修补已知的安全漏洞。
6. 安全培训和意识提升:对项目团队进行安全培训,提高安全意识和技能,确保他们了解和遵守安全最佳实践。
7. 实施安全监控和日志记录:建立完善的安全监控和日志记录系统,监测项目的运行状况和安全事件,及时发现和应对潜在的安全威胁。
8. 漏洞管理和应急响应:建立漏洞管理和应急响应计划,制定相应的流程和措施,以应对可能的安全事件和漏洞。
9. 定期安全评估和渗透测试:定期进行安全评估和渗透测试,以发现潜在的安全弱点和漏洞,并及时修复。
10. 合规和法规遵循:确保项目符合适用的法律法规和合规要求,例如数据保护、隐私保护等。
通过综合以上措施和实践,可以提高项目的安全性和稳定性,并及时响应和应对潜在的安全风险和威胁。
~~~
### `bind`、`call`和`apply`
~~~
`bind`、`call`和`apply`都是JavaScript中用于改变函数执行上下文(即函数内部的`this`值)的方法,它们的区别如下:
1. `bind`方法:`bind`方法会创建一个新函数,并将指定的对象绑定为新函数内部的`this`值。它不会立即执行函数,而是返回一个绑定了`this`值的函数。我们可以稍后调用该函数来执行,并传递参数。
2. `call`方法:`call`方法会立即调用函数,并将指定的对象绑定为函数内部的`this`值。我们可以通过逗号分隔的方式,将参数依次传递给函数。
3. `apply`方法:`apply`方法也会立即调用函数,并将指定的对象绑定为函数内部的`this`值。不同之处在于,`apply`方法接受一个参数数组,而不是逗号分隔的参数列表。
以下是关于这些方法的示例:
~~~
1const person = {
2 name: "John",
3 age: 30,
4};
5
6function sayHello(greeting) {
7 console.log(greeting + ", " + this.name);
8}
9
10const boundFunction = sayHello.bind(person); // 创建一个新函数并绑定person对象
11boundFunction("Hello"); // 输出: Hello, John
12
13sayHello.call(person, "Hi"); // 直接调用函数,绑定person对象和传递参数
14// 输出: Hi, John
15
16sayHello.apply(person, ["Welcome"]); // 直接调用函数,绑定person对象和传递参数数组
17// 输出: Welcome, John
~~~
在上述示例中,我们定义了一个`person`对象和一个`sayHello`函数。通过使用`bind`、`call`和`apply`方法,我们可以将`person`对象作为函数`sayHello`的`this`值,并调用该函数。
需要注意的是,`bind`方法返回的是一个新函数,而`call`和`apply`方法会立即执行函数。另外,`call`和`apply`方法在参数的传递方式上有所不同,`call`方法使用逗号分隔的参数列表,而`apply`方法接受一个参数数组。
这些方法在实际开发中经常用于确定函数的执行上下文,并传递参数。通过选择正确的方法,我们可以更灵活地控制函数的执行方式。
~~~
### 手写bind call apply
~~~
当你需要手动实现`bind`、`call`和`apply`方法时,可以按照以下方式进行:
1. 实现`bind`方法:
~~~
1Function.prototype.myBind = function(context, ...args) {
2 const fn = this;
3 return function(...innerArgs) {
4 return fn.apply(context, args.concat(innerArgs));
5 };
6};
~~~
2. 实现`call`方法:
~~~
1Function.prototype.myCall = function(context, ...args) {
2 const fn = this;
3 context = context || window;
4 context.fn = fn;
5 const result = context.fn(...args);
6 delete context.fn;
7 return result;
8};
~~~
3. 实现`apply`方法:
~~~
1Function.prototype.myApply = function(context, args) {
2 const fn = this;
3 context = context || window;
4 context.fn = fn;
5 const result = context.fn(...args);
6 delete context.fn;
7 return result;
8};
~~~
在上述代码中,我们使用了原型链的方式,将这些方法添加到`Function`对象的原型上,以使所有的函数实例都可以调用它们。然后,我们在每个方法的实现中,通过`this`关键字来引用调用方法的函数。我们使用剩余参数(`...args`)来接收传入的参数,然后在适当的时候调用`apply`方法传递参数。
需要注意的是,在`call`和`apply`方法中,我们对未提供上下文(`context`)的情况做了处理,将上下文设置为全局对象(`window`)。
这些手写的实现并不是完全准确,仅供参考。在实际开发中,建议使用原生的`bind`、`call`和`apply`方法,因为它们已经经过优化和测试,能够更好地处理各种情况和边界条件。
~~~