js相关面试题总结
Cookie、LocalStorage、sessionStorage 区别
window.onload 和 DOMContentLoaded 区别
new Object() 和 Object.create()区别?
33. HTMLCollection 和 NodeList 的区别
37、什么是JS垃圾回收?垃圾回收的算法?JS 内存泄漏如何检测?场景有哪些?
38、浏览器和 nodejs 事件循环(Event Loop)有什么区别?
42、是否了解过requestIdleCallback? 和 requestAnimationFrame有什么区别?
45、基于 Session 和 JWT 用户认证的区别和优缺点
47、script 标签的defer 和 async 有什么区别?
49、dns-prefetch 和 preconnect有什么作用?
51、webSocket 和 http协议有何区别?有何应用场景?
一、异步加载JS有哪些方式?
  浏览器在解析HTML中,如果遇到script脚本,会停止页面的解析和渲染,去下载script脚本,多个script脚本下载是并行的,但按照html中的先后顺序依次执行(即使后面的脚本先下载完,也要等到前面的脚本下载完并执行完后,才能执行)。script脚本执行时,页面也是停止解析和渲染的。页面渲染完,才会触发DOMContentLoaded(所以script的下载和执行如果慢,会延迟DOMContentLoaded事件的触发时间)。下面的图展示了浏览器下载和执行script过程

在开发中脚本往往比HTML页面更“重”,处理时间更长,所以会造成页面的解析阻塞,在脚本下载和执行之前,用户在界面上什么都看不到。为了解决这个问题,我们可以采用异步加载JS脚本的方式。
异步加载js脚本常用方式:
1. defer 异步加载
* 在script标签中添加defer属性
* defer脚本的 下载和执行 都不会阻塞页面的解析渲染。因为要等到页面解析渲染完毕后,defer脚本才执行
* 多个defer脚本的下载是并行的,但执行却是按照顺序依次执行
* 当页面的解析和渲染完毕后,触发HTMLContentLoaded事件前,依次执行defer脚本。所以defer脚本的下载和执行如果慢,会延迟DOMContentLoaded触发时间
* 浏览器加载和执行 defer脚本的顺序如下图
  
2.async异步加载
*在script标签中添加async属性
* async 脚本的 下载 不会阻塞页面的解析和渲染。但async脚本的 执行 会阻塞页面的解析和渲染
* 多个async脚本的下载是并行的,哪个先下载完哪个脚本就立即执行,所以async不会按照页面的脚本顺序执行
* async 脚本的执行 只有在DOMContentLoaded事件之前时,才会影响DOMContentLoaded触发时间。又因为脚本的执行时间一般都比较短,所以可以认为async脚本基本不影响DOMContentLoaded事件的触发时间
* 浏览器加载和执行 async脚本的顺序如下图

3. 动态创建script标签 (基本不用了)
* 在还没有定义defer和async 前,异步加载的方式是通过动态创建script,通过window.onload方法确保页面加载完毕,再将script标签插入到DOM中
二、var let const 区别
- 变量提升:var 有声明提升,let与const 不存在变量声明提升的问题。
    console.log(num) //undefined
    console.log(num1) //Cannot access 'num1' before initialization
    //console.log(num2) //Cannot access 'num2' before initialization
    var num = 1;
    let num1 = 2;
    //const num2 = 3
上面例子中,var如果在声明之前调用会显示undefined,但let和const 则直接报错
- 重复声明:var定义的变量可以覆盖,即可以重复定义(后面定义的覆盖前面定义的);let和const定义的变量不可以重复定义
var num = 1; var num = 2; console.log(num) //2 // let num1 = 1; // let num1 = 2 //Identifier 'num1' has already been declared //const num1 = 1; //const num1 = 2 //Identifier 'num2' has already been declared
- 块级作用域:var定义的变量没有块级作用域;let和const定义的变量存在块级作用域
    for(var k = 0; k < 3 ;k++){
      console.log(k) //0,1,2
    }
    console.log(k) //k is not defined
    for(let i = 0; i < 3 ;i++){
      console.log(i) //0,1,2
    }
    console.log(i) //i is not defined
- 初始值设置:const声明的变量必须赋值,否则会报错;var 和 let 则允许先声明后赋值
const a; //控制台会报错
- 是否可修改:const 定义的变量不允许修改,否则会报错;var 和let 则允许修改
- 暂时性死区:let 和 const 在变量声明之前,该变量都是不可用的。这在语法上称之为暂时性死区;var不存在暂时性死区
- 给全局添加属性:浏览器的全局对象是window。var声明的变量为全局变量时会将该变量添加为window的属性。let和const不会
三、JS数据类型有哪些?
基本数据类型:
- 字符串 string
字符串与任何类型的数据结合,都转化成字符串拼接形式 。如下例子:
var a = 'hello' var c = true var b var d = null alert(a + true) //hellotrue alert(a + b) //helloundefined alert(a + d) //hellonull alert(a + 1) //hello1
- 数值 number
- 取值范围 (-2^53, 2^53)范围内,开区间。超出范围,会表示为Infinity 或者-Infinity;超出范围的大整数可以用 bigint 来表示
- 数字类型采用64位浮点数表示,从最左边开始:
- 第1位:符号位,决定了一个数的正负。0表示正数,1表示负数
- 第2位~第12位:存储指数部分(共11位),指数部分决定了数值的大小
- 第13位~第64位:存储小数部分,决定了数值的精度。(有效数字52位)
- NaN是number类型,但不是一个具体的数字。NaN与任何值都不相等,包括NaN本身即 NaN != NaN
- isNaN() 判断数据是否是NaN
- isFinite() 判断数据是否在范围内
- 
var a = 1 alert(a + true) //2 alert(a + undefined) //NaN alert(a + null) //1 
 
- 布尔值 boolean
- 布尔值只能有两个值:true 或者 false
 
- undefined
- 表示未定义或不存在,本来应该有值但没定义
 
- null
- 表示空值,即此处的值为空
- typeof null 是 object
 
- symbol
- 表示独一无二的值
- 格式: let xxx = Symbol('标识字符串')
- Symbol()通常作为属性名,在开发中需要对第三方插件或者框架添加自定应属性或者方法时会用到,防止跟框架的原有属性或者方法重名
- bigint
- 是一种数据类型的数字,可以表示任意精度格式的整数
- 可以安全的存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围
 
引用类型(对象类型):
- 对象Object
- 数组Array
- 函数Function
- 正则RegExp
- 日期Date
number、string、boolean、undefined、object、function、symbol、bigint
五、null 和 undefined 区别
大多数计算机语言,有且仅有一个表示“无”的值(比如:C语言NULL,java的null),但是JavaScript居然有两个表示“无”的值:null和 undefined.这又是为什么呢?
这与JsvaScript的历史有关。JsvaScript诞生时,借鉴了Java,只设置了一个null作为表示“无”的值,被当成对象。有根据C语言的传统,将null设计成可以自动转为0。但是设计者后来又觉得null像Javay一样被当成一个对象不是很好,再加上作者又觉得null自动转为0很不容易发现错误,因此,作者就又设计了一个undefined,为了填补之前的坑。
二者的具体区别:
null 是表示一个“无”的对象,转为数值时为0,typeof 为 object
undefined 是表示一个“无”的原始值,转为数值时为NaN, typeof 为 undefined
null 和 undefined 进行 == 比较时两者相等,全等===比较时不相等
六、== 和 === 有什么不同?
==:表示相等(比较的是值)
1.如果两边数据类型不同时,会将两个数据先转换为同一类型(隐式转换),再进行比较。具体的规则如下:
1)数字 和 字符串 进行比较时,会将字符串转换成为数字值
      2)如果操作数之一是Boolean,会将Boolean转换成1或者0
3)如果操作数之一是对象,另一个是数字或者字符串,则会调用对象的 valueOf() 和 toString() 方法将对象转换为数字或者字符串
4)只有 null == undefined 情况为true,其他null和undefined以任何类型组合都是false
2.如果两边数据类型相同,则直接比较值、
1 == '1' // true '1' -> 1 1 == 'true' // false 0 == false // true false转化成数值0 [] == false // true []先调用valueOf()->转化成空字符串''->空字符串再转化成数值0; false转化成数值0 [] == 0 // true []先调用valueOf()->转化成空字符串''->空字符串再转化成数值0 ![] == [] //true ![]-> false -> false转化成数值0 ;[]先调用valueOf()->转化成空字符串''->空字符串再转化成数值0 null == undefined // true null == 0 // false ( 虽然null转换为数值是0,但依然不相等 ) null == false // false undefined == 0 // false
==:表示严格相等(除了比较值,没还比较类型)
1.如果两边数据类型不同时,会直接返回false,不会进行比较
2.只有两边数据类型相同,会判断值是否相同
3. 如果两个都是对象类型,那么会潘丹她们引用地址是否一致
0 === '0' //false 0 === false // false null === undefined // false let a = {} let b = {} let c = a a === b // false a === c // true
typof和instanceof都是判断数据类型的方法,区别如下:
- typeof 返回的是数据类型;而insfanceof返回的是布尔值;
- typeof 虽然可以判断数据类型,但判断一个object的数据的时候不能细致到具体是哪一种object;而instanceof可以用来判断对象是否是某一具体的类型
- instanceof 可以判断一个对象是否属于某个构造函数的实例
instanceof 原理:是实例对象的__proto__ 和 构造函数的prototype 是否相等,如果相等则返回true;如果不想等则判断实例对象的__proto__ 的__proto__ 和 构造函数的prototype是否相等,相等返回true,不想等则继续原型练一层一层查找,直到原型链的尽头(Object.prototype.__proto__ == null )还未找到就放回false。
具体代码实现如下:
function MyInstanceof(L, R){ if(typeof L !== 'object' || L !== null){ return false } if(typeof R !== 'function'){ return false } while(L !== null){ if(L.__proto__ === R.prototype) { return true; //找到返回true }else{ L = L.__proto__ ; // 继续原型链上查找 } } return false //为查找到返回 false }
都是截取字符串的方法,区别如下:
substring(startIndex,endIndex): 一个起始索引和一个结束索引来指定范围,如果省略第二个参数,则截取字符串末尾
substr(startIndex,length): 从startIndex开始,长度为length的字符串,如果省略第二个参数,则截取到字符串末尾
const str = "hello, world!"; console.log(str.substring(7,12)); //world console.log(str.substr(7,5)); //world
十、for...in和 for...of区别?for await ....of 有什么作用
for...of: es6新增的遍历方式
-  适用于遍历可迭代对象:数组、类数组对象、字符串、Map、Set 、以及Generator 对象
- 不能遍历普通对象,要想遍历对象,可以给对象添加一个Symbol.iterator属性,并指向一个迭代器即可
- 遍历获取的是对象的键值(value)
- 循环能保证按照对象元素的顺序进行迭代
- 只遍历自身的可枚举属性,不会遍历原型链上的可枚举属性值
for...in:
- 适用于遍历 可枚举属性:像 对象、数组、字符串
- 遍历获取的是对象的键名(key)
- 遍历对象的属性不能保证按顺序进行迭代
- 遍历自身的可枚举属性 和 原型上的可枚举属性
总结: for...in 循环主要是为了遍历对象,不适用于遍历数组;for...of循环可以用来遍历数组、类数组、字符串、Set、Map以及 Generator对象
for await ....of
- 用于遍历异步请求的可迭代对象
- 如果有一个数组,里面有那个 Promise 对象
- 可以使用 Promise.all 执行,返回一个数组
const p1 = new Promise(resolve =>{ setTimeout(()=>{ resolve(10) },1000) }) const p2 = new Promise(resolve =>{ setTimeout(()=>{ resolve(20) },1000) }) const p3 = new Promise(resolve =>{ setTimeout(()=>{ resolve(30) },1000) }) const list = [p1,p2,p3] Promise.all(list).then(res=>{ console.log('333333',res) //[10,20,30] }) 
- 也可以使用 for await ...of 遍历执行 
const p1 = new Promise(resolve =>{ setTimeout(()=>{ resolve(10) },1000) }) const p2 = new Promise(resolve =>{ setTimeout(()=>{ resolve(20) },1000) }) const p3 = new Promise(resolve =>{ setTimeout(()=>{ resolve(30) },100) }) const list = [p1,p2,p3] for await (let p of list){ console.log(p,'p') // 分别打印出 10、20、30 } 
 
for...of 作为ES6新增的的遍历方式,能遍历的数据都有一个遍历器iterator接口 而数组、字符串、Map、Set其内部已经实现,普通对象内部没实现。所以遍历对象时会报错。
要想遍历对象,可以给对象添加一个特殊的属性 Symbol.iterator ,并将其指向一个迭代函数。
示例代码如下:
const obj = {a:1,b:2,c:3}
//为对象添加[Symbol.iterator] 属性,柄指向一个迭代器
obj[Symbol.iterator] = function* (){
    for(let key in obj){
        yield obj[key]
    }
}
AJAX 是异步的Javascript 和 XML 技术。是与服务器进行交互功能的实现
AJAX 运行当中经历了5种状态,分别是:
- 0 - (未初始化) 还没有调用send() 方法
- 1 - (载入) 已调用send()方法,正在发送请求
- 2 - (载入完成) send()方法执行完成
- 3 - (交互) 正在解析相应内容
- 4 - (完成) 响应内容解析完成,成功返回
- ajax
- 一种技术统称,异步的Javascript 和 XML
- 基于XHLHttpRequest 对象
- 多个请求之间如果有先后关系的话,会出现回调地狱
- 需要手动处理各种请求和响应,代码冗长
 
- fetch
- 是浏览器原生API
- fetch是ajax的替代品
- fetch 不是ajax的进一步封装,而是用原生js,没有使用XMLHttpRequest对象
- 使用了promise对象,支持async/await
- 使用.then方式处理回调结果,解决了回调地狱问题
- 响应数据需要手动处理JSON数据
- 默认不会带cookie,需要添加配置项
 
- axios--首选
- 是第三方库
- 内部基于XMLHttpRequest对象实现的
- 通过Promise封装的网络请求库,使用时需要引入这个库
- 支持请求拦截和响应拦截
- 自动转换JSON数据
- 客户端支持跨站请求伪造(CSRF/XSRF)预防
尾调用:就是在函数的最后一步调用函数(即最后一步是返回的函数调用)。例子:
function f(x){ return g(x) }
下面几种请款都不属于尾调用:
情况一:
function f(x){ let y = g(x) return y } 情况二: function f(x){ return g(x) + 1 } 情况三: function f(x){ g(x) }
使用尾调用的好处:在函数里调用另外一个函数时,会保留当前的执行上下文,如果在函数的尾部调用,因为已经是最后一步,所以这时可以不用保留当前的执行上下文,从而节省内存。
  浅拷贝:只复制了第一层
1.实现的方法:
1)Object.assign()方法 拷贝对象
2)扩展运算符(...)
2.手写一个浅拷贝
function shallCopy(obj){ if(!obj || typeof obj !== 'object'){ return obj } const newObj = Array.isArray(obj) ? [] : {} for(let key in obj){ if(obj.hasOwnProperty(key)){ newObj[key] = obj[key] } } return newObj }
深拷贝:将原对象的各个属性值复制过来,是值而不是引用,新旧两个对象互不影响
1.实现方法:
1)JSON.parse(JSON.stringify(obj))
- 
- 
- 
- 缺陷:对象中的函数、undefined、symbol 会丢失
 
 
- 
 
- 
2.手写一个深拷贝
- 箭头函数是匿名函数,所以不能作为构造函数,不能使用new 关键字
- 箭头函数没有arguments
- 箭头函数没有自己的this,会获取所在的上下文作为自己的this
- call()、apply()、bind()不能改变箭头函数中的this
- 箭头函数没有prototype
不适合使用箭头函数的场景:
- 
- 对象中的方法
cont obj = { name:'zhangsan', getName: ()=>{ return this.name } } 
- 对象原型
- 构造函数
- 动态上下文的会调
- Vue生命周期和 method
 
- 对象中的方法
- Set是值的集合,Map是键值对,键和值可以是任何值
- Map可以通过get方法获取值,而Set不能
- Set的值是唯一的可以做数组去重,Map由于没有格式限制,可以做数据存储
Map 和 Object 都是键值对来存储数据,区别如下:
- 键的类型: Map 的值可以是任意数据类型(包括对象、函数、NaN等);而Object 只能是字符串或者Symbol类型
- API不同:
- 键值对的顺序(Map的强大之处):Map 中的键值对是按照插入的顺序存储的;而Object中的键值对没有顺序。即Map是有序结构(重要),而object是无序结构
- 键值对的遍历:Map 的键值对可以使用for...of进行遍历;而Object的键值对需要手动遍历
- 继承关系:Map 没有继承关系;而Object是所有对象的基类
- Map操作同样很快(Map的强大之处): Map虽然是有序数据结构,但是操作很快;Object是无序数据结构,操作快
什么是有序结构和无序结构,他们的区别:
- 
- 
- 有序: 1.存储是按顺序存储的,如数组。通过索引访问; 2.操作慢(中间插入或者删除元素时,需要移动后面的元素)
- 无序: 1. 如对象,存储没有顺序。需要通过键来访问; 2.操作快,但无序
 
 
- 
1.、Promise 是异步编程的一种解决方案,将异步操作以同步操作流程表达出来,避免地狱回调
2、Promise 的实例有三个状态:
- Pending(初始状态)
- Fulfilled(成功状态)
- Rejected (失败状态)
3、Promise 实例有两个过程
1. pending -> fulfilled : Resolved(已完成)
2. pending -> rejected: Rejected(已拒绝)
注: 一旦从进行状态变为其他状态就永久不能改变状态了,其过程是不可逆的
4、Promise 构造函数接收一个带有 resolve 和 reject 参数的回调函数
- resolve的作用是将 Promise 状态从 pending 变为 fulfilled, 在异步操作成功时调用,将异步结果返回,作为参数传递出去
- reject 的作用是将 Promise 状态从pending 变为 rejected,在异步操作失败后,将异步操作错误的结果作为参数传递出去
5. then 和 catch 是如何影响Promise状态变化的(重要)
then 和 catch 如果正常则返回状态为 fulfilled 的Promise,如果里面有包错则返回 状态为 rejected 的Promise
注意: catch 里面没有报错的话回返回 fulfilled 的 Promise,回触发会面的 then 回调而不是catch 回调
eg: then和catch 链式调用题目
const p1 = Promise.resolve().then(()=>{ console.log('1') //无错误 返回 fulfilled 状态的Promise,触发后面的 then 回调 }).then(()=>{ console.log('3') throw new Error('err') //有错误 返回rejected状态的Promise,触发后面的 catch 回调 }).then(()=>{ console.log('4') }).catch(()=>{ console.log('5') //无错误 返回 fulfilled 状态的Promise,触发后面的 then 回调 }).catch(()=>{ console.log('6') }).then(()=>{ console.log('7') // 无错误 返回 fulfilled 状态 的Promise,触发后续的 then 回调
})
上面代码打印结果是:1 3 5 7
5. Promise 的缺点:
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消
- 如果不设置回调函数,Promise 内部会抛出错误,不会返回到外部
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚开始还是即将完成)
6. Promise 方法
- promise.then() 对应resolve 的成功处理
- promise.catch() 对应 reject 失败的处理
- promise.race() 可以完成并行任务,将多个Promise实例数组包装成一个新的Promise实例。有一个完成就算完成
- promise.all() 当所有Promise实例都resolve后,才会resolve返回一个由所有Promise返回值组成的数组。如果有一个Promise实例reject,就会立即被拒绝,返回拒绝原因。all是所有都成功才算,有一个失败就算失败
- promise.allSettled() 等所有Promise执行完毕后,不管成功还是失败,都会把每个Promise状态信息放到一个数组了里返回
十九、cookie、localStorage、sessionStorage 区别
coookie
- 
- 本身用于浏览器和 server 通讯,后来被‘借用’到本地存储
- 存储最大4kb
- http 请求时会带上cookie发送给服务器,增加请求数据量
- 只能用document.cookie = 'name=xxx' 来修改,太过简陋
- 跨域不能共享
- 不安全,容易被劫持
- 只存在请求头中
 
localStorage 和 sessionStorage
- HTML5专门为存储而设计的,最大存储5M
- 不会随http 请求被发送出去
- API简单易用 setItem、getItem:localStorage 数据会永久存储,除非代码或手动删除
- localStorage.setItem('name','xxx')
- localStorage.getItem('name)
- sessionStorage.setItem('xx','xxx')
- sessionStorage.getItem('xx)
 
- session Storage 数据只会存在当前会话,浏览器关闭则清空
- 一般用LocalStorage 会更多一些
1. 网络请求过程:
- DNS域名解析:域名-> IP地址
- 建立TCP连接 (连接需要三次握手过程,建立TCP连接后才可以进行http请求)
- 浏览器根据IP地址向服务器发起http请求
- 服务器处理http请求,并将网页的html源代码返回给浏览器
2. 浏览器解析源代码过程:
- 
- 根据HTML生成DOM 树
- 根据CSS代码生成CSSOM
- 将DOM Tree 和CSSOM整合形成Render Tree
- 根据Render Tree渲染页面
 
二十一、window.onload 和 DOMContentLoaded 区别
window.addEventListener('load',function(){
    //页面全部资源加载完成才会执行,包括页面中的图片、视频等
})
window.addEventListenenr('DOMContentLoaded',function(){
   // DOM 渲染完即可执行,此时页面中的图片、视频等资源可能还没有加载完
})
强制类型转换:
- 
- 
- 转换为数字:Number(value)、parseInt(value,radix)、parseFloat(value)
- 转换为字符串:String(value)、toString()
- 转换为布尔值:Boolean(value)
 
 
- 
隐式类型转换:if、逻辑运算、==、+拼接字符串
二十三、[10,20,22,221].map(parseInt) 最总输出的值
此题有两个考点:
- 
- 一个是map的简写
- 一个是parseInt(string,radix)
- 解析一个字符串并返回一个整数
- string: 必须。要被解析的字符串
- radix: 可选。要解析的数字基数,该值范围 2~ 36。
- 如果该值省略或者值为0,则 数字以 10 为基数来解析;如果数字是以 “0x” 开头,将以 16 为基数
- 如果该值小于2(不为0) 或者大于 36,则返回NaN
 
 
 
- 一个是map的简写
  题目代码可拆解为:
const res = [10,20,30,221].map((item,index)=>{ return parseInt(item,index) })
console.log(res)//[10,NaN,NaN,25]
执行过程:
parseInt(10, 0) //radix = 0 按10 进制处理, 1 * 10 + 0 parseInt(20,1) // radix =1 非法的 ,不在 2 ~ 36 之内,所以为NaN parseInt(30,2) // radix = 2 按 二进制处理,二进制里没有3,只能有 0和1,所以为NaN parseInt(221,3) // radix = 3 按3 进制处理,计算结果: 2 * 3 ^2+ 2 * 3 + 1 = 18 + 6 + 1 = 25
结果: map返回的是一个数组:[10,NaN,NaN,25]
window.addEventListener('load',function(){
    //页面全部资源加载完成才会执行,包括图片视频等资源
})
window.addEventListener('DOMContentLoaded',function(){
    //DOM 渲染完就执行,此时图片视频等资源可能还没加载完
})
 1、写法不同
2、函数声明有提升,函数表达式不能提升
console.log(sum1(10,20)) //30 console.log(sum2(10,20)) // sum2 is not a function //函数声明,有提升 function sum1(x,y){ return x+y } // 函数表达式,没有提升 var sum2 = function(x,y){ return x+y }
二十六、new Object() 和 Object.create() 区别
- 两个都是创建一个对象
- {} 等同于 new Object(), 原型 Object.prototype
- Object.create({...}) 创建的对象可指定原型,Object.create(null) 创建的对象没有原型
const obj1 ={ a: 10, b:20, sum(){ return this.a + this.b } } const obj2 = new Object({ a: 10, b:20, sum(){ return this.a + this.b } }) //创建一个空对象,并且原型对象指向传入的对象 const obj3 = Object.create({ a: 10, b:20, sum(){ return this.a + this.b } }) //创建一个空对象,并且没有原型 const obj4 = Object.create(null) console.log('obj1:' ,obj1) console.log('obj2:',obj2) console.log('obj3:',obj3) console.log('obj4:' ,obj4)

let i for(i = 1;i<=3; i++){ setTimeout(()=>{ console.log(i) },0) } // 输出 4 4 4
String.prototype.trim = function(){ return this.replace(/^\s+/,'').replace(/\s+$/,'') } let str = ' sdd ' console.log(str.trim())
1.传统方式: location.search 查找
function query (name){ const search = location.search.substr(1) //search: a=10&b=20&c=30 const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`,'i') const res = search.match(reg) if(res === null){ return null } return res[2] }
2. es6 新的API, URLSearchParams
function query(name){ const search = location.search const params = new URLSearchParams(search) //search: ?a=10&b=20&c=30 return params.get(name) //get: 根据参数名称获取对应的值 }
简单的动画: 使用css animate,如淡入淡出效果等使用css animate 更为方便和高效。但对于复杂的动画效果,如需要动态计算位置、尺寸等就没法满足了。
复杂动画:css animate处理不了的复杂动画,只能使用js 和 requsetAnimationFrame来实现
requestAnimationFrame:
- 
- 
- setTimeout 要手动控制频率,而requstAnimationFrame浏览器会自动控制
- 当标签不可见或iframe中隐藏时,requstAnimationFrame会暂停,而setTimeout依然执行
- requstAnimationFrame 优化了动画的渲染过程
 
 
- 
- 计算机使用二进制存储数据
- 整数转换二进制没有误差
- 而小数转化二进制可能会出现误差
- 0.1 转换成二进制是:0.000110011...0011.... (0011无限循环)
- 0.2 转成二进制是:0.00110011 ...0011....(0011无限循环)
- 0.1 + 0.2 是两个近似值进行了计算,导致最后结果是0.30000000000000004
- 所以 0.1 + 0.2 !== 0.3
三十三、HTMLCollection 和 NodeList 的区别
- HTMLCollection 是Element集合,只包含元素节点
- elem.children
- document.getElementByTagName('p')
 
- NodeList 是 Node 集合,包括元素节点、文本节点、注释节点等 
- document.querySelectotAll('p')
- elem.childNodes
 
- HTMlColection 和 NodeList 都不是数组,而是“类数组”。将类数组转成数组的方式 
- Array.from(list)
- Array.prototype.slice.call(list)
- [...list]
 
- 开启严格模式
- 全局开启严格模式
'use strict' //全局开启 
- 某个函数中开启严格模式 
function fn(){ ''use strict' } 
 
- 全局开启严格模式
- 严格模式的特点
- 全局变量必须声明
'use strict' //错误写法 n= 10 // ReferenceError: n is not defined //正确写法 var n = 10 
- 禁止使用with
'use strict' var obj = {x:10,y:20} // 禁止使用width,会报错 with(obj){ console.log(x,y) } 
- 禁止this 指向window
'use strict' function fn(){ console.log(this) //undefined } fn() 
- 函数参数不能重复
- 创建eval 作用域
 
- 全局变量必须声明
- options 请求,是跨域请求之前的预检请求
- 是浏览器自行发起的,无需我们干涉
- 不会影响实际的功能
三十七、什么是JS垃圾回收?垃圾回收的算法?JS 内存泄漏如何检测?场景有哪些?
- 什么是垃圾回收GC:
- JS运行代码时,需要分配内存空间来存储变量和值
- 在JS中,变量的回收主要基于垃圾回收机制
- 全局变量在整个程序执行期间都保持存在,因此不会被垃圾回收
- 局部变量则不同,在函数执行完毕后通常会被垃圾回收、释放占用的内存空间
- 如果不及时清理,就会造成系统卡顿、内存溢出
- 例子:
- 正常情况,一个函数执行完,其中的变量都会被JS垃圾回收
function fn(){ const a =1 const obj = {x:100} console.log(a,obj) } fn() //函数执行完,内部的变量a 和 obj 就会被垃圾回收 
- 但某些情况,变量是销毁不了的,因为可能被再次使用 
function fn(){ const obj = {x:100} window.obj = obj. // 引用到了全局变量,obj不会被销毁 } function getData(){ const data = {} //闭包,data销毁不了 return { get(key){ return data[key] }, set(key,val){ data[key] = val } } } fn() const {get,set} = getData() 注意: 变量销毁不了,不一定就是内存泄漏,有些情况是符合用户预期的就不属于内存泄漏(比如闭包) 
 
- 正常情况,一个函数执行完,其中的变量都会被JS垃圾回收
 
- 基本类型和引用类型的变量垃圾回收区别:
- 基本类型
- 基本类型由于存储在栈中
- 当函数执行完毕,栈中的变量会被销毁
- 因此变量自然会被垃圾回收机制回收
- 基本类型的回收相对简单直接
 
- 引用类型(Object、Function、Array等等) 
- 引用类型的值存储在堆中
- 而栈内存中的变量保存的是指向堆内存中对象的指针
- 当函数执行完毕,变量本身可以被回收
- 但堆中的对象可能被其他变量引用,因此垃圾回收机制需要更复杂的算法来处理,确保没有任何指向某个对象时才会被垃圾回收(具体的算法看下面的垃圾回收算法)
 
 
- 基本类型
- 垃圾回收算法:(函数中的引用类型变量回收算法)
- 1、引用计数 -- 早期的
- 早期的垃圾回收算法,JS引擎目前不使用这种算法
- 引用计数算法:是通过维护一个计数器,来跟踪对象被引用的次数,当一个对象的引用计数变为零时,说明该对象不再被使用,因此该对象可以被回收了
function fn(){ //对象被 a 引用,引用计数加1 let a = {x:10} let b = a // 对象又被 b引用 引用计数再加1 a = 10 // 对象不再被a应用,引用计数减1 b = null // 对象不再被b引用,引用计数减1, // 最总,对象的引用计数为0,对象被回收 } 
- 算法的缺点: 如果对象A引用对象B,而对象B反过来又引用A,则他们的引用计数将永不为零,因此导致内存泄漏
function fn(){ const obj1 = {} const obj2 = {} obj1.a = obj2 obj2.a = obj1 //循环引用,无法回收obj1 和 obj2 在堆中的存储 } 
 
- 2、标记清除 -- 现代 
- 标记-清除是目前许多现代JS引擎(如:V8)采用的主要算法
- 标记-清除该算法是:通过标记所有可达到的对象,然后清除所有未标记的对象,来回收内存
- 优点: 解决了计数无法处理循环引用的问题
- 缺点:执行时会暂停JS代码执行,可能导致性能下降
 
 
- 1、引用计数 -- 早期的
- 什么是内存泄漏:
- 在程序运行过程中分配的内存,由于某种原因未能被释放,无法被垃圾回收
- 内存泄漏通常是指由于代码的错误或不当使用导致的,是非预期的
- 向JS中的闭包,虽然变量不会被回收,但是我们预期的结果,这种情况不属于内存泄漏
 
- JS内存泄漏如何检测(了解):
- 使用Chrome 浏览器DevTools 中的 Performance 来检测内存变化
- 点击"colection garbage" 收集垃圾按钮
- 点击"Pecord"按钮开始记录,然后操作界面
- 操作结束,看分析结果
 
- 内存泄漏的场景:
- 使用未声明的变量,而意外创建了一个全局变量。使这个变量一直留在内存中无法被回收
- 设置了setInterval定时器,而忘记取消它,如果循环函数有对外变量的引用,那么这个变量就会一直留在内存中
- 获取了一个DOM元素的引用,然后这个元素被删除了,由于一直保留了这个元素的引用,所以它也无法被回收
- 不合理的使用闭包(合理的情况不算,合理的闭包引起的变量不被释放是我们想要的结果),本来不想某个变量一直被留在内存当中,却由于代码写的不合理而导致的情况
- vue项目中 组件中有全局变量、函数的引用,但在组件销毁时没有删除的情况
- vue项目中 组件中有定时器,在销毁组件时没有删除的情况
- vue项目中 组建中有全局事件的引用,在组件销毁时没有及时解绑的情况
- vue项目中 组件中使用了自定义事件,销毁组件时没有及时解绑的情况
 
三十八、什么是浏览器事件循环(Event Loop)和 nodejs 事件循环(Event Loop)? 二者有什么区别?
- 浏览器事件循环(Event Loop):
- nodejs 事件循环 (Event Loop):
- 执行同步代码
- 执行微任务(procss.nextTick优先级要高于promise、async/await)
- 按照顺序执行6个类型的宏任务,每个类型的宏任务开始之前都要执行当前的微任务
- timers -- 执行 setTimeout 以及 setInterval 回调
- I/O callbacks -- 处理网络、流、TCP错误回调
- idle,prepare -- 闲置阶段(node 内部使用)
- poll(轮循) -- 执行poll中 的 I/O队列,检查定时器是否到期
- check -- 存放 setImmediate 回调
- close callbacks -- 关闭毁掉,例如 socket.on('close')
 
 
- 浏览器和nodejs 事件循环的区别: 
- 浏览器和nidejs的事件循环 流程基本相同
- nodejs 的宏任务有优先级区分;而浏览器的各种宏任务是按照代码的顺序执行的,没有其他优先级
- nodejs 事件循环中,process.nextTick在微任务的优先级更高(但在新版nodejs中不被推荐使用,推荐使用setImmediate)
 
- vdom并不快,JS直接操作DOM才是最快的
- 但是“为了数据分离,数据驱动试图”的实现,就要有合适的技术方案,不能全部DOM都重建
- vdom 就是目前最合适的技术方案
- 所以vdom并不比DOM操作更快,反而更慢(它做了JS运算),它只是在特定的场景下,无法做到精准DOM修改时的一个更优的选择
- for 更快,for直接在当前函数中执行
- forEach 每次都要创建一个函数,函数需要独立的作用域,会有额外的开销
什么是 JS Bridge:
- 
- JS 无法直接调用native API (app 的 API)
- 需要通过一些特定的方式来调用
- 这些特定的方式 就统称 JS-Bridge,例如微信JSSDK
 
常见的JS Bridge 实现方式:
- 
- 注册全局API
- 客户端为webview 做定制开发,在window增加一些API,供前端调用
- 但这种发式都是同步的
- 例如:增加一个window.getVersion API,前端JS即可调用它来获取app版本号
const v = window.getVersion() 
 
- 劫持 URL Scheme
- 自定义协议的方式(my-app-name://),就叫做 url scheme
- app 监听所有的网络请求,遇到自定义的协议,解析path并返回相应的内容
- 例如 window.location.href = my-app-name://api/getVersion
 
 
- 注册全局API
四十二、是否了解过requestIdleCallback ,和requestAnimationFrame有什么区别?
什么是requestIdleCallback?
- 
- 
- 在JS中, requestIdleCallback 是一个用于执行回调函数的API
- 该回调函数会在浏览器空闲时执行,不会影响页面的性能和用户体验
- 主要作用就是在浏览器的空隙时段执行任务,以确保任务不会阻塞主线成
- 可用用于执行一些不那么紧急的任务,比如:
- 执行复杂的计算
- 后台数据同步
- 图片懒加载
 
 
 
- 
requsetIdleCallback 和requestAnimationFrame区别:
- 
- 
- requestAnimation 主要用于创建高性能动画和动态UI渲染,浏览器渲染每一帧时都会执行,确保了动画尽可能在每一帧都进行渲染
- requstCallback 适用于执行优先级低的任务。它会在浏览器空闲时执行任务,不会阻塞线程
 
 
- 
背景,为何会有300ms延迟:
- 
- 智能手机刚开始流行的前期,浏览器可以点击缩放网页(double tap),这样在手机上就可以浏览pc网页了
- 浏览器为了分辨click还是 “点击缩放”,就强行把click时间延迟300ms触发
 
   解决方案:
- 
- 初期解决方案:用 FastClick 解决这个问题
- 现代浏览器(Chrome 32+ )可以设置 width=device-width 禁止缩放功能 来规避300ms 的问题
<head> <meta name="viewport" content="width=device-width,initial-scale=1.0"></meta> </head>
 
- cookie 
- http请求会默认自动带上cookie,是http协议规的
- cookie 只能存储有限的信息,如用户ID、过期时间等
- cookie有跨域限制,设置cookie的域名不能跨域使用
- 可配合 session 实现登录
- 每次请求都需要将cookie发送给服务器,如果cookie过多,会增加服务器的压力
 
- token 
- http请求中,token是自定义的设置的,可用于任何方式传输(如 head、body 、query-string等)
- token可以存储更多信息,如用户角色、权限等
- token没有跨域限制。
- 可用于JWT 登录
- 服务器只需要验证token的有效性,减轻服务器的压力。token可以设置长时间的有效期,减少频繁获取的次数,提高性能
 
四十五、基于 Session 和 JWT 用户认证的区别和优缺点
目前主流的用户认证方法有基于token和基于session两种方式:
- 
- 基于session的用户认证流程如下:
- 用户输入登录信息,传给服务端
- 服务器验证成功后,创建一个session(用户信息存储在session中),然后将其存储在数据库中。
- 服务器为用户生成一个sessionId,并通过set-cookie,将sessionId设置到客户端的浏览器中
- 在后续的请求中,都会自动带上cookie。服务端会验证sessionId,如果有效则接受请求
 
- 基于token的用户认证:最常用的是 JSON Web Token(JWT)
- 用户输入登录信息,传给服务端
- 服务器验证成功后,给客户端返回一个包含用户身份信息的加密字符串的token ,类似于
{ 
 "username": "www.json.cn",
 "sub": "demo",
 "iat": 1742289537,
 "nbf": 1742289537,
 "exp": 1742375937
 }
 
 
- 基于session的用户认证流程如下:
经过加密、签名等等生成最终的字符串
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ind3dy5qc29uLmNuIiwic3ViIjoiZGVtbyIsImlhdCI6MTc0MjI4OTUzNywibmJmIjoxNzQyMjg5NTM3LCJleHAiOjE3NDIzNzU5Mzd9.F2k-M696Mu2cKhRJJe04nLeEVe6pYhItxX3R_lz0H3I
- 
- 
- 之后的请求都通过 Athorization 将token添加到请求头里
 
 
- 
        
- 
- 服务器只需要验证有效性即可,验证通过则接受请求
 
  session 和 JWT 优缺点:
- 
- session 优点:
- 原理简单,易于学习
- 用户信息存储在服务器上
 
- session 缺点:
- 占用服务端内存,有硬件成本
- 多进程、多服务时,不好同步
- 跨域传递 cookie受限制,需要特殊的配置
 
- JWT 优点:
- 不占用服务器的内存,用户信息都存在客户端
- 多进程、多服务不受涌向
- 不受跨域限制
 
- JWT 缺点:
- JW T的大小通常比传统的session ID大,特别是在包含大量信息十,可能会增加网络传输的负担
 
 
- session 优点:
什么是单点登录(SSO):
- 
- 在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统
- 举个例子,淘宝、天猫都属于阿里旗下的,当用户登录淘宝后,再打开天猫,系统会自动登录了天猫,这种现象就叫做单点登录
 
单点登录的实现:
- 
- 同域名下的单点登录:基于cookie实现
- 如果业务都在同一主域名下,比如 wenku.baidu.com 、tieba.baidu.com
- 直接将cookie的domain设置为主域名 baidu.com
- 同时将cookie 的path 属性设置为跟路径
document.cookie = "domain=baidu.com;path=/" 
 
- 不同域名下的单点登录 
- 如果业务不在同一主域名下,比如aa.com 、bb.com
- 我们可以部署一个认证中心,用于专门处理登录请求的独立的 web服务
- 用户统一在认证中心登录
- 用户第一次登录aa.com网站,会跳转到认证中心进行登录,登录成功后将token 写到cookie中(注意,这个cookie是认证中心的),并将token拼接到目标URL后面
- 用户再次登录bb.com网站时,第一次登录未获取到token,会跳转到认证中心进行登录
- 认证中心能根据cookie知道,该用户已经登录过了,就会直接将token拼接到目标URL后面,回传给目标系统
- 应用系统拿到token之后,还需要向认证中心确认一下token的合法性,防止用户伪造。确认有效后,应用系统会将token存在cookie中。这样下次访问当前系统时,会自动带上token。应用系统验证token 发现已经登录,于是就不会有认证中心什么事了
 
 
- 同域名下的单点登录:基于cookie实现
      
四十七、script 标签的defer 和 async 有什么区别?
- <script src="xxx.js">当解析该标签时,会暂停html解析,并触发js下载、执行。然后再继续html解析
- <script async scr="xxx.js">js下载和html解析可并行,下载完之后暂停html解析,执行js。然后再继续html解析
- <script defer src="xxx.js"> js下载和html解析可并行 。等html解析完成后再执行js
  
- preload 
- 预加载当前页面需要的资源(js、css、图片等等),只是提前下载资源,不执行资源。只有正常引入的时候才会执行
- <link rel="preload">基本使用如下:
<head> <link rel="preload" href="common.css" as="style"> <link rel="preload" href="app.js" as="script"> <link rel="preload" href="image.jpg" as="image"> </head>
- 资源在当前页面使用
- 浏览器会优先加载,在渲染之前同步加载,优先级高。确保资源在页面渲染时可用。
- 它会阻塞页面渲染,直到资源加载完成
- 适用于当前页面的关键资源,如首屏渲染的css、JavaScript文件等
- 如果资源未被使用,可能会造成性能浪费,因此确保资源确实会被使用
 
- prefetch 
- 预加载未来可能需要的资源,只是提前下载资源,不执行资源。
- <link rel="prefetch" >基本用法如下:
<head> <link rel="prefetch" href="common.css" as="style"> <link rel="prefetch" href="app.js" as="script"> <link rel="prefetch" href="image.jpg" as="image"> </head>
- 资源可能在未来的页面使用
- 浏览器将在空闲时加载,异步加载,优先级比较低
- 不会阻塞页面渲染
- 适用于未来可能访问的资源,如 图片、视频后脚本,以提升后续的加载速度
 
- 预加载未来可能需要的资源,只是提前下载资源,不执行资源。
代码例子:
<!DOCTYPE html>
<html> 
    <meta charset="utf-8">
    <head>
        <!---preload 预先加载当前页面需要的资源,只下载不执行-->
        <link rel="preload" href="common.css" as="style">
        <link rel="preload" href="main.js" as="script">
        <!---prefetch 预先在加载未来页面可能需要的资源, 只下载不执行-->
        <link rel="other.js" as="script">
        <!---正常引用 common.css 并执行-->
         <link rel="stylesheet" href="common.css">
    </head>
    <body>
        <h1>hello</h1>
       <!---正常引用 main.js 并执行-->
       <script src="main.js" defer><script>
    </body>
</html>    
一个http请求,第一步就是DNS解析的到IP,然后进行TCP连接。连接成功后再发送http请求
- dns-prefetch 是DNS与获取
- preconnect 是DNS预连接
- 当王爷请求第三方资源时,可以提前进行DNS查询、TCP连接。以减少请求时间
五十一、webSocket 和 http协议有何区别?有何应用场景?
区别:
- 
- 协议名称不同:ws 和 http
- http 每次请求都会开启一个新的连接,处理完请求后关闭。虽然可以使用keep-alive 保持连接,但仍是单向的请求(有浏览器单向发起)
- webSocket 建立连接后是持久连接,客户端和服务端可以实时发送数据。每次交互不需要重新连接
 
适用场景:
- 
- http适用于单次请求响应的场景,如网页浏览、API调用等
- webSocket 适合需要实时性的双向通讯场景,如在线游戏、聊天等
 
网络多标签之前如何通讯,例如打开两个chrome 标签,一个访问列表,一个详情编辑页面,修改了详情页面的标题后,列表页面的标题在不刷新浏览器的情况同步更新
- 
- 方案一: webSocket
- 无跨域限制
- 需要服务端支持,成本高
 
- 方案二:localStorage(优选方案)  
- 只能实现同域下的不同tab页面间通讯
- 再一个页面修改locaStorage 的值,再另一个页面监听storage事件变化
- 代码:
// detail 页面 localStorage.setItem('changeInfo','xxxxx') //list 页面 window.addEventListener('storage',event =>{ console.log('key:', evnet.key) console.log('value:',event.newValue) }) 
 
 
- 方案一: webSocket
- 通过window.postMessage(message,'*') 发送消息,第二个参数可以限制域名
// iframe 向副页面发送消息 window.parent.postMessage('hello','*') //父页面向 iframe 发送消息 window.iframe1.contentWindow.postMessage('你好','*') 通过监听message来接受消息 window.addEventListener('message',event =>{ console.log('origin',event.origin) // 通过origin 可以判断来源合法 console.log('接收消息',event.data) })
- 路由懒加载,单页面应用先保证首页加载
- 服务端渲染SSR。
- 传统的服务端模板,如:ejs、jsp等
- Nuxt.js(Vue 服务端渲染框架)
- Next.js(React 服务端渲染框架)
 
- app预取
- 如果 H5在 APP webview 中展示,可以使用app预取资源
- 例如,在列表也中,app中预先获取好数据(一般是标题、首页文本)
- 进入详情页时,H5直接用app预先取的数据进行渲染
- 可能会造成浪费,比如用户未点击进入详情页
 
- 分页
 - 首屏内容尽量少,其他内容上滑时加载
 
- 图片懒加载 lazyLoad
- 离线包 hybird
- 提前将html css js 等资源下载到app 内
- 当在app 内打开页面时,webview 使用 file://协议加载本地资源,然后再ajax请求资源,再渲染
 
- 性能优化也需要配合体验,如股价瓶,loading 动画等
- 这个题目首先沟通设计是否不合理?
- 如果就是普通的新闻列表,明显不合理。可以提出采用分也来实现
- 如果该题目就是要一次返回这么多,可以考虑用虚拟列表实现
- 只渲染可视区域DOM
- 其他隐藏区域不渲染,只用一个<div> 撑开
- 监听容器滚动,随时创建和销毁DOM
 
    
- 虚拟列表实现比较复杂,可以用第三方库
- vue-virtual-scroll-list
- react-virtualized      
 
- 通过工具分析性能参数
- 方法一:chrome devtools
- performance 可以检测如下性能指标
- FP:首次绘制像素到屏幕上的时间
- FCP:首次绘制来自DOM的内容的时间
- DCL:即DOMContentLoaded 触发事件,DOM 全部解析并渲染完成
- LCP:可视区域中追打的内容元素呈现到屏幕上的时间
- L:即window.onload 触发事件,页面内容包括图片全部加载完成
 
- 如果指标是紧挨着或者间距不大,说明渲染没有问题
- 可以看查看各个资源的加载时间,来确定是否是加载慢
 
- performance 可以检测如下性能指标
- 方法二:使用lighthose第三方测评工具
- 
# 安装 npm i lighthouse -g # 检测一个网页,检测完毕之后会打开一个报告网页 lighthouse https://imooc.com/ --view --preset=desktop # 或者 mobile 
- 
测试完成之后,lighthouse 给出测试报告,并且会给出一些优化建议 
 
- 
 
- 方法一:chrome devtools
- 识别问题:加载慢?渲染慢?
- 根据指标识别是加载慢?还是渲染慢?
- 分析清楚很重要,因为前后端负责不同
 
- 解决问题
- 解决加载慢
- 优化服务端接口
- 使用CDN
- 压缩文件
- 拆包,异步加载
 
- 解决渲染慢
- 解决首屏慢的问题
 
 
- 解决加载慢
- 以下代码会输出什么?
function Foo() { Foo.a = function() { console.log(1) } this.a = function() { console.log(2) } } Foo.prototype.a = function() {console.log(3)} Foo.a = function() { console.log(4) } Foo.a() let obj = new Foo() obj.a() Foo.a() 
- 分析:
- 这种代码题,我们要学会方法,不是一上来就是从头到尾每一行代码都读一遍
- 我们要把自己想象成JS引擎,你不是读代码,而是在执行代码
- 定义的函数如果不执行,就不要去看它里面的内容,在执行函数的时候再去看里面的内容。
- 下面是我做这个题的思路:
- 首先定义了一个函数 Foo,至于函数里面的内容暂时不管
- 函数Foo 的原型上定义了一个方法 a
- 函数Foo 本身上定义了一个方法 a
- Foo.a() 执行就是Foo本身上的方法,即 function(){console.log(4)}, 所以最总输出4
- 接下来let obj = new Foo() , 这行代码执行完会返回一个实例对象{a: fn},以及重新定义了Foo.a 方法(覆盖了之前的Foo.a)
- obj.a() 就是实力对象上的方法a(如果没找到再去原型上找),即function a (){console.log(2)} 所以最后打印: 2
- Foo.a(); 此时的方法已经被修改了,即 function (){console.log(1)}。所以最后打印: 1
 
 
- 结果:4 2 1
- 题目:以下代码,执行会输出什么
Promise.resolve().then(() =>{ console.log(0) return Promise.resolve(4) }).then((res) => { console.log(res) }) Promise.resolve().then(() => { console.log(1) }).then(() => { console.log(2) }).then(() =>{ console.log(3) }).then(() =>{ console.log(5) }).then(() => { console.log(6) }) 
- 
知识点 - then 交替执行
- 如果有多个 fulfilled 状态 的promise实例,同时执行then 链式调用,then会交替调用,这是编译器的优化,防止一个promise持续占据事件
- 举个例子,如下代码:
Promise.resolve().then(() => { console.log(1) }).then(() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(4) }) Promise.resolve().then(() => { console.log(10) }).then(() => { console.log(20) }).then(() => { console.log(30) }).then(() => { console.log(40) }) Promise.resolve().then(() => { console.log(100) }).then(() => { console.log(200) }).then(() => { console.log(300) }).then(() => { console.log(400) }) 最总输出:1 10 100 2 20 200 3 30 300 4 40 400 
 
- 
then 返回一个promise 对象时“慢两拍” - 
当then返回promise对象 时,可以认为是多出一个 promise 实例 
- 
then 返回 promise 实例和 直接执行 Promise.resolve()不一样,它需要等待两个过程 - 
promise 状态由 pending 变为 fufilled 
- 
then 函数挂载到微任务队列microTaskQueue中 
- 所以,它会“慢两拍”
 
- 
- 例子:
Promise.resolve().then(() => { console.log(1) return Promise.resolve(100) // 相当于多处一个 promise 实例,并且会慢两拍 }).then(res => { console.log(res) }).then(() => { console.log(200) }).then(() => { console.log(300) }).then(() => { console.log(400) }) Promise.resolve().then(() => { console.log(10) }).then(() => { console.log(20) }).then(() => { console.log(30) }).then(() => { console.log(40) }) - 
第一个 Promise 首先输出1,遇到了 return Promise.resolve(100),这里会慢两拍在执行 
- 交替执行第二个Promise,输出 10
- 本应交替执行第一个Promise ,但由于 上一个返回了一个新的Promise 实例,所以需要等两拍后在执行
- 所以继续执行第二个Promise,输出 20
- 所以继续执行第二个Promise,输出 30
- 两拍已过,开始交替执行第一个Promise,输出 100
- 交替执行第二个Promise, 输出 40,此时第二个Promise已经执行完
- 交替执行第一个Promise ,输出 200
- 由于第二个Promise已经执行完,所以继续执行第一个Promise,连续输出300 和 400
 
- 
- 最总结果为: 1,10,20, 30,100,40,200,300,400
 
- 
 
- then 交替执行
- 解析该题目:
- 首先执行第一个Promise.then,输出 0
- 交替执行第二个Promise,输出1
- 此时本应该交替执行第一个Promise接下来的then,但是由于返回了Promise.resolve(4),相当于一个新的Promise 实例,需要慢两拍
- 所以继续执行第二个Promise 接下来的then,输出 2
- 继续执行第二个Promise 接下来的then,输出 3
- 两拍已过,交替执行第一个Promise,输出 4
- 交替执行第二个Promise,输出 5
- 第一个Promise已经执行完,所以继续执行第二个Promise,输出6
 
- 输出结果: 1,2,3,4,5,6
- 题目:以下代码,运行会输出什么
let a = { n: 1 } let b = a a.x = a = { n: 2 } console.log(a.x) console.log(b.x)
- 
知识点: - 
值类型 和 引用类型,堆栈模型 - 值类型存储在栈中
- 
let a = 100 let b = a
 
- 
 
- 值类型存储在栈中
 
- 
              
- 
- 
- 引用类型储存在堆中,栈中存储的是指向对象的引用
- 
let a = { n: 1 } let b = a![]() 
 
- 
 
- 引用类型储存在堆中,栈中存储的是指向对象的引用
- 
连续赋值时倒叙执行 - 
let n1, n2 n1 = n2 = 100相当于 let n1, n2 n2 = 100 n1 = n2 
 
- 
- 
连续赋值时,.优先级更高 - 
例子: let a = {} a.x = 100a.x = 100 由于.比赋值优先级高,所以先会在a对象初始化一个x属性,然后再进行赋值,即: a.x = undefiend //先出初始化属性 a.x = 100 //再赋值 
- 
例子2: let a = { n: 1 } a.x = a = { n: 2}
 console.log(a.x) //undefined- 首先定义一个变量a,值为引用类型{ n: 1}, 堆栈模型如下:
 
 
- 
 
- 
              
- 
- 
- 
- 
a.x = a = { n:2 },由于 . 优先级高于赋值,所以先会在a对象初始化一个x属性 即{n: 1, x: undefined}。 
 
- 
 
- 
 
- 
              
- 
- 
- 
- 
连续赋值倒叙执行,所以先执行 a = { n: 2 }, 此时a 对象 变成了 {n: 2};a.x = a 即把{n: 1, x: undefined} 中的 x 指向了 {n: 2} 
 
- 
 
- 
 
- 
               
 
- 
- 
- 
- 
最后变量a为:{n: 2}, {n:1,x: {n:2}}没有再引用,最总会被垃圾回收。 
 
- 
 
- 
 
- 
               
- 
- 
- 
- a 最总为 {n: 2}, 所以 a.x 为 undefined
 
 
- 
 
- 
- 解题思路:
- let a = { n: 1 } ,定义变量a 指向 {n: 1}
 
      
- 
- let b = a ; b也指向 {n: 1}
 
      
- 
- a.x = a = { n: 2 }; .优先级高于赋值,所以先把a 对象 初始化属性x。即:{n:1, x:undefined}
 
        
- 
- 接下来是赋值,连续赋值是倒叙执行,所以先执行 a = {n: 2}。相当于a 重现赋值了一个新对象,不再是之前的 {n: 1, x: undefined} 了
 
        
- 
- 继续 a.x = a 的赋值,即 a.x = {n: 2}, 这里的a.x 指的是{ n: 1, x } 这个对象的x。
- 其实如果把 a.x = a = {n: 2} 换成 b.x = a = {n: 2} 更好理解一些
- 或者把连续的赋值拆分为: a.x = {n: 2}; a = {n: 2}; 优先级高的先执行(a.x的优先级高,所以先执行 a.x = {n: 2})
 
      
- 
- 最后 a = {n: 2}; b={n: 1, x: {n: 2}}
- 所以: a.x 为 undefined ; b.x 为 {n: 2}
 
- 结果:undefiend 、 {n: 2}
- 题目:执行以下代码,会输出什么
//example1 let a = {}, b = '123', c = 123; a[b] = 'b' a[c] = 'c' console.log(a[b]) //example2 let a = {}, b = Symbol('123'), c = Symbol('123') a[b] = 'b' a[c] = 'c' console.log(a[b]) //example3 let a = {}, b = { key: '123' }, c = { key: '456' } a[b] = 'b' a[c] = 'c' console.log(a[b]) 
- 
知识点: - 对象的键名只能是 字符串 和 Symbol 类型
- 其他类型的键名 会被转换成字符串类型
- 对象转字符串默认会调用 toString 方法, 结果为:'[object Object]'
- 
const obj = {} obj[0] = 100 const x = { s: 'abc' } obj[x] = 200 const y = Symbol() obj[y] = 300 const z = true obj[z] = 400 obj对象的属性为: 
 
         
  
- 解题:
- 第一个例子中,数值类型的123 会转成字符串'123',所以最后a = {123: 'c'}。所以a[b] 输出 'c'
- 第二个例子,Symbol() 作为对象的key 时 是不会重复的,即不会被覆盖。a= {Symbol('123'): 'b', Symbol('123'): 'c'}。所以a[b] 输出 'b'
- 第三个例子对象作为key,会调用toString()方法
- ({key: '123'}) .toString() === '[object Object]' ; ({key: '456'}).toString() === '[object Object]'
- 所以最总 a = {'[object Object]': 'c'} 。
- 所以a[b] = 'c'
 
 
- 输出结果:c b c
- 扩展: 
- 如果改题把对象换成Map,就不会出现覆盖了
- Map 的key 可以是任意类型,他不需要进行类型转换。只有类型和值完全相等时才会进行覆盖
 
六十一、
六十二、
六十三、
六十四、
六十五、
六十六、
六十七、
六十八、
六十九、
七十、
 
                    
                     
                    
                 
                    
                

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号