1. JaveScript
1.1 Child.prototype = Object.create(Parent.prototype)和Child.prototype = new Parent()的区别
| 标题 | Object.create(Parent.prototype) | new Parent() |
|---|---|---|
| 是否执行Parent | 否 | 是 |
| 子类原型上是否父类"实类字段" | 没有 | 有 |
| 父构造函数有副作用是 | 安全 | 可能出问题 |
| 常见推荐 | ES5里更推荐这种寄生组合 | 老写法,能跑单易多月/易踩坑 |
1.2 var,let和const的区别
- 变量提升
- var声明的变量存在变量提升
- let和const不存在变量提升
- 块级作用域
- var不存在块级作用域
- let和const存在块级作用域
- 暂时性死区
- var不存在暂时性死区
- let和const存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
- 重复声明
- var允许重复声明
- let和const在同一作用域下不允许重复声明变量
- 修改声明的变量
- var和let可以修改声明的变量
- const声明一个只读的变量
1.3 作用域和作用域链
- 作用域:即变量和函数生效的区域或集合,换句话说,作用域决定了代码区块中变量和其他资源的可见性。
- 作用域分为:
- 全局作用域:在代码中任何地方都能访问到的变量,拥有全局作用域
- 局部作用域:函数内部定义的变量只能在函数内部访问,称为局部变量
- 块级作用域:使用let和const关键字声明的变量具有块级作用域。块级作用域指的是变量只在声明它的代码块内有效
- 作用域链:当在JavaScript中使用一个变量的时候,首先JavaScript引擎会尝试在当前作用域下寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
1.4 原型和原型链
- 每个构造函数都有一个原型属性prototype保存了所有用构造函数实例化出来的对象和方法,用构造函数创建一个对象,这个对象上会有一个属性__proto__指向构造函数的原型属性。
- 当我们使用一个对象的属性或方法的时候,首先在自身内存查找,找到就用,找不到就去原型上找。原型的本质就是对象,对象又有自己的原型,原型如果没有这个方法,就去原型的原型中查找,这个查找的链条就叫做原型链,最后找到Object.prototype,如果Object.prototype没有这个属性,直接返回undefined
1.5 typeof和instanceof区别
- typeof操作符返回一个字符串,表示未经计算的操作数的类型,返回一个变量的基本类型(返回值有number,string,boolean,function,undefined,object)
- instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上,返回一个布尔值。
1.6 new操作符具体干了什么
- 创建了一个新对象
- 将对象和构造函数通过原型链链接起来
- 将构造函数的this执行新对象
- 返回新对象
1.7 call, apply和bind的区别
| 方法 | 是否立即执行 | 参数形式 | 返回值 |
|---|---|---|---|
| call | 立即执行 | 逗号分隔的实参 | 函数执行结果 |
| apply | 立即执行 | 数组 | 函数执行结果 |
| bind | 不执行 | 可先传部分参数 | 新的绑定函数 |
1.8 事件循序的理解
js是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所有会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。
- 微任务:Promise.then/catch/finally,await后面
- 宏任务:setTimeout,setInterval,AJAX,事件
- await前面是同步,后面当then
1.9 函数式编程的优缺点
- 优点
- 更容易推导与测试:同样的输入必然得到同样的输出,出问题更好定位
- 可读性高
- 代码复用性好:通过函数组合,很多逻辑可以抽成小函数复用,形成积木
- 缺点
- 上手门槛与思维成本
- 性能与内存开销可能更大:频繁创建新对象/新数组可能带来额外GC压力。
- 可能导致过渡抽象:为了追求函数式风格,把简单问题写得很复杂,反而降低可维护性。
1.10 web常见攻击
- XSS攻击(跨站脚本工具)
- 攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息和cookie,sessionID等,进而危害数据安全
- 防范XSS攻击
- httpOnly:在Cookie中设置httpOnly属性后,js脚本将无法读取到Cookie信息
- 输入过滤
- 检测按照规定的格式输入
- 转义HTML,把括号,尖括号,斜杠进行转义
- CSRF攻击(跨站请求伪造)
- 一种挟持用户在当前已登录的web应用程序上执行非本意的操作的攻击方法
- 防御CSRF攻击
- 验证码
- 设置token
- 防御CSRF攻击
- 一种挟持用户在当前已登录的web应用程序上执行非本意的操作的攻击方法
1.11 地址栏输入地址回车发生了什么
- 解析URL:对url进行解析,判断所需要使用的传输协议和请求的资源的路径是否合法,如果合法,浏览器会检查url中可能存在的非法字符进行转义,进而进行下一个过程;不合法的话会将输入的内容传递给搜索引擎。
- 缓存判断:浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
- DNS解析:获取输入的URL中的域名的IP地址,首先会判断本地是否有该域名的IP地址缓存,如果有则使用,如果没有则向本地DNS服务器发起请求。
- 获取MAC地址:数据传输还需要知道目的主机的MAC地址。从应用层一直下发到数据链路层,数据链路层的发送需要加入通信双方的MAC地址,本机的MAC地址作为源MAC地址,目的MAC地址需要分情况处理。如果与请求主机在同一个子网里,可以使用APR协议获取到目的主机的MAC地址,否则转发给网关,由它代为转发。
- TCP三次握手:如下
- 返回数据:服务端返回一个html文件作为响应,浏览器接收到响应后,开始对html文件进行解析,开始页面的渲染过程。
- 页面渲染:html文件生成DOM树,css文件生成CSSDOM树,如果遇到Script标签,则判断是否含有defer和async属性,要不然script的加载和执行会造成页面的渲染和阻塞。根据DOM树和CSSDOM数来构建渲染树,进而进行布局渲染。
1.12 三次握手,四次挥手
-
HTTP本身并不直接涉及三次握手和四次挥手,这些实际上是TCP(传输控制协议)的特性。HTTP是应用层协议,而TCP是传输层协议。当HTTP通信发生时,它是建立在TCP连接上的。
-
TCP三次握手
- 第一次握手(SYN)
- 客户端发送一个SYN(同步)包到服务器
- 此包包含客户端的初始序列号(ISN)
- 第二次握手(SYN+ACK)
- 服务器收到SYN包后,回复一个SYN-ACK包
- 此包确认客户端的SYN,并包含服务器自己的SYN(初始序列号)
- 第三次握手(ACK)
- 客户端收到SYN-ACK后,发送一个ACK包
- 此包确认服务器的SYN
- 第一次握手(SYN)
-
为什么要三次握手?
- 确保双方都有发送和接收能力
- 同步双方的初始序列号
- 防止旧的重复链接初始化造成混乱
-
四次挥手
- 第一次挥手(FIN)
- 客户端发送一个FIN包,表示客户端没有数据要发送了
- 第二次挥手(ACK)
- 服务器收到FIN包,发送一个ACK确认
- 此时服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态
- 第三次挥手(FIN)
- 服务器准备关闭连接时,向客户端发送一个FIN包
- 第四次挥手(ACK)
- 客户端收到FIN后,发送一个ACK确认
- 客户端进入TIME_WAIT状态,等待2msl(最大报文生存时间)后关闭连接
- 第一次挥手(FIN)
-
为什么需要四次挥手?
- 全双工通信:TCP是全双工的,每个方向都需要单独关闭
- 优雅关闭:需要服务器在客户端请求关闭后继续发送未传输完的数据
- 确保数据的完整性:通过ACK确认,确保所有数据都被正确接收。
-
为什么三次握手而四次挥手
- 握手阶段
- 服务器在收到客户端的SYN后,可以在一个包中发送SYN和ACK
- 挥手阶段
- 当服务器收到客户端的FIN后,它不能立即关闭连接,因为可能还有数据需要发送
- 服务器需要先发送ACK确认收到客户端的FIN,然后在准备好再发送自己的FIN
- 握手阶段
1.13 重绘,回流
-
回流
- 当元素的尺寸,布局,隐藏等改变而重新构建时,就称之为回流
-
回流
- 当元素的外观,风格改变,不会影响布局的时候,就称之为重绘
- 回流必将引起重绘,而重绘不一定会影响回流
-
如何避免重绘和回流
- 尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。
- 避免设置多层内联样式,css选择符从右往左匹配查找,避免节点层级过多。
- 使用动画时尽可能使用requestAnimationFrame
- 使用visibility替换display: none。因为前者只会引起重绘,后者会引发回流。
1.14 强缓存,协商缓存
- 强缓存
- 缓存资源在“新鲜期”内,浏览器/中间层直接命中本地缓存,通常不会发请求到服务器。
- 常见控制方式
- Cache-Control: max-age=xxx
- Expires: <具体时间> (较老,精度依赖客户端时间)
- 也常见配合
- Cache-Control:public/private
- s-maxage(CDN/共享缓存用,浏览器端不一定用)
- immutable(文件内容不会变,客户端可长期不再校验,常用于带hash的静态资源)
- 强缓存流程(浏览器视角)
- 第一次请求资源GET/xxx,服务器返回响应头(例如: Cache-Control: max-age=31536000)
- 浏览器把资源和元信息一起缓存。
- 过一段时间再次访问同一资源:
- 若仍在max-age有效期内 -> 直接使用缓存;
- 实际通常体现为响应状态码类似200(from memory cache / disk cache)(不同浏览器标记不同)
- 不会发送条件请求(也就没有304这种协商请求)
- 优点
- 最快:不需要RTT(往返时间)和服务器处理
- 省带宽:不下载响应体
- 抗服务器压力:流量主要在本地命中
- 缺点/风险
- 内容更新的失效问题:如果服务端内容变了单缓存没变,客户端可能仍然用旧资源。
- 需要配合策略:
- 静态资源用 ”文件名带hash“(如app.8f3a1.js),就可以设置很长max-age
- 动态内容通常不适合长强缓存
- 协商缓存
- 本地缓存先拿出来用,但会带条件去服务器问一句:你这份资源没变吗?如果没变,服务器会 304 Not Modified,浏览器继续使用本地缓存的数据体。
- 常见控制方式
- ETag(推荐)
- 响应头:ETag:"..."
- 再次请求时带条件头
- If-None-Match: etag
- Last-Modified(也常见)
- 响应头:Last-Modified: 时间
- 再次请求时带条件头:
- If-Modified-Since:时间
- ETag(推荐)
- 协商缓存流程(以Etag为例:)
- 第一次请求 GET/xxx:
- 服务器返回200 + ETag:abc123
- 浏览器缓存资源。
- 第二次请求时,如果缓存认为可能过期/需要校验(例如: max-age=0,或缓存已过新鲜期)
- 浏览器会发请求到服务器,并带上 If-None-Match:abc123
- 服务器判断:
- 若内容未变:返回304 Not Modified(不返回响应体)
- 若内容已变:返回200 Ok + 新响应体,并给新的 ETag
- 浏览器
- 304:继续使用本地缓存的数据体
- 200:替换为新数据
- 第一次请求 GET/xxx:
1.15 为什么会走协商缓存
- 通常是因为强缓存不成立或者过期,常见触发方式:
- Cache-Control:max-age=0(直接进入校验)
- 或资源已超过强缓存时间(freshness失效)
- 也有可能服务器/缓存策略要求重新验证,例如 no-cache(注意:no-cache不等于缓存,它是每次都要校验,是否用本地仍取决于协商结果)
1.16 垃圾回收机制
JavaScript具有自动垃圾回收机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。
-
原理
- 垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放内存
- 有以下两种方式:
- 标记清除
- 当变量进入执行环境时,会标记一个状态,当离开执行环境时,会标记一个离开的状态。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并回收它们的内存
- 引用计数
- 如果一个值的引用次数为0时,就表示这个值不再用到了,因此可以将这块内存释放
- 标记清除
-
V8垃圾回收机制:
- 基本原理
- v8采用了分代式回收机制,将内存分为新生代和老生代两个部分:
- 新生代:存放存活时间短的对象
- 老生代:存放存活时间长或常驻内存的对象
- 新生代内存回收
- 空间划分
- 新生代内存分为两个等大的空间:From空间和To空间
- 对象总是先分配到From空间
- 空间划分
- Scavenge算法:
- 当From空间快满时,触发Scavenge垃圾回收
- 过程
- 遍历From空间,将存活对象复制到To空间
- 清空From空间
- 交换From和To空间的对象
- 对象晋升
- 经过两次Scavenge后仍存活的对象会被晋升到老生代
- To空间的使用率超过25%时,存活对象也会被晋升。
- v8采用了分代式回收机制,将内存分为新生代和老生代两个部分:
- 基本原理
1.17 preload和prefetch的区别
- preload:浏览器立即加载资源
- prefetch:浏览器在空闲时才开始加载资源;也就是预加载
1.18 CommonJS,ES6模块,AMD,CMD的区别
- CommonJS:服务端的实现,module.exports或exports来暴露模块,通过require来加载模块。同步加载,也就是只有加载完成,才能执行后面的操作。代码都运行在模块作用域,不会造成全局污染。
- AMD:异步加载,允许指定回调函数。通过define()方法定义模块:require([module],callback),通过require方法加载模块,推崇依赖前置
- ES6:静态依赖+支持异步加载场景。一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。export命令用于规定模块的对外接口。import命令用于输入其他模块提供的功能。
- CMD:依赖就近,延迟执行,也是浏览器异步模块方案。
1.19 async await和Promise的区别
一句话记住:async await是基于Promise的语法糖
- 关系
- async函数一定返回Promise
- await只能在async函数里,用来暂停当前函数后续逻辑,等待Promise结果
- 本质函数Promise+事件循环
1.20 script标签里defer和async的区别
- 下载时机
- async:和HTML解析并行下载
- defer:和HTML解析并行下载
- 执行时机
- async:脚本一下载完就立即执行,会中断HTML解析
- defer:等HTML全部解析完成后再执行,不阻塞解析过程
- 多个脚本顺序
- async:不保证顺序(看网络谁快)
- defer:保证按标签出现顺序执行
1.21 forEach怎么停止
运用抛出异常(try catch)可以终止forEach循环
try {
[1,2,3,4,5,6].forEach(function(item, index){
console.log(item);
if(item === 3){
throw new Error('阻止');
}
});
} catch (error) {
console.log('error_阻止成功', error);
}
1.22 webSocket和ajax的区别
- 通信模式
- ajax:通常指用XMLHttpRequest/fetch之类发起HTTP请求,请求谁触发,服务器就回应一次
- webSocket:先进行一次握手建立连接,之后在同一个连接上双向发送消息(客户端和服务端都能随时发)
- 连接和开销
- ajax:每次通信都要走一次http请求流程(头部,TCP/TLS连接复用等都会有开销),频繁轮询会更耗资源。
- webSocket:连接建立后持续保持,后续只传消息帧,相对更适合高频实时通信。
- 实时性和服务器推送
- ajax:服务器不能主动推送(除非你用轮询等方式反复请求)
- webScoket:服务器可以在任意时刻把新消息推给客户端,延迟通常更低,更省。
- 适用场景
- ajax:普通的表单提交,拉取数据,低频更新。
- webSocket:聊天,在线状态,多人协作,实时行情/游戏状态,通知推送等。
1.23 !!和??
- !!是将一个值强制转为boolean值
- ??是当一个表达式是null或者undefined时为变量设置一个默认值
1.24 super()和super(props)
- super(props)将传入的props,赋值给组件实例props属性中,如果只调用了super(),那么this.props在super()和构造函数结束之间仍是undefined
1.25 基本数据类型和引用数据类型有什么区别
- 存储方式:基本数据类型存储在栈里,引用数据类型存储在堆
- 使用方式:引用数据类型在使用时,使用的是指向这个对象的指针,而基本数据类型使用的是数据本身。
1.26 如何判断定义一个对象没有原型
const obj = Object.create(null);
1.27 怎么判断是否是Promise
const isPromise = (var) => {
return isObject(var) && isFunction(val.then) && isFunction(val.catch)
}
1.28 如何判断是否是一个数组
1. instanceof
a instanceof Array
2. constructor
a.constructor === Array
3. Object.prototype.toString.call()
Object.prototype.toString.call(a) === '[Object Array]'
4. Array.isArray
Array.isArray(2)
1.29 js css会阻塞dom吗
- css不会阻塞DOM的解析(HTML还是会继续被解析成DOM)
- css会阻塞渲染(会阻塞CSSOM/Render Tree的构建,页面可能”白屏“一段时间)
- css还会间接阻塞后面的JS执行:如果js(尤其是同步脚本)可能读取样式,浏览器通常会等前面的css下载解析完再执行该js
- 同步js会阻塞DOM解析:解析HTML遇到它会停下来,先执行js,再继续解析
- defer脚本不阻塞DOM解析,会在DOM解析完成后,DOMContentLoaded前执行
- async脚本不阻塞DOM解析,下载完就立刻执行(执行时可能打断解析),顺序不保证
1.30 原型检测
- instanceof
- isPrototypeof
1.31 属性检测
- in
- hasOwnProperty
1.32 class声明的方法为什么不能遍历
1.33 普通函数和箭头函数的区别
1.34 js上下午执行栈
1.35 浏览器不同标签页之间如何通信
1.36 如何实现页面每次打开时清除本页缓存
1.37 Object.is与比较操作符===,==的区别
1.38 Object.assign和扩展运算符都是浅拷贝,两者的区别
1.39 __proto__和prototype的区别
1.40 详细介绍requestAnimationFrame,优缺点,应用场景等
1.41 jsonp原理,postMessage原理
1.42 js中,var,let,const定义的变量可以挂载到window下么
2. CSS
2.1 BFC
2.2 BFC可以清除浮动吗?为什么
2.3 层叠上下文和层叠顺序
2.4 src和href的区别
2.5 1px像素问题
2.6 怎么实现0.5px
2.7 高度塌陷
2.8 元素水平居中的方法
2.9 盒模型
2.10 css选择器优先级
2.11 link和@import的区别
2.12 伪类和伪元素的区别和作用
2.13 z-index在什么情况下会失效
2.14 margin重叠
3. React
3.1 diff算法
- diff算法是调和的具体表现,将虚拟dom转换为真实dom最少的操作过程称之为调和。
- 策略一:tree diff分层求异
- 同一层级进行比较,如果发现不同,直接删除,创建
- 如果跨层级的操作,不是移动,而是重新创建
- 策略二:component diff 相同的类生成类似树形结构,不同的树生成不同树形结构
- 组件之间进行比较,如果是同一类型的组件,则按照策略一进行比较
- 反之,则将该组件判定为dirty component(脏组件),从而替换整个组件下的所有节点
- 策略三:element diff 设置唯一的key
- 对同一层级的同组子节点,添加唯一的key进行区分
3.2 react声明周期
3.3 react和vue的区别
3.4 react性能优化
3.5 react通信方式
3.6 useMemo和useCallback
3.7 Real DOM和Virtual DOM的区别
3.8 state和props的区别
3.9 setState的执行机制
3.10 react事件机制
3.11 react类组件和函数组件的区别
3.12 受控组件和非受控组件
3.13 redux
3.14 react中key的作用
3.15 react hooks的优势
3.16 Fiber
3.17 纯函数组件
3.18 useEffect和useLayoutEffect的区别
3.19 hook useState可不可以拿到最新的值
3.20 useState为什么返回的是一个数组
3.21 useEffect在一个函数式组件中放三个依赖什么都一样,它们的执行顺序是什么?
3.22 在第一个useEffect中用setState,在第二个中能得到吗
3.23 useEffect等hooks为什么不能在条件语句中(if)调用
3.24 为什么react需要合成事件
3.25 为什么建议传递给setState的参数是一个callback而不是一个对象
3.26 React.Component和React.PureComponent的区别
3.27 为什么列表循环渲染的key最好不要用index
3.28 为什么使用jsx的组件中没有看到使用react,却需要引入react
3.29 React.Children.map和js的map的区别
3.30 什么是prop drlling,如何避免
3.31 当调用setState时,react render是如何工作的
3.32 react是怎么保证多个useState的相互独立的
3.28 React.createClass和extends Component的区别有哪些
3.29 hooks为什么不能放在条件判断里?
3.30 父子组件在渲染的时候生命周期执行顺序
4. Vue
5. TypeScript
6. git
7. 网络相关
7.1 http和https的区别
- 安全性
- http:明文传输,请求/响应内容可能被窃听或篡改
- https:http+TLS/SSL,加密传输,具备机密性,完整性,身份认证。
- 端口
- http:80
- https:443
- 是否需要证书
- http:不需要证书
- https:需要服务器证书(CA签发或受信任链),用于证明"你连的是谁"
- 抗攻击能力
- http:容易被中间人攻击,劫持,注入
- https:能显著降低中间人攻击风险(前提是证书校验正确)
7.2 TCP和UDP的区别
- 是否建立连接
- TCP:先建立连接(三次握手),再通信
- UDP:不需要建立连接,直接发数据(发出去不管对方收没收)
- 可靠性/是否重传
- TCP:可靠数据(有序到达,丢包重传,校验)
- UDP:不保证可靠性(丢包,乱序都可能发生,不重传)
- 数据顺序
- TCP:有序字节流(接收端按发送顺序交付)
- UDP:没有字节流的概念,通常以数据报为单位,顺序不保证
- 拥塞控制和流量控制
- TCP:内置拥塞控制,流量控制(更稳,但可能增加延迟)
- UDP:没有这些机制(更快,但需要应用自己处理)
- 首部开销/效率
- TCP:首部相对更大,协议开销更复杂
- UDP:首部更小,开销更低
7.3 304过程
当浏览器第一次请求一个资源时(如图片/接口响应),会把响应体以及缓存元信息(例如Cache-Control/Expires决定新鲜期,以及ETag或Last-Modified用于协商)一并存入缓存。之后再次访问同一资源时:如果仍在max-age的新鲜内,就直接命中强缓存,不会向浏览器发请求;如果新鲜期已过或策略要求重新校验(例如:max-age=0,no-cache等),浏览器会向服务器发起“条件请求”(conditional request),带上缓存标识头;若有ETag就带If-None-Match:etag,若是 Last-Modified则带If-Modified-Since:time。服务器收到后会对比当前资源是否与该标识一致:如果资源没有变化,就返回 304 Not Modified(通常响应体为空),并可附带一些新的缓存头(如更新后的Cache-Control/ETag等);浏览器拿到304后不再下载资源内容,而是继续使用本地缓存中的旧响应体,只把缓存的有效期/校验信息按服务器返回的头来更新。若服务器判定资源已变,则返回 200 ok并携带新的响应体和新的ETag/Last-Modified,浏览器用新内容替换缓存。
7.4 http1和http2的区别
-
连接管理
- http/1.0
- 默认短连接:一次请求响应后就断开TCP
- 每个资源(HTML,CSS,JS,图片)可能都要新建连接,开销大
- http/1.1
- 默认长连接(Connection:keep-alive),一个TCP可复用多个请求
- 减少频繁三次握手和慢启动损耗
- http/2
- 也是长连接,但更进一步:一个TCP连接上并发多路复用多个请求/响应流
- http/1.0
-
并发与队头阻塞(HOL)
- http/1.0
- 基本串行,请求效率低
- http/1.1
- 支持管线化(pipelining)理论并发,但实践中几乎不用
- 仍有应用层队头阻塞:前一个响应慢会卡后面的
- http/2
- 真正多路复用,多个流交错传输,显著环境http层阻塞
- 但仍可能有TCP层队头阻塞(一个包丢失会影响该连接内数据)
- http/1.0
-
数据格式与传输效率
- http/1.0/1.1
- 文本协议(明文头部),可读性好但冗余较大
- http/2
- 二进制分帧,解析效率更高
- 支持HPACK头部压缩,减少重复Header传输成本。
- http/1.0/1.1
-
新能力支持
- http/1.1
- 新增host头(支持虚拟主机)
- 新增分块传输chunked
- 更改缓存控制(Cache-Control,ETag等)与状态码语义完善
- http/2
- 流优先级(priority)
- 服务端推送(Server Push,现实中已较少使用)
- 更适合高并发资源加载场景
- http/1.1
-
一句话总结
- http/1.0:短连接时代,简单但低效
- http/1.1:长连接+更完整语义,成为长期主力
- http/2.0:二进制分帧+多路复用+头压缩,性能大幅提升。
7.5 网络层级协议
8. 笔试题 场景题
8.1 下面代码输出什么
function SetCount(count) {
this.count = count
}
SetCount.prototype.printCount = function () {
console.log(this.count)
}
let a = new SetCount(100)
a.count = 200
a.__proto__.count = 300
a.__proto__.printCount()
a.printCount()
解析:
让我们逐步分析这段代码:
首先,定义了一个构造函数 SetCount,它接受一个参数 count 并将其赋值给 this.count。
然后,在 SetCount 的原型上添加了一个 printCount 方法,这个方法会打印 this.count。
创建了一个 SetCount 的实例 a,初始 count 值为 100。
将 a.count 设置为 200。这会在 a 对象上直接创建一个 count 属性。
将 a.__proto__.count 设置为 300。这实际上是在 SetCount.prototype 上设置了 count 属性。
调用 a.__proto__.printCount()。这里直接在原型上调用 printCount 方法。在这种情况下,this 指向 a.__proto__,即 SetCount.prototype。因此,它会打印原型上的 count 值,也就是 300。
调用 a.printCount()。这里是通过实例 a 调用 printCount 方法。在这种情况下,this 指向实例 a。因为 a 自身有 count 属性(值为 200),所以会打印 200。
重要的是要理解 JavaScript 的原型链和 this 的绑定机制:
当通过对象调用方法时,this 通常指向该对象。
属性查找首先在对象自身进行,如果没找到,则沿着原型链向上查找。
直接在 __proto__ 上调用方法会改变 this 的指向。
8.2 请实现一个方法,可求出数组的最大值和最小值
例如: [2,4] => 2, [3,4] => 4
function getMaxOrMin(arr, type = 1){
// type等于1求最大值
if (type === 1) {
const res = arr.sort((a, b) => {
return b - a;
})
return res[0];
} else {
const res = arr.sort((a, b) => {
return a - b;
})
return res[0];
}
}
8.3 请实现一个或多个方法,能将字符串中重复的部分去重
例如:'hello' => 'helo'
function deduplication_one(data) {
const dataArr = data.split('');
const set = new Set(dataArr);
return [...set].join('')
}
function deduplication_two(data) {
const dataArr = data.split();
const str = '';
const obj = {};
for (let i = 0; i < dataArr.length; i++) {
const chat = dataArr[i];
if (!obj[chat]){
obj[chat] = true;
str += chat;
}
}
return str;
}
8.4 用多个方法实现继承
原型链实现继承
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function() {
console.log(this.name, 'ParentGetName')
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const childOne = new Child('zhangsan', 19)
childOne.getName();
类实现继承
class Parent {
constructor(name){
this.name = name;
}
getName() {
console.log(this.name, 'ParentGetName');
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
getAge() {
console.log(this.age, 'age');
}
}
const childOne = new Child('zhangsan', 19)
childOne.getName();
childOne.getAge();
构造函数实现继承
function Parent(name) {
this.name = name;
this.getName = function() {
console.log(this.name, 'ParentGetName');
}
}
function Child(name, age) {
Parent.call(this,name);
this.age = age;
this.getAge = function() {
console.log(this.age, 'childAge');
}
}
const childOne = new Child('zhangsan', 19);
childOne.getName();
childOne.getAge();
组合继承
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function() {
console.log(this.name, 'parentGetName')
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.getAge = function() {
console.log(this.age, 'childAge');
}
let childOne = new Child('zhangsan', 19);
childOne.getName();
childOne.getAge();
寄生组合继承
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function() {
console.log(this.name, 'parentGetName');
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
function inherit(Child, Parent) {
const F = function() {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
inherit(Child, Parent);
Child.prototype.getAge = function() {
console.log(this.age, 'childAge');
}
const childOne = new Child('zhangsan', 19);
childOne.getName();
childOne.getAge();
- 原型链继承:简单,但不能向父类构造函数传参
- 类继承:语法简洁,但需要ES6支持
- 构造函数继承:可以传参,但方法不能复用
- 组合继承:结合了前两种的优点,但会调用两次父类构造函数
- 寄生组合继承:被认为是引用类型继承的最佳模式,但实现较复杂。
8.5 请回答以下console输出结果
var name = 'tom';
(function(){
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye', name)
} else {
console.log('hello', name)
}
})()
// Goodbye Jack
// 立即执行函数可以创建一个作用域,保护私有变量不会污染全局变量,内部无法直接访问外部变量
var a = 10;
(function (a) {
console.log(a);
a = 5;
console.log(window.a)
var a = 20;
console.log(a)
})()
// undefined
// 10
// 20
// var定义的变量可以挂载到window下
function changeObjProperty(o) {
o.siteUrl = 'https://www.example.com'
o = new Object();
o.siteUrl = 'http://example.com'
}
let webSite = new Object();
changeObjProperty(webSite)
console.log(webSite.siteUrl);
// https://www.example.com
// 在函数中,首先修改了对象o的siteUrl属性为 https://www.example.com,
// 然后又新建了一个对象o并修改了其siteUrl属性为http://example.com,但这个新建的对象o只在函数内部有效
// 不然影响到外部的webSite对象。
// 因此,最终输出的是在函数内部修改过的webSite对象的siteUrl属性,即:https://www.example.com
8.6 完成下列不等式
class A {}
class B extends A {}
const a = new A()
const b = new B()
a.__proto__ === A.prototype
b.__proto__ === B.prototype
B.__proto__ === A
B.prototype.__proto__ === A.prototype
b.__proto__.__proto__ === A.prototype
8.7 请写出以下代码运行的结果
function Person() {}
var person1 = new Person()
var person2 = new Person()
Person.prototype.getName = function() {
return this.name
}
Person.prototype.name = 'tom'
person1.name = 'jerry'
var name = person2.getName()
console.log(name)
// tom
// 因为在Person的原型对象上定义了getName方法和name属性
// 并将name属性的值设置为tom,在实例化person2时,
// 没有为其单独设置name属性,因此调用getName方法时返回的是原型对象上的name属性值tom
// 而在实例化person1时,单独为其设置了name属性值为jerry
// 但是在调用getName方法时,this指向的是person1实例,因此返回的是person1的name属性值jerry
setTimeOut(() => {
console.log(1)
},0)
new Promise(function execulor(resolve) {
console.log(2)
for (var i = 0; i < 10000; i += 1) {
i == 9999 && resolve()
}
console.log(3)
}).then(function () {
console.log(4)
})
console.log(5)
// 2,3,5,4,1
8.8 深拷贝和浅拷贝实现
- 浅拷贝:只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享一块内存;修改新对象影响原对象
- 深拷贝:会创造一个一模一样的对象,新对象和原对象不共享内存,修改新对象不会改变原对象
- 深拷贝实现方法:
- JSON方法:缺点,不能深拷贝函数,undefined,symbol
- 函数库lodash的_.cloneDeep方法
- 浅拷贝实现方法:
- Object.assign(目标对象,源对象),源对象的所有可枚举属性都复制到目标对象上
- 扩展运算符
- concat方法
// 浅拷贝
function shallowClone(data) {
const newObj = {};
for (let prop in data) {
if (obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop]
}
}
return newObj;
}
// 深拷贝
function deepClone(data) {
if (data === null || type data !== 'object') return;
if (data instanceof Date) return new Date(data);
if (data instanceof RefExp) return new Date(data);
let newObj = {};
for (let prop in data) {
if (data.hasOwnProperty(prop)) {
newObj[prop] = deepClone(data[prop])
}
}
return newObj;
}
8.9 实现sum(1)(2,3)(4)()
function getSum() {
const args = Array.from(arguments)
return args.reduce((pre, cur) => pre + cur, 0)
}
function curriy(fn) {
let newArgs = [];
return function curried(...args) {
if (args?.length === 0) {
return fn.apply(fn, newArgs)
} else {
newArgs.push(...args);
return curried
}
}
}
const sum = curriy(getSum);
sum(1)(2,3)(3)()
8.10 实现new操作符
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype);
const result = obj.apply(obj, args);
const isObject = result !== null && (typeof result === 'object' || typeof result === 'function')
return isObject ? result : obj;
}
8.11 防抖和节流
在不影响客户体验的前提下,将频繁的回调函数,进行次数缩减,避免大量计算导致的页面卡顿
- 防抖
- 将多次执行变为最后一次执行,就是指触发事件后在n秒内函数只能执行一次,如果在n秒内又触发了事件,则会重新计算函数执行事件
- 将多次执行变为在规定时间内只执行一次,就是指连续触发事件但在n秒内只执行一次函数
// 防抖
function debounce(func, wait) {
let timer;
return function() {
let context = this;
let args = argument;
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
},wait)
}
}
// 防抖立即执行版
function debounce(func, wait, immediate) {
let timer;
return function() {
let context = this;
let args = arguments;
if (timer) clearTimeout(timer);
if (immediate) {
let callNow = !timer;
timer = setTimeout(() => {
timer = setTimeout(() => {
timer = null
})
}, wait)
if (callNow) {
func.apply(context, args)
}
} else {
timer = setTimeout(() => {
func.apply(context, args);
}, wait)
}
}
}
// 节流
function throttled(func, delay) {
let oldDate = new Date();
return function (...args) {
let newDate = new Date();
if (newDate - oldDate >= delay) {
func.apply(this, args);
oldDate = newDate;
}
}
}
function throttled(func, delay) {
let timer;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, delay)
}
}
}
8.12 Promise内部是如何实现的,generator函数内部是如何实现的,如何通过generator实现一个async await方法
8.13 怎么让Promise返回有顺序
let promiseArr = [a, b, c];
const promiseOneByOne = (arr) => {
arr.reduce((pre, cur) => {
return pre().then((res) => {
return cur().then((res2) => {
console.log(res3)
})
})
}, Promise.resolve())
}
let promiseOneByOne = async (data) => {
for (let item of data) {
const res = await item().then()
}
}
8.14 怎么判断两个引用类型的值是否相等
// 对象是否相等
function objEqual(a, b) {
const aProps = Object.getOwnPropertyNames(a);
const bProps = Object.getOwnPropertyNames(b);
if (aProps.length !== bProps.length) return false;
for (let i = 0; i < aProps.length; i++) {
const propName = aProps[i];
if (a[propName] !== b[propName]) {
return false;
}
}
return true;
}
// 数组是否相等
// 把数组转换为字符串,然后再判断:JSON.stringify(), toString(), join()
// 循环
function arrEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++ ) {
if (a[i] !== b[i]) {
return false
}
}
return true
}
8.15 for of为什么不能遍历对象
- for of作为es6新增的遍历方式,允许遍历一个函数iterator接口的数据结构(数组,对象)并且返回各项的值,
var obj = {
a: 1,
b: 2,
c: 3
}
obj[Symbol.iterator] = function() {
let keys = Object.keys(this);
let counts = 0;
return {
next() {
if (counts < keys.length) {
return { value: this[keys[counts++]], done: false}
} else {
return { value: undefined, done: true}
}
}
}
}
// 使用Generator函数生成迭代器
obj[Symbol.iterator] = fucntion()* {
const keys = Object.keys(this);
for (let k of keys) {
return [k, this[k]]
}
}
8.16 怎么监听对象的变化
// Object.defineProperty
function observeKey(obj, key) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log('获取值', value);
return value;
},
set(newValue) {
console.log('改变值', newValue);
value = newValue;
}
})
}
let obj = { name: 'zhangsan', age: 19};
observeKey(obj, 'name');
console.log(obj.name);
obj.name = 'lisi';
// Proxy
let proxy = new Proxy(obj, {
get(target, key) {
},
set(target, key, value) {
}
})
8.17 下方代码输出什么
for (var i = 0; i < 2; i++) {
setTimeout(() => {
console.log(i);
})
}
for (let i = 0; i < 2; i++) {
setTimeout(() => {
console.log(i);
})
}
// 第一个:2 2
// 第二个:0 1
// setTimeout里的回调是异步执行的,会等当前同步代码(for循环)跑完后再执行
// var是函数作用域,循环里只有一个共享的i。当两个回调真正执行时,循环早就结束了,i已经变成2,所以打印两次2
// let是块级作用域,每次循环都会创建一个新的i(可以理解为每轮一个独立副本)。所以两个回调分别拿到的是当轮的i:第一轮0,第二次1
8.18 改变对象的原型
1.Object.serPrototypeOf(obj, proto)
直接修改已有对象的原型
Object.setPrototypeOf(obj, newProto)
2.obj.__proto__ == proto(不推荐)
这是早期的访问器写法,兼容性广但规范上不鼓励在生产中使用
obj.__proto__ = newProto
3.基于原型”创建对象“而不是改老对象:Object.create(proto)
严格说不是”改“,而是创建时指定原型
const obj = Object.create(newProto)
4.构造函数/class场景下改”实例原型来源“
改的是构造函数的prototype,不是已创建实例
Foo.prototype = newProtoObj
const a = new Foo()