# 前言
最近想跳槽了,问就是工资太低- -。所以记录一下面试中可能遇到的以及自己不太了解的题,巩固一下老知识和学习新知识。
内容为空的后续会补上,也会更新新的题目。
# 跨域、CORS是什么?
网站之间互相请求时协议、域名和端口有一个不一样则称之为跨域。
cors(跨域资源共享)是一种策略,一般在跨域的情况下是不允许资源共享的。
一般需要通过后端或配置服务器解决,设置access-control-allow-origin来指定允许跨域的网站。或者通过jsonp的方式来进行跨域请求。
jsonp通常是利用标签中src或者href的属性来进行跨站请求的,一般请求返回结果为可执行的函数名,其中带有从后端携带过来的参数。如 xxx/xxx?callback=fn,返回结果为fn('hello'),则会自动执行该函数(前提是该函数在网站中存在)
还有html5新增postMessage,可以解决不同窗口以及嵌套iframe中的跨域问题,从而进行消息传递。
# 闭包
闭包(closure)是一个函数以及其捆绑的周边环境状态的引用的组合,通俗来讲就是函数中引用了外部变量,通常通过嵌套函数的形式来体现。
通过闭包可以私有化变量,也因此可以延长变量的生命周期,更进一步还可能带来内存泄露。为了防止内存泄露可以手动将变量设置为null,触发垃圾回收机制。
常见的场景有防抖、节流、vue和react中的useXXX等。
经典题目:
for (var i = 0; i <= 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
2
3
4
5
结果是每经过1秒输出一个6。 因为setTimeout是异步的,而for循环是同步的,会先等待for循环走完才一起执行setTimeout,又因为var定义的变量i是在全局作用域内的,所以当setTimeout回调函数引用i的时候已经是6了。
那为什么明明都是i,为什么不是每隔6秒输出一个6?答案其实也很简单,上面的概念就已经提到,闭包是一个函数以及其捆绑的周边环境状态的引用的组合,也就是说需要根据上下文环境来决定引用变量的值。
虽然setTimeout是宏任务的一种,但是他本身是个同步代码,只是注册的回调函数是一个宏任务,所以当setTimeout执行时,延时参数中i即是循环中的i,而非循环结束后的。
而如果使用let来定义下标i,则可以按照预期输出0 1 2 3 4 5
,这是因为let具有块级作用域的概念,即使是for先执行完,setTimeout回调函数也只能拿到当时循环中的i。
如果不使用let,则需要用立即执行函数(IIFE),并把i作为参数传递进去,才能在IIFE内部创建一个闭包,并记录下当前的i。
for (var i = 0; i <= 5; i++) {
(function (){
setTimeout(() => {
console.log(i);
}, i * 1000)
})(i)
}
2
3
4
5
6
7
# 原型链
原型链看起来挺简单的,但是其中大有学问。
说简单是从表面上理解,如果我们从一个对象上去获取一个属性或者方法,如果这个对象本身没有这个属性或者方法,就会去他的原型对象中去查找,如果找到了就返回,如果没有则继续往上找,直到最后的Object原型上,如果还没有则返回undefined。
其中的学问则涉及到这是如何做到的,其实也就是面向对象中的继承,这其中还涉及另外一个问题,就是在创建实例的时候发生了什么,也就是new的过程发生了啥,我们逐个来学习。
先重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象(prototype
),原型有一个属性(constructor
)指回构造函数,而实例有一个内部指针(__proto__
)指向原型。
那如果这个原型是另一个类型的实例呢?也就是说这个原型也有一个内部指针指向了另一个原型,而这另一个原型也有一个属性指向他的构造函数。这其实就是原型链的基本构想。先看个简单的例子:
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () { return this.property }
function SubType() {
this.subproperty = false
}
// 继承
SubType.prototype = new SuperType()
// 自己的获取方法,与上一句顺序不能反,否则会被覆盖
SubType.prototype.getSubValue = function () { return this.subproperty }
const sub = new SubType()
console.log(sub.getSuperValue()) // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
现在知道了继承的原理,再来看一下new的过程发生了什么:
- 创建一个空对象。
- 将这个空对象的原型指向构造函数的 prototype 属性。
- 将构造函数的 this 绑定到这个空对象上,并执行构造函数中的代码(为这个对象添加属性)。
- 如果构造函数返回一个对象,则返回该对象;否则,返回步骤1创建的空对象。
function myNew(constructor, ...args) {
// 1. 创建一个空对象
const obj = {};
// 2. 将空对象的原型指向构造函数的prototype属性
obj.__proto__ = constructor.prototype;
// 3. 将构造函数的this绑定到空对象上,并执行构造函数中的代码
const result = constructor.apply(obj, args);
// 4. 如果构造函数返回一个对象,则返回该对象;否则,返回步骤1创建的空对象
return (typeof result === 'object' && result !== null) ? result : obj;
}
2
3
4
5
6
7
8
9
10
11
12
13
从里面我们能看到,会把空对象的原型指向构造函数的prototype属性,看起来是不是有点熟悉,其实就是上面提到的,实例有一个内部指针指向原型。
到这里就串起来了,最开始说的沿着原型链去找方法和属性的时候,其实就是通过__proto__这个内部指针去沿着原型一步一步往上查找的。
这里讲的原型链只是集成的一种最基础的方式,此外还有盗用构造函数、组合继承、原型式继承、寄生式继承等等方式,这些也是围绕原型链这种最基础方式展开的,这里就不展开了。
# XSS攻击
跨站脚本攻击。脚本执行是重点,分3种。
反射型:通过诱导你点击某个链接,该链接是正常访问某个网站的链接(可能还是官方链接),但链接中包含恶意代码,如果页面中刚好对链接中的某些部分进行展示,那么恶意代码就有可能被嵌入到页面中,也就给了js脚本执行的机会。
常见的例子是搜索页面,当你搜索某些关键词之后,url中会包含关键词,搜查的结果页面中通常也包含关键词。如果关键词包含恶意代码且没有被过滤掉,那么就会被执行。
存储型:通过提交数据将恶意代码保存到服务器的数据库中,当其他人的页面展示这条数据的时候,恶意代码就会被执行。
常见的例子是评论区,如果提交了包含恶意代码的评论,且没有被过滤就保存到了服务器,当其他人访问这个评论区的时候,恶意代码就会被执行。
DOM型:需要有两个条件,一个是需要有可以用来注入脚本的方法,另一个是需要有可以被注入的属性或者值。与其他类型的区别就是修改了原本页面的dom节点。
常见的例子是,使用了innerHTML来注入时,使用的字符串中包含了恶意代码,通过修改dom的属性来触发脚本。
document.querySelector("#content").innerHTML("<img src='/xxx/xxx/" + num + ".jpg />")
那么num就可以提前结束src,然后编写onerror属性来触发js事件,如
document.querySelector("#content").innerHTML("<img src='/xxx/xxx/" + "xx' onerror='alert()' class=" + ".jpg />")
num可能出现在url中。
# SQL注入
sql注入指的是,后端在进行数据库查询的时候,查询语句中附带了用户提交的字段,例如用户名等等。那么用户的字段即可参与到sql查询中去,修改查询语句。例子如下:
select * from user where username = #{userName} and password = #{password};
如果userName为'zhangsan -- ',即可注释掉后面的代码,只需要账号名就能查询到该用户的信息。
甚至,即使你不知道账号名,把userName修改为 'xxx or 1=1 -- ',由于1=1为真,导致where条件为真,也能查到所有信息。
2
3
4
# CSRF攻击
跨站请求伪造攻击。主要依赖浏览器在请求时自动带上cookie的机制。黑客在不知道你的cookie的情况下,伪装成你对你的个人信息进行套取和修改。 常见的例子是,你登录了x网站(黑客知道这个网站,并且知道接口的地址),并且cookie未过期。当你点击了黑客的链接后或者打开黑客html网站后,浏览器跨站请求x网站的接口,此时会把你未过期的cookie带上,让服务器误以为是你本人。
这种攻击可以通过后端对cookie设置samesite为strict(不允许跨站携带cookie),或者加上secure属性(只有https请求可以跨站携带cookie)。 也可以设置origin或者referer属性,甄别请求来源。 也可以利用cors来防范。
# 浏览器事件循环
首先了解JavaScript是单线程的机制,为了防止代码阻塞,把代码分为同步代码和异步代码。
同步代码交给JS引擎去执行,而异步代码先交给宿主环境(浏览器/node)。
同步代码会放到执行栈中,宿主环境会在时机成熟时将异步代码(任务)推送到任务队列。
当执行栈里面的同步代码执行完成时,会检查任务队列是否有异步任务,如果有就加入到执行栈中。如此反复执行即是事件循环。
而异步任务中又分宏任务和微任务。
宏任务(宿主环境)有setTimeout、setInterval、setImmediate、script脚本等,微任务有Promise.then() catch()、Async/Await、process.nextTick(node)等等。 需要注意的是创建Promise这个过程是同步任务,而非异步。
宏任务和微任务也对应宏任务队列和微任务队列。
上面提到当执行栈里面的同步代码执行完成时,会检查任务队列是否有异步任务,这时会优先检查微任务队列,并且按顺序执行其中的代码,如果期间产生了新的微任务,也会加入到微任务队列中被执行。
直到微任务队列清空之后,才去检查宏任务队列,然后按顺序执行。
例子1:
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
const p = new Promise((resolve, reject) => {
console.log(3)
resolve(4)
console.log(5)
})
p.then(res => {
console.log(res)
})
console.log(6)
2
3
4
5
6
7
8
9
10
11
12
13
根据上面的分析可得,输出的顺序为1 3 5 6 4 2。
例子2:
console.log(1)
const p1 = new Promise((resolve, reject) => {
console.log(2)
resolve(3)
})
setTimeout(() => {
console.log(4)
p1.then(res => {
console.log(res)
})
console.log(5)
}, 0)
const p2 = new Promise((resolve, reject) => {
console.log(6)
resolve(7)
console.log(8)
})
p2.then(res => {
console.log(res)
})
console.log(9)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
根据上面分析可得,输出的顺序为 1 2 6 8 9 7 4 5 3。
# Webpack
流程:初始化 -> 编译 -> 输出
初始化是合并和读取配置。
编译过程:通过入口文件,读取文件内容,对不同文件类型通过loader进行转换,然后构建ast语法树(利用babel),找出文件之间的依赖关系(import、require),打包成一个个chunk,最后输出。
loader在其中负责代码的转换。plugin则贯穿整个流程,可以在编译的生命周期中做各种事情。
chunk:一些互相引用的文件共同打包成一个文件就是一个chunk。
# loader和plugin有什么区别
loader能力有限,只负责代码的转换,例如less-loader只负责将less转换成css。而plugin能够介入到整个打包编译的过程中,在生命周期中拥有完整的文件访问权,可以做很多事情。
# plugin是如何实现的?
plugin本质是一个对象,该对象含有apply属性,是一个函数,可以接受compiler参数。
而complier本质也是一个对象,在整个生命周期只会初始化一次。
在compiler内部钩子中,还有一个compilation对象,当文件发生改变时,触发钩子重新初始化compilation,以做到对文件的修改。
# Vite打包配置兼容es6+新特性
虽然ie浏览器已经被淘汰了,大部分浏览器也已经兼容es6语法,但耐不住语法更新快呀。所以有些浏览器还不支持新特性,但我们在项目中又想用,就可以通过配置vite来兼容。
vite.config.js文件中有自带的属性可以去配置兼容浏览器,主要是legacyPlugin方法,传入配置参数,可以指定是否兼容ie,以及转化指定的es6语法中的方法。大概配置如下:
export default ({ mode }) => defineConfig({
base: './',
plugins: [
vue(),
// 浏览器兼容问题配置
legacyPlugin({
targets: ['defaults', 'not IE 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
renderLegacyChunks: true,
polyfills: [
'es.symbol',
'es.promise',
'es.promise.finally',
'es/map',
'es/set',
'es.array.filter',
'es.array.for-each',
'es.array.flat-map',
'es.array.at',
'es.object.define-properties',
'es.object.define-property',
'es.object.get-own-property-descriptor',
'es.object.get-own-property-descriptors',
'es.object.keys',
'es.object.to-string',
'web.dom-collections.for-each',
'esnext.global-this',
'esnext.string.match-all'
]
})
]
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
最后打包结果会多出兼容版本的,名称中带有legacy的js、css文件,在需要的时候引用。没错,除了js还能兼容css。
# ES6
# WeakMap
# Symbol
# Iterator
# 回流与重绘
回流(Reflow)和重绘(Repaint)是网页性能优化中的重要概念,它们描述了浏览器在处理DOM变化时的两种不同渲染过程。
回流是指当DOM元素的几何属性(如尺寸、位置)发生变化时,浏览器需要重新计算元素的布局,这个过程称为回流。回流不仅影响发生改变的元素本身,还可能影响其后代元素以及同级元素的位置。常见的触发回流的操作包括修改元素的宽度、高度、边框、外边距、内边距等样式属性,以及添加或删除可见的DOM元素,改变元素的定位方式,或者激活CSS伪类等。
重绘则是指当元素的外观属性(如颜色、背景色、边框样式)发生变化,但不影响其几何属性时,浏览器只需要重新绘制受影响的部分,这个过程称为重绘。例如,改变元素的颜色、背景色、边框样式等外观属性,或者修改文本内容,都可能触发重绘。
在性能优化方面,由于回流通常涉及更复杂的计算,它比重绘更消耗资源。因此,开发者通常会尝试减少回流的次数和范围,例如通过将多次DOM操作合并来减少回流,或者使用CSS3的transform和opacity属性来触发重绘而不是回流,因为这些属性的更改不会影响元素的几何属性,从而避免了回流。
# 浏览器缓存机制
强缓存与协商缓存。如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。
浏览器缓存的全过程:
- 浏览器第一次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header,以供下次加载时对比使用;
- 下一次加载资源时,由于强制缓存优先级较高,先比较当前时间与上一次返回 200 时的时间差,如果没有超过 cache-control 设置的 max-age,则没有过期,并命中强缓存,直接从本地读取资源。如果浏览器不支持HTTP1.1,则使用 expires 头判断是否过期;
- 如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向服务器发送带有 If-None-Match 和 If-Modified-Since 的请求;
- 服务器收到请求后,优先根据 Etag 的值判断被请求的文件有没有做修改,Etag 值一致则没有修改,命中协商缓存,返回 304;如果不一致则有改动,直接返回新的资源文件带上新的 Etag 值并返回 200;
- 如果服务器收到的请求没有 Etag 值,则将 If-Modified-Since 和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回 304;不一致则返回新的 last-modified 和文件并返回 200;
很多网站的资源后面都加了版本号,这样做的目的是:每次升级了 JS 或 CSS 文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的 JS 或 CSS 文件 ,以保证用户能够及时获得网站的最新更新。