Q:1、CSS的position的属性一共有多少种,他们对应的表现是什么?
A:
CSS position属性用于指定一个元素在文档中的定位方式。top、right、bottom、left 属性则决定了该元素的最终位置。
属性值 | 描述 |
---|---|
absolute | 生成绝对定位的元素,相对于 static 定位以外的第一个父元素进行定位。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。 |
fixed | 生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。 |
relative | 生成相对定位的元素,相对于其正常位置进行定位。因此,“left:20” 会向元素的 LEFT 位置添加 20 像素。 |
static | 默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明)。 |
inhert | 规定应该从父元素继承 position 属性的值。 |
Q:2、你知道哪些本地存储的方案,他们各自的优缺点是什么?
A:
浏览器的本地存储主要分为Cookie、WebStorage、IndexDB
,其中WebStorage又可以分为localStorage和sessionStorage
Cookie:
存放4KB左右、每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题、可设置失效时间,没有设置的话,默认是关闭浏览器后失效localStorage:
可以保存5MB的信息、仅在客户端(即浏览器)中保存,不参与和服务器的通信、除非被手动清除,否则将会永久保存。sessionStorage:
可以保存5MB的信息、仅在客户端(即浏览器)中保存,不参与和服务器的通信、 仅在当前网页会话下有效,关闭页面或浏览器后就会被清除。IndexedDB:
IndexedDB是运行在浏览器中的非关系型数据库, 本质上是数据库,绝不是和刚才WebStorage的 5M 一个量级,理论上这个容量是没有上限的。Q:3、你知道浏览器的缓存机制嘛?在你的项目中是如何设置缓存策略的?
A:
浏览器缓存机制
浏览器缓存主要分为强缓存
和协商缓存
如何来检查是否命中缓存呢?通过相应的字段来进行,但是说起这个字段就有点门道了
强缓存
HTTP/1.0 :Expires
Expires: Wed, 22 Nov 2019 08:41:00 GMT
HTTP/1.1:Cache-Control
Cache-Control:max-age=3600
Cache-Control还有以下几个字段:
协商缓存
强缓存失效之后,浏览器在请求头中携带相应的缓存tag来向服务器发请求,由服务器根据这个tag,来决定是否使用缓存,这就是协商缓存。
具体来说,这样的缓存tag分为两种: Last-Modified 和 ETag。这两者各有优劣,并不存在谁对谁有绝对的优势,跟上面强缓存的两个 tag 不一样。
主要有Last-Modified
和ETag
两个字段。这两个字段都是服务端响应头中的字段
Last-Modified
即最后修改时间。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。
浏览器接收到后,如果再次请求,会在请求头中携带If-Modified-Since
字段,这个字段的值也就是服务器传来的最后修改时间。
服务器拿到请求头中的If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间对比:
如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
否则返回304,告诉浏览器直接用缓存。
ETag
ETag 是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头把这个值给浏览器。
浏览器接收到ETag的值,会在下次请求时,将这个值作为If-None-Match
这个字段的内容,并放到请求头中,然后发给服务器。
服务器接收到If-None-Match后,会跟服务器上该资源的ETag进行比对:
如果两者不一样,说明要更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
否则返回304,告诉浏览器直接用缓存。
Q:4、script标签的async属性和defer属性有什么作用?
A:
HTMl中的script中的async/defer有什么作用和区别
如果script标签是引用的外部js文件, 那就会有一个下载js文件这一过程, 为了不因为这个下载过程而阻塞页面解析与渲染, 我们需要一种机制来解决这一问题, 方法之一就是使用 defer和async属性
<script defer src="./test1.js"></script>
//console.log("defer是DOM载入完成后执行.")
<script async src="./test2.js"></script>
// console.log("async是新开一个进程, 下载完成后就暂停主进程的解析, 执行下载的脚本.")
使用async不能保证脚本的执行顺序, 而是谁先下载完, 就先执行谁, 因此async适用于脚本之间没有依赖关系的情况. 反之再用defer;
如果一个scritp标签同时有defer和async属性, 则defer失效, script的行为由async决定;
在脚本中还是不能使用document.write()方法.
总结起来, defer和async区别在于, 前者是在html解析完毕后按顺序执行, 而async是单独下载, 完成后立即执行.
绿色(解析/parsing)停止的地方就是碰到script标签的地方,js文件下载完,执行之后,html文件才再次继续进行解析;
问题在于,如果js文件比较大,这样会极大的阻碍网页页面的生成,页面出于短暂长时间空白,看起来页面像是卡在了那里,这种情况是我们需要极力避免的。
async 属性告诉我们HTML解析器(parser),它可以在后台下载这个JS文件,并且它可以继续向下解析当js文件在后台下载的时候,之后,只要js文件一下载完毕,这个时候如果解析工作还没有完成,还在解析, 那么立马停下手头的解析工作,开始执行js文件,执行完之后,再恢复解析工作,继续向下解析;如果此时解析完了,那就更不用说了,直接开始执行js文件。
问题在于,如果你header里面有多个script标签,肯定从上到下有个顺序吧,如果彼此互相没有依赖都能独立执行,那还好说,如果存在依赖关系,那么此刻先执行哪个文件,后执行哪个文件是有要求的时候,那么我们就没办法控制,因为哪个文件先执行,完全取决于哪个文件先下载完毕,先下载完毕的就先执行,后下载完毕的就后执行,所以不可避免的就会乱序执行起来。
defer 属性和async有点像,async是js文件刚下载完毕就开始执行,而defer是等待 HTML的解析所有都完毕之后,才会进行js文件的执行,并且这个js文件的执行是按照script标签定义的顺序来执行的,所以这就和在body中的最末尾定义普通没有任何属性的多个script标签一样,从上到下按顺序开始执行,并且没有阻塞HTML文件的解析,所以defer这个属性的添加,完美的解决了async的问题,还有没有属性却还定义在header中script标签,阻塞html文件解析的情况。
Q:5、从浏览器输入url到页面展示经历了什么?
A:
从输入URL到看到页面发生了什么
主要有以下几个过程
域名解析:
DNS域名解析的过程
DNS解析视频讲解
如果某个用户正在用浏览器mail.baidu.com的网址,当你敲下回车键的一瞬间:
1、检查浏览器缓存中是否存在该域名与IP地址的映射关系,如果有则解析结束,没有则继续
2、到系统本地查找映射关系,一般在hosts文件中,如果有则解析结束,否则继续
3、到本地域名服务器去查询,有则结束,否则继续
4、本地域名服务器查询根域名服务器,该过程并不会返回映射关系,只会告诉你去下级服务器(顶级域名服务器)查询
5、本地域名服务器查询顶级域名服务器(即com服务器),同样不会返回映射关系,只会引导你去二级域名服务器查询
6、本地域名服务器查询二级域名服务器(即baidu.com服务器),引导去三级域名服务器查询
7、本地域名服务器查询三级域名服务器(即mail.baidu.com服务器),此时已经是最后一级了,如果有则返回映射关系,则本地域名服务器加入自身的映射表中,方便下次查询或其他用户查找,同时返回给该用户的计算机,没有找到则网页报错
8、如果还有下级服务器,则依此方法进行查询,直至返回映射关系或报错
TCP四次挥手
数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,假设客户端主动关闭,服务器被动关闭。
浏览器的渲染流程
- 解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件
- CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成
RenderObject 树- 布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算
- 绘制 RenderObject 树 (paint),绘制页面的像素信息
- 浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面
Q:6、在你的项目中用过哪些性能方面的优化?
A:
https://juejin.cn/post/6892994632968306702
arr.splice()
splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。
arr.reduce()
reduce() 方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。
const arr = [1, 2, 3, 4]
const initialValue = 0
const sum = arr.reduce((previousValue, currentValue, currentIndex, array) => {
/*
previousValue:
上一次调用 callbackFn 时的返回值。在第一次调用时,
若指定了初始值 initialValue,其值则为 initialValue,否则为数组索引为 0 的元素 array[0]。
currentValue:
数组中正在处理的元素。在第一次调用时,若指定了初始值 initialValue,
其值则为数组索引为 0 的元素 array[0],否则为 array[1]。
currentIndex:
数组中正在处理的元素的索引。若指定了初始值 initialValue,则起始索引号为 0,否则从索引 1 起始。
array:用于遍历的数组。
*/
return previousValue + currentValue
}, initialValue)
console.log(sum);
// expected output: 10
// arr.reduce()函数的参数
// 1、callbackFn
// 一个 “reducer” 函数,包含四个参数:
// 2、initialValue 可选
// 作为第一次调用 callback 函数时参数 previousValue 的值。若指定了初始值 initialValue,则 currentValue 则将使用数组第一个元素;
// 否则 previousValue 将使用数组第一个元素,而 currentValue 将使用数组第二个元素。
// 返回值
// 使用 “reducer” 回调函数遍历整个数组后的结果。
Q:7、你是如何保证前端应用的稳定行的?比如代码规范,数据监控?
A:
Q:你做过哪些前端提升人效的解决方案,怎么去评估它的效果?
A:
一下午面了三面,面过了感觉我不回去,然后没发offer!
一面(1h)
事件冒泡和事件捕获区别?
事件冒泡:
假设我要点击的是div,点击后会一层一层的往上。
事件捕获:
事件捕获与事件冒泡完全相反。是从上至下到指定元素。
事件委托
利用事件冒泡和事件源对象进行处理
优点:
1、性能 不需要循环所有的元素一个个绑定事件
2、灵活 当有新的子元素时不需要重新绑定事件
我们有下面结构的一个列表:
<ul>
<li>1li>
<li>2li>
<li>3li>
<li>4li>
<li>5li>
ul>
如果我们想实现当每个li点击自身时,都在控制台打印自己的内容,要怎么做呢?
按照我们之前的理解,肯定是先获取li标签,利用for循环给每个li标签添加点击事件。
但是当我们了解了事件冒泡和事件对象之后,我们有了更好的写法:
let ul = document.getElementsByTagName('ul')[0]
ul.onclick = function(e){
console.log(e.target.innerText) // 1、 2、 3、 4、 5
}
addEventListener()
方法用来做事件监听
你可以使用 removeEventListener() 方法来移除事件的监听。
window.addEventListener("resize", function(){
document.getElementById("demo").innerHTML = sometext;
});
element.addEventListener(event, function, useCapture);
第一个参数是事件的类型 (如 “click” 或 “mousedown”).
注意:不要使用 “on” 前缀。 例如,使用 “click” ,而不是使用 “onclick”。
第二个参数是事件触发后调用的函数。(回调函数)
event.target
代表的是触发事件的元素,而event.currentTarget
代表的是那个绑定了事件处理函数的元素。
当第三个参数设置为true就在捕获过程中执行,反之就在冒泡过程中执行处理函数
第三个参数是个布尔值用于描述事件是冒泡还是捕获。该参数是可选的。
可能值:
true - 事件句柄在捕获阶段执行
false false是默认值。事件句柄在冒泡阶段执行
面试官:请实现一个大文件上传
大文件上传
XMLHttpRequest
的 upload.onprogress
对切片上传进度的监听断点续传
function myRandom (a, b) {
console.log(Math.floor(Math.random()*(b-a+1) + a))
return Math.floor(Math.random()*(b-a+1) + a)
}
myRandom(80, 90)
Vue组件之间的通信
1、bus总线传值
//bus.js
import Vue from 'vue';
export default new Vue;
//使用 兄弟A 传值
import bus from '路径'
bus.$emit('自定义事件名称',输出数据)
//使用 兄弟B 接值
import bus from '路径'
bus.on('自定义事件名',(res)=>{})
eventbus原理:
VUE
中eventBus
可以用来进行任何组件之间的通信,我们可以把eventBus当成一个管道,这个管道两端可以接好多组件,两端的任何一个组件都可以进行通信。其实这个管道就是Vue实例,实例中的$on, $off, $emit
方法来实现此功能。
2、常规子1传父->父传子2
3、vuex
父子组件通信: props; $parent / $children; provide / inject ; ref ; $attrs / $listeners
兄弟组件通信: eventBus ; vuex
跨级通信: eventBus;Vuex;provide / inject 、$attrs / $listeners
动态切换组件
有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里:
上述内容可以通过 Vue 的 元素加一个特殊的 is attribute 来实现:
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>
1、从ajax到fetch、axios
2、Axios 如何取消重复请求?
3、使用 AbortController 取消 Fetch 请求和事件监听
XMLHttpRequest.abort()
如果请求已被发出,则立刻中止请求。
Axios 是一个基于 Promise 的 HTTP 客户端,同时支持浏览器和 Node.js 环境。它是一个优秀的 HTTP 客户端,被广泛地应用在大量的 Web 项目中。对于浏览器环境来说,Axios 底层是利用 XMLHttpRequest 对象来发起 HTTP 请求。如果要取消请求的话,我们可以通过调用 XMLHttpRequest 对象上的 abort 方法来取消请求
axios的请求如何取消?
而对于 Axios 来说,我们可以通过 Axios 内部提供的 CancelToken 来取消请求:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post('/user/12345', {
name: 'semlinker'
}, {
cancelToken: source.token
})
source.cancel('Operation canceled by the user.'); // 取消请求,参数是可选的
此外,你也可以通过调用 CancelToken 的构造函数来创建 CancelToken,具体如下所示:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
});
cancel(); // 取消请求
当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
watch: {
// 每当 question 发生变化时,该函数将会执行
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer()
}
}
},
除了 watch 选项之外,你还可以使用命令式的 vm.$watch API。
$watch 返回一个取消侦听函数,用来停止触发回调:
const vm = app.mount('#app')
const unwatch = vm.$watch('a', cb)
// later, teardown the watcher
unwatch()
解答:
闭包
闭包是指有权访问另一个函数作用域中变量的函数
var a = 0
function foo(){
var b =14
function fo(){
console.log(a, b)
}
fo()
}
foo()
这里的子函数 fo 内存就存在外部作用域的引用 a, b,所以这就会产生闭包
闭包的应用:
1、面试官:你真的了解v-mode吗?
2、v-model可以在组件中用吗?
面试官:谈谈你对虚拟DOM的理解
Vue核心之虚拟DOM
其实回调函数就是异步编程的一种解决方案
js中的异步解决方案
原理
讲项目,讲技术栈
兼容性问题
2022/04/15 08:37:56
,而安卓不是封装vue组件的一些小技巧
keep-alive:有activated 和 deactivated两个生命周期方法
使用方式:
// App.vue
<div class="app">
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
在我们使用vue进行开发的过程中,可能会遇到一种情况:当生成vue实例后,当再次给数据赋值时,有时候并不会自动更新到视图上去; 当我们去看vue文档的时候,会发现有这么一句话:如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。 如下代码,给 student对象新增 age 属性
data () {
return {
student: {
name: '',
sex: ''
}
}
}
mounted () { // ——钩子函数,实例挂载之后
this.student.age = 24
}
受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。
正确写法:this.$set(this.data,”key”,value’)
mounted () {
this.$set(this.student,"age", 24)
}
Vue实现面包屑
const webpack = require('webpack')
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const BundleAnalyzerConfig = process.env.npm_config_report ? [new BundleAnalyzerPlugin()] : []
const path = require('path')
module.exports = {
publicPath: './',
filenameHashing: true,
devServer: {
host: 'local.intra.xiaojukeji.com',
port: '8777',
hot: true, // 开启热更新,提高开发效率
headers: {
'Access-Control-Allow-Origin': '*'
},
// 配置代理
proxy: {
'/api': {
target: 'http://czp-test.intra.xiaojukeji.com', // 想要访问接口域名
changeOrigin: true, // 开启跨域,在本地创建一个虚拟服务,然后发送请求的数据,并同时接收请求的数据,这样服务端和服务端进行数据交互就不会有问题
pathRewrite: {
'^/api': '' // 利用这个地面的值拼接上target里面的地址
}
}
}
},
configureWebpack: {
devtool: 'source-map',
plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new HtmlWebpackExternalsPlugin({
externals: [{
module: 'waterMark',
entry: 'https://sec-aegisfe.didistatic.com/static/aegisfe/water-mark1.0.js?v=' + Date.now(),
global: 'waterMark'
}]
}),
...BundleAnalyzerConfig
],
optimization: {
splitChunks: {
chunks: 'all',
maxInitialRequests: 10,
maxAsyncRequests: 10,
minSize: 30 * 1024,
minChunks: 1,
name: true,
cacheGroups: {
'chunk-vendors_1': {
name: 'chunk-vendors_1',
test: /[\\/]node_modules[\\/](mime-db|elliptic|lodash|asn1.js|core-js)/,
priority: -1
},
'chunk-vendors_2': {
name: 'chunk-vendors_2',
test: /[\\/]node_modules[\\/](xgplayer|minio|moment|zrender)/,
priority: -2
},
echarts: {
name: 'echarts',
test: /[\\/]node_modules[\\/]echarts/,
priority: -2
},
vue: {
name: 'vue',
test: /[\\/]node_modules[\\/](vue|vuex)/,
priority: -2
},
antd: {
name: 'antd',
test: /ant-design/,
priority: -2
},
iview: {
name: 'iview',
test: /[\\/]node_modules[\\/]iview/,
priority: -2
}
}
},
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
}
},
resolve: {
alias: {
'@src': path.resolve('./src'),
'@api': path.resolve('./src/api'),
'@assets': path.resolve('./src/assets'),
'@common': path.resolve('./src/common'),
'@components': path.resolve('./src/components'),
'@router': path.resolve('./src/router'),
'@store': path.resolve('./src/store'),
'@views': path.resolve('./src/views')
}
}
},
productionSourceMap: false,
chainWebpack: config => {
config
.plugin('html')
.tap(args => {
args[0].title = '车载屏管理系统'
return args
})
}
}
思路:给div盒子设置宽高为0,然后设置border的宽度,然后给任意三边的颜色设置为transparent即可分别实现任一方向的三角形。
.sanjiao {
width: 0;
height: 0;
border-top: 100px solid #f00;
border-right: 100px solid #0f0;
border-bottom: 100px solid #00f;
border-left: 100px solid #ff0;
}
上述代码实现的效果如下:
然后我们可以通过给任意三边的颜色设置为 transparent 即可分别实现任一方向的三角形。通过设置某条边的宽度比其它边宽,来调整三角形的高度。
.triangle {
width: 0;
height: 0;
border: 100px solid transparent;
border-bottom: 200px solid #0ff;
}
width: 0;
height: 0;
border-left: 69px solid transparent;
border-right: 69px solid transparent;
border-bottom: 120px solid skyblue;
前端性能优化
p { margin: 1px 2px 3px 4px; }
target { background-color: #ffffff; }
target { background: #fff; }
padding: 0.2em;
margin: 20.0em;
avalue: 0px;
// 优化后
padding: .2em;
margin: 20em;
avalue: 0;
加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
history的一些API
hash路由和history路由的区别
Vuerouter两种模式的区别
面试官:你了解过移动端适配嘛?
6、北京红棉小冰科技有限公司
position的含义是指定位类型,取值类型可以有:static、relative、absolute、fixed、inherit和sticky,这里sticky是CSS3新发布的一个属性。
最容易忽略的属性position: sticky
fixed定位的一些特殊情况
居中的解决方案
深拷贝与浅拷贝
赋值和深/浅拷贝的区别
这三者的区别如下,不过比较的前提都是针对引用类型:
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
浅拷贝的实现方式
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
该函数库也有提供_.clone用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
深拷贝实现方法
let arr = [1, 3, {
username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)
这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
JSON.parse(JSON.stringify(obj))深拷贝的问题
JSON.parse(JSON.stringify(obj))
之后,时间对象变成了字符串。RegExp、Error
对象,则序列化的结果将只得到空对象。JSON.stringify()
只能序列化对象的可枚举的自有属性。如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))
深拷贝后,会丢弃对象的constructor
。function Person (name) {
this.name = 20
}
const lili = new Person('lili')
let a = {
data0: '1',
date1: [new Date('2020-03-01'), new Date('2020-03-05')],
data2: new RegExp('\\w+'),
data3: new Error('1'),
data4: undefined,
data5: function () {
console.log(1)
},
data6: NaN,
data7: lili
}
let b = JSON.parse(JSON.stringify(a))
console.log(b)
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
// 可以避免循环引用
function deepClone (obj, hash = new WeakMap()) {
if (hash.has(obj)) {
// 如果已经有这个对象,直接返回这个对象
return hash.get(obj)
}
const targetObj = Array.isArray(obj) ? [] : {}
hash.set(obj, targetObj)
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
if (typeof obj[i] === 'object') {
targetObj[i] = deepClone(obj[i], hash)
} else {
targetObj[i] = obj[i]
}
}
}
return targetObj
}
nginx 中 etag 由响应头的 Last-Modified
与 Content-Length
表示为十六进制组合而成。
$ curl --head 10.97.109.49
HTTP/1.1 200 OK
Server: nginx/1.16.0
Date: Tue, 10 Dec 2019 06:45:24 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 23 Apr 2019 10:18:21 GMT
Connection: keep-alive
ETag: "5cbee66d-264"
Accept-Ranges: bytes
// 1、前2种的html布局
<div class="outer">
<div class="left">div>
<div class="right">div>
<div class="center"> this is centerdiv>
div>
// 2、后四种的html布局
<div class="outer">
<div class="left">div>
<div class="center"> this is centerdiv>
<div class="right">div>
div>
使用float布局
左右中三个盒子,左右宽度各300,并且左右浮动
.outer div{
min-height: 200px;
}
.left {
float: left;
width: 300px;
background-color: pink;
}
.right {
float: right;
width: 300px;
background-color: skyblue;
}
.center {
background-color: #ccc;
}
使用position定位
三个盒子都绝对定位,中间盒子左右各定位300,左盒子左边0,右盒子右边0
.outer div{
min-height: 200px;
position: absolute;
}
.left {
left: 0;
width: 300px;
background-color: pink;
}
.right {
right: 0;
width: 300px;
background-color: skyblue;
}
.center {
right: 300px;
left: 300px;
background-color: #ccc;
}
使用flex布局
外层盒子flex布局,内层盒子中间flex:1,其它的固定宽度300px
.outer {
display: flex;
min-height: 200px;
}
.left {
width: 300px;
background-color: pink;
}
.right {
width: 300px;
background-color: skyblue;
}
.center {
flex: 1;
background-color: #ccc;
}
使用table布局
需要给外层盒子定义display:table,内层盒子定义diplay:table-cell,再分别给左右宽度即可,中间就可以自适应
.outer {
display: table;
width: 100%;
min-height: 200px;
}
.outer div{
display: table-cell;
}
.left {
width: 300px;
background-color: pink;
}
.right {
width: 300px;
background-color: skyblue;
}
.center {
background-color: #ccc;
}
grid布局
.outer {
display: grid;
grid-template-rows: 400px;
grid-template-columns: 300px auto 300px;
}
.left {
background-color: pink;
}
.right {
background-color: skyblue;
}
.center {
background-color: #ccc;
}
都2022了还不知道箭头函数和普通函数的区别
BFC 全称:Block Formatting Context, 名为 “块级格式化上下文”。
W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。
简单来说就是,BFC是一个完全独立的空间(布局环境),让空间里的子元素不会影响到外面的布局。那么怎么使用BFC呢,BFC可以看做是一个CSS元素属性
怎么触发BFC?
BFC有哪些规则?
可以用来解决哪些问题
margin重叠的规则?
margin同正,则取最大值;
margin同负,则取最小值;
margin一正一负,则取二者之和。
在做算术运算时,JS 会先把十进制数转换成二进制数后再计算,十进制小数转二进制数的方式是 x 2 取整,0.1 和 0.2 的二进制数是个无限循环小数。
而 JS 中表示一个数字只有 64 位,其中精度位(有效数位)只有 52 位,所以当出现无限循环小数时,会通过 0 舍 1 入 的规则截取前 52 位(类似十进制的四舍五入),这样导致了精度位的丢失。0.1 实际参与计算的数变大了,0.2 参与计算的数变小了,所以运算结果不一定等于 0.3。
console.log( 0.1 + 0.2) //0.30000000000000004
为什么0.1+0.2 != 0.2
彻底搞懂为什么0.1+0.2 != 0.3
Etag主要为了解决Last-Modified无法解决的一些问题:
defer:这个属性表示脚本在执行的时候不会改变页面的结构。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在script元素上设置defer属性,相当于告诉浏览器立即下载,但延迟执行。
async:async属性与defer类似。当然,它们两者也都只适用于外部脚本,都会告诉浏览器立即开始下载。不过,与defer不同的 是,标记为async的脚本并不保证能按照它们出现的次序执行。
若有多个script含defer属性,推迟执行的脚本不一定总会按顺序执行或者在DOMContentLoaded事件之前执行,因此最好只包含一个这样的脚本。
typeof只能准确判断原始数据类型和函数,无法精确判断出引用数据类型(统统返回 object)。有一点需要注意,调用typeof null返回的是object,这是因为特殊值null被认为是一个对空对象的引用(也叫空对象指针)。
console.log(typeof 666); // number
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof NaN) // number
console.log(typeof Symbol()); // symbol
console.log(typeof '1233') // string
console.log(typeof 1n); // bigint
console.log(typeof function name() {}); // function
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof new String('xxx')); // object
console.log(typeof null); // object
console.log(typeof new RegExp()) // object
console.log(typeof new Error()) // object
console.log(typeof new Map()) // object
console.log(typeof new WeakMap()) // object
console.log(typeof new Set()) // object
console.log(typeof new WeakSet()) // object
function getType (num) {
return Object.prototype.toString.call(num).toLowerCase().substring(8, Object.prototype.toString.call(num).length-1)
}
console.log('------------------')
console.log(getType([])); // array
console.log(getType({})); // object
console.log(getType(new String('xxx'))); // string
console.log(getType(null)); // null
console.log(getType(new RegExp())) // regexp
console.log(getType(new Error())) // error
console.log(getType(new Map())) // map
console.log(getType(new WeakMap())) // weakmap
console.log(getType(new Set())) // set
console.log(getType(new WeakSet())) // weakset
事件循环
console.log('script start')
async function async1() {
await async2() // await后面是简单的输出
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// script start
// async2 end
// Promise
// script end
// async1 end
// promise1
// promise2
// setTimeout
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
})
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
script start
async2 end
Promise
script end
async2 end1
promise1
promise2
async1 end
setTimeout
https中的数字证书
采用二进制协议
HTTP/1.1 版的头信息肯定是文本(ASCII编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为”帧”:头信息帧和数据帧。二进制协议解析起来更高效、“线上”更紧凑,更重要的是错误更少。
服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段。
多路复用
HTTP1.1 如果要同时发起多个请求,就得建立多个 TCP 连接,因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。
在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。同一个请求和响应用一个流来表示,并有唯一的流 ID 来标识。
多个请求和响应在 TCP 连接中可以乱序发送,到达目的地后再通过流 ID 重新组建。
首部压缩
HTTP 协议是没有状态,导致每次请求都必须附上所有信息。所以,请求的很多头字段都是重复的,比如Cookie,一样的内容每次请求都必须附带,这会浪费很多带宽,也影响速度。
对于相同的头部,不必再通过请求发送,只需发送一次;
HTTP/2 对这一点做了优化,引入了头信息压缩机制;
一方面,头信息使用gzip或compress压缩后再发送;
另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,产生一个索引号,之后就不发送同样字段了,只需发送索引号。
服务器推送
HTTP2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。
例如当浏览器请求一个网站时,除了返回 HTML 页面外,服务器还可以根据 HTML 页面中的资源的 URL,来提前推送资源。
http和https的区别
const a = new Promise((resolve, reject) => {
console.log('promise1')
resolve()
}).then(() => {
console.log('promise2')
})
setTimeout(() => {
console.log('timeout')
})
const b = new Promise(async (resolve, reject) => {
await a
console.log('after1')
await b
console.log('after2')
resolve()
})
console.log('end')
let、const
箭头函数 =>
箭头函数没有自己的arguments、没有prototype属性、不能作为构造函数、没有自己的this,箭头函数的this指向即使使用call、apply、bind也无法改变
Map、Set数据结构
iterator迭代器
对于可迭代的数据解构,ES6在内部部署了一个[Symbol.iterator]属性,它是一个函数,执行后会返回iterator对象(也叫迭代器对象),而生成iterator对象[Symbol.iterator]属性叫iterator接口,有这个接口的数据结构即被视为可迭代的
数组中的Symbol.iterator方法(iterator接口)默认部署在数组原型上:
默认部署iterator接口的数据结构有以下几个,注意普通对象默认是没有iterator接口的(可以自己创建iterator接口让普通对象也可以迭代)
Array
Map
Set
String
TypedArray(类数组)
函数的 arguments 对象
NodeList 对象
next方法返回又会返回一个对象,有value和done两个属性,value即每次迭代之后返回的值,而done表示是否还需要再次循环,可以看到当value为undefined时,done为true表示循环终止
可迭代的数据结构会有一个[Symbol.iterator]
方法
[Symbol.iterator]
执行后返回一个iterator
对象
iterator对象有一个next方法
执行一次next方法(消耗一次迭代器)会返回一个有value,done属性的对象
解构赋值
let { res } = { res: 'msg' }
let arr = [1,2,3,4]
[...arr] // [1,2,3,4]
function func1 (a =1) {}
Vue.$nextTick你真的懂了吗???
this.$ nextTick–将回调延迟到下次DOM更新循环之后执行。
在修改数据之后立即使用它,然后等待DOM更新。官方给出的this.$nextTick的作用是:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
Vue. $nexttick主要用来获取数据改变之后的dom结构,放在其回调函数中的操作不会立即执行,而是等数据更新,DOM更新完之后才开始执行,这样拿到的是最新的数据
State
Getters
Mutations
Actions
Modules
state,getters,mutation,action
Vue2和Vue3的数据双向绑定
Vue数据双向绑定
Object.defineProperty
监听的一些缺点:
push、unshift
方法增加的元素,也无法监听在有宏任务和微任务的概念加入后,JS代码的整体执行逻辑变为:
堆与栈实际上是操作系统对进程占用的内存空间的两种管理方式,主要有如下几种区别:
滴滴开源小程序框架 MPX
MPX
Mpx是一款致力于提高小程序开发体验的增强型小程序框架,通过Mpx,我们能够以最先进的web开发体验(Vue + Webpack)来开发生产性能深度优化的小程序,Mpx具有以下一些优秀特性:
designWidth
设计稿宽度,单位为px。默认值为750px。
mpx会基于小程序标准的屏幕宽度baseWidth
750rpx,与option.designWidth
计算出一个转换比例transRatio
转换比例的计算方式为transRatio = (baseWidth / designWidth)
。精度为小数点后2位四舍五入
所有生效的rpx注释样式中的px会乘上transRatio得出最终的rpx值
/* 转换前:designWidth = 1280 */
.btn {
width: 200px;
height: 100px;
}
/* 转换后: transRatio = 0.59 */
.btn {
width: 118rpx;
height: 59rpx;
}
CSS预处理器
CSS预处理器Sass为主
主要有变量、常量、嵌套、混入(@mixin)、函数等功能
CSS预处理器主要有Sass、Less、Stylus
Less 的基本语法属于CSS 风格
Sass,stylus 相比之下更激进一些, 可以利用缩进,空格和换行来减少需要输入的字符
不过区别在于 Sass, stylus同时兼容CSS风格的代码
H5页面适配安全区
body {
padding: constant(safe-area-inset-top)
constant(safe-area-inset-right)
constant(safe-area-inset-bottom)
constant(safe-area-inset-left);
// constant在iOS<11.2的版本中生效
padding: env(safe-area-inset-top)
env(safe-area-inset-right)
env(safe-area-inset-bottom)
env(safe-area-inset-left);
//env在iOS>=11.2的版本中生效
}
延伸: meta标签有一个属性,叫viewport-fit,顾名思义,就是网页内容在设备可视窗口的填充方式。有两种填充方式:
cover:
网页内容完全覆盖可视窗口
contain:
可视窗口完全包含网页内容
我们做安全区的适配前,需要把我们的viewport-fit设置成cover之后,再去做padding的适配。
也就是说需要把整个网页内容撑满,在撑满的基础上再去设置上下左右的间距,这样就一步一步比较严谨的做好了我们的安全区适配。
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
Vue路由守卫
1、全局守卫
vue-router全局有三个守卫:
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子');
});
to和from是将要进入和将要离开的路由对象,路由对象指的是平时通过this.$route获取到的路由对象。
next:Function 这个参数是个函数,且必须调用,否则不能进入路由(页面空白)。
2、路由组件内的守卫:
beforeRouteEnter (to, from, next) {
// 在路由独享守卫后调用 不!能!获取组件实例 `this`,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 `this`
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用,可以访问组件实例 `this`
}
3、路由独享守卫
如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
// ...
}
}
]
})
4、路由钩子函数的错误捕获
如果我们在全局守卫/路由独享守卫/组件路由守卫的钩子函数中有错误,可以这样捕获:
router.onError(callback => {
// 2.4.0新增 并不常用,了解一下就可以了
console.log(callback, 'callback');
});
路由传参数有三种方式
在路由的path里面写参数
this.$router.push({
path:`/home/${id}`,
})
// 路由配置
{
path:"/home/:id",
name:"Home",
component:Home
}
在Home组件中获取参数值
this.$route.params.id
params传参数
通过name来匹配路由,通过param来传递参数
this.$router.push({
name:'Home',
params:{
id:id
}
})
用params传递参数,不使用:/id
{
path:'/home',
name:Home,
component:Home
}
Home组件中获取参数
this.$route.params.id
query传参数
this.$router.push({
path:'/home',
query:{
id:id
}
})
路由配置
{
path:'/home',
name:Home,
component:Home
}
获取参数的方法
this.$route.query.id
params传参,必须使用命名路由的方式传参;
params传参,不会显示在地址栏上,会保存在内存中,刷新会丢失,可以配合本地存储进行使用;
query的参数会显示在地址栏上,不会丢失;
computed: {
// 计算属性对数据进行处理
frontEndPageChange() {
let start = (currentPage - 1) * pageSize;
if (start >= thistableData.length) start = 0;
let end = currentPage * pageSize;
if (end >= thistableData.length) end = thistableData.length;
return thistableDataslice(start, end);
}
}
前端登录鉴权逻辑
1.v-for的优先级高于v-if
原因:v-for比v-if优先级高,所以使用的话,每次v-for都会执行v-if,造成不必要的计算,影响性能,尤其是当前需要渲染很小一部分的时候。
<ul>
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
</ul>
如上情况,即使有很多user 但是只要有一个需要使用v-if,也需要循环整个数组,这在性能上是极大的浪费。
但是我们可以使用computed计算属性解决:
<div>
<div v-for="(user,index) in activeUsers" :key="user.index" >
{{ user.name }}
</div>
</div>
data () { // 业务逻辑里面定义的数据
return {
users,: [{
name: '111111',
isShow: true
}, {
name: '22222',
isShow: false
}]
}
}
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isShow;//返回isShow=true的项,添加到activeUsers数组
})
}
}
// 1:全局环境下this默认指向window
console.log(this) // window
// 2:函数独立调用,函数中的this也默认指向window
function getThis () {
console.log(this) // window
}
getThis()
//3:被嵌套的函数独立调用的时候,this默认指向了window
let obj1 = {
a: 2,
foo: function () {
console.log(this) // obj1
function test () {
console.log(this) // window
}
test()
},
foo2: () => {
console.log(this) // window
},
// 闭包函数中的this默认指向window
foo3: function () {
return function () {
console.log(this) // window
}
},
foo4: function () {
return () => {
console.log(this) // obj1
}
}
}
obj1.foo()
obj1.foo2()
obj1.foo3()()
obj1.foo4()()
// 4:IIFE自执行函数内的this默认指向window
let a = 10
function myFoo () {
(function test(){
console.log(this) // window
})()
}
let obj2 = {
a: 2,
foo: myFoo
}
obj2.foo()
let obj1 = {
name: 'obj1',
fun1: function () {
console.log(this.name)
},
fun2: () => {
console.log(this.name)
},
fun3: function () {
return function () {
console.log(this.name)
}
},
fun4: function () {
return () => {
console.log(this.name)
}
}
}
let obj2 = {
name: '2323232'
}
obj1.fun1() // obj1
obj1.fun2() // undefine
obj1.fun3()() // undefine
obj1.fun4()() // obj1
console.log('--------------------------------------')
obj1.fun1.call(obj2) // 2323232
obj1.fun2.call(obj2) // undefine
obj1.fun3().call(obj2) // 2323232
obj1.fun4().call(obj2) // obj1
async、await会阻塞其后面内容的输出
promise不会阻塞其后面内容的输出
async function test1 () {
console.log('1111111111')
const res = await test2()
console.log('res', res)
console.log('22222222222')
}
async function test2 () {
console.log('test2')
}
console.log('scri1')
setTimeout(() =>{
console.log('123')
}, 1000)
test1()
new Promise((resolve, reject) => {
console.log('before Promise')
resolve('promise')
console.log('after Promise')
}).then(res => {
console.log(res)
})
console.log('scri2')
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
重排:
当DOM的变化影响了元素的几何信息(DOM对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
回流就好比向河里(文档流)扔了一块石头(dom变化),激起涟漪,然后引起周边水流受到波及,所以叫做回流
深浅拷贝
js的数据类型主要有基本数据类型
和引用数据类型
两种
基本数据类:
String、Number、Boolean、Null、Undefined、Symbol
引用数据类型:
Object(Object、Array、Function)
javascript的变量的存储方式–栈(stack)和堆(heap)
栈:自动分配内存空间,系统自动释放,里面存放的是基本类型的值和引用类型的地址
堆:动态分配的内存,大小不定,也不会自动释放。里面存放引用类型的值。
浅拷贝—浅拷贝是指复制对象的时候,只对第一层键值对进行独立的复制,如果对象内还有对象,则只能复制嵌套对象的地址深拷贝—深拷贝是指复制对象的时候完全的拷贝一份对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。其实只要递归下去,把那些属性的值仍然是对象的再次进入对象内部一 一进行复制即可。
let a = 1
let b = a
a = 2
console.log(b) // 1
let obj1 = {
a: 123,
b: {
dataB: 'kell'
}
}
let obj2 = obj1
obj1.a = 456
console.log(obj2.a) // 456
let obj3 = {
a: 123,
b: {
dataB: 'kell',
olo: {
po: 'okoko'
}
}
}
let obj4 = Object.assign({}, obj3)
obj3.a = 90
console.log(obj4.a) // 123
obj3.b.dataB = 'koko'
console.log(obj4.b.dataB) // koko
let obj5 = {...obj3}
console.log(obj5) // { a: 90, b: { dataB: 'koko', olo: { po: 'okoko' } } }
对象浅拷贝的方式:
let obj4 = Object.assign({}, obj3)
。Object.assign()
方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。let obj5 = {...obj3}
数组的浅拷贝方式:
let arr = ['one', 'two', 'three'];
let newArr = arr.concat();
let newArr = arr.slice();
正则匹配所有域名及其子域名
const regStr = /^([a-zA-Z\d-_\*@]+\.)*baidu\.com$/
let str = 'wenku.baidu.com'
let str2 = 'baidu.com'
console.log(regStr.test(str))
console.log(regStr.test(str2))
比如:
const arr = [{
id: 0,
pid: -1
}, {
id: 1,
pid: 0
}, {
id: 2,
pid: 0
}, {
id: 3,
pid: 1
}, {
id: 4,
pid: 2
}];
经过处理后转化成:
[{
id: 0,
pid: -1,
children: [{
id: 1,
pid: 0,
children: [{
id: 3,
pid: 1,
children: []
}]
}, {
id: 2,
pid: 0,
children: [{
id: 4,
pid: 2
}]
}]
}]
处理的方法如下:
function toTree(data) {
let result = []
if (!Array.isArray(data)) {
return result
}
data.forEach(item => {
delete item.children;
});
let map = {};
data.forEach(item => {
map[item.id] = item;
});
console.log(map)
data.forEach(item => {
let parent = map[item.pid];
console.log('parent', parent)
if (parent) {
(parent.children || (parent.children = [])).push(item);
} else {
result.push(item);
}
});
console.log(result)
return result;
}
toTree(arr)
CSS的可继承属性
font、font-size、font-family、font-style、font-variant、font-stretch、font-size-adjust
text-algin、text-indent、line-height、word-spacing、letter-spacing、text-transform、color、direction
正则表达式