# 经典面试题

# 复习完所有知识点之后,建议熟读

https://github.com/i-want-offer/FE-Essay/tree/master/docs/JavaScript (opens new window)

https://juejin.im/post/6844904116552990727 (opens new window)

https://juejin.cn/post/6844904004007247880#heading-13 (opens new window)

https://muyiy.cn/question/ (opens new window)

https://q.shanyue.tech/weekly/ (opens new window)

http://bigerfe.com/ (opens new window)

# 如何理解执行上下文

函数在每次执行的时都会产生一个执行上下文对象的内部对象(Activation Object),一个执行上下文对象定义一个执行环境,执行过程中每个执行上下文对象都是独一无二的,所以多次调用会导致创建多个执行上下文对象,当函数执行完成,执行上下文就会被销毁。

执行上下文总共有三种类型:

  • 全局执行上下文: 只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。
  • 函数执行上下文: 存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
  • Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用。

# es5 类和es6中class的区别

  1. class有静态方法,static静态方法只能通过类调用
  2. class类必须new调用,不能直接执行。
  3. class封装的更加合理,方便维护和使用
  4. class类不存在变量提升
  5. class类无法遍历它实例原型链上的属性和方法

# typeof作用以及封装自己的typeof

  • 判断值类型变量的类型
  • 判断是否是引用类型,但是无法具体判断类型
  • 可以判断function

typeof(undefined) = 'undefined' typeof(NAN) = number typeof(a) = 'undefined' typeof(null) = object

封装typeof

function mytypeof(target){
  var ret = typeof(target)
  const typeMap = {
    "[object Array]":"Array",
    "[object Object]":"Object",
    "[object Number]":"number-Object",
    "[object Boolean]":"boolean",
    "[object String]":"string-Object"
  }
  if(target == null){
    return 'null'
  }
  if(ret === "object"){
    const str =Object.prototype.toString.call(target)
    return typeMap[str]
  }else{
    return ret
  }
}

# js 脚本 defer 和 async 的区别

  • defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。当整个 document 解析完毕后再执行脚本文件,在 DOMContentLoaded 事件触发之前完成。多个脚本按顺序执行。

  • async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行,也就是说它的执行仍然会阻塞文档的解析,只是它的加载过程不会阻塞。多个脚本的执行顺序无法保证。

# 在页面插入10000个元素,如何进行优化?

使用 Fragment 文档片段,另外还可以借助 requestAnimationFrame

	var container = document.getElementById('container')
	var fragment = document.createDocumentFragment()
	for(let i = 0; i < 10000; i++){
		let li = document.createElement('li')
    li.innerHTML = 'hello world'
    // 所有构造的节点加入文档片段
	    fragment.appendChild(li)
  }
  // 节点构造完成,将文档对象添加到页面中
	container.appendChild(fragment);

JavaScript 提供了一个文档片段 DocumentFragment 的机制。把所有要构造的节点都放在文档片段中执行,这样可以不影响文档树,也就不会造成页面渲染。当节点都构造完成后,再将文档片段对象添加到页面中,这时所有的节点都会一次性渲染出来,这样就能减少浏览器负担,提高页面渲染速度

setTimeout(() => {
  // 插入十万条数据
  const total = 100000;
  // 一次插入的数据
  const once = 20;
  // 插入数据需要的次数
  const loopCount = Math.ceil(total / once);
  let countOfRender = 0;
  const ul = document.querySelector('ul');
  // 添加数据的方法
  function add() {
    const fragment = document.createDocumentFragment();
    for(let i = 0; i < once; i++) {
      const li = document.createElement('li');
      li.innerText = Math.floor(Math.random() * total);
      fragment.appendChild(li);
    }
    ul.appendChild(fragment);
    countOfRender += 1;
    loop();
  }
  function loop() {
    if(countOfRender < loopCount) {
      window.requestAnimationFrame(add);
    }
  }
  loop();
}, 0)

# 什么情况下会发生布尔值的隐式强制类型转换

  1. if (..) 语句中的条件判断表达式。
  2. for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
  3. while (..) 和 do..while(..) 循环中的条件判断表达式。
  4. ? : 中的条件判断表达式。
  5. 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。

# == 的转换规则

  • 一个是字符串一个是数字 :转为Number类型,再比较值
  • 类型相同 : 比大小
  • 有一个是布尔类型。 : boolean转为数字类型
  • 有一侧是object类型` - Object转为字符串 :'[object Object]',数组: ""
[] == ![] //true

1. !运算符优先,!运算符会将数转为布尔值 
[] == !Boolean([])
2. [] == false,符合第三条第四条规则转为:
"" == 0
3. Number("") == 0 // true

# Object.is和===的区别?

Object在严格等于的基础上修复了一些特殊情况下的失误,具体来说就是+0和-0,NaN和NaN

手写实现 Object.is


function is(x, y) {
  if (x === y) {
    //运行到1/x === 1/y的时候x和y都为0,但是1/+0 = +Infinity, 1/-0 = -Infinity, 是不一样的
    return x !== 0 || y !== 0 || 1 / x === 1 / y;
  } else {
    //NaN===NaN是false,这是不对的,我们在这里做一个拦截,x !== x,那么一定是 NaN, y 同理
    //两个都是NaN的时候返回true
    return x !== x && y !== y;
  }

# 构造函数的原理

构造函数和普通函数并无区别(构造函数通常大写),但是如果使用new操作符实例化一个对象,这个函数就被视为一个构造函数了

new之后,内部执行机制。

首先隐式创建一个var this = {__proto__:Constructor.prototype},然后执行赋值this.xxx = xxx,也就是说常见的a和this就是一码事`

function A(){
  // 隐式 var this ={__proto__:A.prototype} 
  this.a = a
}
var a = new A()
 // 隐式 return this

也就是隐式的将传入的obj的隐式原型赋值为构造函数的显示原型。然后绑定this。最后返回,判断最后执行apply的变量是否是object,如果不是返回obj

# for...in 和 for...of

  • for...of使用遍历数组/数组对象/字符串/map/set等拥有迭代器对象,但是不能遍历对象,因为对象没有迭代器对象。他可以使用break,continue,return
  • for...in来遍历对象,或者使用Object.keys()
  • for...in遍历的是数组的索引(键名),for...of是数组的值
const arr = [5, 4, 3, 2, 1]
arr.name = 'name'

for(let i in arr){
  console.log(i) // 0 1 2 3 4 name(字符串类型的索引)
}

# addEventListener 的第三个参数的作用

设置为true,则代表再事件捕获中执行,否则在事件冒泡中执行

在JS中,绑定的事件默认的执行时间是在冒泡阶段执行,而非在捕获阶段(重要)

# ES6 中的 map 和原生的对象有什么区别

map: 键值可以为任意类型,map.keys()获取key数组 Object: 键值只能为字符串,Object.keys()获取key数组

# 在 map 中和 for 中调用异步函数的区别

  • map:等待同步操作执行完成,再一步一步执行异步
  • for:等待异步结果,再进入下一次循环

map 函数的原理是

  1. 循环数组,把数组每一项的值,传给回调函数
  2. 将回调函数处理后的结果 push 到一个新的数组
  3. 返回新数组

map 函数是同步执行的,循环每一项时,到给新数组值都是同步操作。

# target 和 currentTarget的区别

  • target:返回触发事件的源对象
  • currentTarget:返回事件绑定的对象

# 内存泄漏的表现形式

  • 意外的全局变量
  • 闭包
  • 未被清空的定时器
  • 未被销毁的事件监听
  • dom引用

# requestAnimationFrame详解

  • 要想动画流畅,需要一秒六十帧,即16.67ms更新一次视图
  • 相比于setTimeout,setTimeout需要手动控制频率,而RAF不需要。自动控制
  • 后台标签或者隐藏在iframe中,setTimeout需要手动清除,不然会一直执行,RAF自动暂停

# 异步和同步区别

  1. 异步: js是单线程语言,一次只能做一件事,所以就有了异步的存在,异步不会阻塞其他线程,不会影响其他的代码执行。

  2. 同步: 按一定的顺序执行

# 如何理解arguments

arguments 代表实参数组,arguments有两个特殊属性

  • callee:值为函数的引用,就是指向自己
理解callee: 实现100的阶乘

var num = (function(n){
 // 定义出口
 if(n === 1){
    return 1
  }
  return n * arguments.callee(n - 1) 
  // 因为无法找到立即执行函数的函数名 只能使用callee来调用递归
}(100))

还有一个caller容易混淆,每个函数都会有一个caller属性,指代谁调用这个方法的人所处的环境
  • length

arguments如何转换为真正的数组

  • Array.from(arguments)
  • ...(arguments)
  • Array.prototype.slice.call(arguments)

# 函数的arguments为什么不是数组?还有那些类数组

因为首先函数的参数被设计时就是不需要怎么改动的,并且如果使用数组的方式去访问某个参数需要使用到下标,没有对象访问好用

常见的类数组还有:

  • 用getElementsByTagName/ClassName()获得的HTMLCollection
  • 用querySelector获得的nodeList

# 解释下变量提升?

js引擎在预编译环节执行代码的时候会将声明变量代码放置顶部,然后再依次执行。所以所有的声明变量的语句都会在顶部最先执行,这就是变量提升。

var a = 1
//等同于

var a
a= 1

# AMD和CMD的区别

  1. 两种加载模块都是异步的,只不过 AMD 依赖前置,js可以方便知道依赖模块是谁,立即加载

  2. CMD 就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略

  3. 同样都是异步加载模块,AMD在加载模块完成后就会执行改模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行

  4. CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的,只有用户需要的时候才执行

1.对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。

2. CMD 推崇依赖就近,AMD 推崇依赖前置。


// CMD
 define(function(require, exports, module) {
     var a = require('./a')
     a.doSomething()
     // 此处略去 100 行
     var b = require('./b') // 依赖可以就近书写
     b.doSomething()
     // ...
 })

 // AMD 默认推荐
 define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
     a.doSomething()
     // 此处略去 100 行
     b.doSomething()
     // ...
 })

# ES6模块与CommonJS模块有什么区别?

  1. CommonJS输出值的拷贝,ESM输出值的引用
  • CommonJS一旦输出一个值,模块内部的变化就影响不到这个值
  • ESM是动态引用且不会缓存值,模块里的变量绑定其所在的模块,等到脚本真正执行时,再根据这个只读引用到被加载的那个模块里去取值
  1. CommonJS是运行时加载,ESM是编译时加载
  • CommonJS加载模块是对象(即module.exports),该对象只有在脚本运行完才会生成
  • ESM加载模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

# 说一下module.exports和exports的区别,export和export default的区别

moudle.exports 和 exports es5就有这种写法

二者区别:

  1. module.exports :module变量代表当前模块。这个变量是一个对象,module对象会创建一个叫exports的属性,这个属性的默认值是一个空的对象.加载使用 require
module.exports.Name="我是电脑";
module.exports.Say=function(){
  console.log("我可以干任何事情")}
module.exports = {

}
  1. exports:Nodejs的每个模块都有一个exports变量指向module.exports,二者几乎相同,但是 exports不允许直接导出函数和值
module.exports=function(){
  var a="Hello World"  
  return   a;
} // 可以

exports = function(){
  var a="Hello World"  
  return   a;
} // 报错

export和export default是es6引出的语法,用于导出模块中的变量,对象,函数,类。对应的导入关键字是import。

二者区别:

  1. export default 在一个模块中只能有一个,当然也可以没有。export在一个模块中可以有多个
  2. export default的对象、变量、函数、类,可以没有名字。export的必须有名字
export default {a:1}
export {a:1}//报错
export const a = {a:1}//正确
  1. export导出的模块需要使用大括号import,export default不需要

# 模块方案

  • CommonJS:用于服务器(动态化依赖)
  • AMD:用于浏览器(动态化依赖)
  • CMD:用于浏览器(动态化依赖)
  • UMD:用于浏览器和服务器(动态化依赖)
  • ESM:用于浏览器和服务器(静态化依赖)

# null与undefined的区别是什么?

  1. null表示空值,一个对象可以是null,代表空对象,他是存在的但是值为空

  2. undefined代表不存在,除了有存在值为空,也存在根本不存在的成员

# DOM的事件模型是什么

  1. 脚本模型
  2. 内联模型
  3. 动态绑定
<body>
<!--行内绑定:脚本模型-->
<button onclick="javascrpt:alert('Hello')">Hello1</button>
<!--内联模型-->
<button onclick="showHello()">Hello2</button>
<!--动态绑定-->
<button id="btn3">Hello3</button>
</body>
<script>
/*DOM0:同一个元素,同类事件只能添加一个,如果添加多个,
* 后面添加的会覆盖之前添加的*/
function shoeHello() {
alert("Hello");
}
var btn3 = document.getElementById("btn3");
btn3.onclick = function () {
alert("Hello");
}
</script>

# 说说DOM事件流

  1. 事件捕获阶段
  2. 目标接收事件
  3. 事件冒泡阶段

# 事件代理/事件委托 以及 优缺点

事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。

使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。

<ul>
  <li>苹果</li>
  <li>香蕉</li>
  <li>凤梨</li>
</ul>

// good
document.querySelector('ul').onclick = (event) => {
  const target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

// bad
document.querySelectorAll('li').forEach((e) => {
  e.onclick = function() {
    console.log(this.innerHTML)
  }
}) 

事件委托的优点:

  • 减少内存消耗,不必为大量元素绑定事件
  • 可以为动态添加的元素绑定事件

事件委托的缺点:

  • 部分事件如 focus、blur 等无冒泡机制,所以无法委托。
  • 事件委托有对子元素的查找过程,委托层级过深,可能会有性能问题
  • 频繁触发的事件如 mousemove、mouseout、mouseover等,不适合事件委托

# 如何阻止事件冒泡

// w3c
e.stopPropagation()

// IE
e.cancelBubble = true

# 阻止默认行为

//谷歌及IE8以上
e.preventDefault();

//IE8及以下
window.event.returnValue = false;

# 箭头函数和普通函数的区别?

  1. 箭头函数的this是定义时所在的上下文决定的
  2. 不能作为构造函数, Generator函数,不能使用new操作符
  3. 参数不能使用 arguments 访问,需要使用Es6的不定参数访问;
  4. 不可以使用 bind 改变this指向

# var、let、const的区别 ?

  1. var类型会有变量提升的情况
  2. letconst没有变量提升的情况,必须要先声明再使用,否则就会出现暂时性死区的情况(声明之前不能使用)。
  3. constlet的区别在于一经定义后不得再次改变const定义的值(注意引用类型可能会被问到)
  4. const声明之后必须赋值
  5. 不允许重复声明一个变量,例如let a =1 let a=2

# 字符串的test、match、search它们之间的区别?

  • test是reg的方法,返回当前字符串是否匹配规则
  • match是字符村的方法,返回当前匹配的字符串数组结果
  • search是字符串的方法,返回正则匹配的下标
/[a-z]/.test(1);  // false
'1AbC2d'.match(/[a-z]/ig);  // ['A', 'b', 'C', 'd']
'1AbC2d'.search(/[a-z]/);  // 2

# 懒加载(lazyload)原理

首先将页面上的图片的src属性设置为空字符串,而图片的真实路经则设置带data-original属性中,当页面滚动的时候需要去监听scroll事件,在scroll事件的回调中,判断我们的懒加载的图片是否进入到可视区域,如果图片在可视区域将图片的src属性设置为data-original的值,这样就可以实现延迟加载。

# axios基本使用方法

# GET

axios({
  method:"GET",
  headers:{
    "Content-Type":"application/json",
    "Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ZWQzZGEzMDVkMDc4NjdjZjg4N2RhY2IiLCJpYXQiOjE1OTEwOTUxNjEsImV4cCI6MTU5MTI2Nzk2MX0.m4DLxbhchw3lpOQf8vY_1R985y9zPds_2kK1xnGVRxA"
  },
  url:"http://localhost:3000/v1/public/getlist",
 params:{
   category:'音乐'
 }
}).then(res=>{
  console.log(res.data)
})


# POST

上传文件

let formdata = new FormData();
    formdata.append('file',event.target.files[0]);
    const res = await axios.post('https://imgkr.com/api/files/upload', data, {
        headers: {
          'Content-Type': 'multipart/form-data',
       }
      })
    
    
 export function createArticle(data) {
  return request({
    url: '/article/create',
    method: 'post',
    data
  })
}

# 拦截器

// 封装 axios
// 1.封装请求返回数据 2.异常统一处理
// 鉴权处理

import axios from 'axios'
import errorHandle from './errorHandle'

const instance = axios.create({
  // 统一请求配置
  baseURL:
    process.env.NODE_ENV === 'development'
      ? config.baseURL.dev
      : config.baseURL.pro,
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  },
  timeout: 10000
})


// 请求拦截器
instance.interceptors.request.use(
  config => {
    return config
  },
  // 请求异常处理
  err => {
    errorHandle(err)
    return Promise.reject(err)
  }
)

// 请求结果封装  
instance.interceptors.response.use(
  res => {
    if (res.status === 200) {
       // 直接返回res.data
      return Promise.resolve(res.data)
    } else {
      return Promise.reject(res)
    }
  },
  error => {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error  处理非200的请求返回的异常: 比如404 ,API服务暂停(没有状态码)等
    errorHandle(error)
    return Promise.reject(error)
  }
)
import {
  Message
} from 'element-ui'
const errorHandle = (error) => {
  const errorStatus = error.response.status
  switch (errorStatus) {
    case 401:
      console.log('刷新token')
      break
    case 500:
      Message({
        type: 'error',
        message: error.response.data.msg,
        duration: 4000
      })
      break
    case 404:
      Message({
        type: 'error',
        message: '网络异常',
        duration: 4000
      })
      break
    default:
      break
  }
}

# load 和 DOMContentLoaded 事件的区别

  • 当整个页面及所有依赖资源如样式表和图片都已完成加载时,将触发load事件。它与DOMContentLoaded不同,后者只要页面DOM加载完成就触发,无需等待依赖资源的加载。

  • 当纯HTML被完全加载以及解析时,DOMContentLoaded 事件会被触发,而不必等待样式表,图片或者子框架完成加载。

# Element和Node的区别

简单的说就是 Node 是一个基类,DOM中的 ElementTextComment 都继承于它。换句话说,ElementTextComment 是三种特殊的Node

element.children 只返回element element.childNodes 返回NodeList

# js判断图片是否加载完毕的方式

  1. 监听 onload 事件
  document.getElementById('img').onload = function() {
        document.getElementById('p').innerHTML = 'loaded';
    }
  1. 监听 readystatechange 事件,通过判断 readyState 是否为 competed 或者 loaded
var img = document.getElementById('img');
    img.onreadystatechange = function () {
        if (img.readyState == 'complete' || img.readyState == 'loaded') {
            document.getElementById('p').innerHTML = 'readystatechange:loaded';
        }
    }

# 数组的方法中那些会改变原数组呢?

pop()---删除数组的最后一个元素并返回删除的元素。
push()---向数组的末尾添加一个或更多元素,并返回新的长度。
shift()---删除并返回数组的第一个元素。
unshift()---向数组的开头添加一个或更多元素,并返回新的长度。
reverse()---反转数组的元素顺序。
sort()---对数组的元素进行排序。
splice()---用于插入、删除或替换数组的元素。

# 说一下对BigInt的理解,在什么场景下会使用

BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对 大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。

# WeakMap 和 Map 有什么差别?

  1. WeakMap的键名只支持对象,map的键名可以是任意值。

  2. Map可以遍历,WeakMap不可以

  3. WeakMap是弱引用,成员随时可以消失(垃圾回收),可以防止内存泄露

# 如何让Promise.all在抛出异常后依然有效

通过 catch 方法,因为 Promise 状态具有传递性,然后 catch 方法执行后会返回一个状态为 resolvedPromise 实例,所以在 Promise.reject 之前先 catch 一遍

var promiseArr = [p1, p2];
var promiseArr_ = promiseArr.map(function (promiseItem) {
    return promiseItem.catch(function (err) {
        return err;
    })
});

# 垃圾回收

找出那些不再继续使用的变 量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间), 周期性地执行这一操作。

  1. 标记清除

先所有都加上标记,再把环境中引用到的变量去除标记。剩下的就是没用的了

  1. 引用计数

跟踪记录每 个值被引用的次数。清除引用次数为0的变量 ⚠️会有循环引用问题 。循环引用如果大量存在就会导致内存泄露。

# Symbol的使用场景

  1. Symbol类型的key是不能通过Object.keys()或者for...in来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义
let obj = {
   [Symbol('name')]: '一斤代码', age: 18, title: 'Engineer' } Object.keys(obj) // ['age', 'title'] for (let p in obj) { console.log(p) // 分别会输出:'age' 和 'title' } Object.getOwnPropertyNames(obj) // ['age', 'title'] 

如果要取取出 Symbolkey,可以使用 Object.getOwnPropertySymbols(obj) 或者还有一个反射API Reflect.ownKeys(obj) // [Symbol(name), 'age', 'title']

  1. 使用Symbol来替代常量
const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol() const TYPE_IMAGE = Symbol() 
1
  1. 应用在重写 callbind 里面

# JS的常见错误类型

# 扫码登录原理

分为三个步骤

  1. 浏览器生成二维码流程
  • 首先打开登录页面
  • 服务器会随机生成一个 uuid ,然后将 uuid 以及过期的时间写入 redis
  • 通过 uuid 生成二维码返回给浏览器
  • 浏览器拿到二维码和 uuid 之后每隔一段时间向服务器发送请求,判断是否登录成功
  1. 手机扫一扫流程
  • 手机登录账号后扫描二维码
  • 扫描之后得到一个验证信息和 uuid
  • 点击确定后,手机将 token 以及 uuid 传回服务器
  • 服务器接收到后比对验证信息以及 uuid,返回确认消息给手机,然后等待用户二次确认
  • 用户二次确认后再次发送请求,然后服务器会用 user_id 替换 redis 的键 uuid
  1. 二次登录成功之后
  • 浏览器轮询请求服务器是否已经登录成功,如果当前 redis 的键已经被更新,将 user_id 返回给浏览器
  • 浏览器收到 user_id 后请求服务器登录接口获取 token 和用户信息
分为三个步骤,首先是浏览器端生成二维码,然后是手机扫码,然后是二次登录成功

第一步,浏览器打开登录页面时,服务器会随机生成一个uuid,然后通过uuid生成一个二维码,服务器将uuid和二维码返回给浏览器,然后浏览器就会每隔一段时间向服务器发出请求,判断当前是否登录成功

第二步,手机扫码,手机在登录之后,扫描二维码,然后手机可以拿到一个验证信息还有uuid,接着如果用户确定,会将token和uuid传给服务器,然后服务器进行比对验证,然后等待用户二次确认,如果确认,服务器就会用user_id更新redis的键,也就是uuid

第三步,二次登录成功之后,如果当前的redis的key已经被更新,并且将user_id传给手机,然后手机端就会通过user_id调用登录接口获取token和用户信息

# 0.1 + 0.2 为什么不等于0.3

0.1的二进制表示的是一个无限循环小数,该版本的 JS 采用的是浮点数标准需要对这种无限循环的二进制进行截取,从而导致了精度丢失,造成了0.1不再是0.1,截取之后0.1变成了 0.100…001,0.2变成了0.200…002。所以两者相加的数大于0.3。

解决办法

  1. 书写大数相加函数
function add(num1,num2){
  while(num1.length > num2.length) num2 = '0' + num2
  while(num2.length > num1.length) num1 = '0' + num1
  let res = ''
  let carry = 0
  for(let i=num1.length-1;i>=0;i--){
    let sum = +num1[i]+ +num2[i] +carry
    res =  sum % 10  + res 
    carry = sum > 9 ? 1 : 0
  }
  return carry === 1 ? '1' + res : res
}
  1. toFixed(2) 强制转换,但存在兼容问题

Last update: 3/26/2021, 1:11:58 AM