# 说说 Promise
Promise 是一种异步编程的解决方案,比传统的【回调函数】/【事件】等异步解决方案更合理、更强大,主要解决的回调地域问题,类似的还有Q和bluebird。
生命周期主要有三种状态,分别为:pending、resolved和rejected。状态之间只能改变一次,即pending--resolved,或者pending--rejected,且发生改变后不可再次改变。
常用的实例方法有.then回调函数和.catch异常捕捉。
常用的类方法有:.resolve、.reject、.race 和 .all
.resolve发起一个成功的请求,.reject发起一个失败的请求。
.race与.all都是多个Promise任务同时执行,.race返回的是最先执行结束的Promise任务的结果,不管这个Promise结果是成功还是失败。而.all的返回值份两种情况,如全部执行成功,则以数组的方式返回所有Promise任务的执行结果。如有一个Promise任务rejected,则只返回rejected任务的结果。
Promise的缺点
1、promise一旦建立就会立即执行,且中途无法取消。
2、当处于pending状态时,无法得知当前是刚结束还是刚开始。
3、如果不设置回调函数,promise无法将内部错误反应到外部。
# 说说 Axios
Axios是一个基于Promise封装的HTTP客户端,支持Promise所有的的API。它可以用在浏览器和Node环境中,对于浏览器环境来说Axios底层是利用XMLHttpRequest对象来发起HTTP请求,是目前前端比较流行的ajax请求库。
可以使用axios.create创建一个新的axios实例,并设置它的timeout(请求超时的时间)、baseURL(基础地址)以及headers(请求头)等配置,可按需设置。
可以使用axios.interceptors.request.use((config) => { return config; })设置请求拦截器。使用axios.interceptors.response.use((res) => { return res; });设置响应拦截器。用于处理在请求时携带参数校验及配置请求头等设置。并在响应数据时统一处理数据格式及请求失败的内容提示等操作。
常用到的请求方式有axios.get、post、put、delete、all
取消请求/重复请求
1、取消请求
通过Axios内部提供的CancelToken来取消请求
方式一:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post(
'http://xxx.xxx.xxx/getInfo/',
{ id: '123456' },
{ cancelToken: source.token }
)
source.cancel('请求被取消了'); // 取消请求,参数是可选
通过调用 CancelToken 的构造函数来创建 CancelToken
方式二:
const CancelToken = axios.CancelToken;
let cancel;
axios.get(
'http://xxx.xxx.xxx/getInfo/',
{ id: '123456' },
{
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
}
);
cancel(); // 取消请求
2、取消重复请求
重复请求:请求方式、请求URL地址和请求参数都一样的请求。
解决思路:
- 每次发起请求时,根据当前的请求方式、请求URL和请求参数来生成一个唯一的
key。 - 为每个请求创建一个专属的
CancelToken。 - 把
key和cancel函数以键值对的形式保存到Map对象中。
解决思路--代码实现:
import qs from 'qs'
// 创建一个空的 Map 对象
const pendingReq = new Map();
// 根据请求方式、地址、参数生成唯一的 key
const requestKey = [method, url, qs.stringify(params), qs.stringify(data)].join('&');
// 把 key 和 cancel 函数以键值对的形式保存到 Map 对象中
const cancelToken = new CancelToken(function executor(cancel) {
if(!pendingReq.has(requestKey)){
pendingReq.set(requestKey, cancel);
}
})
处理思路
- 重复请求时使用
cancel函数来取消前面已经发出的请求。 - 在取消请求之后的同时把取消的请求从
pendingReq中移除。
处理思路--代码实现
1. 定义辅助函数
// 根据当前请求的信息,生成请求 Key
function generateReqKey(config) {
const { method, url, params, data } = config;
return [method, url, qs.stringify(params), Qs.stringify(data)].join("&");
}
// 把当前请求信息添加到 pendingReq 对象中
const pendingReq = new Map();
function addPendingReq(config) {
const requestKey = generateReqKey(config);
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pendingReq.has(requestKey)) {
pendingReq.set(requestKey, cancel);
}
});
}
// 检查是否存在重复请求,若存在则取消已发的请求
function removePendingReq(config) {
const requestKey = generateReqKey(config);
if (pendingReq.has(requestKey)) {
const cancelToken = pendingReq.get(requestKey);
cancelToken(requestKey);
pendingReq.delete(requestKey);
}
}
2. 设置请求/响应拦截器
// 请求拦截器
axios.interceptors.request.use(
(config) => {
// 检查是否存在重复请求,若存在则取消已发的请求
removePendingReq(config);
// 把当前请求信息添加到 pendingReq 对象中
addPendingReq(config);
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
axios.interceptors.response.use(
(response) => {
// 从 pendingReq 对象中移除请求
removePendingReq(response.config);
return response;
},
(error) => {
// 从 pendingReq 对象中移除请求
removePendingReq(error.config || {});
if (axios.isCancel(error)) {
console.log("已取消的重复请求");
} else {
// 异常处理
}
return Promise.reject(error);
}
);
cancel对象是一个函数,当调用该函数后,会创建Cancel对象并调用resolvePromise方法。该方法执行后,CancelToken对象上promise属性所指向的promise对象的状态将变为resolved。
# 说说 async / await
async函数返回值是一个Promise对象,结果由async 函数执行的返回值决定。
awate右侧表达式一般为Promise对象,但也可是其他值。
如果是Promise对象,awate返回值就是Promise成功的值,如果是其他的值,直接将此值作为awate的返回值。
注意:
1、await必须写在async 函数中,但async函数中可以没有await。
2、如果await的Promise失败了,就会抛出异常,需要try...catch来捕获处理。
3、await只能拿到Promise的成功的值,想要拿到失败或异常的值,只能通过try... catch来捕获。
async函数调用不会造成阻塞,但它内部awate右侧若为Promise对象,则会阻塞后面的代码,等Promise对象得到resolve的值,作为await表达式的运算结果,才会执行后边的代码。
# 原生AJAX请求步骤
- 创建XMLHTTPRequest对象
cont xhr = new XMLHTTPRequest();
- 使用open方法和服务器交互信息
xhr.open('GET', 'test.txt', true);
- 使用send发送数据,开始和服务器端交互
xhr.send('msg');
- 接收响应
xhr.onreadystatechange = function(){
...
};
(1). 当`readystate`值从一个值变为另一个值时,都会触发`readystatechange` 事件。
(2). 当`readystate==4`时,表示已经接收到全部响应数据。
(3). 当`status ==200`时,表示服务器成功返回页面和数据。
(4). 如果(2)和(3)内容同时满足,则可以通过`xhr.responseText`获得服务器返回的内容。
# 事件队列 Event Loop
JavaScript语言的一大特点就是单线程,即太同一时间只能做一件事。
作为浏览器语言,JavaScript的重要用途是与用户互动以及操作DOM。这就决定了它只能是单线程,否则会带来复杂的问题:JavaScript同时又两个线程,一个在某DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但子线程受主线程控制,且不得操作DOM。所以这个标准并没有改变JavaScript单线程的本质。
任务队列的本质
- 所有同步任务都在
主线程上执行,形成一个执行栈 (execution context stack)。 - 主线程之外有一个
任务队列 (task queue),只要异步任务有了运行结果就在任务队列里放一个事件。 - 等
执行栈中的所有同步任务执行完毕,系统就会读取任务队列及其里面的事件。并找到事件对应的异步任务,让它结束等待状态,进入执行栈开始执行 - 主线程会不断重复以上三步。
主线程 (执行栈)和任务队列 先进先出 的通信称为事件循环(Event Loop)。
主要分为:
宏任务(macro-task):DOM事件绑定、定时器、Ajax回调
微任务(micro-task):Promise、MutationObserver(html5新特性)
事件循环机制:主线程 -> 所有微任务 -> 宏任务
先进先执行,如果里面有微任务,则下一步先执行微任务,否则继续执行宏任务
setTimeout():
将事件插入到了事件队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。
当主线程时间执行过长,无法保证回调会在事件指定的时间执行。
浏览器端每次setTimeout会有4ms的延迟,当连续执行多个setTimeout,可能会阻塞进程及性能问题。
setImmediate():
事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行,和setTimeout(fn,0)效果类似。
# 说说 堆 栈
栈内存:一般存储基础数据类型,遵循先进先出 与 后进后出的原则,大小固定并有系统自动分配内存空间,运行效率高且是有序存储。
堆内存:一般存储引用数据类型,JavaScript不允许直接访问堆内存中的位置,需要从栈中获取该对象的地址引用/指针,再从堆内存中获取数据。存储值大小不定可动态调整,主要用来存放对象。空间大运行效率相对较低,是无序存储。
# JS 中有哪些数据类型
- 基本数据类型:String、Number、Boolen
- 引用数据类型:Object、Array、Function
- 特殊数据类型:Null、Undefined
- ES6新增:Symbol表示独一无二的值,BigInt表示任意大小的整数
判断数据类型的几种方法
在项目中常用的有:typeof、instanceof,不常用的有:constructor、toString
-
typeof是个一元运算符,放在任意类型的运算数之前,返回一个说明运算数类型的字符串'string','number','bollen','undefined','function','symbol','object'其中'object'包括:Object、Array、new RegExp()以及Null,故此typeof通常用来判断基本数据类型和函数。 -
instanceof运算符用来检测一个对象在其原型链中是否存在一个构造函数的prototype属性注意:
instanceof能够判断出[]是Array的实例,也是Object的实例。因为[].__proto__指向Array.prototype,而Array.prototype.__proto__又指向了Object.prototype,最终Object.prototype.__proto__指向了null原型链结束。 类似的还有new Date(),new Error()和new 自定义类()。故此
instanceof通常用来检测引用数据类型。 -
根据对象的
contructor判断原理:就是每个构造函数都一个
contructor属性指回它本身,如下[].coconstructor === Array ==> true注意:判断 数字、字符串、函数 和 日期时,必须得用关键字
new创建才行。因为只有构造函数才有constructor属性。还有两点需要注意:null和undefined是无效的对象,因此不会有constructor存在,- 函数的
constructor是不稳定的,当重写prototype后,原有的constructor引用会丢失,constructor会默认为Object。
-
使用
toString判断toString()是Object的原型方法,该方法默认返回当前对象的[[Class]]。这是一个内部属性,其格式为[object Xxx],其中Xxx就是对象的类型。对于Object对象,直接调用toString()就能返回[object Object]。而对于其他对象,则需要通过call / apply来调用才能返回正确的类型信息。Object.prototype.toString.call(undefined) === '[object Undefined]' Object.prototype.toString.call(null) === '[object Null]' Object.prototype.toString.call(123) === '[object Number]' '[object Number]','[object String]','[object Boolean]', '[object Array]','[object Function]','[object Date]', '[object RegExp]','[object Error]''[object Undefined]','[object Null]',
# Null 和 Undefined 有啥区别
null是Null类型,表示一个空对象指针或尚未存在的对象,即此处不应该有值。
使用typeof运算得到object,参与计算时默认转数值为0。
也可以理解是表示程序级的、正常的或在意料之中的值的空缺:
- 作为函数的参数,表示该函数的参数不是对象
- 作为对象原型链的终点
null不是一个对象,但typeof null === object原因是不同的对象在底层都会表示为二进制,在JS中如果二进制的前三位都为0,就会被判断为object类型,null的二进制全为0,自然前三位也是0,所以typeof null === 'objcet'。
undefined是Udefined类型,表示一个无的原始值或缺少之值,即此处应该有值,但还没定义。
使用typeof undefined === undefined,计算时转数值为NaN。
它是在ECMAScript第三版引入的预定义全部变量,为了区分空指针对象和未初始化的变量。可以理解是系统级的、出乎意料的或类似错误的值的空缺:
- 变量被声但没有赋值时
- 调用函数时,应该提供的参数没有提供时
- 对象没有赋值的属性时,属性值为
undefined - 函数没有返回值时,默认返回值为
undefined
# 说说闭包
闭包就是有权限访问其他函数作用域内变量的函数,因此它又是将函数内部和外部链接起来的桥梁。起到了保护全部变量不受污染又能隐藏变量的作用。
在JS中,变量分为全局变量和局部变量。局部变量的作用域数据函数作用域,在函数执行完成以后作用域就会被销毁,内存也会被回收。但是闭包是建立在函数内部的子函数,可访问上级作用域,所以上级函数执行完,作用域也不会被销毁。
闭包在项目中应用主要是防抖和节流:
防抖(debounce)就是当调用函数N秒后才会执行该动作,若在这N秒内有调用该函数则取消前一次的计时并重新计算执行时间。
function debounce(func, wait) {
let timeout = null
return function () {
let context = this
let args = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
节流(throttle)的基本思想是函数先预定一个执行周期,当调用动作的时刻大于执行周期则执行该动作,然后进入下一个新周期。
function throttle(func, wait) {
let timeout = null
return function () {
let context = this
let args = arguments
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
# 垃圾回收机制
标记清除法:此方法分为标记和清除两个阶段。
(1) 标记阶段:垃圾回收器会从根对象开始遍历。每一个可以从根对象访问到的对象都会被添加一个标识,并称为可到达对象;
(2) 清除阶段:对堆内存从头到尾进行线性遍历,如发现有对象没有被标识为可到达对象,则将此对象占用的内存回收,并且将原标记为可到达对象的标识清除,以便进行下一次垃圾回收操作。
引用计数清除法:
(1) 引用计数的含义是跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型赋值给该变量时,这个值的引用次数就是 1 。如果包含对这个值引用的变量又取得了另外一个值,这个值的引用次数就减 。
(2) 当这个引用次数变成0时,则说明没有办法再访问这个值了,就可以将其所占的内存空间给回收。垃圾收集器下次再运行时,就会释放那些引用次数为0的值所占的内存。
存在的问题: 如何避免 GC 造成的长时间停止响应?
- 原因:
GC 时为了安全考虑会停止响应其他操作。而 Javascript 的 GC 在 100ms 甚至以上,对一般的应用还好,但对于JS 游戏、动画这些要求连贯性比较高的应用就麻烦了。 - 优化策略:
(1). 分代回收(Generation GC)
通过区分临时与持久对象;多回收临时对象区(young generation),少回收持久对象区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。
(2). 增量GC
每次处理一点,下次再处理一点,如此类推
# 深拷贝 和 浅拷贝
浅拷贝是只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。如果其中一个对象改变了就会影响到另一个对象,在项目中常用的方法有以下几种:
- 直接用
=赋值 Object.assign
只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是仍是对象的话依然是浅拷贝。
Object.assign还有一些注意的点是:
(1). 不会拷贝对象继承的属性。 (2). 不可枚举的属性
(3). 属性的数据属性/访问器属性。 (4). 可以拷贝Symbol类型for in循环只遍历第一层
深拷贝是将一个对象从内存完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象,项目中常用方法有以下几种:
- 用
JSON.stringify把对象转换成字符串,再用JSON.parse把字符串转换成新的对象
注意:属性值为函数时该属性会丢失,为正则时会转为空对象,为new Date()时会转为字符串 \ - 采用
递归去拷贝所有层级属性 - 用
Slice实现对数组的深拷贝 - 使用
...扩展运算符实现深拷贝
// 递归算法实现深克隆
function deepClone(obj){
if(obj === null) return null;
if(typeof obj !=='object') return obj;
if(obj instanceof RegExp) return new RegExp(obj);
if(obj instanceof Date) return new Date(obj);
// 克隆的结果和之前保持相同的所属类
let newObj = new obj.constructor;
for(let key in obj){
if(obj.hasOwnProperty(key)){
newObj[key] = deepFn(obj[key]);
}
}
return newObj
}
# 数组扁平化
数组扁平化就是将一个多维数组变为一维数组,项目中常用的方法如下:
flat( ES 6)
flat()方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
let newArray = arr.flat([depth]);
depth值可选: 指定要提取嵌套数组的结构深度,默认值为 1,不确定层级也可写 `Infinity`。
- reduce
function flatten(arr) {
return arr.reduce((result, item)=> {
return result.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
- toString & split
function flatten(arr) {
return arr.toString().split(',').map(function(item) {
return Number(item);
})
}
- join & split
function flatten(arr) {
return arr.join(',').split(',').map(function(item) {
return parseInt(item);
})
}
- 扩展运算符
[].concat(...[1, 2, 3, [4, 5]]); // [1, 2, 3, 4, 5]
也可以做一个遍历,若 arr 中含有数组则使用一次扩展运算符,直至没有为止,如下:
扩展运算符每次只能展开一层数组
function flatten(arr) {
while(arr.some(item=>Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
- 递归
function flatten(arr) {
var res = [];
arr.map(item => {
if(Array.isArray(item)) {
res = res.concat(flatten(item));
} else {
res.push(item);
}
});
return res;
}
# 说说你对原型和原型链的理解
原型:每一个构造函数都会自动带一个prototype属性,是个指针,指向一个对象,就是原型对象。
原型对象上默认有一个属性constructor,也是个指针,指向构造函数本身。
- 优点:原型对象上所有的
属性和方法都能被构造函数的实例对象共享访问。 - 缺点:多个实例对引用类型的操作会被篡改。
因为每次实例化,引用类型的数据都指向同一个地址,所以它们 读/写 的是同一个数据,当一个实例对其进行操作,其他实例的数据就会一起更改( 这也是 Vue 中 data 为什么是一个函数的原因 )。
原型链: 每个实例对象都有一个原型__proto__,这个原型还可以有它自己的原型,以此类推,形成一个链式结构即原型链。
每个构造函数都有一个原型对象prototype,原型对象上包含一个指向构造函数的指针constructor
而每个实例都包含着一个指向原型对象的内部指针 __proto__。
可以通过内部指针__proto__访问到原型对象,原型对象通过 constructor 找到构造函数。
如果 A对象 在 B 对象的原型链上,可以说它们是 B对象继承了 A对象。
原型链作用:如果试图访问对象的某个属性,会首先在 对象内部 寻找该属性,直至找不到,然后才在该对象的原型里去找这个属性,以此类推。
new 关键字创建一个实例都做了什么?
- 像普通对象一样,形成自己的私有作用域( 形参赋值,变量提升 )
- 创建一个新对象,将
this指向这个新对象( 构造函数的作用域赋给新对象 ) - 执行构造函数中的代码,为这个新对象添加属性、方法
- 返回这个新对象(
新对象为构造函数的实例 )
手写一个 new 原理如下:
function myNew(fn, ...arg){
// 创建一个对象,让它的原型链指向 fn.prototype
// 普通方法
// let obj = {};
// obj.__proto__ = fn.prototype;
// 使用 Object.create([A对象]):创建一个空对象 obj,并让 obj.__proto__ 等于 A对象
let obj = Object.create(fn.prototype);
fn.call(obj, ...arg);
return obj;
}
# 说说继承的几种方式
- 原型链继承,2. 构造函数继承
- 组合继承,4. 寄生式继承
- 组合寄生式继承,6. extends 继承
ES6、ES7、ES8 的新特性
ES6的特性:
- 类(class)
- 模块化(
Module)导出(export)导入(import) - 箭头(Arrow)函数
- 函数参数默认值
- 模板字符串
- 延展操作符(Spread operator) 和 剩余运算符(rest operator)
- ES6中允许我们在设置一个对象的属性的时候不指定属性名
- Promise 异步编程的解决方案
- 支持
let与const块级作用域
ES7的特性
includes()函数用来判断一个数组是否包含一个指定的值,返回true/false- 指数操作符在ES7中引入了指数运算符,具有与Math.pow(..)等效的计算结果
ES8的特性
- 加入了对
async/await的支持,也就我们所说的异步函数 Object.values()是一个与Object.keys()类似的新函数,但返回的是Object自身属性的所有值,不包括继承的值- Object.entries() 函数返回一个给定对象自身可枚举属性的键值对的数组
- String.padStart(targetLength,[padString]) 和 String.padEnd(targetLength,padString])\
- Object.getOwnPropertyDescriptors() 函数用来获取一个对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
# 列举几条 JS 的基本代码规范
- 变量和函数命名要见名知意
- 当命名对象、函数和实例时使用驼峰命名规则
- 请使用 === / !== 来值的比较
- 对字符串使用单引号 ''(因为大多时候我们的字符串。特别html会出现")
- switch 语句必须带有 default 分支
- 语句结束一定要加分号
- for 循环必须使用大括号
- 使用 /* ... / 进行多行注释,包括描述,指定类型以及参数值和返回值