2024知识点查漏补缺

2024/9/1 前端

# 前言

最近想跳槽了,问就是工资太低- -。所以记录一下面试中可能遇到的以及自己不太了解的题,巩固一下老知识和学习新知识。

内容为空的后续会补上,也会更新新的题目。

# 跨域、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);
}
1
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)
}
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

现在知道了继承的原理,再来看一下new的过程发生了什么:

  1. 创建一个空对象。
  2. 将这个空对象的原型指向构造函数的 prototype 属性。
  3. 将构造函数的 this 绑定到这个空对象上,并执行构造函数中的代码(为这个对象添加属性)。
  4. 如果构造函数返回一个对象,则返回该对象;否则,返回步骤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;
}
1
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 />")
1

那么num就可以提前结束src,然后编写onerror属性来触发js事件,如

document.querySelector("#content").innerHTML("<img src='/xxx/xxx/" + "xx' onerror='alert()' class=" + ".jpg />")
1

num可能出现在url中。

# SQL注入

sql注入指的是,后端在进行数据库查询的时候,查询语句中附带了用户提交的字段,例如用户名等等。那么用户的字段即可参与到sql查询中去,修改查询语句。例子如下:

select * from user where username = #{userName} and password = #{password};

如果userName为'zhangsan -- ',即可注释掉后面的代码,只需要账号名就能查询到该用户的信息。
甚至,即使你不知道账号名,把userName修改为 'xxx or 1=1 -- ',由于1=1为真,导致where条件为真,也能查到所有信息。
1
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)
1
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)
1
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'
      ]
    })
  ]
})
1
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 文件 ,以保证用户能够及时获得网站的最新更新。

# 垃圾回收机制

Last Updated: 2024/10/20 08:22:07