百度t5面试题
CSS部分:
- 重绘和回流
- 水平垂直居中
- flex布局
- position
- css3动画
- BFC&清除浮动
- css预处理器
- 盒模型
- css选择器
- 响应式布局
- 实现三角形
- 移动端适配rem
HTML5部分:
- 新特性
- 本地存储
- 语义化
主流框架:
- Vue:双向数据绑定实现、vuex、路由
- React:虚拟dom、生命周期、redux
- flux思想
JavaScript:
- 内存泄漏
- GC原理
- 原因
- 闭包
- 循环引用
- dom事件
- 全局变量
- ES6语法
- 箭头函数
- let & const ---块级作用域 & 暂时性死区
- set & map
- promise 实现原理 --- 自己写一个promise
- generator函数
- async/await
- 原型链
- 模块化
- 事件代理
- 冒泡流
- 捕获流
- 事件循环机制
- setTimeout 和 setInterval
- 任何队列
- 微任务 && 宏任务
- 跨域
- 同源策略
- jsonp实现原理
- 其他方法
- CORS:Access-Control-Allow-Origin
- document.domain + iframe
- window.name + iframe
- window.postMessage
- nodejs转发
- 继承
- 寄生组合式继承
- call & apply & bind
- 函数节流&函数防抖
- 基本数据类型及对应内存(堆、栈) ---数据类型 ---判断数组的方法 ---判断对象的方法
- this指针
- 箭头函数
- 隐式绑定
- call & apply 修改this
- new 过程绑定
- new一个对象
- 深浅拷贝 --- 深度拷贝所有层级 JSON.parse(JSON.stringify(obj))
- axios&fetch
- 异步并发实现
- typeof & instanceof
- 区别
- instanceof内部实现
- 函数式编程
- 高阶函数
- 函数科里化 & 偏函数
网络相关
- 浏览器解析全过程(输入url -> 页面展现)
- 浏览器缓存
- cookie & session -区别
- http & https
- 报文格式及传输方式
- http头
- 状态码
- keep-alive
- https过程&加密算法
- tcp三次握手 & 四次握手
- get & post
- 正向代理 & 反向代理
- DNS解析
- 跨标签网页通讯
- postmessage
- localStorage & sessionStorage
web安全
xss & csrf
常见算法
- 快速排序 & 冒泡排序
- 二分查找
性能优化
- defer & async
- css加载
- css-sprite
- 缓存静态文件 --- localStorage
----------以下为网上找的答案----------
css部分
1、重绘和回流
什么是回流?
当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就成为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候,这时候是一定会发生回流的,因为要构建 render tree。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。
什么是重绘?
当 render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color。则称为重绘。
区别:
回流必将引起重绘,而重绘则不一定引起回流。比如:只有颜色改变的时候就只会发生重绘而不会引起回流。
当页面布局和几何属性改变时就需要回流
比如:添加或者删除可见的DOM元素,元素位置改变,元素尺寸改变 --- 边距、填充、边框、宽度和高度,内容改变
扩展:
浏览器的帮忙
我们能得知回流比重绘的代价要更高,回流的花销跟 render tree有多少节点需要重新构建有关系。
因为这些机制的存在,所以浏览器会帮助我们优化这些操作,浏览器会维护一个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会 flush 队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。
自己的优化:
但是靠浏览器不如靠自己,我们可以改变一些写法减少回流和重绘。
比如改变样式的时候,不会去改变他们每个的样式,而是直接改变 className ,可以采用 cssText 累加的方法。
还有添加节点的时候,比如要添加一个 div ,里面有三个子元素 p ,如果添加 div 后再往里面添加三次 p ,这样就触发很多次回流和重绘,我们可以用 cloneNode 来避免,一次把要添加的第一克隆好再 append 就好了。
2、水平垂直居中
当居中元素定宽高时
<style> /* 公共代码 */ .parent{ border: 1px solid red; width: 300px; height: 300px; } .box{ background-color: green; width: 100px; height: 100px; } /* 公共代码 */ </style> <body> <div class="parent"> <div class="box">123123</div> </div> </body>
absolute + 负margin
绝对定位的百分比是相对于父元素的宽高,通过这个特性可以让子元素居中显示,但绝对定位是基于子元素的左上角,期望的效果是子元素的中心居中显示。为了修正这个问题,可以借助外边距的负值,负的外边距可以让元素向相反方向定位,通过指定子元素的外边距为子元素宽度一半的负值,就可以让子元素居中了
.parent{ position: relative; } .box{ position: absolute; top: 50%; left: 50%; margin-left: -50px; margin-top: -50px; }
缺点是需要知道子元素的宽高
absolute + margin auto
这种方式也要求居中元素的宽高必须固定
这种方式通过设置各个方向的距离都是0,再将margin设置为auto,就可以在各个方向上居中了
.parent{ position: relative; } .box{ position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto; }
absolute + calc
这种方式也要求居中元素的宽高必须固定
.parent{ position: relative; } .box{ position: absolute; top: calc(50% - 50px); left: calc(50% - 50px); }
当居中元素不定宽高时
absolute + transform
.parent{ position: relative; } .box{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
css-table
css新增的table属性,可以让我们把普通元素变为table元素的实现效果,通过这个特性也可以实现水平垂直居中
.parent{ display: table-cell; text-align: center; vertical-align: middle; } .box{ display: inline-block; }
flex
.parent{ display: flex; justify-content: center; align-items: center; }
grid
css新出的网格布局,目前兼容性不太好
.parent{ display: grid; } .box{ align-self: center; justify-self: center; }
3、flex布局
https://www.cnblogs.com/qcloud1001/p/9848619.html
4、position
5、CSS3动画
6、BFC & 清除浮动
利用 clear: both 清除浮动
.clearfix::after{ content: ''; display: block; height: 0; clear: both; font-size: 0; visibility: hidden; } .clearfix{ /* 触发 hasLayout,这又涉及到IE浏览器hack的知识 */ zoom: 1; }
BFC:
父元素形成BFC可以解决父容器内部子元素浮动引起的坍塌问题。
BFC元素特性就是不论内部怎样,都不会影响外部布局。
如何触发BFC?
- float:值不为none
- 绝对定位元素:position(absolute,fixed)
- display :inline-block,table-cell,table-caption
- overflow:(hidden,auto,scroll)
常见作用:清浮动、阻止外边距折叠
7、盒模型
- 基本概念:标准盒模型 和 IE模型(怪异盒模型)
- CSS如何设置这两种模型
- JS如何设置获取盒模型对应的宽和高
- 实例题(根据盒模型解释边距重叠)
- BFC(边距重叠解决方案)
基本概念
盒模型由里向外 content、padding、border、margin
两种标准:一个是标准盒模型,一个是IE模型
在标准盒模型中,盒模型和宽高只是内容(content)的宽高
而在IE模型中,盒模型的宽高是 内容(content)+ 填充(padding)+ 边框(border) 的总宽高
CSS如何设置两种模型
/* 标准模型 */ box-sizing: content-box; /* IE模型 */ box-sizing: border-box;
JS获取宽高
1、dom.style.width / height
这种方式只能取到dom元素内联样式所设置的宽高,也就是说如果该节点的样式是在 style 标签中或外联的CSS文件中设置的话,通过这种方法是获取不到 dom 的宽高的。
2、dom.currentStyle.width / height
这种方式获取的是在页面渲染完成后的结果,就是说不管是哪种方式设置的样式,都能获取到。
但这种方式只有IE浏览器支持
3、window.getComputedStyle(dom).width / height
这种方式的原理和2是一样的,这个可以兼容更多的浏览器,通用性好一些
4、dom.getBoundingClientRect().width / height
这种方式是根据元素在视窗中的绝对位置来获取宽高的
5、dom.offsetWidth / offsetHeight
最常用的,也是兼容性最好的
8、css选择器
优先级: 内联 > ID选择器 > 类选择器 > 标签选择器 > 通配符
!important 例外规则:当在一个样式声明中使用一个 !important 规则时,此声明将覆盖任何其他声明。虽然从技术上将,!important 与优先级无关,但它与最终的结果直接相关。使用 !important 是一个坏习惯,应该尽量避免,因为这破坏了样式表中固有的级联规则,使得调试找bug变得更加困难了。当两条互相冲突的带有 !important 规则的声明被应用到相同的元素上时,拥有更大优先级的声明将会被采用。
- 内联样式优先级 1000
- id选择器优先级 100
- 类选择器优先级 10
- 元素选择器优先级 1
- 通配选择器优先级 0
- 继承的样式优先级 没有
注意:
1、如果是两种相同优先级为同一个元素,同一个属性设置的话,哪个写在代码靠后,最终就按照哪个的样式
2、交集选择器的优先级:所有优先级加起来运算,然后比较
3、并集的话,就是是各算各的
9、响应式布局
使用 @media 的三种方式
第一:直接在CSS文件中使用
@media 类型 and (条件1) and (条件2){ css样式 } @media screen and (max-width:980px){ body{ background-color: red; } }
第二:使用@import导入
@import url("css/moxie.css") all and (max-width:980px)
第三,也是最常用的:使用link链接,media属性用于设置查询方式
<link rel="stylesheet" type="text/css" href="css/moxie.css" media="all and (max-width=980px)" />
10、实现三角形
- 首先,创建一个class名为box的div
.box{ width: 20px; height: 20px; background-color: blue; }
然后,给box添加四条边框样式,边框设置的宽一点,四条边框的颜色给不一样的值,方便我们查看,
.box{ width: 20px; height: 20px; background-color: blue; border-top: 50px solid red; border-right: 50px solid yellow; border-bottom: 50px solid green; border-left: 50px solid pink; }
效果如下
然后,把box的宽高设置为0,去掉蓝色的背景色,就只剩下四周的边框的
假设我们想要一个向上的三角形,只需要把box的上左右方向的边框样色设置为透明,就只剩下下边框了。
.box{ width: 0; height: 0; border-top: 50px solid transparent; border-right: 50px solid transparent; border-bottom: 50px solid green; border-left: 50px solid transparent; }
11、移动端适配rem
// rem适配 (function(){ var styleN = document.createElement('style') var width = document.documentElement.clientWidth / 75 styleN.innerHTML = `html{font-size:${width}px!important}` document.head.appendChild(styleN) })()
上述代码的作用就是把手机屏幕分成 75 份,每份设置为 font-size,就是每份代表 1rem
如果设计师给的图是 750px;那么1rem就是 750 / 75 = 10px;也就是 1rem代表 10px
JavaScript部分
1、GC机制(垃圾回收机制)
固定时间间隔,周期性的释放不再使用的变量所占内存。全局变量的生命周期直至浏览器卸载页面才会结束,局部变量只在函数的执行过程中存在。
垃圾回收有两个方法:1.标记清除 2.引用清除。各大浏览器常用的是标记清除
标记清除:当变量进入环境时,将变量标记 “进入环境”,当变量离开环境时,标记为:“离开环境”。某一个时刻,垃圾回收器会过滤掉环境中的变量,以及被环境变量引用的变量,剩下的就是被视为准备回收的变量。
- 1、垃圾回收器,在运行的时候回给存储在内存中的所有变量都加上标记
- 2、去掉环境中的变量以及被环境中的变量引用的变量标记
- 3、再被加上标记的会被视为准备删除的变量
- 4、垃圾回收器完成内存的清除工作,销毁那些带着标记的值并回收它们所占用的内存空间
引用计数:跟踪记录每个值被引用的次数
基本原理:就是变量的引用次数,被引用一次则 +1,当这个引用计数为0时,被视为准备回收的对象
工作流程:
- 1、声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值得引用次数就是1
- 2、同一个值又被赋值给另一个变量,这个引用类型值的引用次数 +1
- 3、当包含这个引用类型值得变量又被赋值成另一个值了,那么这个引用类型值得引用次数 -1
- 4、当引用次数变成 0 时,说明没办法访问这个值了
- 5、当垃圾收集器下一次运行时,它就会释放引用次数是 0 的值所占的内存
但是循环引用的时候就会不释放掉内存。循环引用就是对象A中包含另一个指向对象B的指针,B中也包含一个指向A的引用。
解决:手动断开它们直接的连接
function fna(){ var a = 0 } var a = fna() function fnb(){ var b = 0 return b } var bf = fnb()
a 在 fna 执行之后跟全局已经没有关联的,就会被回收,b因为return给了全局变量 bf,跟全局还关联着,就不会被回收。
JavaScript还能手动解除引用以便回收,比如上面的 b 被 bf 关联着,那么令 bf = null ;就能解除 b 的引用,在下次垃圾回收的时候,b 就能被回收
内存管理
1、上面时候触发垃圾回收?
垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。
2、合理的GC方案:(1)遍历所有可访问的对象; (2)回收已不可访问的对象。
3、GC缺陷:停止响应其他操作
4、GC优化策略:(1)分代回收(Generation GC);(2)增量GC
开发过程中遇到的内存泄漏情况
1、定义和用法
内存泄漏是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。C# 和 Java 等语言采用了自动垃圾回收方法管理内存,几乎不会发生内存泄漏。我们知道,浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄漏。
由于每次垃圾回收开销都相对较大,并且由于机制的一些不完善的地方,可能会导致内存泄漏。我们可以利用一些方法减少垃圾回收,并且尽量避免循环引用的问题。
例如在对象结束使用后,令 obj = null 。这样利于解除循环引用,使得无用变量及时被回收。
再如,js中开辟空间的操作有 new() ,[ ], { },function(){...} 。在创建新对象的时候要尽量考虑增大对象的复用性。
2、内存泄漏的几种情况
虽然有垃圾回收机制,但是,我们编写代码操作不当还是会造成内存泄漏
- 意外的全局变量引起的内存泄漏
- 原因:全局变量,不会被回收
- 解决:使用严格模式避免
- 闭包引起的内存泄漏
- 原因:闭包可以维持函数内局部变量,使其得不到释放。
- 解决:将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对DOM的引用
没有清理的DOM元素应用
- 原因:虽然别的地方删除了,但是对象中还存在对DOM的引用
- 解决:手动删除
- 被遗忘的定时器或者回调
- 原因:定时器中有DOM的引用,即使DOM删除了,但是定时器还在,所以内存中还是有这个DOM
- 解决:手动删除定时器和DOM
- 子元素存在引用引起的内存泄漏
- 原因:div中的 ul li 得到这个div,会间接引用某个得到的 li,那么此时因为 div 间接引用 li,即使 li 被清空,也还是在内存中,并且只要 li 不被删除,它的父元素都不会被删除。
- 解决:手动删除清空
2、块级作用域 & 暂时性死区
es6块级作用域 {} for if while
块级作用域:只跟 let 或者 const 声明的变量起作用;对于 var 没有块级作用域的限制
暂时性死区
用var 声明过的变量,会有变量提升,给一个默认值是 undefined
用 let 和 const 声明的变量,不会进行变量提升,但是他也会对里面带 let 和 const 声明的变量进行预览,这时,浏览器不会在往上级欧耶耶查找相应变量
var a =13; function f1() { console.log(b); // undefined console.log(a); // Error let a = 15; var b = 10; console.log(b,a); } f1();//报错函数无法执行
因为暂时性死区的影响,使 console.log(a) 无法调用上级作用域,对 let 和 const 进行预览,但是 let 和 const 没有变量提升,而且按代码执行顺序,也调用不到下面 let a 的变量,所以 a 没有定义过,报错,暂时性死区会造成函数无法执行
3、Promise实现原理
标准:
- 一个promise的当前状态只能是 pending、resolved、rejected 三种之一。状态改变只能是 pending到 resolved 或者 pending 到 rejected。状态改变不可逆
- promise 的then方法接收两个可选参数,表示该 promise 状态改变时的回调 promise.then(onResolved, onRejected) 。then方法返回一个 promise,then方法可以被同一个 promise 调用多次
其实,promise就是三个状态。利用观察者模式的编程思想,只需要通过待定书写方式注册对应状态的事件处理函数,然后更新状态,调用注册过的处理函数即可。
这个特定方式就是 then,done,fail,always 等方法,更新状态就是 resolve、reject 方法
实现代码
/** * Promise 类实现原理 * 构造函数传入一个 function,有两个参数,resolve:成功回调;reject:失败回调 * state:状态存储【PENDING-进行中,RESOLVED-成功,REJECTED-失败】 * doneList:成功处理函数列表 * failList:失败处理函数列表 * done:注册成功处理函数 * fail:注册失败处理函数 * then:同时注册成功和失败处理函数 * always:一个处理函数注册到成功和失败 * resolve:更新state为 RESOLVED,并且执行成功处理队列 * reject:更新state为 REJECTED,并且执行失败处理队列 */ class PromiseNew{ constructor(fn){ this.state = 'PENDING' this.doneList = [] this.failList = [] fn(this.resolve.bind(this), this.reject.bind(this)) } // 注册成功处理函数 done(handle){ if(typeof handle === 'function'){ this.doneList.push(handle) }else{ throw new Error('缺少回调函数') } return this } // 注册失败处理函数 fail(handle){ if(typeof handle === 'function'){ this.doneList.push(handle) }else{ throw new Error('缺少回调函数') } return this } // 同时注册成功和失败处理函数 then(success, fail){ this.done(success || function(){}).fail(fail || function(){}) return this } // 一个处理函数注册到成功和失败 always(handle){ this.done(handle || function(){}).fail(handle || function(){}) return this } // 更新state为 RESOLVED,并且执行成功处理队列 resolve(){ this.state = 'RESOLVED' let args = Array.prototype.slice.call(arguments) setTimeout(() => { this.doneList.forEach((item, key, arr) => { item.apply(null, args) arr.shift() }) }, 2000); } // 更新state为 REJECTED,并且执行失败处理队列 reject(){ this.state = 'REJECTED' let args = Array.prototype.slice.call(arguments) setTimeout(() => { this.failList.forEach((item, key, arr) => { item.apply(null, args) arr.shift() }) }, 2000); } } // 使用 new PromiseNew((resolve, reject) => { resolve('hello world') }).done(res => { console.log(res) }).fail(res => { console.log(res) })
4、原型链
所有引用类型(函数,数组,对象)都拥有 __proto__ 属性(隐式原型)
所有函数拥有 prototype 属性(显示原型)(仅限函数)
原型对象:拥有 prototype 属性的对象,在定义函数时就被创建
当调用某种方法或查找某种属性时,首先会在自身调用和查找,如果自身并没有该属性或方法,则会去它的 __proto__ 属性中调用查找,也就是它构造函数的 prototype 中调用查找。
5、new一个对象的过程
首先了解 new 做了什么,使用 new 关键字调用函数(new ClassA(...))的具体步骤:
1、创建一个新对象:
var obj = {}
2、设置新对象的 constructor 属性为构造函数的名称,设置新对象的 __proto__ 属性指向构造函数的 prototype 对象
obj.__proto__ = ClassA.prototype
3、使用新对象调用函数,函数中的this被指向新实例对象
ClassA.call(obj) // {}.构造函数()
4、将初始化完毕的新对象地址,保存到等号左边的变量中
注意:若构造函数中返回 this 或返回值是基本类型(number、string、boolean、null、undefined)的值,则返回新实例对象;若返回值是引用类型的值,则实际返回值为这个引用类型。
用原生JS实现 new 方法
// 通过分析原生的 new 方法可以看出,在 new 一个函数的时候, // 会返回一个 func 同时在这个 func 里面会返回一个对象 Object // 这个对象包含父类 func 的属性以及隐藏的 __proto__ function New(f){ // 返回一个func return function(){ var o = {"__proto__": f.prototype} f.apply(o, arguments) // 继承父类的属性 return o // 返回一个Object } }
首先写一个父类方法(包含参数 name,age)
function Person(name, age){ this.name = name this.age = age } // new一个Person的实例 p1 做研究对比 var p1 = new Person('Dylan', 25) // 此时p1包含name、age属性,同时p1的 __proto__ 指向Person的prototype p1.name // Dylan p1.age // 25
通过自定义 New 方法创建一个实例对象p2
var p2 = New(Person)("Joker", 22) p2.name // Joker p2.age // 22
此时p2 instanceof Person 返回的是 true
Person.prototype.gender = "male" p1.gender // male p2.gender // male
6、跨域
同源策略
同源策略(协议、域名、端口号(默认80))是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。所以在 xyz.com 下的js脚本采用ajax读取 abc.com 里面的文件数据是会被拒绝的。
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
方案一:JSONP
JSONP的基本思想:通过<script>的 src,向服务器请求数据,且这不受同源策略限制(img和iframe的src也是如此);然后服务器将相应的数据放入指定的函数回调名中,返回给前端
下面使用node简单实现JSONP
<script> function test(data){ console.log(data) } </script> <script src="http://127.0.0.1:3000/jsonp?callback=test"></script>
// 后端代码 const http = require('http') const querystring = require('querystring') const server = http.createServer((req, res)=>{ const { method, url } = req console.log(method, url) // GET /jsonp?callback=test const qs = querystring.parse(url.split('?')[1]) console.log(qs) // { callback: 'test' } var data = { name: 'Dylan', age: 25 } // 如果是GET请求(jsonp只能发get请求)并且有callback参数 if(method === 'GET' && qs.callback){ data = JSON.stringify(data) var callback = `test(${data})` console.log(callback) // test({"name":"Dylan","age":25}) res.end(callback) }else{ res.end(data) } }) server.listen(3000)
跑起后端代码,然后将前端html在浏览器中打开即可看到效果(跨域成功!)
改进前端代码:
// 动态创建script标签,并请求 function addScriptTag(src){ var script = document.createElement('script') script.setAttribute('type', 'text/javascript') script.src = src document.body.appendChild(script) } // 在onload后,跨域请求 window.onload = function(){ addScriptTag('http://127.0.0.1:3000/jsonp?callback=money') } // 回调的方法,必须是全局方法,不然会报错。因为是通过script的src请求的,请求成功后立即执行 function money(data){ console.log(data) }
方案二:后端设置响应头
const http = require('http') const server = http.createServer((req, res)=>{ const { method, url } = req console.log(method, url) var data = { name: 'Dylan', age: 25 } if(method === 'GET' && url === '/user'){ data = JSON.stringify(data) // 允许访问的源 *代表所有 res.setHeader("Access-Control-Allow-Origin", "*") // 允许的方法 res.setHeader('Access-Control-Allow-Methods', "POST, GET, OPTIONS, DELETE") // 允许携带cookie res.setHeader('Access-Control-Allow-Credientials') res.end(data) }else{ res.end('Error') } }) server.listen(3000)
方案三:document.domin + iframe
document.domain 用来得到当前网页的域名
比如在百度(https:www.baidu.com)页面控制台中输入:
alert(document.domain) // www.baidu.com
我们也可以给 document.domain 属性赋值,不过是有限制的,你只能赋成当前域名或者一级域名,比如:
alert(document.domain = "baidu.com") // baidu.com alert(document.domain = "www.baidu.com") // www.baidu.com
上面的赋值都是成功的,因为 www.baidu.com 是当前的域名,而 baidu.com 是一级域名
但是下面的赋值就会出来 “参数无效” 的错误
alert(document.domain = "qq.com") alert(document.domain = "www.qq.com")
因为qq.com 与 baidu.com 的一级域名不相同,所以会报错
这是为了防止有人恶意修改 document.domain 来实现跨域偷取数据
利用 document.domain 实现跨域
前提条件:这两个域名必须属于同一个一级域名。而且所用的协议,端口都要一致,否则无法利用 document.domain 进行跨域。
JavaScript处于安全性的考虑,禁止两个或者多个不同域的页面进行相互操作。而相同域的页面在相互操作的时候不会又任何问题。
比如:baidu.com 的一个网页(baidu.html)里面利用 iframe 引入了一个 qq.com 的一个网页(qq.html)
这时在 baidu.html 里面可以看到 qq.html 里的内容,但是却不能利用 JavaScript 来操作它。因为这两个页面属于不同的域,在操作之前,js 会检测两个页面的域是否相等,如果相等,就允许其操作,如果不相等,就会拒绝操作。
这里不可能把 baidu.html 与 qq.html 利用JS改成同一个域的。因为它们的一级域名不相等。(强制用JS将它们改成相等域的话会报错 “参数无效错误”)
但如果在 baidu.html 里引入 baidu.com 里的另一个网页,是不会有这个问题的,因为域相等。
方案四:window.name + iframe
原理:
- window对象的 name 属性特征 --- name属性可设置或返回存放窗口的名称的一个字符串,同一个浏览器窗口或同一个 iframe载入的页面共享一个 window.name。意思是只要你不关闭页面,仅仅改变网址的话,window.name 是不会变得,而且每一个打开的页面都能获取并修改 window.name 的值
- 父页面能够通过 iframe 的 contentWindow 的属性,访问到子页面的内容(不跨域的情况下),意思是利用 iframeObj.contentWindow.name 是能够获取到 iframe窗口内同一域名页面的 window.name
例子:www.aaa.com/index.html 想要获取到 www.bbb.com.data.html 内的数据(辅助页面 www.aaa.com/null.html)
www.aaa.com/index.html 页面
<body> <iframe id="proxy" src="http://www.bbb.com/data.html" style="display: none;" onload="getData()"></iframe> <script> function getData(){ var iframe = document.getElementById('proxy') iframe.onload = function(){ // 覆盖掉载入时绑定的 getData() 函数 var data = iframe.contentWindow.name console.log(data) } iframe.src = 'http://www.aaa.com/null.html' } </script> </body>
www.aaa.com/null.html
空白页,和 index.html 在同一域名下,为了让父页面能取到 iframe 的window.name 值而存在
www.bbb.com/data.html
准备传给index.html 的数据存放在 window.name 字符串中
<script> window.name = "somedata" </script>
方案五:window.postMessage
window.postMessage() 方法可以安全地实现跨域通信。
window.postMessage()方法被调用时,会在所有页面脚本执行完毕之后,向目标窗口派发一个 MessageEvent 消息。该 MessageEvent 消息有四个属性需要注意: message 属性表示该message的类型;data属性为 window.postMessage 的第一个参数;origin 属性表示调用 window.postMessage() 方法时调用页面的当前状态;source 属性记录调用 window.postMessage() 方法的窗口信息。
发送方:
var domain = 'http://scriptandstyle.com' var iframe = document.getElementById('myIframe').contentWindow // 发送消息 setInterval(function(){ var message = 'Hello! the time is : ' + (new Date().getTime()) console.log(message) iframe.postMessage(message, domain) }, 6000)
确保使用的是iframe的contentWindow属性,而不是节点对象
接收方:
// 响应事件 window.addEventListener('message', function(event){ if(event.origin !== 'http://scriptandstyle.com') return console.log(event.data) event.source.postMessage('hello back boy', event.origin) }, false)
如果你不校验数据源的合法性,那使用这种技术将会变得很危险;你的应用安全需要你对它负责
方案六:反向代理
7、寄生组合式继承
利用构造函数和寄生方式继承属性,利用原型链继承方法
function selfExtents(Child, Parent){ // 将子类的原型改成父类的原型 Child.prototype = Object.create(Parent.prototype) // 直接对原型进行赋值,会造型 构造函数 的指向错误 // 修复子类的构造函数指向 Child.prototype.constructor = Child } function Person(name){ this.name = name this.friends = ['大魔王', '人类', '魔鬼'] } Person.prototype.getName = function(){ console.log(this.name) } function Child(name, age){ Person.call(this, name) // 使用构造函数方式 继承父属性,支持传参 this.age = age } // 继承原型上的属性和方法,实际就是修改原型链 selfExtents(Child, Person) // 继承后,才能在原型链上写属性和方法,不然会被替换掉 Child.prototype.getAge = function(){ console.log(this.age) }
Object.create() 的浅拷贝作用,类似下面的函数:
function create(obj){ function F(){} F.prototype = obj return new F() }
http://ghmagical.com/article/page/id/omIoPb1AIBPu
8、call & apply & bind
调用一个函数,并具有一个指定的 this 值
不同点:
- call 和 bind 第一个参数为 this ,后面跟一个传值列表
- apply 第一个参数为 this,第二个参数为传值数组
- call 和 apply 直接执行,bind 返回一个函数,需手动执行
使用选择:
- 参数为数组格式时,用 apply 方法
- 需要延后执行时,用 bind 方法
基础用法:
var a = {} var b = {} var c = {} function func(x, y){ this.x = x this.y = y } func.call(a, 1, 2) // 列表 func.apply(b, [3, 4]) // 数组 func.bind(c, 5, 6)() // 不直接执行 console.log(a) // {x:1, y:2} console.log(b) // {x:3, y:4} console.log(c) // {x:5, y:6}
用途
1、类的继承
function Life(status){ // 生命体 this.status = status } function Person(name, age, status){ // 人 this.name = name this.age = age Life.call(this, status) } function Student(name, age, status, grade){ // 学生 this.grade = grade Person.call(this, name, age, status) } var instance = new Student('Dylan', 25, '活着', '研三') console.log(instance.status) // 活着 console.log(instance.name) // Dylan console.log(instance.age) // 25 console.log(instance.grade) // 研三
2、回调函数
var a = { 'x': 1, 'y': function(){ setTimeout(function(){ console.log(this.x) // this指向window }) }, 'z': function(){ setTimeout(function(){ console.log(this.x) // this指向a }.call(this)) } } a.y() // undefined a.z() // 1
3、内置函数
var arr = [5,9,2,7,12,3]; // Math的 min 和 max 方法,都只接收参数的传值列表,而不接收数组 console.log(Math.min.apply(null, arr)); console.log(Math.max.apply(null, arr));
注意:当参数过多(比如超过 1w个),可能会参数个数越界,建议切块后执行。(js中最大参数个数为 65536 个(2的16次方))
4、伪数组转数组
var a = { 0: 1, 1: 2, length: 2 } console.log(Array.prototype.slice.call(a)); // [1, 2] var b = [] console.log(b.slice.call(a)) // [1, 2] console.log(Array.from(a)); // [1, 2] ES6
9、js基本类型与引用类型(堆内存、栈内存的理解)
两种不同的数据类型:基本类型、引用类型
基本类型
基本数据类型:undefined、null、boolean、number、string 基本类型的访问是按值访问的,就是说你可以操作保存在变量中的实际的值
特点:
(1)基本类型的值是不可变的。任何方法都无法改变一个基本类型的值
比如一个字符串:
var name = 'dylan' name.toUpperCase() // DYLAN console.log(name) // dylan
会发现原始的name并未改变,而是调用了 toUpperCase() 方法后返回的是一个新的字符串
再来看个例子:
var person = 'dylan' person.age = 22 person.method = function(){} console.log(person.age); // undefined console.log(person.method); // undefined
通过上面代码可知,我们不能给基本类型添加属性和方法,再次说明基本类型是不可变的。
(2)基本类型的比较是值得比较
只有在它们的值相等的时候它们才相等。
var a = 1 var b = true console.log(a == b); // true
在用 == 比较两个不同类型的变量时会进行一些类型转换。像上面的比较会先把true转换为数字 1 再和数字 1 进行比较,结果就是 ture了
隐式类型转换规则
- 转成 string 类型
- 字符串连接符:+
- 转成 number 类型
- 自增自减运算符:++/--
- 算数运算符:+ - * / %
- 关系运算符:
- >
- <
- >=
- <=
- == / ===
- != / !==
- 转成 boolean 类型
- 逻辑非运算符: !
(3)基本类型的变量是存放在栈区的(栈区指内存里的栈内存)
假如有以下几个基本类型的变量:
var name = 'jozo'
var city = 'guangzhou'
var age = '22'
那么它的存储结构如下图
栈区包括了变量的标识符和变量的值
引用类型
引用类型也就是对象了,对象是属性和方法的集合。
引用类型可以拥有属性和方法,属性又可以包含基本类型和引用类型。
(1)引用类型的值是可变的
我们可以为引用类型添加属性和方法,也可以删除其属性和方法:
var person = {} // 创建一个空对象 -- 引用类型 person.name = 'jozo' person.age = 22 person.sayName = function(){console.log(person.name)} person.sayName() // jozo delete person.name // 删除person对象的name属性 person.sayName() // undefined
(2)引用类型的值是同时保存在栈内存和堆内存中的对象
JavaScript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间,那么我们操作啥呢?实际上,是操作对象的引用,所以引用类型的值是按引用访问的。
准确的说,引用类型的存储需要内存的栈区和堆区(堆区是指内存里的堆内存)共同完成,栈区内存保存变量标识符和指向堆内存中该对象的指针,也可以说是该对象在堆内存中的地址
加入有以下几个对象
var person1 = {name: 'jozo'}
var person2 = {name: 'xiaom'}
var person3 = {name: 'xiaoq'}
则这三个对象在内存中保存的情况如下
(3)引用类型是引用的比较
var person1 = '{}' var person2 = '{}' console.log(person1 == person2) // true var person3 = {} var person4 = {} console.log(person3 == person4) // false
引用类型是按引用访问的,换句话说就是比较两个对象的堆内存的地址是否相同。很明显,person3 和 person4 在堆内存中地址是不同的
3、简单赋值
在从一个变量向另一个变量赋值基本类型时,会在该变量上创建一个新值,然后再把该值复制到新变量分配的位置上:
var a = 10 var b = a a++ console.log(a) // 11 console.log(b) // 10
此时,a中保存的值为10,当使用a来初始化b时,b中保存的值也为10,但b中的10和a中的是完全独立的,该值只是a中的值得一个副本,此后,这两个变量可以参加任何操作而相互不受影响。
也就是说,基本类型在赋值操作后,两个变量是相互不受影响的
4、对象引用
当从一个变量向另一个变量赋值引用类型的值时,同样也会将存储在变量中的对象的值复制一份放到新变量分配的空间中。与简单赋值不同,这个只的副本实际上是一个指针,而这个指针指向存储在堆内存的一个对象。那么赋值操作后,两个变量都保存了同一个对象地址,则这两个变量指向了同一个对象。因此,改变其中任何一个变量,都会相互影响。
var a = {} // a保存了一个空对象的实例 var b = a // a和b都指向了这个空对象 a.name = 'dylan' console.log(a.name) // dylan console.log(b.name) // dylan b.age = 25 console.log(a.age) // 25 console.log(b.age) // 25 console.log(a === b) // true
它们的关系如下:
引用类型的赋值其实是对象保存在栈区地址指针的赋值,因此两个变量指向同一个对象,任何的操作都会相互影响
10、JavaScript的深浅拷贝
(1)浅拷贝
浅拷贝的意思就是只复制引用,而未复制真正的值。
const originArr = [1,2,3,4,5] const originObj = { a:'a', b:'b', c:[1,2,3], d:{dd: 'dd'} } const cloneArr = originArr const cloneObj = originObj console.log(cloneArr) // [1, 2, 3, 4, 5] console.log(cloneObj) // {a: "a", b: "b", c: [1,2,3], d: {dd: 'dd'}} cloneArr.push(6) cloneObj.a = {aa: 'aa'} console.log(originArr) // [1, 2, 3, 4, 5, 6] console.log(cloneArr) // [1, 2, 3, 4, 5, 6] console.log(originObj) // {a: {aa: 'aa'}, b: "b", c: [1,2,3], d: {dd: 'dd'}} console.log(cloneObj) // {a: {aa: 'aa'}, b: "b", c: [1,2,3], d: {dd: 'dd'}}
上面的代码是最简单的利用 = 赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着 cloneArray 和 cloneObj 改变,originArr 和 originObj 也随之发生了变化
(2)深拷贝
深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了。
只要进行了深拷贝,它们老死不相往来,谁也不会影响谁。
目前实现深拷贝的方法不多,主要是两种:
- 利用 JSON 对象中的 parse 和stringify
- 利用递归来实现每一层都重新创建对象并赋值
JSON.stringify / parse 的方法
JSON.stringify 是将一个 JavaScript 值 转成一个 JSON 字符串
JSON.parse 是将一个 JSON 字符串转成一个JavaScript值或对象
它能实现深拷贝?我们来试试
const originArr = [1,2,3,4,5] const cloneArr = JSON.parse(JSON.stringify(originArr)) console.log(originArr == cloneArr) // false const originObj = {a:'a', b:'b', c:[1,2,3], d:{dd:'dd'}} const cloneObj = JSON.parse(JSON.stringify(originObj)) console.log(originObj == cloneObj) // false cloneObj.a = 'aa' cloneObj.c = [1,1,1] cloneObj.d.dd = 'double' console.log(originObj) // {a: "a", b: "b", c: [1, 2, 3], d: {dd: "dd"}} console.log(cloneObj) // {a: "aa", b: "b", c: [1, 1, 1], {dd: "double"}}
确实是深拷贝,也很方便。但是,这个方法只能适用于一些简单的情况。比如下面这样的一个对象就不适用:
const originObj = { name: 'dylan', sayHello: function(){ console.log('hello world') } } console.log(originObj) // {name: "dylan", sayHello: ƒ} const cloneObj = JSON.parse(JSON.stringify(originObj)) console.log(cloneObj) // {name: "dylan"}
发现在 cloneObj 中,有属性丢失了。那是为什么呢?
因为在JSON.stringify 时,undefined、function、symbol 会在转换过程中被忽略
也就是说如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝
递归的方法
递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象 -> 对象赋值 的操作
function deepClone(source){ const targetObj = source.constructor === Array ? [] : {} // 判断赋值的目标是数组还是对象 for(let keys in source){ // 遍历目标 if(source.hasOwnProperty(keys)){ if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下 targetObj[keys] = source[keys].constructor === Array ? [] : {} targetObj[keys] = deepClone(source[keys]) }else{ // 如果不是,就直接赋值 targetObj[keys] = source[keys] } } } return targetObj }
下面来试一试
const originObj = {a:'a', b:'b', c:[1,2,3], d:{dd:'dd'}} const cloneObj = deepClone(originObj) console.log(originObj == cloneObj) // false cloneObj.a = 'aa' cloneObj.b = [1,1,1] cloneObj.d.dd = 'double' console.log(originObj) // {a: "a", b: "b", c: [1, 2, 3], d: {dd: "dd"}} console.log(cloneObj) // {a: "aa", b: [1, 1, 1], c:[1, 2, 3], d: {dd: "double"}}
可以。再试一下带有函数的
const originObj = { name: 'dylan', sayHello: function(){ console.log('hello world') } } console.log(originObj) // {name: "dylan", sayHello: ƒ} const cloneObj = deepClone(originObj) console.log(cloneObj) // {name: "dylan", sayHello: ƒ}
搞定~
JavaScript中的拷贝方法
在JavaScript中,数组有两个方法 concat 和 slice 是可以实现对原数组的拷贝的,这两个方法都不会修改原数组,而是返回一个修改后的新数组。
同时,ES6中引入了 Object.assgin 方法和 ... 扩展运算符,也能实现对对象的拷贝
那它们是深拷贝还是浅拷贝呢?
concat
定义:该方法可以连接两个或者更多的数组,但是它不会修改已存在的数组,而是返回一个新数组。
看着这意思,很像是深拷贝啊,我们来试试
const originArr = [1,2,3,4,5] const cloneArr = originArr.concat() console.log(originArr == cloneArr) // false cloneArr.push(6) // [1,2,3,4,5,6] console.log(originArr) // [1, 2, 3, 4, 5]
看上去是深拷贝的。
我们来考虑一个问题,如果这个对象是多层的,会怎样
const originArr = [1, [1,2,3], {a:1}] const cloneArr = originArr.concat() console.log(originArr == cloneArr) cloneArr[1].push(4) cloneArr[2].a = 2 console.log(originArr) // [1, [1, 2, 3, 4], {a: 2}]
我们修改 cloneArr 中的数组 [1,2,3] 或对象 {a: 1} 时,发现 originArr 也发生了变化
结论:concat只是对数组的第一层进行了深拷贝
slice
const originArr = [1, [1,2,3], {a:1}] const cloneArr = originArr.slice() console.log(originArr == cloneArr) cloneArr[1].push(4) cloneArr[2].a = 2 console.log(originArr) // [1, [1, 2, 3, 4], {a: 2}]
和 concat 结果是一样的。
结论:slice 只是对数组的第一层进行深拷贝
Object.assign()
const originObj = { name: 'dylan', sayHello: function(){ console.log('hello world'); } } const cloneObj = Object.assign(originObj) console.log(cloneObj) // {name: "dylan", sayHello: ƒ} originObj.age = 25 console.log(cloneObj) // {name: "dylan", age: 25, sayHello: ƒ}
结论:Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝哪个引用值。(浅拷贝)
... 扩展运算符
const originArr = [1,2,3,4,5,[6,7,8]] const originObj = {a:1, b:{bb: 1}} const cloneArr = [...originArr] cloneArr[0] = 0 cloneArr[5].push(9) console.log(originArr); // [1, 2, 3, 4, 5, [6, 7, 8, 9]] 第一层没改变,第二层改变了 const cloneObj = {...originObj} cloneObj.a = 2 cloneObj.b.bb = 2 console.log(originObj) // {a: 1, b: {bb: 2}} 第一层没改变,第二层改变了
结论: ... 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值
首层浅拷贝
我们知道了,会有一种情况,就是对目标对象的第一层进行深拷贝,然后后面的是浅拷贝,可以称作 “首层浅拷贝”
我们可以自己实现一个这样的函数:
function shallowClone(source){ const targetObj = source.constructor === Array ? [] : {} for(let keys in source){ if(source.hasOwnProperty(keys)){ targetObj[keys] = source[keys] } } return targetObj }
我们来测试一下
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}; const cloneObj = shallowClone(originObj); console.log(cloneObj === originObj); // false cloneObj.a='aa'; cloneObj.c=[1,1,1]; cloneObj.d.dd='surprise'; console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'surprise'}} console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}}
originObj中 a、c 都没被影响,但是 d 中的一个对象被修改了。
- 从 shallowClone 的代码中我们可以看出,我们只对第一层目标进行了深拷贝,而第二层开始的目标我们是直接利用 = 赋值操作符进行拷贝的
- 所以第二层后的目标都只是复制了一个引用,也就是浅拷贝
总结
- 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值
- JavaScript 总数组的对象自带的拷贝方法都是“首层浅拷贝”
- JSON.strubfuft 实现的是深拷贝,但是对目标对象有要求
- 若想真正意义上的深拷贝,请使用递归
11、axios&fetch
前端三个发展迅速的领域,前端请求自然也发展迅速,从原生的XHR到 jquery ajax,再到现在的 axios 和 fetch
jquery ajax
$.ajax({ type: 'POST', url: url, data: data, dataType: dataType, success: function(){}, error: function(){} })
它是对原生XHR的封装,还支持 JSONP,非常方便;但是随着 react,vue 等前端框架的兴起,jquery 早已不复当年之勇。很多情况下我们只需要使用 ajax,但是却需要引入整个 jquery,这非常的不合理,于是便有了 fetch 解决方案。
fetch
fetch号称是 ajax 的替代品,它的API是基于 Promise 设计的,旧版本的浏览器不支持Promise,需要使用 polyfill es6-promise
// 原生XHR var xhr = new XMLHttpRequest() xhr.open('GET', url) xhr.onreadystatechange = function(){ if(xhr.readyState === 4 && xhr.statue === 200){ console.log(xhr.responseText) // 从服务器获取数据 } } xhr.send() // fetch fetch(url) .then(response => { if(response.ok){ return response.json() } }) .then(data => console.log(data)) .catch(err => console.log(err))
在MDN上,讲到它跟 jquery ajax 的区别,这也是 fetch 很奇怪的地方:
当接收到一个代表错误的 HTTP 状态码时,从 fetch() 返回的 Promise不会被标记为 reject,即使该 HTTP响应的状态码是 404 或 500.相反,它会将 Promise 状态标记为 resolve(但是会将 resolve 的返回值的ok属性设置为false),仅当网络故障时或请求被阻止时,才会标记为 reject。默认情况下,fetch 不会从服务端发送或接收任何 cookies,如果站点依赖于用户 session,则会导致未经认证的请求(要发送cookies,必须设置 credentials 选项)
突然感觉这还不如 jquery ajax 好用。别急,再搭配 async/await 将会让我们的异步代码更加优雅
async function test(){ let response = await fetch(url) let data = await response.json() console.log(data); }
fetch是比较底层的API,很多情况下都需要我们再次封装。比如:
// jquery ajax $.post(url, {name: 'test'}) // fetch fetch(url, { method: 'POST', body: Object.keys({name: 'test'}).map(key => { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) }).join('&') })
由于fetch是比较底层的API,所以需要我们手动将参数拼接成 'name=test' 的格式,而 jquery ajax已经封装好了。所以 fetch并不是开箱即用的。
另外,fetch还不支持超时控制。感觉它还需要继续成长。
axios
它也是对原生XHR的封装。特性:
- 可以在 node.js 中使用
- 提供了并发请求的接口
- 支持Promise API
简单使用
axios({ method: 'GET', url: url }) .then(res => {console.log(res)}) .catch(err => {console.log(err)})
并发请求
function getUserAccount(){ return axios.get('/user/1') } function getUserPermissions(){ return axios.get('/user/1/permissions') } axios.all([getUserAccount(), getUserPermissions()]) .then(axios.spread(function (acct, perms){ // both requests are now complete }))
axios体积比较小,也没有fetch的各种问题
12、typeof 和 instanceof 的区别
typeof
检测一个对象的数据类型,能检测并返回的类型有(array,number,function,string,undefined 和 object 6种类型),返回值为字符串类型。
在这里要说明一下,NaN返回的数据也是 number,但是NaN不等于它本身,所以可以用 (typeof x == 'number' && x != x) 来判断是否为NaN。
像包括 {}、[]、null 这三种对象都会返回 object 类型,要想对这三种类型区分可以使用 toString.call(x) 这种方式来判断,这种方式返回的是 [object Object]、[object Array]、[object Null] 这类的字符串,当然其他五种数据类型也可以这么进行判断。
instanceof
创建类的方法(es5)
function Parent(){} :为了区别一般的方法,这个方法名一定要首字母大写
创建一个实例
var a = new Parent() :我们平常说的new一个对象,就是创建一个类的实例,a 就是Parent的实例
每个实例都会有一个 __proto__ 属性,这个属性指向了它的构造函数(类),也就是说 a 的 __proto__ 指的是 Parent,instanceof 就是用来判断一个实例的指向的, a instanceof Parent 结果为 true
我们都知道 var b = [];b instanceof Array 的值肯定为true,因为 b=[] 等价于 b = new Array();b是Array的实例,所以 b 的 __proto__ 指向的就是 Array
总结:typeof 判断的是类型,instanceof 判断的是实例指向(是否为后面跟的构造类)
关于instanceof的原理以及内部实现:https://www.cnblogs.com/haishen/p/12052751.html
题目:
1、如何获取一个页面中有多少种html标签
(1)获取页面标签
document.querySelectorAll('*')
(2)展开
[...document.querySelectorAll('*')]
(3)获取节点名称
[...document.querySelectorAll('*')].map(v=>v.nodeName)
(4)使用Set去重
new Set([...document.querySelectorAll('*')].map(v=>v.nodeName)).size
2、获取当前页面数量前5的标签
(1)使用reduce进行统计
var ret = [...document.querySelectorAll('*')].map(v=>v.nodeName).reduce((ret,a)=>{ ret[a] = (ret[a]||0) + 1 return ret }, {})
(2)进行排序
ret = Object.entries(ret).sort((a,b)=>b[1]-a[1]).slice(0,5)
(3)table打印
ret = ret.map(v=>({'标签名':v[0], '次数':v[1]}))
console.table(ret)
1