面试题
浏览器篇
从输入URL到浏览器显示页面过程中都发生了什么
1. 发送至 DNS 服务器并获得域名对应的 WEB 服务器的 ip 地址
2. 通过Cache-Control和Expires来检查是否命中强缓存,命中就从本地磁盘/缓存中取html(200 from disk cache)
3. 2没命中来到3,建立TCP连接,向服务器发起请求,服务器通过Etag和Last-Modify来与服务器确认返回的响应是否被更改(协商缓存);若未改,取浏览器本地缓存(304 Not Modified)
4. 若强缓存和协商缓存都没有命中则返回请求结果
浏览器下载 HTML 数据,将html文档解析成为一个个标签,这些标签组成了树状结构
如果解析到style标签则开始解析css,如果解析到link标签则先异步下载,完成后解析css。
如果遇到script标签,判断是行内写法则直接解析执行,如果是src引入则同步下载脚本文件,下载完成立即执行,注意这里下载过程是阻塞的,其他流程都会等下载完成后执行。
浏览器会将HTML解析成一个DOM树
将CSS解析成 CSS Rule Tree(css规则树)
解析完成后,浏览器引擎会根据DOM树和CSS规则树来构造渲染树
浏览器更具渲染树进行渲染
内核有哪些?
- Cecko => Netscape6
- Presto => Opera7+, 现Opera时blink
- Trident => IE, 360, 搜狗..
- Webkit => 火狐, 谷歌
cookie, sessionStroage, localStroage, indexDB区别
特性 | cookie | localStorage | sessionStorage | indexDB |
---|---|---|---|---|
数据生命周期 | 一般由服务器生成,可以设置过期时间 | 除非被清理,否则一直存在 | 页面关闭就清理 | 除非被清理,否则一直存在 |
数据存储大小 | 4K | 5M | 5M | 无限 |
与服务端通信 | 每次都会携带在 header 中,对于请求性能影响 | 不参与 | 不参与 | 不参与 |
html篇
h5新特性
- input的type型增了email, url, number min="" max="" step="", tel, search, range min="" max="" step="", color, date, month, week
- video, audio, canvas
- sessionStorage, localStorage, webWorker, webSocket
- drag拖拽事件
解释webWorker:
在当前JavaScript主进程中开启一个异步线程(独立工作,在主线程以外运行)
window.postMessage可用于解决跨域数据传输
解释webSocket:
- 新的协议,实现了浏览器与服务器全双工通信, 一个持久化的协议
- 能更好的节省服务器资源和带宽并达到实时通讯的目的
和http对比:
异 | 同 |
---|---|
1. WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息2. HTTP是单向的3. WebSocket是需要浏览器和服务器握手进行建立连接的 4. 而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接 | 1. 都是基于tcp的,都是可靠性传输协议 2. 都是应用层协议 |
对语义化的理解
- 便于开发者理解,维护
- 便于搜索引擎解析,和SEO
title和alt区别
- title是给用户看的,hover的时候做提示用的
- alt是给搜索引擎读识别的,当图像无法显示是,以alt中的文字替代
iframe得优缺点
优点:
- 解决第三方图标/广告加载缓慢问题
缺点:
- iframe会阻塞主页面的onload事件
href和src区别
- href是建立html与资源之间的链接或者关系,或者建立元素与锚点之间的联系,目的不是引用资源,而是建立联系
- src是把当前带有src的标签替代成src所指向的资源
- 浏览器解析方式不同:遇到src会暂停其它资源的下载/处理,直到把src资源编译加载完毕,这也是为什么要把src放在html末尾的原因
块级元素和行内元素的区别
- 行内元素与块级函数可以通过display属性相互转换,inline是行内,block是块级,inline-block是行内块级
- 行内元素不可调width,height;padding和margin也只能设置左右不能上下;但可以设置line-height;块级则都可以。block和inline-block的唯一区别是inline-block不独占一行
css篇
css3新特性有哪些?
- border-radius/shadow/image
- rgba()
- 渐变色liner-grandient / radial-grandient / conic-grandient
- 字体text-overflow, word-warp, text-shadow
- 图片background-origin/clip/size
- transform的值rotate, scale
- 标准盒模型box-sizing
- 动画animation
选择器有哪些?
-
选择器only-child, last-child, first-of-type, nth-last-chile, nth-of-type等
-
全局选择器 *
-
标签选择器 body
-
类选择器.app
-
ID选择器 #app
-
组合选择器 .container .head
-
继承选择器 body div
-
伪类选择器 :hover
@import和link的区别
- link是XHTML标签,除了引入css外还可以用于RSS等其它事物,@import只能用于css
- link在页面加载的同时引入css,@import在页面加载完后引入
- link无兼容问题,@import低版本浏览器不支持
- link能够被js控制,从而改变样式,@import不能
display:none和visibility:hidden的区别
- none只当那元素不存在,hidden会保留该元素的位置,栗子
<span>span1</span>
<div>div</div>
<span>span2</span>
div:display: none时,span1和span2是同一行的,visibility:hidden时它俩是换行的(即div只是被隐藏了,实际div独占一行的位置还保留着)
position的值, relative和absolute分别是相对于谁进行定位的?
- static:默认值,没有定位,元素出现在正常的文档流中。
- relative:相对定位,相对于自己本身在正常文档流中的位置进行定位。
- absolute:生成绝对定位,相对于最近一级定位不为static的父元素进行定位。
- fixed: (老版本IE不支持)生成绝对定位,相对于浏览器窗口或者frame进行定位。
- sticky:生成粘性定位的元素,容器的位置根据正常文档流计算得出,scroll滚动它也不动。
常见的css兼容问题
- padding和margin不同,解决方法:
* { margin: 0; padding: 0; }
js篇
对AMD, CMD,es6模块的理解
- CommonJs是服务端模块的规范,CommonJs是同步加载的,不适合客户端,Nodejs就采用此规范,AMD是异步加载的,CMD曾经是社区推行的实现模块化的方式,后来被广泛采纳
- es6是官方推荐模式,在标签中用必须标注type="module",默认同步加载,可以异步
- 导出导入方式不同,AMD导出导入是通过define函数包裹,内部return模块实现,CMD则通过module.exports实现
AMD
a.js
define(funtion() {
let a = 1
return { a }
})
b.js
define(['./a.js', './c.js'], function(moduleA, moduleC) {
......
})
对闭包的了解
- 闭包是有由权限访问另一个函数作用域中的变量的函数
- 常见方式:在一个函数中返回一个函数
- 缺点:闭包函数中的参数,变量不能被垃圾回收机制回收,当数据太大时会造成内存泄漏
描述冒泡,捕获过程
- 事件捕获(event capturing): 当鼠标点击或者触发dom事件时(被触发dom事件的这个元素被叫作事件源),浏览器会从根节点(html) =>事件源(由外到内)进行事件传播。
- 事件冒泡(dubbed bubbling): 事件源 =>根节点(由内到外)进行事件传播。
- dom标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。
- addEventListener的第三个参数,参数默认值是false,表示在事件冒泡阶段调用事件处理函数;如果参数为true,则表示在事件捕获阶段调用处理函数。
<img />
document.addEventListener('click', console.log('dpcument'), false)
document.body.addEventListener('click', console.log('body'), false)
img.addEventListener('click', console.log('img'), false)
// 点击后
/*
img => body => document
*/
// 如果都改为true,则在捕获阶段触发,执行顺序:
/*
document => body => img
*/
cookie sessionStorage localStorage 区别
同:
- 都是浏览器存储
- 都存储在浏览器本地
异:
- cookie由服务器写入, sessionStorage以及localStorage都是由前端写入
- cookie的生命周期由服务器端写入时就设置好的,localStorage是写入就一直存在,除非手动清除,sessionStorage是由页面关闭时自动清除
- cookie存储空间大小约4kb, sessionStorage及localStorage空间比较大,大约5M
- 3者的数据共享都遵循同源原则,sessionStorage还限制必须是同一个页面
- 前端给后端发送请求时,自动携带cookie, session 及 local都不携带
- cookie一般存储登录验证信息或者token,localStorage常用于存储不易变动的数据,减轻服务器压力,sessionStorage可以用来监测用户是否是刷新进入页面,如音乐播放器恢复进度条功能
es5和es6的区别
- let,const替代var
- 箭头函数,还有一些新的数据结构,如Set,Map
- promise解决回调地狱
- 语法糖:class,和对象里函数的写法func() {},解构赋值
- 模板字符串
map和object的区别
- map的键可以是任意数据类型,obj只能是字符串和Symbol
- map是干净的,默认情况不包含任何键。只包含显式插入的键。而Object有一个原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。栗子:
m = new Map()
o = {}
Map的原型上有get,set等方法,Object原型上有toString等方法
m.set(get, 'get') 不会改变原型上的get,因为它是这样获取的,m.get('get')
o.toString = 'toStr' 下次再调用o.toString()就不行,因为已经被改了(冲突)
js数据类型
- 基本类型 undefined,null,boolean,number,string
- 引用类型 object,array,function
- 独一无二且不可变型数据 symbol
判断数据类型方式
- typeof
- instanceof
- object.prototype.toString
判断变量是否为数组(typeof不行)
- arr insanceof Array
- arr.constructor === Array
- Object.prototype.toString(arr) === '[Object Array]'
箭头函数特点
- 没有this
- 特殊情况可以省略return
- 没有arguements
new操作符做了什么?
- 创建一个空对象obj
- 把obj的__proto__指向构造函数的原型prototype
- 返回改变构造函数的this指向(指向obj)后的结果
const new = function(Func, ...args) {
//创建空对象
let obj = {}
//把obj的__proto__指向构造函数Func的原型
obj.__proto__ = Func.prototype
//返回改变this指向后的Func
return Func.apply(obj, ...args)
}
document.write和innerHTML的区别
- document.write只能重绘整个页面,innerHTML可以局部
innerHTML=''和removeChild的区别
- innerHTML不能再利用,而remove还可以,const dom = remov...得到该dom,而且再插入到别的地方任然可以像原来一样工作,而str = dom.innerHTML得到的字符串把dom结构破坏了,再去别的地方设置dom2.innerHTML = str,它原来绑定的js事件等就没法用了
对json的理解
- json是一种轻量级的数据格式,基于js的一个子集,格式简单,占用带宽小
网络请求篇
与 WEB 服务器建立 TCP 连接。
TCP 协议通过三次握手建立连接。
客户端通过 SYN 报文段发送连接请求,确定服务端是否开启端口准备连接。状态设置为 SYN_SEND;
服务器如果有开着的端口并且决定接受连接,就会返回一个 SYN+ACK 报文段给客户端,状态设置为 SYN_RECV;
客户端收到服务器的 SYN+ACK 报文段,向服务器发送 ACK 报文段表示确认。此时客户端和服务器都设置为 ESTABLISHED 状态。连接建立,可以开始数据传输了。
翻译成大白话就是:
客户端:你能接收到我的消息吗?
服务端:可以的,那你能接收到我的回复吗?
客户端:可以,那我们开始聊正事吧。
为什么是3次?:避免历史连接,确认客户端发来的请求是这次通信的人。
为什么不是4次?:3次够了第四次浪费
作者:前端君
链接:https://juejin.cn/post/6905931622374342670
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
前后端传输的常用数据格式有哪些?
- json各种字符数据
- XML,如:返回一个html页面
- form,传输图片等
ajax过程
- 建立XMLHttpRequest对象
- 创建一个新的http请求,指定方法,url,验证信息
- 设置监听http请求状态变化函数
- 发生请求
- 获取返回的数据
vue篇
对MVVM的理解
- MVVM:M(model数据模型),V(view视图UI),VM(viewModel负责监听model变化,实时更新view)
- view和model之间无直接联系,而是靠viewModel,把view层的交互改变后的数据同步到model层,当model中数据改变时触发view更新
- 这种模式实现了model和view的自动同步,开发者只需要专注于操作数据,而不用操作频繁dom去更新view
对spa的理解,优缺点
-
优点:
- 内容改变不需要重新加载整个页面,避免不必要的提跳转和重复渲染
- 不用频繁跳转/渲染,就减少了不必要的http请求,对服务器压力小
- 前后端职责分离,架构清晰
-
缺点:
- 初次加载耗时多,因为需要一次把页面需要的js,css加载
- SEO难度大,因为所有页面都在一个页面中动态替换显示,SEO天然弱势
- 因为是单页面应用,浏览器跳转自带的的前进后退功能没法用,需要自己建堆栈管理
对单向数据流的了解
- 所有的
prop
都使得其父子prop之间形成一个单向向下绑定,父级prop的更新会向下流动到子组件中,但反过来不行 - 优点:防止从子组件意外改变父级组件的状态
双向绑定是怎么实现的
- vue中内部采用了发布-订阅模式,使用了Object.defineProperty这个es5特性
- 对vue传入的数据进行了相应的数据拦截,动态添加get与set方法
- 当数据变化的时候,就会触发对应的set方法,set会进一步触发watcher,当数据改变了,接着进行虚拟dom对比,执行render,后续视图更新操作完毕
$set的原理
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象:
- 会先判读属性是否存在、
- 对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理 (defineReactive 方法就是 Vue 动态添加 getter 和 setter 的功能所调用的方法)
虚拟 DOM 的优缺点
-
优点:
- 保证性能的下限:虚拟 DOM 为适配上层 API 可能产生的操作,它必须是普适的,所以性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多
- 无需手动操作 DOM :只需要写好view-model的代码逻辑
- 跨平台:DOM 与平台相关性强,而虚拟 DOM 本质是 js ,例如服务器渲染,、weex开发等
-
缺点:
- 有上限,无法进行极致优化,不像自己写js那样自由(如果有这个水平的话)
虚拟 DOM 实现的原理
-
虚拟 DOM 主要包含一下3部分:
- 对真实 DOM 进行抽象,用 JS 模拟真是的 DOM 树
- diff 算法:比较两棵 DOM 树的差别
- pach 算法:将两个虚拟 DOM 对象的差异应用到真正的 DOM 树上
key的作用
- key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
- 更准确、更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
v-model 的原理
- v-model 主要使用在 input、textarea、select 等元素上创建双向数据绑定
- v-model 在内部对不同的元素绑定不同的事件和属性,(eg:text元素绑定value属性和input事件)
<input v-model='something'>
// 相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
为什么组件中的data是个函数,而App组件的data是对象
- 因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响
- 而 App组件是根组件,不是用来复用的,因此不用担心上面的问题问题。
computed 和 watch 的区别和运用的场景
- computed:计算属性,依赖其它属性值,并且computed的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
- watch:更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
- 当我们需要进行数值计算,并且依赖于其它数据时,利用 computed 的缓存特性,避免每次获取值时,都要重新计算
- 当我们需要在数据变化时执行异步或开销较大的操作时,因为watch后跟的是个函数,可以在这个函数里进行复杂操作
谈谈你对 keep-alive 的了解
-
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
- 一般结合路由
<router-view>
和动态组件<component :is=''>
一起使用,用于缓存组件 - 提供 include 和 exclude 属性,两者都支持字符串或正则表达式,exclude 的优先级比 include 高
- 对应两个钩子函数 activated 和 deactivated
- 一般结合路由
父组件可以监听到子组建的生命周期吗?
- 方法一:
$emit
可以通过$emit实现
// Parent.vue
<Child @mounted="doSomeThing" />
methods: {
doSomeThing() {
console.log('子组件到了mounted阶段')
}
}
// Chile.vue
mounted() {
this.$emit('mounted')
}
- 方法二:
@hook
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
$nextTick和updated的区别
- 相同点:都是在dom更新后触发
- 不同点:updated只能操作全局,而nextTick可以这样用
// 在修改数据之后立即使用它,它是一个异步函数,会在dom更新后执行回调函数
methods: {
test() {
console.log('还没更新')
this.$nextTick(function() {
console.log('更新了')
})
}
}
组件通信的方法有哪些
- ref与 $parent / $children适用 父子组件通信
- ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
- $parent / $children:访问父 / 子实例
- EventBud($emit,$on)
- props
- provide/inject
- vuex
- $attrs/$listeners适用于 隔代组件通信
- $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs"传入内部组件。通常配合 inheritAttrs 选项一起使用。
- $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners"传入内部组件
vue2和vue3的区别
-
setup:setup函数处于生命周期beforeCreated和created之间
-
生命周期函数不同
-
vue3支持组合式api,对ts支持更好
-
v3使用es6的proxy替代v2的Object.definePropert()代理数据
-
监听:vue3 proxy,vue2 Object.defineProperties
-
vue3对ts支持更好
-
vue3有更多的api,setup语法糖
vue全局变量和vuex的区别
- vuex是管理状态的库,存储项目中的一些数据,某一个组件更改了vuex中的数据,其他相关的组件也会得到快速更新
- 全局变量可以任意修改可能命名污染,但是vuex不会,每个组件可以根据自己vuex的变量名引用不受影响
- vuex处理复杂的项目有明显的优势(结构清晰,更正规),如果是小项目的话用全局变量也行
vue父组件和子组件生命周期函数的执行顺序
- 挂载:父beforeCreate --> 父caeated --> 父beforeMount --> 子beforeCreate --> 子created --> 子beforeMount --> 子mounted -->父mounted
- 更新:父beforeUpdate --> 子beforeUpdate --> 子updated --> 父updated
- 销毁:父beforeDestroy --> 子beforeDestroy --> 子destroyed --> 父destroyed
vue-router
有没有出现过:使用同一个组件(但传递不同的参数,获取数据),导航跳转后,数据还是旧的数据的情况?
- 这个问题是由于跳转前后,使用的是同一个组件造成的,因为跳转前后使用的是同一个组件,导致没有触发befoeCreate,created等一系列函数,而我们的请求放在created里就不会被触发
- 解决办法,用watch监听路由,把请求放到watch的回调函数里
watch: {
$route(to, from) {
console.log("路由变化", to.path);
if (to.path == "/practice-center/path1") {
this.getmessage("admin");
} else {
this.getmessage("student");
}
}
}
使用过 Vue SSR 吗?说说 SSR?
- SSR大致的意思就是vue在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的html 片段直接返回给客户端这个过程就叫做服务端渲染。
服务端渲染 SSR 的优缺点
-
优点:
- 更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
- 更快的内容到达时间(首屏加载更快):SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;
-
缺点:
- 更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
- 更多的服务器负载:渲染html片段交给服务端,那毫无疑问压力来到服务器这边
vue-router 路由模式有几种?
- hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
- history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;
- abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API(比方weex中就只能用这个,因为它是为开发app/小程序,不运行在浏览器里),路由会自动强制进入这个模式.
webpack篇
优化篇
图片懒加载
1. 语法糖
<img loading="lazy" />
2. img.getBoundingClientRect()
返回值为{ top, left, width, height, x, y }
top时小于屏幕高度document.documentElement.clientHeight的时候,就是图片出现在可视区域时
3. 判断图片是否出现在可视区域,替换src
document.documentElement.clientHeight + document.documentElement.scrollTop == $(img).offsetTop()时就是图片出现时
触底刷新
监听window.onscroll
document.documentElement.clientHeight
document.documentElement.scrollTop
document.documentElement.scrollHeight
屏幕高度 + 滚动条距上高度 = 滚动条高度时,即使触底时候
css优化,提高性能有那些方式?
- 多个css合并,尽量减少HTTP请求
- 抽象提取公共样式,减少代码量
- 将css文件放在页面最上面
- 避免使用CSS表达式
- 选择器优化嵌套,尽量避免层级过深
- 充分利用css继承属性,减少代码量
- 移除空的css规则
- 属性值为0时,不加单位
网页前端性能优化的方式有哪些?
https://www.cnblogs.com/chris-oil/p/9122633.html
- 压缩/合并 css, js, 图片(雪碧图),从而减少 http 请求次数
- 减少 dom 元素数量/访问(缓存 dom)
- 图片懒加载
- 静态资源另外用无 cookie 的域名,因为cookie每次请求都自动携带
- 巧用事件委托
- 样式表置顶、脚本置低(因为,script是并行加载的,要下载完了才会去解析后面的html结构)
在 vue 中有哪些优化项目的方式
- 代码层面:
- v-if,v-show,computed,watch 的使用
- addEventListener,$on事件销毁
- 图片/路由懒加载
- 按需引入第三方插件
- hash / history路由模式的选择,SSR / 预渲染
- 长久不变的大数据,可以用Object.freeze()去除Observer监听
data () {
return {
list: Object.freeze([
{ value: 1 },
{ value: 2 }
])
}
}
- webpack层面:
- 压缩图片/代码
- 提取公共代码
- sourceMap
- 浏览器层面:
- 开启gzip压缩
- 浏览器缓存
常用算法
防抖,节流
节流
const throtle = (callback, time) => {
let flag = true;
return function (event) {
if (!flag) return;
flag = false;
callback.call(this, event);
setTimeout(() => {
flag = true;
}, time);
};
};
防抖
const debounce = (callback, time) => {
let timer = null;
return function (event) {
if (timer !== null) clearTimeout(timer);
timer = setTimeout(() => {
callback.call(this, event);
}, time);
};
};
字符串中出现最多的元素,和次数
function Fun(str) {
let times = 0
let chart = ''
// 把排序后的str按内容分组(相同的在一组)
const regExp = /(\w)\1+/g
str.split('').sort().join('').replace(regExp, ($0, $1) => {
// $0是匹配到的所有元素,$1是元;eg:'aaa'的元是'a','abab'的元是'ab'
if ($0.length > times) {
times = $0.length
chart = $1
}
})
return { times, chart }
}