2022届秋招保利威前端面试
原文网址:https://www.nowcoder.com/discuss/353158719997419520?sourceSSR=search
对象的浅拷贝和深拷贝如何实现?
浅拷贝:
浅拷贝只复制对象的第一层属性,不会递归复制嵌套对象。
使用Object.assign()
Object.assign()
静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象。
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);
// Expected output: Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget === target);
// Expected output: true
使用展开运算符
let obj1 = {name: "jack",age: 18}let obj2 = {}obj2 = { ...obj1 }console.log(obj2)输出{"name": "jack","age": 18
}
深拷贝
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
使用第三方库_.cloneDeep()
const _ = require('lodash');
const obj1 = {a: 1,b: { f: { g: 1 } },c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
使用JSON.stringify()
let newobj = JSON.parse(JSON.stringify(obj))//深拷贝
JSON.stringify()
方法将一个 JavaScript 对象或值转换为 JSON 字符串
JSON.parse()
方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象
但是这种方式存在弊端,会忽略undefined
、symbol
和函数
1.如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象;
2.如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象;
3.如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
4.如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
5.JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;
6.如果对象中存在循环引用的情况也无法正确实现深拷贝;
手写深拷贝(通过递归实现)
function checkType(target){return Object.prototype.toString.call(target).slice(8,-1)
}
function deepClone(data){let type = checkType(data)let objif(type === 'Array'){obj = []for(let i = 0; i < data.length; i++){obj.push(deepClone(data[i]))}}else if(type === 'Object'){obj = {}for(const key in data){if(data.hasOwnProperty(key)){obj[key] = deepClone(data[key])}}}else{return data}return obj
}
事件委托是什么?有什么例子?为什么要绑定在父元素而不是子元素?
事件委托(Event delegation)是一种事件处理模式,它将事件处理程序绑定到一个父元素上,以代替直接在子元素上进行事件绑定。当事件触发时,事件会冒泡到父元素,然后由父元素的事件处理程序来处理。
举个例子,假设有一个HTML列表,其中包含多个列表项(li元素)。如果要为每个列表项添加点击事件处理程序,传统的做法是为每个列表项分别绑定事件处理程序。而采用事件委托的方式,可以将事件处理程序绑定到整个列表的父元素上,然后通过事件冒泡机制来处理每个列表项的点击事件。
<ul id="myList"><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li>
</ul>
为什么要将事件绑定在父元素而不是子元素呢?有以下几个原因:
- 减少事件绑定的数量:通过事件委托,只需要为父元素绑定一个事件处理程序,而不是为每个子元素分别绑定。这在处理大量元素时可以提高性能和代码的可维护性。
- 动态添加或删除元素:如果通过JavaScript动态地添加或删除子元素,事件委托可以自动地处理这些新添加或删除的元素的事件,无需手动重新绑定事件处理程序。
- 简化代码结构:通过将事件处理程序集中在父元素上,可以简化代码结构,减少重复的代码,并使代码更易读和维护
axios的返回值是promise,请说说你对promise的理解?
Promise(承诺)是JavaScript中用于处理异步操作的对象。它代表了一个尚未完成但最终会完成的操作,并可以获取其结果或错误。
Promise有三种状态:
- Pending(进行中):初始状态,表示操作尚未完成。
- Fulfilled(已完成):表示操作成功完成。
- Rejected(已拒绝):表示操作失败。
当创建一个Promise对象时,可以传入一个执行器函数(executor function),该函数接受两个参数:resolve和reject。通过调用resolve函数,可以将Promise从Pending状态转变为Fulfilled状态,并传递一个值作为操作的结果。通过调用reject函数,可以将Promise从Pending状态转变为Rejected状态,并传递一个错误对象作为操作的原因。
Promise对象具有then方法,它接受两个回调函数作为参数:onFulfilled和onRejected。当Promise状态变为Fulfilled时,会调用onFulfilled回调函数,并传递操作结果作为参数;当Promise状态变为Rejected时,会调用onRejected回调函数,并传递错误对象作为参数。
Promise还提供了其他方法,例如catch方法用于捕获错误、finally方法用于指定无论Promise状态如何都要执行的回调函数等。
Promise的主要优势在于它简化了处理异步操作的流程和提供了更好的代码结构。它可以避免回调地狱(callback hell)的问题,使异步操作更易于理解和组织。通过使用Promise,可以将异步代码写成连续的链式调用,使其更具可读性和可维护性。
对于axios来说,它返回的是一个Promise对象,可以通过调用then方法来处理异步请求的结果,或者通过catch方法来捕获可能发生的错误。这样可以更方便地进行异步请求的处理和错误处理。
vue生命周期的理解
按照套路背就好了
v-for的key的作用是什么?
在Vue.js 2中,key
属性用于辨识和跟踪在使用v-for
指令时渲染的DOM元素的身份。它在Vue的虚拟DOM算法中起着重要的作用。
当使用v-for
指令循环渲染一个数据列表时,Vue会生成一组DOM元素来表示每个列表项。为了优化性能和提高渲染效率,Vue会尽可能地重用已经存在的DOM元素,而不是重新创建新的元素。
这就引出了一个问题:如何判断两个元素是否是相同的,并且可以重用?这就是key
属性的作用。Vue使用key
属性来标识每个生成的DOM元素的唯一性。它们不是组件的属性,而是Vue特定的属性。
当数据发生变化,Vue会比较新的数据和旧的数据,并根据key
属性来判断哪些元素需要被更新、重用或删除。如果两个元素具有相同的key
,Vue会假定它们是相同的元素,从而重用之前的DOM元素,避免不必要的重新渲染。
key
属性的原理可以总结如下:
1.key属性必须是唯一的,通常使用具有唯一标识的数据来生成。
2.在使用v-for指令渲染列表时,为每个列表项添加一个key属性。
3.当数据更新时,Vue会比较新旧数据,并根据key属性来确定哪些元素需要进行重新渲染、重用或删除。
4.如果两个元素具有相同的key,Vue会假定它们是相同的元素,并重用之前的DOM元素。
通过合理使用key
属性,可以最大限度地提高Vue应用的性能和渲染效率,特别是在涉及列表渲染的情况下。
虚拟DOM中key的作用
key是虚拟DOM对象的标识,当数据发生变化的时候,vue会根据新数据生成新的虚拟DOM,随后进行新虚拟DOM和旧的虚拟DOM比较,比较规则如下:
比较规则:
1.旧虚拟DOM中找到了与新虚拟DOM相同的key:
若虚拟DOM中内容没有发生改变,则直接使用之前的真实DOM
若虚拟DOM中的内容发生改变,则生成新的真实DOM,替换掉页面中真实的DOM
2.旧虚拟DOM中未找到与新虚拟DOM相同的key:
创建新的真实DOM,随后渲染到页面上
使用index作为key可能会发生的问题:
1.若对数据进行逆序添加、逆序删除等破坏顺序的操作:
会产生没有必要的真实DOM更新,界面效果没问题,但是效率低下
2.如果结构中还包含输入类的DOM:
会产生错误的DOM更新,界面有问题
说说vuex的属性?
- State(状态): Vuex的核心是一个包含所有组件共享状态的单一数据源,称为state。State是响应式的,当它发生变化时,所有依赖于它的组件都会自动更新。
- Getters(获取器): Getters用于从state中派生出一些衍生数据,类似于Vue组件中的计算属性。它们可以对state进行过滤、计算和组合,然后在组件中使用。
- Mutations(变更): Mutations是一种修改state的方式。它们是同步的操作,用于处理同步任务。每个mutation都有一个字符串类型的事件类型和一个回调函数,当触发一个mutation时,回调函数会被调用来修改state。
- Actions(动作): Actions用于处理异步任务和复杂的业务逻辑。它们可以包含多个mutation的提交,可以通过调用mutations来间接修改state。Actions可以是异步的,可以执行一些异步操作(如API请求),然后提交一个或多个mutation。
- Modules(模块): Vuex允许将store分割为模块,每个模块拥有自己的state、getters、mutations和actions。这使得大型应用程序的状态管理更加灵活和可维护。
vue-router的history和hash模式的区别?什么时候用history?什么时候用hash
Vue Router提供了两种路由模式:history模式和hash模式。它们在URL的表现形式和浏览器兼容性上有所不同。
- Hash模式:在hash模式下,URL中会以"#“符号来表示路由的路径。例如,
http://example.com/#/about
。这种模式的好处是它在所有现代浏览器中都能正常工作,因为改变hash部分不会导致浏览器向服务器发送请求。但是,它的缺点是URL中带有冗余的”#"符号,不够美观,可能不太友好。 - History模式:在history模式下,URL没有冗余的"#"符号,而是直接使用常规的URL路径来表示路由的路径。例如,
http://example.com/about
。这种模式看起来更加干净和友好。它通过使用HTML5的History API来管理URL,可以动态地修改URL而不刷新页面。然而,history模式在一些较旧的浏览器上可能不被支持,因此需要服务器的支持来处理路由请求。
什么时候使用History模式?
- 当你想要更美观、更符合常规URL的路由路径时,可以使用history模式。
- 当你的应用程序的部署环境可以处理路由请求并返回正确的页面时,可以使用history模式。
- 当你的应用程序使用Vue.js和服务器端渲染(SSR)时,推荐使用history模式,因为SSR可以处理路由请求。
什么时候使用Hash模式?
- 当你的应用程序需要在所有现代浏览器中运行,包括较旧的浏览器时,可以使用hash模式。
- 当你的应用程序部署在没有服务器端支持的环境中(例如静态文件服务器),可以使用hash模式。
请说说你对缓存的理解?还有没有对其他缓存有了解?(除了强缓存、协商缓存)
- 本地缓存(Local Cache): 本地缓存是将数据存储在客户端(通常是内存或磁盘)上的一种缓存方式。它可以减少对服务器的请求次数,提高响应速度。常见的本地缓存技术有:内存缓存(如Redis)、文件缓存和数据库缓存。
- 分布式缓存(Distributed Cache): 分布式缓存是将数据存储在分布式环境中的多个节点上,以提供高可用性和可伸缩性。常见的分布式缓存系统有:Memcached和Redis。
- CDN缓存(Content Delivery Network): CDN缓存是通过将内容分发到位于全球各地的边缘节点上,使用户能够更快地访问数据。CDN缓存可以存储静态文件(如图片、CSS和JavaScript文件)以及动态内容的缓存副本,减轻源服务器的负载并提高用户访问速度。
- 数据库查询缓存: 数据库查询缓存是将数据库查询的结果缓存起来,以避免重复的数据库查询操作。当相同的查询被频繁执行时,可以从缓存中获取结果,减少对数据库的访问压力。
- 对象缓存: 对象缓存是将对象存储在缓存中,以避免重新计算或重新获取对象的开销。对象缓存通常用于存储经过计算或从其他数据源获取的复杂对象,以提高应用程序的性能和响应速度。
强制缓存
不会向服务器发送请求,直接从缓存中读取资源,从chrome控制台中的network选项中可以看到该请求返回的状态码是200
协商缓存
在使用本地缓存之前,需要向服务器发送请求,服务器会根据这个请求的request header中的一些参数来判断是否命中协商缓存,如果名字,则返回304的状态码并带上新的response header通知浏览器从缓存中读取资源,协商缓存可以解决强制缓存的情况下资源不更新的问题
强制缓存中header的参数(响应头)
Expires:response header里的过期时间
Cache-Control:当值设为max-age=数字时,则代表这个请求返回资源的缓存时间
Cache-Control除了该字段外,还有下面几个比较常用的设置值:
no-cache:不使用本地缓存。需要使用协商缓存,先与服务器确认返回的响应是否被修改,如果之前的响应中存在ETag,那么请求的时候会与服务器验证,如果资源未被修改,则可以避免重新下载
no-store:禁止使用浏览器缓存
public:可以被所有用户缓存,包括终端用户和CDN等中间代理服务器
private:只能被终端用户的浏览器缓存,不允许CDN等中间缓存服务器缓存
协商缓存
Cache-Control:no-cache
Last-Modify/If-Modify/Since:浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-Modify是一个时间标识该资源的最后修改时间,当浏览器再次请求资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。 因为Last-Modify的单位是秒,所以1秒钟之内被修改多次是不知道的,所以就有了下面的ETag
Etag/If-none-Match:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识。web服务器收到的请求会根据收到的哈希值进行比较判断资源是否被修改
什么是跨域?如何实现跨域?cors跨域的原理?
跨域(Cross-Origin)是指在前端网页中,当一个请求的源(Origin)与当前页面的源不同,即两个网页的域名、协议或端口不一致时,就会发生跨域问题。
由于安全原因,现代浏览器限制了跨域请求的访问。这是因为如果没有跨域限制,恶意网站就可以通过在用户浏览器中执行脚本来获取其他网站的数据,从而导致安全问题。
要实现跨域请求,有几种常见的方法:
-
JSONP(JSON with Padding):利用
<script>
标签没有跨域限制的特性,通过动态创建<script>
标签来请求其他域的数据。但是 JSONP 只能用于 GET 请求,且需要后端支持返回包裹在函数调用中的 JSON 数据。 -
CORS(Cross-Origin Resource Sharing):CORS 是一种现代浏览器提供的机制,通过在请求和响应头部添加特定的字段,允许跨域访问。CORS 使用预检请求(OPTIONS 请求)来检查服务器是否允许特定的跨域请求,并且支持各种类型的请求方法(GET、POST 等)。
CORS 的原理是在请求和响应的头部添加特定的字段来告知浏览器是否允许跨域访问。当浏览器发起跨域请求时,会首先发送一个 OPTIONS 预检请求,以检查服务器是否支持跨域请求。如果服务器返回的响应中包含允许跨域的头部字段,浏览器就会继续发送实际的请求。
CORS 的关键头部字段包括:
Origin
:指示请求的源,即当前页面的域名。Access-Control-Allow-Origin
:指示服务器允许的跨域请求源。可以设置为具体的域名或使用通配符*
表示允许任意域名的请求。Access-Control-Allow-Methods
:指示服务器允许的请求方法。Access-Control-Allow-Headers
:指示服务器允许的额外请求头部字段。Access-Control-Allow-Credentials
:指示是否允许发送跨域请求的凭据(如 Cookie、HTTP 认证)。
-
代理:通过在同一域名下设置代理服务器,将跨域请求转发到目标服务器,然后将响应返回给前端。这种方法需要在后端进行相应的配置。
简单请求和复杂请求的理解?
在跨域请求中,根据请求的类型和内容的复杂程度,可以将请求分为简单请求(Simple Request)和复杂请求(Complex Request)两种。
简单请求满足以下条件:
- 请求方法是以下之一:GET、POST、HEAD。
- 除了以下的请求头字段之外,没有自定义的请求头。请求头部只包含以下字段:Accept、Accept-Language、Content-Language、Content-Type(只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain)。
简单请求的特点是在发送请求时,浏览器会自动处理跨域请求,不会发送预检请求(OPTIONS 请求),而是直接发送实际请求。简单请求不会触发跨域安全机制的预检步骤,因此服务器的响应头部不需要特殊设置。
对于复杂请求,当请求不满足简单请求的条件时,浏览器会自动发送一个预检请求(OPTIONS 请求)到服务器,以获取服务器允许的跨域规则。
复杂请求的特点包括:
- 使用以下之外的请求方法,如PUT、DELETE等。
- 发送自定义的请求头部字段,或者Content-Type 的值为 application/json 之类的复杂类型。
预检请求中会包含一个Access-Control-Request-Method
字段,用于指示实际请求的方法。服务器在收到预检请求后,会根据该字段判断是否允许实际请求。如果服务器允许跨域请求,会在预检请求的响应头部中设置允许跨域的字段,然后浏览器才会发送实际的请求。
需要注意的是,预检请求是在浏览器自动发送的,并且预检请求和实际请求是分开发送的,因此可能会导致额外的网络开销和延迟。为了避免频繁的预检请求,服务器可以在响应头部中设置Access-Control-Max-Age
字段,指定预检请求的缓存时间,减少预检请求的发送频率。
需要注意的是,浏览器会自动处理简单请求和复杂请求的跨域安全机制,但服务器端也需要进行相应的配置,确保返回正确的响应头部字段,以便浏览器正确处理跨域请求。
说说实现登录有什么方式?这几种实现方式有什么优缺点?如何改进?
实现登录有多种方式,以下是几种常见的方式:
- 基于Session的登录:用户在登录成功后,服务器会创建一个会话(session),并在服务器端保存用户的登录状态。服务器会为该会话生成一个唯一的会话标识(session ID),然后将该会话标识发送给客户端(通常是通过将该标识存储在Cookie中)。客户端在后续的请求中通过Cookie将会话标识发送给服务器,服务器根据会话标识验证用户的登录状态。
- 基于Token的登录:用户在登录成功后,服务器会生成一个加密的令牌(token),将该令牌发送给客户端。客户端将令牌保存在本地(通常是在LocalStorage或SessionStorage中)。客户端在后续的请求中,将令牌放在请求头部(通常是在Authorization头部)中发送给服务器,服务器根据令牌验证用户的登录状态。
- 基于OAuth的登录:OAuth是一种授权协议,允许用户使用第三方平台(如Google、Facebook等)的账号登录其他网站。在这种方式中,用户先登录第三方平台,然后该平台向客户端颁发一个授权凭证(access token)。客户端使用该凭证向服务器请求登录,服务器通过验证凭证的有效性来实现登录。
每种登录方式都有其优点和缺点,下面是它们的简要介绍:
- 基于Session的登录:
- 优点:简单易用,适用于传统的网页应用。会话信息存储在服务器端,相对较安全。
- 缺点:需要在服务器端维护会话状态,对服务器的资源消耗较大。不适用于分布式系统或多服务器环境。
- 基于Token的登录:
- 优点:无状态,适用于分布式系统和多服务器环境。减轻了服务器的负载和资源消耗。
- 缺点:需要在客户端存储令牌,存在安全风险。令牌可能被窃取或篡改,因此需要采取安全措施,如使用HTTPS。
- 基于OAuth的登录:
- 优点:方便用户,可以使用现有的第三方平台账号登录。减少了用户的注册流程。
- 缺点:需要依赖第三方平台,用户信息可能受到第三方平台的控制和限制。实现复杂,需要与第三方平台进行集成。