八股-JS

6 阅读10分钟

1. 闭包

闭包就是一个函数,他记住了他被创建时候的作用域,即使在这个作用域之外执行这个函数,仍然可以访问到作用域内的东西。

闭包 = 函数 + 对函数外部作用域的引用

作用:实现变量私有化、函数柯里化、防抖节流

弊端:变量不会被垃圾回收,容易造成内存泄漏

function createCounter() {
  let count = 0; // 私有变量

  return {
    increment: function() { count++; },
    get: function() { return count; }
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.get()); // 2
// 直接访问 counter.count 是不行的,count 被“保护”起来了

2. this指向

this是函数调用时确定的对象,不是创建时确定的

谁调用函数,this就指向谁

4原则 + 1例外:

  1. 独立函数调用:当函数被直接调用时,this指向全局对象(window,严格模式是undefined)
  2. 对象方法调用:当函数作为对象的方法被调用时,this指向点前面的对象
  3. call/apply/bind:手动指向this
    1. call:立即执行,参数一个个传,fn.call(obj, 1, 2)
    2. apply:立即执行,参数数组传,fn.apply(obj, [1, 2])
    3. bind:返回新函数,不立即执行,const newFn = fn.bind(obj) newFn(1)
  1. new调用:this指向新创建的实例

  1. 箭头函数没有自己的this,通过继承来,不能手动修改this指向

new做了什么?

  1. 创建一个空对象
  2. 把空对象的__proto__指向构造函数的prototype
  3. 把构造函数的this指向这个空对象
  4. 执行构造函数
  5. 返回这个对象

3. 原型

  1. 定义:每个函数都有prototype属性,每个对象都有__proto__指向函数的prototype属性
  2. 作用:实现方法和属性的共享,节约内存

4. 原型链

当访问对象的一个属性时,如果对象本身没有,就会沿着 proto 向上查找,直到找到或到达 null。这就是原型链。

console.log(Object.prototype.__proto__)  // null

5. 继承

5.1. 原型链继承

function Parent() {
  this.name = '父'
}
Parent.prototype.sayHi = function() {
  console.log(this.name)
}

function Child() {}
Child.prototype = new Parent()  // 关键:把 Child 的 prototype 指向 Parent 的实例

const c = new Child()
c.sayHi()  // 父

缺点:

  • 多个实例共享引用类型属性
  • 创建子类实例时无法向父类传参

5.2. 构造函数继承

function Parent(name) {
  this.name = name
  this.colors = ['red', 'blue']
}
Parent.prototype.sayHi = function() {
  console.log(this.name)
}

function Child(name) {
  Parent.call(this, name)  // 关键:调用父类构造函数
}

const c1 = new Child('张三')
const c2 = new Child('李四')

c1.colors.push('green')
console.log(c1.colors)  // ['red', 'blue', 'green']
console.log(c2.colors)  // ['red', 'blue']  // 互不影响

缺点:无法继承父类 prototype 上的方法

5.3. 组合继承

Parent.call(this) + Child.prototype = new Parent()

function Parent(name) {
  this.name = name
  this.colors = ['red', 'blue']
}
Parent.prototype.sayHi = function() {
  console.log(this.name)
}

function Child(name, age) {
  Parent.call(this, name)  // 第二次调用:继承属性
  this.age = age
}
Child.prototype = new Parent()  // 第一次调用:继承方法
Child.prototype.constructor = Child  // 修复 constructor 指向

const c = new Child('张三', 18)
c.sayHi()  // 张三
console.log(c.colors)  // ['red', 'blue']

5.4. ES6 class 继承(语法糖)

extends + super,语法糖,本质还是原型链

class Parent {
  constructor(name) {
    this.name = name
  }
  sayHi() {
    console.log(this.name)
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name)  // 相当于 Parent.call(this, name)
    this.age = age
  }
}

const c = new Child('张三', 18)
c.sayHi()  // 张三

5.5. 补充

  1. 判断属性是自身的还是原型链上的:hasOwnProperty
  2. instanceof原理:检查右侧函数的 prototype 是否在左侧对象的原型链上。

6. 事件循环

6.1. 分类

6.2. 执行顺序

  1. 执行所有同步代码(这是当前宏任务)
  2. 执行完同步代码后,检查微任务队列
  3. 依次执行所有微任务(直到队列清空)
  4. 执行一次宏任务(比如一个 setTimeout 的回调)
  5. 然后重复第 2 步 → 第 3 步 → 第 4 步(微任务可以产生新的微任务)

6.3. async/await

await 这一行立即执行,它后面的代码才是微任务

await 后面的代码分两段:

  • 紧跟在 await 那一行的同步代码(如 console.log(2))立即执行
  • await 之后剩下的代码(如 console.log(3))被包装成微任务
async function test() {
  console.log('1')           // 同步(async 函数内的普通代码)
  await console.log('2')     // await 后面的代码先执行,然后把后面的代码变成微任务
  console.log('3')           // 微任务
}

console.log('4')
test()
console.log('5')

// 输出顺序:4 → 1 → 2 → 5 → 3

7. 浏览器渲染

7.1. 从输入 URL 到页面显示

  1. DNS 解析:域名 → IP 地址
  2. TCP 连接:三次握手
  3. 发送 HTTP 请求
  4. 服务器处理并返回响应
  5. 浏览器解析 HTML,构建 DOM 树
  6. 解析 CSS,构建 CSSOM 树
  7. 合并成渲染树(Render Tree)
  8. 布局(Layout/Reflow):计算每个节点的位置和大小
  9. 绘制(Paint):填充像素
  10. 合成(Composite):图层合并,显示到屏幕

7.2. 阻塞渲染

CSS 不阻塞 DOM 构建,但会阻塞渲染,因为浏览器必须等 CSSOM 构建完才能渲染。

那为什么还要把 CSS 放头部?

为了让浏览器尽早开始构建 CSSOM,避免页面先出现无样式内容(FOUC)。

JS 会阻塞 DOM 构建,所以 JS 脚本通常放在 body 末尾,或使用 defer/async

<script defer src="a.js"></script>   <!-- DOM 解析完后按顺序执行 -->
<script async src="b.js"></script>   <!-- 下载完立即执行,可能乱序 -->

7.3. 关键事件

document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM 解析完成,可以操作 DOM 了')
})

window.addEventListener('load', () => {
  console.log('所有资源加载完成,包括图片')
})
7.3.1. 结合事件循环
console.log(1)
setTimeout(() => console.log(2))
Promise.resolve().then(() => console.log(3))
document.addEventListener('DOMContentLoaded', () => console.log(4))
window.onload = () => console.log(5)
console.log(6)

答案:1 → 6 → 3 → 4 → 2 → 5(假设所有资源加载完)

分析:

同步:1、6

微任务:Promise → 3

宏任务队列:setTimeout (2)、DOMContentLoaded (4)、load (5)

事件触发顺序:DOMContentLoaded(DOM 解析完)先于 load(所有资源加载完)

setTimeout 2 可能比 4 早或晚,取决于计时器(0ms 通常比 DOMContentLoaded 晚一点点)

8. HTTP缓存

  • 强缓存:直接从本地读,不发请求
  • 协商缓存:发请求问服务器,资源是否过期,没过期则返回 304(不发文件内容),浏览器从本地读

8.1. Cache-Control

区别:

  • no-cache:允许缓存,但每次用之前都要去服务器验证是否过期

Cache-Control: max-age=0 会被缓存吗?

答:会被缓存,但立即过期。下次请求时,会走协商缓存(带 If-None-Match 去问服务器)。

  • no-store:完全不缓存,每次都重新下载

8.2. 协商缓存

协商缓存中,ETag 的优先级比 Last-Modified 更高,因为 ETag 基于文件内容,更精确。

浏览器再次请求资源时,会携带上次服务器返回的 ETag 或 Last-Modified(优先用 ETag),

服务器判断资源是否变化:

  • 没变 → 返回 304 Not Modified,不返回 body
  • 变了 → 返回 200 和新资源

9. 网站类型

9.1. HTTP

  1. HTTP/1
  2. HTTP/2

HTTP/2 核心是多路复用,一个 TCP 连接里可以并发处理多个请求,解决了 HTTP/1.1 的队头阻塞问题;

同时支持头部压缩和服务器推送,整体性能比 HTTP/1.1 好很多。

核心:

多路复用 ---> 队头阻塞问题大大缓解

HTTP/1.1 每个请求一个 TCP 连接,浏览器一般只能开 6 个

HTTP/2 一个 TCP 连接里可以并发发多个请求

头部压缩 ---> 请求体积变小

HTTP/1.1 每次请求都带一大堆重复的 Header(比如 Cookie、User-Agent)

HTTP/2 会压缩并复用之前发过的头部

服务器推送

服务器可以主动把资源推给浏览器,不用等浏览器解析 HTML 再请求

(请求 index.html 时,服务器顺手把 main.css 也推过去)

有人说:“HTTP/2 的多路复用彻底解决了队头阻塞问题。”你觉得这句话对不对?为什么?

精简:不对。HTTP/2 解决了应用层队头阻塞,但 TCP 层的队头阻塞仍然存在,因为它是基于 TCP 的。

答:这句话不完全正确。HTTP/2 的多路复用解决了 HTTP/1.1 中的应用层队头阻塞,即同一个 TCP 连接里,多个请求可以并发发送,一个请求的响应不会阻塞其他请求。

但是,TCP 层的队头阻塞依然存在。因为 HTTP/2 仍然是基于 TCP 的,如果 TCP 传输过程中发生丢包,TCP 会要求重传丢失的数据段,在重传完成之前,同一个 TCP 连接上的所有数据(包括其他请求的响应)都会被阻塞。所以只能说大幅缓解了队头阻塞,而不是彻底解决。真正要彻底解决,需要升级到 HTTP/3(它基于 UDP 的 QUIC 协议,每个请求独立,丢包不会互相阻塞)。

  1. HTTP/3

基于 UDP 的 QUIC 协议,每个请求独立,丢包不会互相阻塞。

彻底解决了TCP层的队头阻塞问题

9.2. HTTPS

面试版本:

  1. Client Hello

浏览器向服务器发送请求,告知自己支持的加密算法和 TLS 版本。

  1. Server Hello + 证书

服务器选择加密算法,并返回证书(包含公钥)。

  1. 验证证书 + 生成密钥

浏览器验证证书是否可信(CA、域名、有效期),验证通过后生成一个随机密钥(Premaster Secret),用服务器的公钥加密后发送给服务器。

  1. 协商完成,开始加密通信

服务器用私钥解密得到密钥,双方用这个密钥进行对称加密通信。

浏览器用服务器的公钥,加密一个自己生成的随机密钥,服务器用私钥解密拿到这个密钥。之后双方都用这个密钥进行对称加密通信,既安全又高效。

握手流程

  1. 客户端 → 服务器:你好,我支持的加密算法列表

  2. 服务器 → 客户端:选一个加密算法,这是我的证书(包含公钥)

  3. 客户端验证证书是否可信

    验证:证书有没有过期、域名是否匹配、证书链是否可信

  4. 客户端生成一个随机密钥(Premaster Secret),用服务器的公钥加密后发送

  5. 服务器用私钥解密,得到这个随机密钥

  6. 双方用这个密钥生成会话密钥(Session Key),后续通信对称加密

简洁:

  1. 浏览器访问 https://xxx
  2. 服务器返回证书(包含公钥)
  3. 浏览器验证证书 → 生成随机密钥 → 用公钥加密后发给服务器
  4. 服务器用私钥解密 → 后续用该密钥对称加密通信

证书的核心作用是证明“我是我”。它解决了身份信任问题,防止中间人攻击。

如果访问一个 HTTPS 网站时,浏览器提示“证书不受信任”,可能是哪些原因造成的?

  1. 证书不是由受信任的 CA 签发(比如自签名)
  2. 证书已过期
  3. 证书的域名和当前访问的域名不一致”

证书就像你的身份证:

  1. 不是公安局发的 → 不信任(不是受信任 CA)
  2. 过期了 → 不信任
  3. 照片和本人不像 → 不信任(域名不匹配)

HTTPS 防中间人攻击,靠的是证书的信任链和数字签名机制:

  1. 浏览器用内置的根证书验证服务器证书的签名,确认证书由可信 CA 签发且未被篡改;
  2. 同时验证证书绑定的域名与访问域名一致、证书未过期。
  3. 中间人无法伪造有效证书,因此无法冒充服务器。

公钥私钥就是非对称加密 拿着随机生成的密钥就是对称加密

9.3. HTTP 🆚 HTTPS

  1. HTTP 是明文传输,容易被窃听、篡改。HTTPS = HTTP + TLS/SSL 加密
  2. HTTPS比HTTP多了一次TLS握手

TLS

目的:建立加密通道

主要做三件事:校验证书,协商加密算法,生成会话密钥

10. 攻击类型 XSS / CSRF

10.1. XSS(跨站脚本攻击)

定义:攻击者在网页中注入恶意脚本,当其他用户访问时,脚本被执行。

防御:

HttpOnly Cookie:让 JS 无法读取 Cookie,防止 XSS 窃取 session

10.2. CSRF(跨站请求伪造)

定义:攻击者诱导用户点击恶意链接,利用用户已登录的身份,在后台发送请求。

  • 用户已登录
  • 浏览器自动带上 Cookie
  • 黑客不需要偷到 Cookie,只需要借用用户的身份

防御:(核心是让攻击者无法伪造合法请求

  1. CSRF Token

流程:

  1. 服务器生成一个随机 Token,存到 Session 或返回给前端
  2. 前端在表单或请求头里带上这个 Token
  3. 服务器校验 Token 是否匹配

为什么有效?

黑客网站拿不到这个 Token(同源策略限制)

缺点:

  • 需要改后端代码
  • 分布式 Session 下要共享 Token
  1. SameSite Cookie

设置 Cookie:

Set-Cookie: sessionId=xxx; SameSite=Lax; Secure

优点: 几乎不用改业务代码

缺点: 老浏览器不兼容

10.3. 对比