前端面试笔记整理——JS
1. 变量类型和计算
1.1 JS值类型和引用类型的区别
- 常见的值类型:
undefine、字符串、布尔值、数值、Symbol - 常见的引用类型
数组、对象、null、函数(特殊的引用类型,但不用于存储数据)
(五种基本类型:undefine null string number boolean) - typeof运算符的作用:
该运算符能识别所有值类型,可以判断是否是引用类型(不可以细分),可以识别函数。 - 深拷贝:
代码一:
function deepClone(obj) {
// 1 判断是否是非应用类型或者null
if (typeof obj !== 'object' || obj == null) return obj
// 2 创建一个容器
let cloneObj = new obj.constructor()
// 3 拿到对象的keys,给容器赋值
Object.keys(obj).forEach(v => cloneObj[v] = deepClone(obj[v]))
// 4 返回容器
return cloneObj
}
代码二:
function deepClone2(obj = {}) {
if (typeof obj !== 'object' || obj == null) {
return obj
}
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// 保证key不是原型的属性
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key])
}
}
return result
}
1.2 变量计算——类型转换
- 字符串拼接
- ==
“==”运算符在运算时会发生变量转换
- if语句和逻辑运算
- truly变量:!!a===true的变量
- falsely变量:!!a===false的变量
- 逻辑判断采用“短路原则”:
2. JS原型和原型链
2.1 class和继承
class是在ES6中提出来的,可以看作是对象的模板,用一个类可以创建出许多不同的对象,其继承由“extends”和“super”两个字段实现。(在ES6之前,对象以构造函数的形式实现,并通过原型继承的方式实现,当然,class继承的本质也是原型继承)
2.2 隐式原型和显式原型
- 每个class都有显示原型prototype;
- 每个实例都有隐式原型__proto__;
- 实例的__proto__指向对应class的prototype。
基于原型的执行规则: 获取实例对象的某个属性或者执行实例属性的某个方法时,先在自身属性和方法中寻找,如果找不到则自动去__proto__中查找。
2.3 原型链
关于原型链的理解: 每一个子类的原型中,其隐式原型指向父类的原型(包括显式原型和隐式原型,类似一种血脉相成的思想)。
2.4 相关常见问题
- 如何准确判断一个变量时数组—— a instanceof Array
- class的原型本质——原型和原型链的图示,属性和方法的执行规则
3. 作用域和闭包
3.1 作用域
作用域链: 当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域的前端始终都是当前执行的代码所在环境的变量对象。下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境,一直延续到全局执行环境。(全局作用环境始终都是作用域链中的最后一个对象)
作用域代表了一个变量的合理使用范围,分为全局作用域、函数作用域和块级作用域(ES6新增)。
块级作用域举例:
3.2 自由变量
- 一个变量在当前作用域没有定义,但被使用了
- 向上级作用域,一层一层依次寻找,直至找到为止
3.3 闭包
定义:闭包是指有权访问另一函数作用域中的变量的函数。
创建闭包的常用方式就是在一个函数的内部创建另一个函数。
自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方!!!
输出均为100!
闭包的缺陷: 由于其会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过度使用闭包可能会导致内存占用过多(内存泄露问题)。
3.4 this指向
- 对象打点调用其本身的方法函数,则this指向这个对象。
- 圆括号直接调用函数,则this指向window对象。
- 数组(类数组)枚举出函数进行调用,this指向这个数组。
- 立即可执行函数中,this指向window对象。
- 定时器、延时器调用函数,this指向window对象。
- 事件处理函数的this指向绑定事件的DOM元素。
- call、apply、bind可以指定this的指向。
判断要点: this指向是由执行环境决定的,判断某个this的指向,就是要明确当前的执行环境是什么。
3.5 相关题目
-
手写call函数、bind函数
-
创建10个a标签,点击的时候弹出对应的序号
-
实际开发中闭包的应用场景,举例说明
- 模拟私有变量
- 防抖(中心思想:在规定时间内无论触发多少次,都使其只执行一次)
- 节流(中心思想:在一定时间内,无论触发多少次,都只认第一次,并在计时结束时给予响应)
4. 异步
-
单线程和异步
JS是单线程语言,只能同时做一件事情。虽然浏览器和nodejs已支持JS启动进程,如Web Worker,但并不能改变JS是单线程语言的事实。并且JS和DOM渲染共用同一个线程(因为JS可以修改DOM结构),这就意味着JS运行时DOM渲染必须停止,DOM渲染时JS必须停止运行。但是遇到等待(网络请求,定时任务)时不能卡住,所以产生了异步的概念。 -
异步的实现形式
异步是由回调函数的形式实现,异步不会阻塞后续代码执行。
4.1 同步和异步的区别是什么?
基于JS是单线程语言,异步不会阻塞代码执行,同步会阻塞代码执行。
4.2 手写Promise加载图片
const loadImg = (src) => {
const p = new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败${src}`)
reject(err)
}
img.src = src
})
return p
}
const url1 = 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fp3.itc.cn%2Fimages01%2F20210828%2Fbcd6b0ef54bc41e59393b75d4bac1e8e.jpeg&refer=http%3A%2F%2Fp3.itc.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659013249&t=44b59d1cef1306900f2fa5a9e0e92340'
const url2 = 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fss2.meipian.me%2Fusers%2F13702902%2F0ce776a0da32ea6598e6899ada3cbd10.jpg%3Fmeipian-raw%2Fbucket%2Fivwen%2Fkey%2FdXNlcnMvMTM3MDI5MDIvMGNlNzc2YTBkYTMyZWE2NTk4ZTY4OTlhZGEzY2JkMTAuanBn%2Fsign%2F6ca68cc38f9dcf818c509455ab7c9481.jpg&refer=http%3A%2F%2Fss2.meipian.me&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659013249&t=f9f0b7c7ad557ce874140f50c8cb5b03'
loadImg(url1).then((img) => {
console.log(img.width)
return loadImg(url2)
}).then((img) => {
console.log(img.width)
}).catch(ex => console.error(ex))
4.3 前端使用异步的场景有哪些?
- 网络请求,如ajax图片加载
- 定时任务,如setTimeout定时函数
4.4 event loop过程
- 程序先执行同步代码(在call stack中),执行完毕后event loop机制开始工作。
- event loop开始轮询查找callback queue中是否有符合条件的事件。
- 如果有,则将其推入call stack中,否则一直自我循环,等待符合条件的事件。
(异步、DOM事件都是使用回调的机制,基于event loop)
4.5 async/await
async/await是消灭异步回调的终极武器,与Promise相辅相成。总结为以下三点“
- 执行async函数,返回的是Promise对象
- await相当于Promise的then
- try…catch可捕获异常,代替了Promise的catch
4.6 微任务和宏任务
- 宏任务:setTimeout,setInterval,Ajax,DOM事件,是浏览器规定的,存放在Web APIs里,在DOM渲染后触发
- 微任务:Promise async/await,是ES6规定的,存放在micro task queue里,在DOM渲染前触发
- 微任务执行时机比宏任务要早
4.7 手写Promise
- Promise定义时立即执行内部函数
- 手写Promise以及静态方法
/**
* @description MyPromise
* @author HYN
* */
class MyPromise {
state = 'pending' // Promise的状态, ‘pending’ 'fulfilled' 'rejected'
value = undefined //resolve函数的返回值
reason = undefined //reject函数的返回值
resolveCallback = []
rejectCallback = []
constructor(fn) {
const resolveHandler = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
this.resolveCallback.forEach(fn => fn(this.value))
}
}
const rejectHandler = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.rejectCallback.forEach(fn => fn(this.reason))
}
}
try {
fn(resolveHandler, rejectHandler)
} catch(err) {
rejectHandler(err)
}
}
then(fn1, fn2) {
fn1 = typeof fn1 === 'function' ? fn1 : (v) => v
fn2 = typeof fn2 === 'function' ? fn2 : (e) => e
// pending状态下,将两种回调函数进行存储,当满足状态变化条件时再执行
if (this.state === 'pending') {
const p1 = new MyPromise((resolve, reject) => {
this.resolveCallback.push(() => {
try {
const newValue = fn1(this.value)
resolve(newValue)
} catch (err) {
reject(err)
}
})
this.rejectCallback.push(() => {
try {
const newReason = fn2(this.reason)
reject(newReason)
} catch (err) {
reject(err)
}
})
})
return p1
}
if (this.state === 'fulfilled') {
const p1 = new MyPromise((resolve, reject) => {
try {
const newValue = fn1(this.value)
resolve(newValue)
} catch (err) {
reject(err)
}
})
return p1
}
if (this.state === 'rejected') {
const p1 = new MyPromise((resolve, reject) => {
try {
const newReason = fn2(this.reason)
reject(newReason)
} catch (err) {
reject(err)
}
})
return p1
}
}
//then方法的语法糖,简单模式
catch(fn) {
return this.then(null, fn)
}
}
MyPromise.resolve = function (value) {
return new MyPromise((resolve, reject) => resolve(value))
}
MyPromise.reject = function (value) {
return new MyPromise((resolve, reject) => reject(reason))
}
MyPromise.all = function(promiseList = []){
const p1 = new MyPromise((resolve, reject) => {
const result = []
let resultLenth = 0
promiseList.forEach(p => {
p.then(data => {
result.push(data)
resultLenth++
if (resultLenth === promiseList.length) {
// 已经遍历到最后一个promise
resolve(result)
}
}).catch(err => reject(err))
})
})
return p1
}
MyPromise.race = function (promiseList = []) {
let resolved = false //标记
const p1 = new MyPromise((resolve, reject) => {
promiseList.forEach(p => {
p.then(data => {
if (!resolved) {
resolve(data)
resolved = true
}
}).catch(err => {
reject(err)
})
})
})
return p1
}
5. JS Web API
5.1 DOM(Document Object Model)
-
DOM的本质是一颗树
-
DOM节点操作
-
DOM节点的property
注意:property修改DOM节点JS变量的属性,不会体现到html结构中,attribute修改DOM节点的html属性,会改变html结构。两者都可能引起DOM节点的重新渲染。 -
DOM结构的操作
-
DOM性能(DOM操作非常消耗资源,要避免频繁操作)
- 对DOM查询做缓存
- 将频繁操作改为一次性操作
- 对DOM查询做缓存
5.2 BOM
- 如何识别浏览器的类型
5.3 DOM事件
- 事件绑定——一个通用的事件绑定函数
function bindEvent(elem, type, selector, fn) {
if (fn == null) {
fn = selector
selector = null
}
elem.addEventListener(type, e => {
const target = e.target
if (selector) {
// 代理绑定
if (target.matches(selector)) {//maches用于判断一个DOM元素是否符合CSS选择器
fn.call(target, e)
}
} else {
fn.call(target, e)
}
})
}
- 事件冒泡(基于DOM树形结构,事件回顺着触发元素往上冒泡)
<div id="div1">
<p id="p1">激活</p>
<p id="p2">取消</p>
<p id="p3">取消</p>
<p id="p4">取消</p>
</div>
<div id="div2">
<p id="p5">取消</p>
<p id="p6">取消</p>
</div>
<script>
const p1 = document.getElementById('p1')
const body = document.body
p1.addEventListener('click', e => {
e.stopPropagation()//阻止冒泡
console.log("激活")
})
body.addEventListener('click', () => {
console.log("取消")
})
</script>
- 事件代理(事件委托)
<div id="div1">
<a href="#" id="a1">a1</a><br>
<a href="#" id="a2">a2</a><br>
<a href="#" id="a3">a3</a><br>
<a href="#" id="a4">a4</a><br>
<button>加载更多......</button>
</div>
<script>
const div1 = document.getElementById('div1')
div1.addEventListener('click', e => {
e.preventDefault()
const target = e.target
if (target.nodeName === 'A') {
alert(target.innerHTML)
}
})
</script>
5.4 Ajax
- 一个简易的Ajax请求
// 第一步:实例化一个XMLRequest对象
const xhr = new XMLHttpRequest()
// 第四步:利用onreadystatechange函数监听readyState的变化
// reasyState有0~4五种状态
// 0:未初始化。尚未调用open()
// 1:启动。以及调用open()但尚未调用send()
// 2:发送。已经调用send()但尚未接收到响应
// 3:接受。已经接收到部分响应数据
// 4:完成。以及接收到全部响应数据,而且已经可以在浏览器中使用
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) {
return
}
if ((xhr.status >= 200) && (xhr.status < 300) || xhr.status === 304) {
console.log(JSON.parse(xhr.responseText))
}
}
// 第二步:利用open函数准备发送请求
// xhr.open(
// http请求类型,可以是GET,POST, PUT, DELETE,
// 请求地址,
// 是否异步
// )
xhr.open('GET', '/data/test.json', true)
// 第三步:利用send函数发送请求,该函数的参数是请求体携带的数据,GET请求为null
xhr.send(null)
Promise版
function ajax(url) {
const p = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.responseText)
} else if (xhr.status === 404) {
reject(new Error('404 not found'))
}
}
}
xhr.send(null)
})
return p
}
ajax('/data/test.json')
.then(data => console.log(data))
.catch(err => console.error)
- 跨域
- 同源策略:ajax请求时,浏览器要求当前网页和server必须同源(为了安全考虑)
- 同源的概念:协议、域名、端口三者必须一致
- 加载图片、css、js可无视同源策略!!!
- 所有的跨域都必须经过server端允许和配合。
- 实现跨越的常见方式
- JSONP:JSONP是由两部分组成,回调函数和数据。回调函数是当响应到来时,应该在页面中调用的函数。回调函数的名字一般是在请求中指定的。而数据就是传入回调函数中的JSON数据。下列代码是红宝书上的一个JSONP跨域请求的例子(博主测试的时候被浏览器阻止了,但是不影响理解JSONP),其中handleResponse()函数就是回调函数。
function handleResponse(response) { alert(`You're at IP address ${response.ip}`) } const script = document.createElement('script') script.src = "http://freegeoip.net/json/?callback=handleResponse" document.body.insertBefore(script, document.body.firstChild)
- CORS:
5.5 存储
- Cookie
-Cookie的缺点:存储大小最大只有4KB;http请求时需要发送到服务端,增加请求数据量;只能用document.cookie='…'来修改,太过简陋。 - localStorage、sessionStorage和Cookie的区别(容量、API易用性、是否跟随http发送出去)
- localStorage、sessionStorage是H5专门为本地存储提出的,容量为5M,设置getItem和setItem两个API,更易用,不会跟随http请求发送出去
- localStorage数据会永久存储,除非代码或手动删除(用的多一些)
- sessionStorage数据只存在于当前会话,浏览器关闭则清空
6 Http协议
6.1 http状态码
- 100~199,服务器收到请求,还需要继续处理。
- 200~299,请求成功,如200
- 300~399, 重定向,如302
- 400~499,客户端错误,如404
- 500~599, 服务器端错误,如500
- 常见的状态码: 200成功,301永久重定向(配合location,浏览器自动处理),302临时重定向(配合location,浏览器自动处理),304资源未被修改,404资源未找到,403没有权限,500服务器错误,504网关超时。
6.2 http methods与Restful-API
- 传统的methods
- get方法,获取数据
- post方法,提交/修改数据
- 现在的methods
- get方法,获取数据
- post方法,新建数据
- patch/put方法,修改数据
- delete方法,删除数据
- 传统的API设计与Restful-API
- 传统API设计将每个url当作一个功能
- Restful-API设计,把每个url当作一个唯一的资源
- 如何将url设计成一个资源?主要是两点。第一点,不使用url参数。第二点,用method表示操作类型。
6.3 http headers
- 常见的Request headers
- Accept 浏览器可接收的数据格式
- Accept-Encoding 浏览器能够处理的压缩编码,如gzip
- Accept-Language 浏览器当前设置的语言,如zh-CN
- Connection 浏览器与服务器之间连接的类型,如keep-alive表示一次TCP连接重复使用
- Cookie 当前页面设置的任何cookie
- Host 发出请求的页面所在的域
- User-Agent 简称UA浏览器信息
- Content-Type 发送数据格式,如application/json
- 常见的Response headers
- Content-type 返回数据的格式,如application/json
- Content-length 返回数据的大小,多少字节
- Content-Encoding 返回数据的压缩算法,如gzip
- Set-Cookie 服务器端凭借Set-Cookie更改Cookie内容
6.4 http 缓存
- 关于缓存,什么是缓存?为什么需要缓存?哪些资源可以被缓存?
http缓存机制如下图所示,引用自(https://blog.csdn.net/qq_39903567/article/details/115281234 作者:LYFlied)
- 浏览器每次发起请求时,都会先在浏览器缓存中查找是否有请求结果以及缓存标志。
- 浏览器每次拿到请求结果都会将对应的缓存标志和结果存入浏览器缓存中。
- 缓存机制的引入主要是解决资源请求慢的问题,将静态资源(js, css, img)缓存到浏览器中,可以减小浏览器的请求体积,从而加快请求速度。
- 强制缓存
强制缓存指想浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,主要有三种,如下:
第一,不存在该缓存结果和缓存标识,强制缓存失效,直接向服务器发送请求,如下图
第二,存在该缓存结果和缓存标准但该结果已失效,强制缓存失效,则使用协商缓存,如下图
第三,存在该缓存结果和缓存标志,且结果尚未失效,强制缓存生效,直接返回该结果,如下图:
以上图片均来自https://blog.csdn.net/qq_39903567/article/details/115281234
控制强缓存的字段有Expires和Cache-Control,现在Expires基本已被Cache-Control替代,主要取值如下:
- public:所有内容都将被缓存(客户端和代理服务器都可缓存)
- private:所有内容只有客户端可以缓存,Cache-Control的默认取值
- no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
- no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
- max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效
- 协商缓存(对比缓存)
是一个服务端缓存策略(应用场景:上一点中的第二种情况),服务端判断客户端资源是否与服务端资源一样(根据资源标识判断),一致则返回304,否则返回200和最新的资源。
资源标识在Response Headers中,有两种:- Last-Modified资源的最后修改时间
- Etag资源的唯一标识(一个字符串,类似人类的指纹)
在上图中,展示了Last-Modified的作用。在浏览器初次请求资源时,服务器端将在Response-Header中返回一个Last-Modified信息,记录的是该资源最后一次修改的时间。当浏览器端缓存失效后,再次向服务器端请求该资源时将在Request-header中携带If-Modified-Since字段,表示的是缓存中上次服务器传来的Last-Modified信息,也就是该资源最后修改时间,服务器端将对比浏览器传来的资源标识,以判断服务器端资源最后修改时间是否与浏览器端一致。一致则表示服务器端的资源与浏览器本地的资源一致,可以继续使用,则返回304。否则返回新的资源和新的Last-Modified。
Etag的工作机制与Last-Modified一样。
注意!!!当Last-Modified与Etag共存的时候会优先使用Etag,Last-Modified只能精确到秒级,如果资源被重复生成,而内容不变,则Etag更精确!!!!
Header示例:
- http缓存综述
- 三种刷新操作的不同缓存策略
- 正常操作(输入URL进行页面跳转):强制缓存有效,协商换成有效。
- 手动刷新(F5,command+r,点击刷新按钮等):强制缓存失效,协商缓存有效。
- 强制刷新(ctrl+f5, shift+command+2):强制缓存失效,协商缓存失效。
6.5 https的加密方式
http是明文传输,敏感信息容易被中间劫持。https=http+加密,劫持了也无法解密。
7. 运行环境
7.1 网页加载过程
- 资源的形式:html代码;媒体文件,如图片、视频等;javascript css
- 加载过程:
- DNS解析:域名 -> IP地址
- 浏览器根据IP地址向服务器发起http请求
- 服务器处理http请求,并返回给浏览器
- 渲染过程
- 根据HTML代码生成DOM Tree
- 根据CSS代码生成CSSOM
- 将DOM Tree和CSSOM整合形成Render Tree
- 根据Render Tree渲染页面
- 遇到script标签则暂停渲染,优先加载并执行JS代码,完成再继续
- 知道把Render Tree渲染完成
- window.onload 和 DOMContentLoaded
7.2 性能优化
优化原则(用空间换时间):
- 多使用内存、缓存或其他方法(让加载更快)
- 减少CPU计算量,减少网络加载耗时(让渲染更快)
具体方法
- 让加载更快
- 减少资源体检:压缩代码
- 减少访问次数:合并代码,SSR服务器端渲染,缓存
- 使用更快的网络:CDN
- 让渲染更快
- CSS放在head,JS放在body最下面
- 尽早开始执行JS,用DOMContentLoaded触发
- 懒加载(图片懒加载,上滑加载更多)
- 对DOM查询进行缓存
- 频繁DOM操作合并到一起插入DOM结构
- 节流throttle 防抖debounce
- 手写防抖
- 监听一个输入框,文字变化后出发change事件,直接用keyup师姐,则会频繁触发change事件。
- 防抖:用户输入结束或暂停时,才会触发change事件
const input = document.getElementById('input')
function debounce(fn, delay = 500) {
let timer = null
return function () {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}
input.addEventListener('keyup', debounce(function () {
console.log(input.value)
}), 600)
- 节流
- 场景:拖拽一个元素时,要随时拿到该元素被拖拽的位置,直接用drag事件则会频繁触发,很容易导致卡顿。
- 节流:无论拖拽速度多快,都会每隔100ms触发一次
function throttle(fn, delay = 100) {
let timer = null
return function () {
if (timer) {
return
}
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}
const box = document.getElementById('box')
box.addEventListener("drag", throttle(function (e) {
console.log(e.offsetX, e.offsetY)
}))
7.3 安全
- 常见的web前端攻击方式有哪些?
- XSS跨站请求请求攻击
场景:一个博客网站,作者发表了一篇博客,其中嵌入了script脚本,脚本内容为获取cookie并发送到作者的服务器。那么当作者发布这篇博客后,只要有人查看它,就可以拿到访问者的cookie。
预防XSS攻击:前后端一起替换特殊字符 - XSRF跨站请求伪造
场景:某人正在购物,看中了某个商品,商品id是100,付费接口为xxx.com/pay?id=100,但没有任何验证。攻击者看中了一个商品,id是200。攻击者向被攻击方发送一封电子邮件,邮件正文隐藏着<img src=xxx.com/pay?id=200>,被攻击者一查看邮件,就会购买id为200的商品。
预防XSRF攻击:使用post接口;增加验证,例如密码、短信验证码、指纹等。
- XSS跨站请求请求攻击