2023 面试官常考的前端面试题
Promise.resolve
Object.assign()
描述:Object.assign()
方法用于将所有可枚举(Object.propertyIsEnumerable()
返回 true
)和自有(Object.hasOwnProperty()
返回 true
)属性的值从一个或多个源对象复制到目标对象。它将返回修改后的目标对象(请注意这个操作是浅拷贝)。
实现:
如何防御 XSS 攻击?
可以看到 XSS 危害如此之大, 那么在开发网站时就要做好防御措施,具体措施如下:
可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到 HTML 中的代码做好充分的转义。对于 DOM 型的攻击,主要是前端脚本的不可靠而造成的,对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断。
使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。
CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。
通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式
对一些敏感信息进行保护,比如 cookie 使用 http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。
代码输出结果
输出结果如下:
(1)第一轮事件循环流程分析如下:
整体 script 作为第一个宏任务进入主线程,遇到
console.log
,输出 1。遇到
setTimeout
,其回调函数被分发到宏任务 Event Queue 中。暂且记为setTimeout1
。遇到
process.nextTick()
,其回调函数被分发到微任务 Event Queue 中。记为process1
。遇到
Promise
,new Promise
直接执行,输出 7。then
被分发到微任务 Event Queue 中。记为then1
。又遇到了
setTimeout
,其回调函数被分发到宏任务 Event Queue 中,记为setTimeout2
。
上表是第一轮事件循环宏任务结束时各 Event Queue 的情况,此时已经输出了 1 和 7。发现了process1
和then1
两个微任务:
执行
process1
,输出 6。执行
then1
,输出 8。
第一轮事件循环正式结束,这一轮的结果是输出 1,7,6,8。
(2)第二轮时间循环从**setTimeout1**
宏任务开始:
首先输出 2。接下来遇到了
process.nextTick()
,同样将其分发到微任务 Event Queue 中,记为process2
。new Promise
立即执行输出 4,then
也分发到微任务 Event Queue 中,记为then2
。
第二轮事件循环宏任务结束,发现有process2
和then2
两个微任务可以执行:
输出 3。
输出 5。
第二轮事件循环结束,第二轮输出 2,4,3,5。
(3)第三轮事件循环开始,此时只剩 setTimeout2 了,执行。
直接输出 9。
将
process.nextTick()
分发到微任务 Event Queue 中。记为process3
。直接执行
new Promise
,输出 11。将
then
分发到微任务 Event Queue 中,记为then3
。
第三轮事件循环宏任务执行结束,执行两个微任务process3
和then3
:
输出 10。
输出 12。
第三轮事件循环结束,第三轮输出 9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为 1,7,6,8,2,4,3,5,9,11,10,12。
call apply bind
题目描述:手写 call apply bind 实现
实现代码如下:
介绍 Loader
常用 Loader:
file-loader
: 加载文件资源,如 字体 / 图片 等,具有移动/复制/命名等功能;url-loader
: 通常用于加载图片,可以将小图片直接转换为 Date Url,减少请求;babel-loader
: 加载 js / jsx 文件, 将 ES6 / ES7 代码转换成 ES5,抹平兼容性问题;ts-loader
: 加载 ts / tsx 文件,编译 TypeScript;style-loader
: 将 css 代码以<style>
标签的形式插入到 html 中;css-loader
: 分析 @import 和 url(),引用 css 文件与对应的资源;postcss-loader
: 用于 css 的兼容性处理,具有众多功能,例如 添加前缀,单位转换 等;less-loader / sass-loader
: css 预处理器,在 css 中新增了许多语法,提高了开发效率;
编写原则:
单一原则: 每个 Loader 只做一件事;
链式调用: Webpack 会按顺序链式调用每个 Loader;
统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;
参考 前端进阶面试题详细解答
行内元素有哪些?块级元素有哪些? 空(void)元素有那些?
行内元素有:
a b span img input select strong
;块级元素有:
div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p
;
空元素,即没有内容的 HTML 元素。空元素是在开始标签中关闭的,也就是空元素没有闭合标签:
常见的有:
<br>
、<hr>
、<img>
、<input>
、<link>
、<meta>
;鲜见的有:
<area>
、<base>
、<col>
、<colgroup>
、<command>
、<embed>
、<keygen>
、<param>
、<source>
、<track>
、<wbr>
。
了解 this 嘛,bind,call,apply 具体指什么
它们都是函数的方法
call: Array.prototype.call(this, args1, args2])
apply: Array.prototype.apply(this, [args1, args2])
:ES6 之前用来展开数组调用, foo.appy(null, [])
,ES6 之后使用 ... 操作符
New 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
如果需要使用 bind 的柯里化和 apply 的数组解构,绑定到 null,尽可能使用 Object.create(null) 创建一个 DMZ 对象
四条规则:
默认绑定,没有其他修饰(bind、apply、call),在非严格模式下定义指向全局对象,在严格模式下定义指向 undefined
隐式绑定:调用位置是否有上下文对象,或者是否被某个对象拥有或者包含,那么隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。而且,对象属性链只有上一层或者说最后一层在调用位置中起作用
显示绑定:通过在函数上运行 call 和 apply ,来显示的绑定 this
显示绑定之硬绑定
New 绑定,new 调用函数会创建一个全新的对象,并将这个对象绑定到函数调用的 this。
New 绑定时,如果是 new 一个硬绑定函数,那么会用 new 新建的对象替换这个硬绑定 this,
深/浅拷贝
首先判断数据类型是否为对象,如果是对象(数组|对象),则递归(深/浅拷贝),否则直接拷贝。
这个函数只能判断 obj
是否是对象,无法判断其具体是数组还是对象。
代码输出结果
输出结果:10 10
我么知道,箭头函数时不绑定 this 的,它的 this 来自原其父级所处的上下文,所以首先会打印全局中的 a 的值 10。后面虽然让 say 方法指向了另外一个对象,但是仍不能改变箭头函数的特性,它的 this 仍然是指向全局的,所以依旧会输出 10。
但是,如果是普通函数,那么就会有完全不一样的结果:
输出结果:20 30
这时,say 方法中的 this 就会指向他所在的对象,输出其中的 a 的值。
说一下 vue3.0 你了解多少?
组件之间的传值有几种方式
说一下前端登录的流程?
初次登录的时候,前端调后调的登录接口,发送用户名和密码,后端收到请求,验证用户名和密码,验证成功,就给前端返回一个 token,和一个用户信息的值,前端拿到 token,将 token 储存到 Vuex 中,然后从 Vuex 中把 token 的值存入浏览器 Cookies 中。把用户信息存到 Vuex 然后再存储到 LocalStroage 中,然后跳转到下一个页面,根据后端接口的要求,只要不登录就不能访问的页面需要在前端每次跳转页面师判断 Cookies 中是否有 token,没有就跳转到登录页,有就跳转到相应的页面,我们应该再每次发送 post/get 请求的时候应该加入 token,常用方法再项目 utils/service.js 中添加全局拦截器,将 token 的值放入请求头中 后端判断请求头中有无 token,有 token,就拿到 token 并验证 token 是否过期,在这里过期会返回无效的 token 然后有个跳回登录页面重新登录并且清除本地用户的信息
instanceof
题目描述:手写 instanceof 操作符实现
实现代码如下:
实现 LazyMan
题目描述:
实现代码如下:
instance 如何使用
左边可以是任意值,右边只能是函数
一个 tcp 连接能发几个 http 请求?
如果是 HTTP 1.0 版本协议,一般情况下,不支持长连接,因此在每次请求发送完毕之后,TCP 连接即会断开,因此一个 TCP 发送一个 HTTP 请求,但是有一种情况可以将一条 TCP 连接保持在活跃状态,那就是通过 Connection 和 Keep-Alive 首部,在请求头带上 Connection: Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条,不过此方式也有限制,可以关注《HTTP 权威指南》4.5.5 节对于 Keep-Alive 连接的限制和规则。
而如果是 HTTP 1.1 版本协议,支持了长连接,因此只要 TCP 连接不断开,便可以一直发送 HTTP 请求,持续不断,没有上限; 同样,如果是 HTTP 2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 HTTP 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限地持续发送
代码输出结果
输出结果为:
代码执行过程如下:
首先遇到定时器,将其加入到宏任务队列;
遇到 Promise,首先执行里面的同步代码,打印出 2,遇到 resolve,将其加入到微任务队列,执行后面同步代码,打印出 3;
继续执行 script 中的代码,打印出 7 和 8,至此第一轮代码执行完成;
执行微任务队列中的代码,首先打印出 4,如遇到 Promise,执行其中的同步代码,打印出 5,遇到定时器,将其加入到宏任务队列中,此时宏任务队列中有两个定时器;
执行宏任务队列中的代码,这里我们需要注意是的第一个定时器的时间为 100ms,第二个定时器的时间为 10ms,所以先执行第二个定时器,打印出 6;
此时微任务队列为空,继续执行宏任务队列,打印出 1。
做完这道题目,我们就需要格外注意,每个定时器的时间,并不是所有定时器的时间都为 0 哦。
代码输出问题
输出结果:
这道题目值得神帝,他涉及到的知识点很多,例如 this 的指向、原型、原型链、类的继承、数据类型等。
解析:
parent.show(),可以直接获得所需的值,没啥好说的;
child1.show(),
Child
的构造函数原本是指向Child
的,题目显式将Child
类的原型对象指向了Parent
类的一个实例,需要注意Child.prototype
指向的是Parent
的实例parent
,而不是指向Parent
这个类。child2.show(),这个也没啥好说的;
parent.show(),
parent
是一个Parent
类的实例,Child.prorotype
指向的是Parent
类的另一个实例,两者在堆内存中互不影响,所以上述操作不影响parent
实例,所以输出结果不变;child1.show(),
child1
执行了change()
方法后,发生了怎样的变化呢?
this.b.push(this.a),由于 this 的动态指向特性,this.b 会指向
Child.prototype
上的 b 数组,this.a 会指向child1
的 a 属性,所以Child.prototype.b
变成了**[1,2,1,11]**;this.a = this.b.length,这条语句中
this.a
和this.b
的指向与上一句一致,故结果为child1.a
变为 4;this.c.demo = this.a++,由于
child1
自身属性并没有 c 这个属性,所以此处的this.c
会指向Child.prototype.c
,this.a
值为 4,为原始类型,故赋值操作时会直接赋值,Child.prototype.c.demo
的结果为 4,而this.a
随后自增为 5(4 + 1 = 5)。
child2
执行了change()
方法, 而child2
和child1
均是Child
类的实例,所以他们的原型链指向同一个原型对象Child.prototype
,也就是同一个parent
实例,所以child2.change()
中所有影响到原型对象的语句都会影响child1
的最终输出结果。
this.b.push(this.a),由于 this 的动态指向特性,this.b 会指向
Child.prototype
上的 b 数组,this.a 会指向child2
的 a 属性,所以Child.prototype.b
变成了**[1,2,1,11,12]**;this.a = this.b.length,这条语句中
this.a
和this.b
的指向与上一句一致,故结果为child2.a
变为 5;this.c.demo = this.a++,由于
child2
自身属性并没有 c 这个属性,所以此处的this.c
会指向Child.prototype.c
,故执行结果为Child.prototype.c.demo
的值变为child2.a
的值 5,而child2.a
最终自增为 6(5 + 1 = 6)。
进程与线程的概念
从本质上说,进程和线程都是 CPU 工作时间片的一个描述:
进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。
线程是进程中的更小单位,描述了执行一段指令所需的时间。
进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。进程是运行在虚拟内存上的,虚拟内存是用来解决用户对硬件资源的无限需求和有限的硬件资源之间的矛盾的。从操作系统角度来看,虚拟内存即交换文件;从处理器角度看,虚拟内存即虚拟地址空间。
如果程序很多时,内存可能会不够,操作系统为每个进程提供一套独立的虚拟地址空间,从而使得同一块物理内存在不同的进程中可以对应到不同或相同的虚拟地址,变相的增加了程序可以使用的内存。
进程和线程之间的关系有以下四个特点:
(1)进程中的任意一线程执行出错,都会导致整个进程的崩溃。
(2)线程之间共享进程中的数据。
(3)当一个进程关闭之后,操作系统会回收进程所占用的内存, 当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
(4)进程之间的内容相互隔离。 进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信的机制了。
Chrome 浏览器的架构图: 从图中可以看出,最新的 Chrome 浏览器包括:
1 个浏览器主进程
1 个 GPU 进程
1 个网络进程
多个渲染进程
多个插件进程
这些进程的功能:
浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
GPU 进程:其实, GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
所以,打开一个网页,最少需要四个进程:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:
更高的资源占用:因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。
更复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。
评论