# 经典面试题
- 经典面试题
- 复习完所有知识点之后,建议熟读
- 如何理解执行上下文
- es5 类和es6中class的区别
- typeof作用以及封装自己的typeof
- js 脚本 defer 和 async 的区别
- 在页面插入10000个元素,如何进行优化?
- 什么情况下会发生布尔值的隐式强制类型转换
- == 的转换规则
- Object.is和===的区别?
- 构造函数的原理
- for...in 和 for...of
- addEventListener 的第三个参数的作用
- ES6 中的 map 和原生的对象有什么区别
- 在 map 中和 for 中调用异步函数的区别
- target 和 currentTarget的区别
- 内存泄漏的表现形式
- requestAnimationFrame详解
- 异步和同步区别
- 如何理解arguments
- 函数的arguments为什么不是数组?还有那些类数组
- 解释下变量提升?
- AMD和CMD的区别
- ES6模块与CommonJS模块有什么区别?
- 说一下module.exports和exports的区别,export和export default的区别
- 模块方案
- null与undefined的区别是什么?
- DOM的事件模型是什么
- 说说DOM事件流
- 事件代理/事件委托 以及 优缺点
- 如何阻止事件冒泡
- 阻止默认行为
- 箭头函数和普通函数的区别?
- var、let、const的区别 ?
- 字符串的test、match、search它们之间的区别?
- 懒加载(lazyload)原理
- axios基本使用方法
- load 和 DOMContentLoaded 事件的区别
- Element和Node的区别
- js判断图片是否加载完毕的方式
- 数组的方法中那些会改变原数组呢?
- 说一下对BigInt的理解,在什么场景下会使用
- WeakMap 和 Map 有什么差别?
- 如何让Promise.all在抛出异常后依然有效
- 垃圾回收
- Symbol的使用场景
- JS的常见错误类型
- 扫码登录原理
- 0.1 + 0.2 为什么不等于0.3
# 复习完所有知识点之后,建议熟读
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的区别
- class有静态方法,static静态方法只能通过类调用
- class类必须new调用,不能直接执行。
- class封装的更加合理,方便维护和使用
- class类不存在变量提升
- 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)
# 什么情况下会发生布尔值的隐式强制类型转换
- if (..) 语句中的条件判断表达式。
- for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
- while (..) 和 do..while(..) 循环中的条件判断表达式。
- ? : 中的条件判断表达式。
- 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。
# ==
的转换规则
- 一个是字符串一个是数字 :转为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 函数的原理是:
- 循环数组,把数组每一项的值,传给回调函数
- 将回调函数处理后的结果 push 到一个新的数组
- 返回新数组
map 函数是同步执行的,循环每一项时,到给新数组值都是同步操作。
# target 和 currentTarget的区别
- target:返回触发事件的源对象
- currentTarget:返回事件绑定的对象
# 内存泄漏的表现形式
- 意外的全局变量
- 闭包
- 未被清空的定时器
- 未被销毁的事件监听
- dom引用
# requestAnimationFrame详解
- 要想动画流畅,需要一秒六十帧,即16.67ms更新一次视图
- 相比于setTimeout,setTimeout需要手动控制频率,而RAF不需要。自动控制
- 后台标签或者隐藏在iframe中,setTimeout需要手动清除,不然会一直执行,RAF自动暂停
# 异步和同步区别
异步:
js
是单线程语言,一次只能做一件事,所以就有了异步的存在,异步不会阻塞其他线程,不会影响其他的代码执行。同步: 按一定的顺序执行
# 如何理解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的区别
两种加载模块都是异步的,只不过 AMD 依赖前置,js可以方便知道依赖模块是谁,立即加载
CMD 就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略
同样都是异步加载模块,AMD在加载模块完成后就会执行改模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行
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模块有什么区别?
- CommonJS输出值的拷贝,ESM输出值的引用
- CommonJS一旦输出一个值,模块内部的变化就影响不到这个值
- ESM是动态引用且不会缓存值,模块里的变量绑定其所在的模块,等到脚本真正执行时,再根据这个只读引用到被加载的那个模块里去取值
- CommonJS是运行时加载,ESM是编译时加载
- CommonJS加载模块是对象(即module.exports),该对象只有在脚本运行完才会生成
- ESM加载模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
# 说一下module.exports和exports的区别,export和export default的区别
moudle.exports 和 exports es5就有这种写法
二者区别:
- module.exports :module变量代表当前模块。这个变量是一个对象,module对象会创建一个叫exports的属性,这个属性的默认值是一个空的对象.加载使用
require
module.exports.Name="我是电脑";
module.exports.Say=function(){
console.log("我可以干任何事情");
}
module.exports = {
}
- 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。
二者区别:
export default
在一个模块中只能有一个,当然也可以没有。export在一个模块中可以有多个- export default的对象、变量、函数、类,可以没有名字。export的必须有名字
export default {a:1}
export {a:1}//报错
export const a = {a:1}//正确
- export导出的模块需要使用大括号import,export default不需要
# 模块方案
- CommonJS:用于服务器(动态化依赖)
- AMD:用于浏览器(动态化依赖)
- CMD:用于浏览器(动态化依赖)
- UMD:用于浏览器和服务器(动态化依赖)
- ESM:用于浏览器和服务器(静态化依赖)
# null与undefined的区别是什么?
null
表示空值,一个对象可以是null
,代表空对象,他是存在的但是值为空undefined
代表不存在,除了有存在值为空,也存在根本不存在的成员
。
# DOM的事件模型是什么
- 脚本模型
- 内联模型
- 动态绑定
<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事件流
- 事件捕获阶段
- 目标接收事件
- 事件冒泡阶段
# 事件代理/事件委托 以及 优缺点
事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。
使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。
<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;
# 箭头函数和普通函数的区别?
- 箭头函数的
this
是定义时所在的上下文决定的 - 不能作为构造函数,
Generator
函数,不能使用new操作符 - 参数不能使用
arguments
访问,需要使用Es6的不定参数访问; - 不可以使用
bind
改变this指向
# var、let、const的区别 ?
var类型
会有变量提升的情况let
和const
没有变量提升的情况,必须要先声明再使用,否则就会出现暂时性死区的情况(声明之前不能使用)。const
和let
的区别在于一经定义后不得再次改变const定义的值(注意引用类型可能会被问到)const
声明之后必须赋值- 不允许重复声明一个变量,例如
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中的 Element
,Text
和 Comment
都继承于它。换句话说,Element
,Text
和 Comment
是三种特殊的Node
element.children 只返回element element.childNodes 返回NodeList
# js判断图片是否加载完毕的方式
- 监听
onload
事件
document.getElementById('img').onload = function() {
document.getElementById('p').innerHTML = 'loaded';
}
- 监听
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 有什么差别?
WeakMap的键名只支持对象,map的键名可以是任意值。
Map可以遍历,WeakMap不可以
WeakMap是弱引用,成员随时可以消失(垃圾回收),可以防止内存泄露
# 如何让Promise.all在抛出异常后依然有效
通过 catch
方法,因为 Promise
状态具有传递性,然后 catch
方法执行后会返回一个状态为 resolved
新 Promise
实例,所以在 Promise.reject
之前先 catch
一遍
var promiseArr = [p1, p2];
var promiseArr_ = promiseArr.map(function (promiseItem) {
return promiseItem.catch(function (err) {
return err;
})
});
# 垃圾回收
找出那些不再继续使用的变 量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间), 周期性地执行这一操作。
- 标记清除
先所有都加上标记,再把环境中引用到的变量去除标记。剩下的就是没用的了
- 引用计数
跟踪记录每 个值被引用的次数。清除引用次数为0的变量 ⚠️会有循环引用问题 。循环引用如果大量存在就会导致内存泄露。
# Symbol的使用场景
- 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']
如果要取取出 Symbol
的 key
,可以使用 Object.getOwnPropertySymbols(obj)
或者还有一个反射API Reflect.ownKeys(obj) // [Symbol(name), 'age', 'title']
- 使用Symbol来替代常量
const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol() const TYPE_IMAGE = Symbol()
1
- 应用在重写
call
,bind
里面
# JS的常见错误类型
# 扫码登录原理
分为三个步骤
- 浏览器生成二维码流程
- 首先打开登录页面
- 服务器会随机生成一个
uuid
,然后将uuid
以及过期的时间写入redis
- 通过
uuid
生成二维码返回给浏览器 - 浏览器拿到二维码和
uuid
之后每隔一段时间向服务器发送请求,判断是否登录成功
- 手机扫一扫流程
- 手机登录账号后扫描二维码
- 扫描之后得到一个验证信息和
uuid
- 点击确定后,手机将
token
以及uuid
传回服务器 - 服务器接收到后比对验证信息以及
uuid
,返回确认消息给手机,然后等待用户二次确认 - 用户二次确认后再次发送请求,然后服务器会用
user_id
替换redis
的键uuid
- 二次登录成功之后
- 浏览器轮询请求服务器是否已经登录成功,如果当前
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。
解决办法
- 书写大数相加函数
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
}
toFixed(2) 强制转换,但存在兼容问题
← 手写题 大厂面试题收集(只含分析) →