JS 面试题(2023-09-20更新)

因JS代码实现面试题较多,移至另外一篇文章:JS面试题-代码实现

基础

JavaScript 是什么?

JavaScript 是一种属于网络的脚本语言,被广泛应用于 Web 应用开发

JavaScript 三大组成部分

  • ECMAScript: JavaScript 的核心,规定了语言标准。描述了语言的基本语法(var、for、if 等)和数据类型(number、string、boolean 等)
  • DOM: 文档对象模型,提供了操作页面元素的功能
  • BOM: 浏览器对象模型,提供了对浏览器窗口交互的功能(弹出新的浏览器窗口、获取网络浏览器信息 navigator、页面信息 location 等)

JavaScript 的数据类型都有哪些?

基本类型:
NumberStringBooleanNullUndefinedNullSymbol(ES6)BigInt(ES10)

引用类型:
Object

null 和 undefined 的区别?

  • null 代表内存中存在这个变量,undefined 代表这个变量完全不存在
  • null 转为数值为 0,undefined 转为数值为 NaN
  • null 是 js 语言的关键字,不允许用来作为标识符声明变量,undefined 不是关键字,可以用于声明变量

为什么 typeof null 是 object?

不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型
null 的二进制表示为全都是 0,所以执行 typeof 会返回 object
这里有五种标志位:

  • 000: 对象
  • 1: 整形
  • 010: 双精度类型
  • 100: 字符串
  • 110: 布尔类型

JavaScript 的参数是以什么方式传递的?

基本类型: 值传递
复杂类型: 地址传递
基本类型的数据是存放在栈内存中的;引用类型的数据是存放在堆内存中的,在栈内存中存的堆地址

  • 基本类型的复制:就是在栈内存中开辟出了一个新的存储区域来存储新的变量,这个变量有他自己的值,只不过和前面一样,所以其中一个变量的值改变,不会影响到另一个

  • 引用类型的复制:就是把堆内存对象在栈内存中存的引用地址,复制一份给了另外一个变量,实际上他们指向同一个堆内存对象,所以其中一个改变,会影响到另一个

什么是变量提升?

变量提升(hoisting)通俗的来说就是声明变量之前就可以获取使用该变量,值为 undefined
主要是负责解析执行代码 JavaScript 引擎,工作方式产生的一个特性
JavaScript 引擎在运行一份代码的过程是:

  1. 首先,对代码进行解析,获取声明的所有变量
  2. 然后,将这些变量的声明语句统一放到代码的最前面
  3. 最后,开始一行一行运行代码

变量的这一转换过程,就被称为变量的声明提升

什么是暂时性死区?

暂时性死区:是指只要已进入当前作用域,所有使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

bind、call、apply 的作用和区别?

作用: 三者的作用都是改变函数执行时的上下文,简而言之就是改变函数运行时的 this 指向
区别:

  • call

call 方法的第一个参数是 this 的指向,后面传入的是一个参数列表
改变 this 指向后原函数会立即执行,这个方法只是临时改变 this 指向一次

function fn(...args) {
  console.log(this, args) // {name: '张三'}  [1, 2]
}

let obj = {
  name: '张三'
}

fn.call(obj, 1, 2)
  • apply

apply 方法第一个参数是 this 的指向,后面传入的是参数,以数组的形式传入
改变 this 指向后,原函数会立即执行,这个方法也只是临时改变 this 指向一次

function fn(...args) {
  console.log(this, args) // {name: '张三'}  [1, 2]
}
let obj = {
  name: '张三'
}
fn.apply(obj, [1, 2])
  • bind

第一个参数也是 this 的指向,后面传入的参数是一个参数列表,可以分开多次传入
改变 this 指向后不会立即执行,而是返回一个永久改变 this 指向的函数

function fn(...args) {
  console.log(this, args) // {name: '张三'}  [1, 2]
}

let obj = {
  name: '张三'
}

const bindFn = fn.bind(obj) // this变成传入的obj,但不会立即执行
bindFn(1, 2)

localStorage 和 sessionStorage 的区别?

共同点: 都是保存在浏览器端,且同源的
不同点:

  1. 存储大小不同
  • cookie 容量不能超过 4k,localStorage 和 sessionStorage 可以达到 5m
  1. 有效期不同
  • cookie:在设置过期时间内一直有效,不受窗口关闭或浏览器关闭影响
  • localStorage:始终有效,窗口或浏览器关闭也一直存在,可以用作持久数据
  • sessionStorage:仅在当前浏览器窗口关闭之前有效
  1. 是否自动携带在请求上
  • cookie 数据始终在同源的 Http 请求中携带,即使不需要

== 和 === 的区别?

  • 前者会自动转化数据类型,再进行判断
  • 后者不会转换数据类型,直接判断

typeof 返回那些数据类型?

functionstringbooleannumberundefinedobject

instanceof 的作用?

instanceof 检测某个对象是不是另一个对象的实例。主要用于引用类型数据的类型检测
弥补 typeof 的不足,typeof 判断一个引用类型变量会出现一个问题,都返回'object'

typeof 和 instanceof 的区别?

相同点: 两者都可以判断数据的类型
不同点:

  • typeof 会返回一个变量的基本类型,instanceof 返回的是布尔值
  • instanceof 可以准确判断引用类型的数据,但是不能正确判断基本类型的数据
  • typeof 可以判断基本类型的数据(null 除外),引用类型中除了 function 类型意外,其他的也无法判断

什么是闭包? 闭包的作用?有什么影响?

定义: 闭包就是能够读取其他函数内部变量的函数
作用:

  • 封装私有变量,使得函数不被 GC 回收
  • 避免全局变量的污染

缺点: 容易导致内存泄露

列举强制类型转换和隐式类型转换?

强制类型转换:

  • String()
  • Boolean()
  • Number()
  • parseInt()
  • parseFloat()

隐式类型转换:

  • 四则运算
  • ==

不通数据类型之间的比较规则?

  • 对象和布尔值比较 两者都需要转换成数字进行比较,对象先转换为字符串,然后再转换为数字,布尔值直接转换为数字,然后两者进行比较
  • 对象和字符串比较 对象转换为字符串,然后两者进行比较
  • 对象和数字比较 对象先转换成字符串,再转换成数字,和数字进行比较
  • 字符串和布尔值比较 二者全部转换成数字进行比较
  • 布尔值和数字比较 布尔值转换为数字,两者进行比较

什么是原型?

prototype
原型(prototype)是 JavaScript 中函数的一个特殊属性。它指向一个对象,这个对象被称为原型对象

__proto__

__proto__ 是对象的一个隐式属性,这个属性也指向原型对象

所以对象的 __proto__ 和 构造函数的 prototype 是相等的

let obj = {}
console.log(obj.__proto__ === Object.prototype) // true

constructor

constructor 是原型的属性,指向关联的构造函数

function Person() {}

console.log(Person.prototype.constructor === Person) // true

原型有什么作用?

  • 继承: 允许对象继承其原型对象上的属性和方法,子对象可以访问父对象的属性和方法
  • 代码复用和共享: 通过将方法和属性定义在原型对象上,可以实现多个对象共享相同的方法和属性。这样可以节省内存空间,提高性能,同时也方便了代码的维护和扩展

什么是原型链?

原型链是对象之间通过原型连接起来的一种机制,用于实现属性和方法的继承。由一系列的原型对象组成,形成了一条链式结构

每个对象都有一个内部属性 [[Prototype]](__proto__),它指向该对象的原型。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会自动沿着原型链向上查找,直到找到匹配的属性或方法或者到达原型链的顶部(Object.prototype)

数组

列举数组的一些常用方法?

1. 增加

  • push()  添加元素到数组的末尾,返回新数组的最新长度
  • unshift() 添加元素到数组的开头,返回新数组的最新长度
  • splice() 可以接收三个参数,开始位置(startIndex)、0(要删除的元素数量)、插入的元素
let arr = [1, 2, 3]
arr.splice(1, 0, 4, 5)
console.log(arr) //1,4,5,2,3
  • concat() 合并数组

2. 删除

  • pop() 删除数组的最后一项,返回被删除的项
  • shift() 用户删除数组的第一项,返回被删除的项
  • splice() 接收两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组,会改变原始数组
  • slice() 接收两个参数,分别是开始位置,结束位置(包括开始,不包括结尾),返回包含截取元素的数组,不会改变原始数组

3. 修改

  • splice 传入三个参数,开始位置、要删除元素的数量、插入的元素
let arr = [1, 2, 3]
arr.splice(0, 1, 5)
console.log(arr) // [5,2,3]

4. 查找

  • indexOf() 如果数组中存在查找的元素,返回元素所在的位置,如果没有则返回-1
  • includes()  如果数组中存在查找的元素,返回 true,如果没有则返回 false
  • find() 返回数组中,符合条件的第一个元素。如果没有则返回 undefined
  • findIndex() 返回数组中,符合条件的一个元素的索引,如果没有则返回-1
  • lastIndexOf() 返回数组中,符合条件的最有一个元素的索引,如果没有则返回-1

如何判断一个变量是不是数组?

有如下变量:

let arr = []
  • Array.isArray(arr)
  • arr instanceof Array
  • Object.prototype.toString.call(arr) === '[object Array]'

split、join 的区别?

前者将字符串分割成数组,后者将数组转为字符串

forEach 和 map 的区别?

两者都是用于遍历数组的方法,他们的区别在于返回值和使用方式

  • forEach 和 map 都不能中断
  • forEach 没有返回值,仅仅是遍历数组
  • map 会将回调函数的返回值组成一个新的数组返回,不会修改原数组
let arr = [1, 2, 3]

arr.forEach((element, index, array) => {
  console.log(element, index, array)
})

let newArr = arr.map((element, index, array) => {
  console.log(element, index, array)
  return element
})
console.log(newArr)

DOM

什么是事件委托?

利用事件冒泡的原理,让子元素所触发的事件,由父元素代替触发

什么是 DOM 事件流?(事件流模型)

事件流:又称为事件传播,是指页面中接收的顺序
举例来说:点击了一个按钮,点击事件不仅仅发生在按钮上,也单击了整个页面。

事件流包括三个阶段:

  • 事件捕获阶段(capture phase)
  • 处于目标阶段(target phase)
  • 事件冒泡阶段(bubbling phase)

事件冒泡:由内而外,事件由具体的元素开始,然后逐级向上传播
事件捕获:由外而内,事件由不具体的元素开始,然后逐级向下传播,到具体的元素
addEventListener 默认 false,处于冒泡阶段,设置为 true,则是捕获

进阶

new 的原理?

  • 创建一个新对象
  • 新对象的原型继承构造函数的原型(将新对象和构造函数通过原型链连接起来)
  • 将构造函数的 this 指向新对象
  • 根据返回值判断
function myNew(Func, ...args) {
  let obj = {}
  obj.__proto__ = Func.prototype
  let result = Func.apply(obj, args)
  return result instanceof Object ? result : obj
}

谈谈你对事件循环的理解?

事件循环(Event Loop)

同步任务进入主线程,即主执行栈。异步任务进入任务列队。主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。不断的重复这一过程就称为事件循环

微任务(microtask)

微任务是在主函数执行结束之后,当前宏任务结束之前
在执行微任务过程中,产生新的微任务并不会推迟到下个事件循环中执行,而是在当前的事件循环中继续执行

常见的微任务有:

  • Promise.then
  • MutaionObserver
  • procss.nextTick(Node.js)

宏任务(macrotask)

宏任务的时间粒度比较大,执行的时间间隔不能精确控制,一般在最后执行
常见的宏任务有:

  • setTimeoutsetInterval
  • UI rendering
  • postMessageMessageChannel
  • setImmediateI/O(Node.js)
  • requestAnimationFrame

js 定时器为什么有误差?怎么优化?

原因解释:
回答这个问题,需要先了解一下 js 代码的运行机制:
浏览器中的 js 代码是在单线程上运行的,同步任务在主线程上运行,异步任务会被加入任务队列中,主线程的任务执行完毕后,会从队列中读取(秉承先进先执行的原则),推入主线程执行


接下来就可以回答这个问题了:

  • 所有异步事件(比如点击、定时器等)仅在线程空闲时才会被调用运行
  • setTimeout()的第二个参数表示等待多长时间,但是该时间到之后并不是立即执行,而是把当前任务添加到任务队列中。如果队列是空的,那么添加的代码会立即执行;如果队列不是空的,那么需要等待前面的任务执行完毕在执行

解决办法:

  • 使用 setTimeout 代替 setInterval,并计算同步任务的时间差
let count = 0
let startTime = Date.now()

function func() {
  let timeGap = Date.now() - startTime
  let nextTime = 1000 - (timeGap % 1000)
  setTimeout(() => {
    count++
    console.log(count + ' --- ' + (Date.now() - (startTime + count * 1000)))
    func()
  }, nextTime)
}

func()
  • 使用 Web Worker

index.html

<script>
  // 增加一个耗时任务
  setInterval(function () {
    for (let i = 0; i < 100000000; i++) {}
  }, 0)

  let worker = new Worker('worker.js')
</script>

worker.js

let startTime = Date.now()
let count = 0
setInterval(function () {
  count++
  console.log(count + ' --- ' + (Date.now() - (startTime + count * 1000)))
}, 1000)

什么是 DNS?

概念: DNS(Domain Names System) 域名系统,提供的是一种主机名到 IP 地址的转换服务。
作用: 当客户端向 DNS 服务器发送域名查询请求,DNS 服务器就会告知客户端 web 服务器的 IP 地址

描述一下 DNS 查询域名的过程?

DNS 查询域名顺序:
浏览器缓存 → 系统缓存 → 路由器缓存 → ISP DNS 缓存 → 根域名服务器

1. 浏览器缓存

浏览器搜索自己的 DNS 缓存,有则返回,没有进入下一步

2. 系统缓存

查看本地的 hosts 文件有没有相应的映射记录,有则返回,没有进入下一步

3. 路由器缓存

常用的路由器也带有自动缓存功能,这里会查询路由器的 DNS 缓存,如果有则返回,没有进入下一步

4. 本地域名服务器缓存

向本地域名服务器发送请求进行查询。本地 DNS 服务器收到请求后,查询自己的缓存记录,如果查到了直接返回就结束了,没有进行下一步

5. 根域名服务器

本地 DNS 服务器向 DNS 的根域名服务器发起查询请求
比如对www.baidu.com域名进行解析

  • 首先本地域名服务器向根域名服务器发起请求,根域名服务器返回顶级域名服务器 .com 的地址
  • 本地域名服务器拿这个顶级域名服务器的地址后,向其发起请求,顶级域名服务器返回权限域名服务器 baidu.com 的地址
  • 本地域名服务器根据权限域名服务器的地址,向其发起请求,最终拿到该域名对应的 IP 地址

谈谈你对三次握手的理解?

作用: 建立 TCP 连接

报文标识符解读:

  • SYN 同步序列编号(Synchronize Sequence Numbers):1 表示建立连接
  • FIN TCP 的码位字段(Function Item Number):1 表示关闭连接
  • ACK 确认字符(Acknowledge character):在数据通信中,接收方返回给发送方,表示发来的数据已确认接收无误
  • ack 确认位
  • seq 初始化序列号(Sequence Number):由客户端或者服务端创建的随机序列号

过程:

  1. 第一次握手:请求连接,客户端发送连接请求报文:
SYN = 1、seq = x
  1. 第二次握手:服务器收到连接请求报文,需要进行确认,然后返回报文:
SYN = 1、ACK = 1、seq = y、ack = x+1
  1. 第三次握手:客户端收到服务器的 SYN + ACK 报文,返回确认报文:
ACK = 1、seq = x+1、ack = y+1

谈谈你对四次挥手的理解?

作用: 关闭 TCP 连接

  1. 第一次挥手:客户端请求断开连接,并且停止发送数据,发起报文:
FIN = 1、seq = u
  1. 第二次挥手:服务端收到连接释放报文,发出确认报文:
ACK = 1、ack = u + 1、seq = v
  1. 第三次挥手:服务端将最后的数据发送完毕后,就像客户端发送连接释放报文,如下:
FIN = 1、ACK = 1、seq = w、ack = u+1
  1. 第四次挥手:客户端收到服务器的连接释放报文后,必须发出确认报文:
ACK = 1、ack = w+1、seq=u+1

建立 TCP 连接为什么需要三次握手?

作用: 三次握手的作用就是双方都能明确自己和对方的收发报文能力是正常的

  1. 第一次握手: 客户端发送报文,服务端收到了,这样服务端就能确认:
    客户端的发送、服务端接收是正常的
  2. 第二次握手: 服务端发送报文,客户端收到了,这样客户端就能确认:
    服务端接收、发送是正常的;客户端自己接收、发送是正常的。不过此时服务端并不能确认客户端的接收能力是否正常
  3. 第三次握手: 客户端发送报文,服务端受到了,这样服务端就能确认:
    客户端接收是正常的、服务端自身接收是正常的

经历了三次握手过程,客户端和服务端都确认了自己接收、发送能力是正常的,然后就可以正常通信了

关闭 TCP 连接为什么需要四次挥手?

关闭连接时,服务端收到 FIN 报文时,很可能不会立即关闭连接,所有只能先回复一个 ACK 报文,告诉客户端,你发送的 FIN 报文我收到了,只有等到我服务端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送,故需要四步握手

谈谈你对 UDP 和 TCP 的区别?

一个 url 包括什么?

当在浏览器上输入 url,浏览器会进行解析,一个 url 包括如下部分:

  • protocal 协议头:http、https
  • host 域名:baidu.com
  • port 端口
  • path 路径:/public/img
  • query 查询字符串:?id=1&type=2
  • fragment 锚点连接:#home

什么是 HSTS?

HSTS(HTTP Strict Transport Security)是 http 严格传输安全的缩写,这是一种网站用来声明他们只能使用安全连接(HTTPS)的方法,如果一个网站声明了 HSTS 策略,浏览器必须拒绝所有的 HTTP 连接并阻止用户接收不安全的 SSL 证书

介绍下浏览器缓存?(什么是强缓存、协商缓存)

1.强缓存(不需要发起请求): 浏览器校验 header 中的 Cache-Control、Expires

Expires(HTTP1.0 提供): 值为一个绝对时间,表示资源缓存的过期时间

Cache-Control: max-age=484200(HTTTP1.1 提供): 值以秒为单位,表示资源被缓存的最大时间

如果同时存在 Cache-ControlExpires,浏览器优先使用 Cache-Control


2. 协商缓存(需要发起请求): 服务器校验 header 中的 Last-Modified、Etag

当浏览器访问资源,服务器会返回这两个字段

Last-Modified: 表示资源的最后修改时间,下一次浏览器请求资源时会发送 If-Modified-Since 字段。服务器用本地的 Last-Modified 时间和 If-Modified-Since 时间比较,如果不一致则认为缓存过期并返回新资源给浏览器;如果时间一致则返回 304(NotChanged)状态码,内容为空

Etag: 资源的实体标识(哈希字符串),当资源内容更新时,Etag 会改变。服务器会判断 Etag 是否发生变化,如果变化则返回新资源,否则返回 304。

两者同时存在,会先判断 last-modified ,再判断 Etag

说说地址栏输入 URL 敲下回车后发生了什么?

一、 简单分析

简单的分析,从输入 URL 到回车后发生的行为如下:

  • URL 解析
  • DNS 查询
  • TCP 链接
  • HTTP 请求
  • 响应请求
  • 页面渲染

二、详细分析

1. URL 解析

浏览器会判断 url 的合法性,以及是否有可用的缓存。如果判断是 url 就会进行域名解析,如果不是 url,则直接使用搜索引擎搜索

当然,这里如果是 url 浏览器还会做一些操作:

HSTS: 如果网站声明了 HSTS,会强制客户端使用 HTTPS 访问页面

其他操作: 还会进行一些额外的操作,比如安全检查、访问限制等

2. DNS 查询
根据域名查询对应的 ip 地址(因为机器只能识别 ip 地址),如果输入的是 ip 地址,则省略这一步

3. TCP 连接
在确定目标服务器的 ip 地址后,则需要进行三次握手建立连接

4. HTTP 请求
当建立 TCP 连接之后,就可以在这基础上进行通信,浏览器发送 http 请求到目标服务器

5. 响应请求
当服务器接收到浏览器的请求之后,就会进行逻辑操作,处理完成之后返回一个 http 响应信息

6. 页面渲染
当浏览器收到服务器响应的资源后,首先会对资源进行解析:

  • 查看响应头的信息,根据不同的指示做对应处理,比如重定向,存储 cookie,解压 gzip,缓存资源等等
  • 查看响应的 Content-Type 的值,根据不同的资源类型采用不同的解析方式

然后接下来进行渲染页面,过程如下:

  • 解析 HTML,构建 DOM 树
  • 解析 CSS,生成 CSS 规则树
  • 合并 DOM 树和 CSS 规则,生成 render 树
  • 布局 render 树(Layout/reflow),负责各元素尺寸、位置的计算
  • 绘制 render 树(paint),绘制页面像素信息
  • 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成(composite),显示在屏幕上

浏览器底层

什么浏览器同源策略?

同源策略是浏览器的一种用于隔离潜在恶意文件的重要安全保护机制(服务器没有这个策略限制)
所谓的同源是指:资源地址的“协议 + 域名 + 端口”三者都相同

在浏览器中,除了以下三个获取资源类型的标签,大部分内容都受同源策略限制
<img><link><script>

怎么解决跨域?

主要有如下 3 种跨域方案:

  • JSONP
  • CORS
  • 服务器代理(webpack 代理、Nginx 反向代理)

JSONP
原理:利用<script>标签不受同源策略的限制的特性,实现跨域效果

  • 前端代码
<script>
  function callback(res) {
    console.log(res)
  }
</script>
<script src="http://192.168.1.1:3000/api/jsonp?callback=callback&name=Jack&age=18"></script>
  • 后端代码(Node)
exports.jsonp = async (ctx, next) => {
  let { callback } = ctx.request.query
  ctx.body = `${callback}(${JSON.stringify({ data: 1 })})`
}

CORS
跨域资源共享(CORS)
原理:它利用一些额外的 HTTP 响应头来通知浏览器,允许访问来自指定 origin 的非同源服务器上的资源

服务端配置(Node):

app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', 'http://baidu.com')
  ctx.set('Access-Control-Allow-Headers', 'content-type')
  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE')
  await next()
})

代理服务器
说明:因为服务器与服务器之间没有跨域问题,所以可以利用代理服务转发请求

  1. 开发环境的跨域问题(使用 webpack 代理服务器解决)
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'www.baidu.com'
      }
    }
  }
}
  1. 生产环境的跨域问题(使用 nginx 服务器代理)

传输协议

get 和 post 的区别?

  • get 比 post 更不安全,因为参数直接暴露在 URL 上,所以不能用来传递敏感信息
  • get 请求有长度限制,这里限制的是整个 URL 长度,而不仅仅是参数值的长度;post 没有限制
  • get 请求参数会被完整保留在浏览器历史记录里,post 的参数不会被保留
  • get 参数通过 URL 传递;post 放在 Request body 中
  • get 请求在浏览器回退时是无害的;post 会再次提交请求

http 和 https 的区别?

  • http 数据传输是明文的,https 使用了 SSL/TLS 协议进行了加密处理,相对更安全
  • http 和 https 使用连接方式不同,默认端口也不一样,http 是 80,https 是 443
  • https 由于需要设计加密以及多次握手,性能方面不如 http
  • https 需要 CA 机构颁发的证书

说说 http 常见的状态码有哪些?

状态码第一位数字决定了不同的相应状态,如下:

  • 1 表示消息
  • 2 表示成功
  • 3 表示重定向
  • 4 表示请求错误
  • 5 表示服务器错误

1xx
代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束

  • 100(客户端继续发送请求,这是临时响应):这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应
  • 101:服务器根据客户端的请求切换协议,主要用于 websocket 或 http2 升级

2xx
代表请求已成功被服务器接收

  • 200(成功):请求已成功,请求所希望的响应头或数据体将随此响应返回

3xx
表示要完成请求,需要进一步操作。通常,用来表示重定向

  • 300(多种选择):针对请求,服务器可执行多种操作。服务器可根据请求者(user agent)选择一项操作,或提供操作列表供请求者选择
  • 301(永久移动):请求的网页已永久移动到新位置。服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置
  • 302(临时移动):服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
  • 307(临时重定向):服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求

4xx
代表客户端可能发生了错误,妨碍了服务器的处理

  • 400(错误请求):服务器不理解请求的语法。有可能参数等错误
  • 401(未授权):请求要求身份验证。对于需要登录的网页,服务器可能返回此响应
  • 403(禁止):服务器拒绝请求
  • 404(未找到):服务器找不到请求的网页
  • 405(方法禁用):禁用请求中指定的方法
  • 407(需要代理授权):此状态码与 401 类似,但指定请求者应当授权使用代理
  • 408(请求超时):服务器等候请求时发生超时

5xx
表示服务器无法完成明显有效的请求。这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生

  • 500(服务器内部错误):服务器遇到错误,无法完成请求
  • 501(尚未实施):服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码
  • 502(错误网关): 服务器作为网关或代理,从上游服务器收到无效响应
  • 503(服务不可用): 服务器目前无法使用(由于超载或停机维护)
  • 504(网关超时): 服务器作为网关或代理,但是没有及时从上游服务器收到请求
  • 505(HTTP 版本不受支持): 服务器不支持请求中所用的 HTTP 协议版本

TCP/Ip 五层协议?

  • 应用层
  • 传输层
  • 网络层
  • 数据链路层
  • 物理层

TCP/IP 模型比 OSI 模型更加简洁,他把 “应用层/表示层/会话层” 全部整合成了“应用层”

OSI 七层模型

  • 应用层
  • 表示层
  • 会话层
  • 传输层
  • 网络层
  • 数据链路层
  • 物理层

手写代码

最新的 url 参数获取的 API?

URLSearchParams

// 有如下一个url: http://localhost?a=1&b=2

function getUrlParam(name) {
  let paramStr = location.search.substr(1)
  let params = new URLSearchParams(paramStr)
  return params.get(name)
}

console.log(getUrlParam('a')) // 1

实现数组去重?

现在有如下一个数组:

let arr = [1, 1, 'true', 'true', true, true, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}]
  • 使用 ES6 的 Set 方法

利用 Set 数据类型的特性,集合内部不会有重复的元素
不能过滤对象

Array.from(new Set(arr))
// 等于
[...new Set(arrr)]
  • 使用对象键值唯一的特性

不能过滤对象

function unique(arr) {
  let obj = {}
  return arr.filter((item) => {
    let key = typeof item + item
    return obj.hasOwnProperty(key) ? false : (obj[key] = true)
  })
}

因为对象的 key 是 string 类型,无论你传入什么他都会解析成字符串,所以就导致了数字 1 和字符串 1,在对象中的 key 是相同的。
所以会把它们当成相同的数据过滤掉,所以可以使用 typeof 简单判断下数据类型

  • 使用 Map
function unique(arr) {
  let map = new Map()
  return arr.filter((item) => {
    return map.has(item) ? false : map.set(item, true)
  })
}
  • indexOf 或 includes

不能过滤对象、NaN

function unique(arr) {
  let list = []
  if (!Array.isArray(arr)) {
    throw new Error('type error')
  }
  for (let i = 0, l = arr.length; i < l; i++) {
    let item = arr[i]
    if (list.indexOf(item) === -1) {
      list.push(item)
    }
  }
  return list
}
  • 双层 for 循环

不能过滤对象、NaN

function unique(arr) {
  // let l = arr.length
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1)
        j--
      }
    }
  }
  return arr
}

实现深拷贝?

有如下的一个数据:

let obj = {
  a: undefined,
  b: 1.7976931348623157e10308,
  c: -1.7976931348623157e10308,
  d: new Date(),
  e: new RegExp('\\w+'),
  f: function () {
    console.log(1)
  }
}
  • 使用 JSON.parse(JSON.stringify(obj))
    弊端如下:

    • 如果 obj 里面有时间对象,时间将只是字符串的形式。而不是时间对象
    • 如果 obj 里有 RegExp、Error 对象,则序列化的结果将只得到空对象
    • 如果 obj 里有函数,undefined,则序列化的结果会把函数或 undefined 丢失
    • 如果 obj 里有 NaN、Infinity(正无穷大)和-Infinity(负无穷大),则序列化的结果会变成 null
    • 如果 obj 中的对象是有构造函数生成的,会丢弃对象的 constructor
  • 手写迭代拷贝

function deepClone(source) {
  if (!source || typeof source !== 'object') {
    return source
  }
  if (source instanceof Date) {
    return new Date().setTime(source.getTime())
  }
  let newObj = Array.isArray(source) ? [] : {}
  for (let key in source) {
    if (source[key] && typeof source[key] === 'object') {
      newObj[key] = deepClone(source)
    } else {
      newObj[key] = source[key]
    }
  }
  return newObj
}

实现扁平数组(降维数组)

  • 重写 concat 方法

只能处理二维数组

let arr = [
  [1, 2],
  [3, 4]
]

Array.prototype.concat.apply([], arr) // [1, 2, 3, 4]
  • 使用 flat 方法

可以处理多维数组

let arr = [
  [1, 2],
  [3, 4]
]

arr.flat(Infinity) // [1, 2, 3, 4]

实现取数组的最大值?

  • Math.max.apply(null, [4, 1, 2, 3])
  • Math.max(...[4, 1, 2, 3])

let a = null 有什么作用?

let a = null 一般用作手动释放该变量的内存,而不是等到离开作用域后被自动回收

如何阻止冒泡事件

function stopBubble(e) {
  if (e && e.stopPropagation) {
    // 非IE浏览器
    e.stopPropagation()
  } else {
    //IE浏览器
    window.event.cancelBubble = true
  }
}

如何阻止浏览器默认事件

function stopDefault(e) {
  //标准浏览器
  if (e && e.preventDefault) {
    e.preventDefault()
  }
  //个别IE
  else {
    window.event.returnValue = fale
    return false
  }
}

获取非行间样式

function getCss(curEle, attr) {
  var val = null
  try {
    val = window.getComputedStyle(curEle, null)[attr]
  } catch (e) {
    val = curEle.currentStyle[attr]
  }
  return val
}
getCss(div, 'width')

排序算法

手写冒泡

名字的由来: 越大(或越小)的元素经过交换慢慢浮到数列的顶端。就如同饮料中的气泡最终会上浮到顶端一样,故名叫“冒泡排序”

规则如下:

  1. 外层循环和内层循环只需要循环 length - 1

两两比较,最后的两个数只需要比较一次就能确定两者的位置
比如 [2, 1, 3, 4, 5],2 和 1 只需要比较一次就能确定位置了[1, 2, 3, 4, 5]

  1. 内层循环在 length - 1 的基础上减去 i,也就是 length - 1 - i

每当外层 for 成功循环一次之后,内层 for 就可以少循环一次,这里注意是当外层 for 循环完毕之后

  1. 内层循环:用当前的值 a(arr[j])和下一个值 b(arr[j + 1])比较大小,如果 a 大于 b,则交换两数的位置,最终的目的是将最大的数移到数组最后面
let arr = [5, 4, 3, 2, 1]

function bubbleSort(arr) {
  let len = arr.length
  for (let i = 0; i < len - 1; i++) {
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        let temp = arr[j]
        arr[j] = arr[j + 1]
        arr[j + 1] = temp
      }
    }
  }
}

bubbleSort(arr)

快排

规则如下:

  1. 在数组中,找到一个元素作为“基准”(pivot)
  2. 小于基准的元素,就移到“基准”的左边;大于“基准”的元素,就移到“基准”的右边
  3. 对“基准”左右两边的集合,进行递归,不断重复第一步和第二步,直到所有子元素只剩一个元素为止
function quickSort(arr) {
  if (arr.length === 0) return arr
  const pivotInx = Math.floor(arr.length / 2)
  const pivot = arr.splice(pivotInx, 1)[0]
  const left = []
  const right = []
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i]
    if (item < pivot) {
      left.push(item)
    } else {
      right.push(item)
    }
  }
  return quickSort(left).concat([pivot]).concat(quickSort(right))
}

看如下代码,输出什么?

  • 考引用数据类型的
let a = { name: 'Jone' }
let b = a
a = { name: 'Sasa' }
console.log(b)

答案:{ name: 'Jone' }
b = a 时,ab 指向同一个堆地址。
a = {name: 'Sasa'} a 被重新赋值,所以 a 指向新的堆地址,b 仍然是原来的堆地址

  • 考基本数据类型的
let a = 1
b = a
b = 2
console.log(a)
posted @ 2019-06-26 15:06  时光凉忆  阅读(444)  评论(0编辑  收藏  举报