JS
JS
1、es6 新特性
这些在日常开发中常用,但是一问起来还是容易堵住脑子说不出话。
- 新增
let | const
- 新增
...
,拓展运算符 - 新增
Promise
- 新增
symbol
原始数据类型 - 新增
解构赋值
,const { name } = person
- 新增
Map | Set
- 新增
Object.assign()
和Object.is()
- 新增
str.includes() | str.startsWith() | str.endsWith() | str.repeat() | str.padStart() | str.padEnd()
- 新增
arr.of() | arr.from() | arr.find() | arr.findIndex() | arr.fill() | arr.copyWithin() | arr.includes()
- 新增
函数参数默认值
,function(a = 1, b = 2) { return (a + b) }
- 新增
箭头函数
,() => {}
- 新增
class
- 新增
导入导出
,import xxx from "xxx" | export {}
2、闭包
简单说就是在一个内部函数(这个内部函数被外部函数返回)中能访问外部函数的变量,如:
function fn() {
let text = 123
return function() {
let mytext = text
}
}
它所造成的影响就是外部函数中被内部函数访问的变量不会被垃圾回收,因此就可能造成内存泄漏。
但他也有一些应用场景,比如我们的防抖节流函数,这其中外部的timer
就被内部函数访问了,这就会导致我们每次调用这个函数时,这个timer
都是同一个变量,以达到我们需要的效果。
3、深拷贝和浅拷贝
浅拷贝
只复制指向对象的指针,不复制对象本身,因此,虽然我们使用时可能使用的是两个不同的变量,但是它们其实读取的是同一个内存地址,这样就会导致我们使用某个变量时会影响到另一个变量。深拷贝
会创造另一个跟被拷贝的对象一模一样的对象,然后将其放置到一个新的内存地址中,这样两个对象之间就不会相互影响。
实现深拷贝:
扩展运算符:这个方法只能实现第一层的深拷贝,当有多层的时候还是浅拷贝
JSON:使用
JSON.parse(JSON.stringify(boj))
,但是它不能处理函数递归的方式:递归实现深拷贝
使用
MessageChannel()
:使用 MessageChannel 实现深拷贝使用
lodash
库
4、将下面代码使用闭包改为从 0 - 9 的输出
题干
var funcs = []
for (var i = 0; i < 10; i++) {
funcs.push(function() { console.log(i) })
}
funcs.forEach(func => {
func()
})
用闭包的方式:
var funcs = [] for (var i = 0; i < 10; i++) { // 闭包 (function(j) { funcs.push(function() { console.log(j) }) })(i) } funcs.forEach(func => { func() })
5、箭头函数与普通函数的区别
- 箭头函数没有自己的 this
- 箭头函数不能作为构造函数
- 箭头函数不绑定 arguments,取而代之使用
...
解决 - 箭头函数不具备 prototype 原型对象
- 箭头函数不能使用 Generator 函数,不能使用 yeild 关键字
- 箭头函数不具备 super
- 箭头函数不具有 new.target
6、隐式转换
这个我们在书写代码时可能都不太会注意到,因为其实一些转换我们都能知道,但是要突然问起然后让说这个,可能会突然脑抽不知从何说起😢
原始值 => 转换数值类型 => 转换字符串类型 => 转换 Boolean 类型
- false => 0 => "false" => false
- true => 1 => "true" => true
- 0 => 0 => "0" => false
- 1 => 1 => "1" => true
- "" => 0 => "" => false
- "0" => 0 => "0" => false
- "1" => 1 => "1" => true
- "hh" => NaN => "hh" => true
- [] => 0 => "" => true
- [10] => 10 => "10" => true
- [10, 11] => NaN => "10,20" => true
- {} => NaN => "[object Object]" => true
- null => 0 => "null" => false
- undefined => NaN => "undefined" => false
- function(){} => NaN => "function(){}" => true
- NaN => NaN => "NaN" => false
- Infinity => Infinity => "Infinity" => true
- -Infinity => -Infinity => "-Infinity" => true
7、js的三个组成部分
- ECMAScript:js的核心内容,描述了语言的基础语法,如 var、if、for,function、数据类型等;
- 文档对象模型(DOM):DOM把整个HTML页面规划为元素构成文档,我们可以使用一些API操作HTML页面
- 浏览器对象模型(BOM):对浏览器窗口进行访问和操作
8、js对数据类型检测的方法
typeof:对基本数据类型没问题,遇到引用数据类型就会出问题
instanceof:对引用数据类型没问题,基本数据类型会出问题
constructor:几乎可以判断基本数据类型和引用数据类型,但是也有局限性
局限性:如果声明了一个构造函数,并把它的原型指向了Array,这样它就检测不出来了
Object.prototype.toString.call():完美解决所有问题
9、内存泄漏
js中已经分配内存地址的对象,但是由于长时间没有释放或者没办法清除,造成长期占用内存的现象,会让内存资源答复浪费,最终导致运行速度慢,甚至崩溃的情况。
因素:一些未声明直接赋值的变量,一些未清空的定时器,过度闭包,一些引用元素没有被清除。
10、事件委托是什么
又叫事件代理,原理就是利用了事件冒泡的机制来实现,也就是将子元素的事件绑定到了父元素身上。
如果子元素阻止了事件冒泡,那么委托失效。
阻止事件冒泡:
- event.stopPropagation()
- addEventListener("", 函数, Boolean),这个布尔值默认为false(事件冒泡),true(事件捕获)。
11、基本数据类型和引用数据类型
基本数据类型:保存在栈内存中,保存的就是一个具体的值
Number | String | Boolean | null | undefined | Symbol
引用数据类型:保存在堆内存中,声明一个引用类型的变量,它保存的是这个引用类型数据的地址
Object | Array | Function
12、原型链
- 原型就是一个普通对象,它是为构造函数的实例共享属性和方法,所有的实例中引用的原型都是同一个对象。使用
prototype
可以将方法挂载到原型上。
原型链:一个实例对象在调用属性和方法时,会依次从实例本身、构造函数原型、原型的原型上去查找,这样的一个链式查询方式就叫做原型链。
prototype是显示原型,__proto__是隐式原型
13、new 操作符具体做了什么
- 先创建一个空对象
- 把空对象和构造函数通过原型链进行连接
- 把构造函数的
this
绑定到新的空对象上 - 根据构造函数返回的类型判断,如果是值类型,则返回对象;如果是引用类型,则返回这个引用类型
function newFun(Fun, ...args) {
// 步骤一
let obj = {}
// 步骤二
obj.__proto__ = Fun.prototype
// 步骤三
const result = Fun.apply(obj, args)
// 步骤四
return result instanceof Object ? result : obj
}
function Person(name) {
this.name = name
}
Person.prototype.say = function() {
console.log("haha")
}
const p = newFun(Person, "zhangsan")
p.say() // haha
console.log(p) // {name: "zhangsan"}
14、js是如何实现继承的
原型链继承:让一个构造函数的原型是另一个构造函数的实例,那么这个构造函数new出来的实例就具有该实例的属性
- 优点:写法方便简洁,容易理解
- 缺点:对象实例共享所有继承的属性和方法,无法向父类构造函数传参
借用构造函数继承:在子类构造函数内部调用父类构造函数:使用
apply | call
方法将父类的构造函数绑定在子对象上- 优点:解决了原型链实现继承的不能传参的问题和父类原型共享的问题
- 缺点:方法都在构造函数中定义,因此无法实现函数的复用。在父类的原型中定义的方法,对子类而言也是不可见的,结果所有类型都只能使用构造函数模式。
组合式继承:将原型链和借用构造函数两种方法组合到一起。使用原型链实现对原型属性和方法阿德继承,使用借用构造函数来实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性。
- 优点:解决了原型链继承和借用构造函数继承两种方式造成的影响
- 缺点:无论在什么情况下,都会调用两次父类构造函数
ES6的class类继承:class 通过 extends 关键字实现继承。其实质是先创造出父类的this对象,然后通过子类的构造函数修改this。
子类的构造函数(constructor)中必须调用 super 方法,且只有调用了 super 之后才能使用 this,因为子类的 this 对象是继承父类的 this 对象,然后对其进行加工,而 super 方法表示的是富力湾i的构造函数,用来创建父类的 this 对象。
- 优点:语法简单易懂,操作方便
- 缺点:浏览器兼容性问题
15、js的设计原理
- js引擎:编译js脚本,将其转换为电脑可执行的代码的引擎,如现在最流行的v8引擎
- 运行上下文:浏览器中可调用的一些API,如 window,document等
- 调用栈:单线程,
- 事件循环:就是一个无限循环,它会持续的调用消息队列中的事件来执行
- 回调:
16、this指向问题
- 全局对象的this:指向 window
- 全局作用域或普通函数中的this:指向全局 window
- 在不是箭头函数的情况下,this永远指向最后调用它的那个对象
- new 关键词改变了this的指向
- apply、call、bind 可以改变this的指向,但前提是不是箭头函数
- 箭头函数的this:它的指向在定义的时候就确定了,它没有this,看外层是否有函数,有就是外层函数,没有就是window
- 匿名函数中的this:永远指向 window,匿名函数的执行环境具有全局性
17、script中的async和defer
- 没有async和defer时,它会立刻加载资源,会阻塞后续资源的加载
- async的情况下:加载和渲染后面元素的过程将和script的加载和执行并行进行(异步)
- defer的情况下:加载和渲染后面元素的过程将和script的加载并行进行(异步),但是执行要等到所有元素解析完成后才会执行
18、setTimeout和setInterval最小执行时间
- setTimeout:4ms
- setInterval:10ms
19、Promise
解决回调地狱的问题。
自身有 call、reject、resolve、race方法,原型上有 then、catch方法。
它自身有三种状态:pending、fulfilled、rejected,只能从pending转变到fulfilled或者rejected,且状态只能转换一次。
20、Promise的内部原理是什么,它的优缺点是什么
构造一个Promise实例,这个实例需要传递一个函数作为参数,这个函数有两个参数,它们也都是函数,分别是resolve、reject,这两个参数表示这个Promise执行成功或失败,调用它们就会将Promise的状态从pending改为fulfilled或者rejected,状态只会改变一次。
Promise还有then、catch等方法,then获取resolve出来的值,catch获取reject或者Promise的报错。
then方法可以链式调用,每个then方法return的值都会传递给下一个then。
- 优点:解决回调地狱问题,代码可读性更高、可以更好进行错误捕获
- 缺点:无法取消Promise、如果不设置回调函数,Promise内部抛出的错误无法反映到外部、当状态处于pending时,无法得知目前进展到了哪一阶段(刚刚开始还是即将完成)
21、Promise和async/await的区别
- 都是处理异步请求的方法
- Promise是ES6的,async/await是ES7的语法
- async/await是基于Promise实现的,它们都是非阻塞性的
优缺点:
- Promise是返回对象,需要我们使用then、catch方法出处理和捕获异常,并且书写方式为链式调用,容易造成代码重叠,不好维护;async/await通过try/catch捕获异常
- async/await最大的优点就是能让代码看起来像同步一样,只要遇到await就会立刻返回结果,然后再执行后面的操作;Promise只能通过.then()的方式返回,会出现请求还未返回就执行后续代码的问题
22、call、apply、bind的区别
- 相同点:都是改变this指向和函数的调用
- 不同:
- call:第一个参数是this的指向,第二个参数传递的是一个参数列表,它的性能比apply更好
- apply:第一个参数是this的指向,第二个参数传递的是一个数组
- bind:第一个参数是this的指向,第二个参数传递的是一个参数列表,传参后不会立刻执行,会返回一个改变了this指向的函数,这个函数还是可以传参的
23、事件循环
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列和微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列中,不同的任务可以属于不同的队列。不同的任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。
24、ajax
- ajax:ajax是创建交互式网页应用的网页开发技术,在不重新加载整个网页的前提下,与服务器交换数据并更新部分内容。
- 实现原理:通过XMLHttpRequest对象向服务器发送异步请求,然后从服务器拿到数据,最后通过js操作DOM更新页面。
- 实现步骤:
- 创建XMLHttpRquest对象,xhr
- 通过xhr对象里的
open()
方法和服务器建立连接 - 构建请求所需的数据,并通过xhr对象的
send()
发送给服务器 - 通过xhr对象的
onreadstate,change
事件监听服务器和页面的通信状态 - 接收并处理服务器响应的数据结果
- 把处理的数据更新到HTML页面上
25、get、post的区别
- get:一般用来获取数据
- post:一般用来提交数据
- GET参数通过URL传递,POST放在Request body中。
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
- GET请求在URL中传送的参数是有长度限制的,而POST没有。
- GET在浏览器回退时是无害的,而POST会再次提交请求。
- GET请求会被浏览器主动缓存,而POST不会,除非手动设置。
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
- GET请求只能进行url编码,而POST支持多种编码方式。
- 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
get和post的原理
我们都知道,get和post只是http协议中的两种发送请求的方法,那么,http是什么呢?
http是基于TCP/IP
的关于数据如何在万维网中通信的协议。这里我们就可以看出来,http底层是TCP/IP
,所以get和post的底层也是如此。所以你要给get加上body,给post带上url参数也是完全莫得问题滴。
那么,它们在底层到底有什么区别呢?
首先,TCP在网络世界中就类似汽车,它用来运输数据,它很可靠,不会出现丢件少件的问题,但是,如果路上的汽车都长一个样,那么看起来就会很混乱,我们无法从外观上分辨出它们内部的货物的区别,因此,http就诞生了,它给这些汽车贴上了集中标签:get、post、delete、put等,这就是这些不同请求方式的由来。
get就相当于将货物放在了车顶,post将货物放在车厢中,这就可以看出get为什么有长度限制,而post相对来说就没有限制了,你总不能将1吨的货物放在车顶吧,这样看起来会很蠢。
但是除了上面我们提到的get和post的一些区别,它俩还有一个很重要的区别:get只会产生一个TCP数据包,而post会产生两个TCP数据包。
也就是说,get将header和data一起打包,一次性运输完成(服务器响应200
);而post会先运输一次header(服务器响应100,表示等待
),告诉收货方,我等下要运货物来了,你开门等着,然后在运输一次data(服务器响应200
)。
因为post需要两次运货,所以感官上来说,它会更慢。但是不能为了优化性能就用get来替换post,原因如下:
- get和post都有自己的语义,不能混用
- 在网络环境好时,get和post的时间差可以忽略不计;在网络环境差时,传输两次包这种方式在验证数据完整性上有很大优势
- 其实不是所有浏览器的post都会发两个包,Firefox就只发一次
26、浏览器存储方式
- cookies
- H5标准前的本地存储方式
- 兼容性好,请求头自带cookie
- 存储量小,资源浪费,使用麻烦(需要封装)
- localStorage
- H5加入的以键值对形式存储的方式
- 操作方便,永久存储,兼容性好
- 存储量大
- 保存值的类型被限定
- 浏览器再隐私模式下不可读取
- 不能被爬虫
- sessionStorage
- 与localStorage类似,区别在于sessionStorage在当前页面关闭后就会立刻清除,且不能在所有的同源窗口中进行共享
- indexedDB
- H5标准的存储方式
- 以键值对进行存储
- 可以快速读取,适合WEB场景
27、token存在sessionStorage还是localStorage
- token:验证身份的令牌,一般就是用户通过账号密码登录后,服务端把这些凭证通过加密等一系列操作后的得到的字符串。
存localStorage:后续每次请求接口都需要把它当作一个字段传给后台
存cookie:会自动发送,缺点就是不能跨域
如果存在localStorage中,容易被xss攻击,但是如果做好了对应的措施,那么是利大于弊的;
如果存在cookie中,会有csrf攻击;
所以总的来说,更加建议存在localStorage中。
28、token的登录流程
- 客户端用账号密码请求登录
- 服务端收到请求后,需要去验证账号密码
- 账号密码验证成功,服务端会签发一个token,把这个token发送给客户端
- 客户端收到token后,将其保存起来
- 后去每次发送请求时,客户端都需要携带token去进行请求
- 服务端收到请求后,验证携带的token
- 验证token成功后,返回相应资源
29、页面渲染的过程
- 输入url
- 进行DNS解析
- 建立TCP连接
- 发送HTTP请求
- 服务器处理请求,返回资源
- 渲染页面
- 浏览器获取HTML和CSS资源,分别将其解析为DOM树和CSS树
- 将DOM树和CSS树合并为渲染树
- 计算每个元素的布局、样式
- 绘制网页到屏幕上
- 断开TCP连接
30、DOM树和渲染树有什么区别
- DOM树和HTML标签一一对应,包括head和隐藏元素;渲染树是不包含head和隐藏元素的
- DOM树在浏览器解析HTML后形成;渲染树是浏览器将DOM树和CSS树结合形成
31、精灵图和base64的区别
- 精灵图是由若干个小图片合并成的大图片,我们通过定位来显示这个精灵图上的不同小图片
- base64是一种基于 64 个可打印字符来表示二进制数据的表示方法。它会和html、css一起下载到浏览器中,减少请求,减少跨域问题,但是一些低版本的浏览器不支持。如果图片转换为base64后体积比原图片大,则不利于css的加载。
精灵图也可以转换成base64格式。
32、svg格式
- 基于XML语法格式的图像格式的可缩放的矢量图,它不会失真。
- 其他图像是基于像素的,而svg是基于图像形状的描述,本质是文本文件,体积小,且不论放大多少都不会失真。
- svg可以直接插入页面中,成为DOM的一部分,然后使用js或css进行操作。
- svg可以作为文件引入,
<img src="pic.svg" />
- svg可以转为base64引入页面
33、JWT
JWT是JSON Web Token的缩写,通过json形式作为在web应用中的令牌,可以在各方之间安全的把信息作为json对象传输、授权。
- 前端将账号密码发送给后端
- 后端验证账号密码成功后,将用户id等其他信息作为JWT负载,把它和头部分别进行base64编码拼接后形成一个JWT(token)
- 前端每次请求时都将JWT放在HTTP请求头的Authorization字段中
- 后端检查是否存在,如果存在就验证JWT的有效性(签名是否正确,token是否过期)
- 验证通过后后端使用JWT中包含的用户信息进行其他操作,并返回对应结果
简洁、包含行、因为token是JSON加密的形式保存在客户端,所以JWT是跨语言的,原则上是任何web形式都支持
34、npm的底层环境是什么
node package manager,node的包管理和分发工具,已经成为分发node模块的标准,是js的运行环境。
npm的组成:网站、注册表、命令行工具
35、HTTP协议规定的协议头和请求头有什么
- 请求头信息:
- Accept:浏览器告诉服务器所支持的数据类型
- Host:浏览器告诉服务器我想访问服务器的哪一台主机
- Referer:浏览器告诉服务器我是从哪里来的(防盗链)
- User-Agent:浏览器类型、版本信息
- Date:浏览器告诉服务器我是什么时候访问的
- Connection:连接方式
- Cookie
- X-Request-With:请求方式
- 响应头信息:
- Location:这个就是告诉浏览器要去找谁
- Server:告诉浏览器服务器的类型
- Content-Type:告诉浏览器返回的数据类型
- Refresh:控制浏览器的定时刷新
36、浏览器的缓存策略
- 强缓存(本地缓存):不发起请求,直接使用缓存里的内容,浏览器把js、css、image等资源存到内存中,下次用户访问时,直接从内存中取,提高性能
- 协商缓存(弱缓存):需要向后台发请求,通过判断来决定是否使用协商缓存,如果请求内容没有变化,则返回304,浏览器就用缓存里的内容
浏览器发起请求前会先查看强缓存是否有内容,如果没有就发起请求并且判断是否需要用到协商缓存。
强缓存的触发:
- http1.0中使用的是时间戳响应标头
- http1.1中使用的是Cache-Control响应标头
协商缓存的触发:
- http1.0使用请求头if-modified-since,响应头last-modified
- http1.1使用请求头if-none-match,响应头Etag
37、同源策略
同源:协议+域名+端口,这三者完全相同才叫同源,否则形成跨域。
同源策略是浏览器的核心,没有这个策略就会遭受网络攻击。
三个允许跨域加载资源的标签:script、link、img
跨域:可以发送请求,后端也会正常返回结果,但是这个结果被浏览器拦截了。
解决跨域:
- JSONP
- CORS
- webSocket
- 反向代理
38、防抖和节流
都是应对页面中频繁触发事件的优化方案
- 防抖:避免事件重复触发
- 使用场景:
- 频繁和服务端交互
- 输入框自动保存事件
- ...
- 使用场景:
- 节流:把频繁触发的事件减少
- 使用场景:scroll事件
39、什么是json
json是一种纯字符串形式的数据,它本身不提供任何方法,适合在网络中进行传输。
json数据存储在.json文件中,也可以将json数据以字符串形式保存在数据库、cookies中。
- JSON.parse():将转换为字符串的json转换回来
- JSON.stringify():将json转换为字符串
什么时候使用json:
- 定义接口
- 序列化
- 生成token
- 配置文件
40、当请求没有返回数据,该怎么做
可以在渲染数据的地方给一些默认值,如骨架屏。
41、有没有做过无感登录
- 在响应器中拦截,判断token过期后,调用刷新token的接口,推荐使用
- 流程:
- 登录成功后保存token和refresh_token
- 在相应拦截器中对401状态码引入刷新token的api方法调用
- 用得到的新的token替换旧的token
- 把错误对象里的token替换
- 再次发送未完成的请求
- 如果refresh_token过期了,则清除所有的token,然后返回登录页面
- 流程:
- 后端返回过期时间,前端判断token的过期时间,然后取调用刷新token的接口
- 写定时器,定时刷新token
42、大文件上传
- 分片上传:
- 将需要上传的文件安装一定的规则,分割成相同大小的数据块
- 初始化一个分片上传任务,返回本地分片上传的唯一标识
- 按照一定的规则把各个数据块上传
- 发送完成后,服务端会判断数据上传的完整性,如果完整,那么就会把数据块合并成原始文件
- 断点续传:
- 服务端返回,从哪里开始重新上传
- 浏览器自己处理