前端知识问答
Brian Lv3

HTML

link标签的行内属性 deffer和async的含义和区别

当script标签设置这两个行内属性后,js文件将会异步加载,不会阻塞dom的渲染。

它们的区别是:defer会在endanger渲染完成后DOMContentLoaded之前按照顺序执行。而async只要加载完就会立即执行

CSS

移动端的适配

参考 https://www.jianshu.com/p/b13d811a6a76

sass/less/stylus如何实现主题切换

基本的思路:把不同主题的样式代码打包在一起,通过不同的主题class来实现主题切换。这个class可以加载body上或者根组件上,也可以加载到具体的元素或者更小的模块上。

下面看看sass的实现

首先,创建一个theme.scss文件

1
2
3
4
5
6
7
8
9
10
$light{
primaryColor:white;
};
$dark{
primaryColor:black;
}
$themes:{
light:$light,
dark:$dark
}

然后创建一个themify.scss

1
2
3
4
5
6
7
8
9
10
11
12
@import './theme.scss'
@mixin themify($themes:$themes){
@each $them-name,$theme-map in $themes {
$theme-map:$theme-map !global //局部变量提升为全局变量
body[data-theme=#{$theme-name}] & {
@content;
}
}
}
@function themed($key){
@return map-get($theme-map,$key)
}

使用:

1
2
3
4
5
6
@import '/themify.scss'
.container{
@include themify($themes){
color:themed('primaryColor')
}
}

实现一个三栏布局

三栏布局有很多种方法:利用绝对定位、浮动、bfc,flex、table等都可以实现

具体可参考 https://zhuanlan.zhihu.com/p/25070186

哪些css属性会自动继承,哪些不会?具体讲讲

字体、文本、可见性属性(opacity visibility)等大多是自动继承属性;盒子模型、背景、定位、生成内容(如content)、轮廓等一般是无继承属性。需要注意:a标签的字体颜色不能被继承,h1-h6的字体大小也不能被继承。

什么是bfc

bfc即块级格式化上下文,它会形成相对于其容器外一个独立的空间,其内部的子元素不会影响到外部的元素,同时不会和float的元素重叠。bfc可应用于防止margin坍塌、实现多栏布局、清除浮动等场景。触发bfc的条件/属性:html、float、值不为visible的overflow、display为inline-block、table、inline-table、table-cell、table-caption、flex、inline-flex、grid、inline-grid.

javascript

普通函数和箭头函数的区别

  • 箭头函数没有arguments绑定,取而代之用rest参数…解决。但是,箭头函数可以访问最近的非箭头函数的arguments对象。
  • 箭头函数没有自己的this,它的this在生命周期中是保持不变的,始终与最近的非箭头函数中的this的值绑定。
  • 箭头函数不能作为构造函数,也就是不能被new,
  • 箭头函数不能存在重复命名的参数
  • 箭头函数没有原型属性
  • 箭头函数不能当做Generator函数,不能使用yield关键字

weakSet和weakMap的应用场景

  • weakSet的成员只能是对象,不能是其他类型。weakSet中的对象都是弱引用,因此垃圾回收机制不考虑weakSet对该对象的引用,如果其他对象不再引用该对象,垃圾回收机制将会自动回收该对象所占用的内存,而不考虑该对象还存在于weakSet中。使用场景:用来存储dom节点,而不用担心这些节点从文档移除会引发内存泄露。
  • weakMap只能接受对象作为键名,不接受其他类型的值作为键名。weakMap同样不会妨碍垃圾回收机制。
  • 使用场景:在网页的dom元素上添加数据,就可以使用weakMap结构,当该dom元素被清除,其对应的weakMap记录会被自动移除。

js如何比较两个对象是否相等

通过遍历一层一层比较值是否相等,当前层遇到对象,则继续遍历,直到最深一层的值为基本类型。

es6的继承

extends

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// es6继承
class Animal {
//构造函数,里面写上对象的属性
constructor(props) {
this.name = props.name || 'Unknown';
}
//方法写在后面
eat() {//父类共有的方法
console.log(this.name + " will eat pests.");
}
}

//class继承
class Bird extends Animal {
//构造函数
constructor(props,myAttribute) {//props是继承过来的属性,myAttribute是自己的属性
//调用实现父类的构造函数
super(props)//相当于获得父类的this指向
this.type = props.type || "Unknown";//父类的属性,也可写在父类中
this.attr = myAttribute;//自己的私有属性
}

fly() {//自己私有的方法
console.log(this.name + " are friendly to people.");
}
myattr() {//自己私有的方法
console.log(this.type+'---'+this.attr);
}
}

//通过new实例化
var myBird = new Bird({
name: '小燕子',
type: 'Egg animal'//卵生动物
},'Bird class')
myBird.eat()
myBird.fly()
myBird.myattr()

object.assign和…拓展符的区别

本质上没有区别,object.assign等同于…拓展符。

谈谈promise

Promise是一种比较常用的异步编程方案。它有三种状态:

pending,fulfiled,rejected,分别表示等待,完成,失败。状态的改变只有两种pending->fulfiled,pending->rejected,这个过程是不可逆的。创建一个Promise实例时需要传入一个函数,这个函数的入参分别是resolve和reject两个函数,用来修改promise的状态。执行resolve后状态为fulfiled,执行reject后状态改为rejected,举个常见的例子:

1
2
3
4
5
6
7
8
9
const p1= new Promise((resolve,reject)=>{
setTimeout(()=>{
if(isGood){
resolve('成功了')
}else{
reject('失败了')
}
})
})

然后通过链式调用的方式去处理状态变更后的结果:

1
2
3
p1.then(res=>{dosth})
.catch(err=>{dosth})
.finally(data=>{dosth})

Promise还提供了一些静态方法,用于做一些其他有趣的处理

1
Promise.all([p1,p2,p3,...])

该方法接受一个promise组成的数组,所有的promise对象都成功才会触发fulfiled,只要有一个失败就会触发rejected

1
Promise.allSettled([p1,p2,p3,...])

只要所有的promise都settled(fulfiled或rejected),返回一个promise,携带一个对象数组,每个对象对应各个promise的结果。

1
Promise.race([p1,p2,p3,...])

返回一个promise,只要有一个promise的状态改为fulfiled或rejected,则立即触发其自身的fulfied或者rejected。

1
Promise.resolve(value)

返回一个状态为成功的promise对象。

1
Promise.reject(error)

返回一个状态为失败的promise对象。

axios请求撤销,重连

撤销主要是利用axios.CancelToken,axios官网介绍了两种使用方法:

方法一(多用于取消单个接口):

通过传入一个执行函数到CancelToken构造函数来创建一个cancel token

1
2
3
4
5
6
7
8
9
10
11
import axios from 'axios'
let cancel
export function getSth(){
return axios({
url:'/abc',
method:'get'.
cancelToken:new axios.CancelToken(funtion executor(c){
cancel = c
})
})
}

需要取消的时候,直接调用cancel即可

方法二(撤销多个请求):

使用CancelToken.source工厂方法创建一个cancel token。

1
2
3
4
5
6
7
8
9
10
import axios from 'axios'
const CancelToken = axios.CancelToken
const source = CancelToken.source()
export function getSth(){
return axios({
url:'/abc',
method:'get',
cancelToken:source.token
})
}

取消调用source.cancel()

关于重连,即网络出现不佳或者请求失败的重新连接机制。

axios的重连可通过响应拦截器来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
request.interceptors.response.use(
response =>response.data || {},
err =>{
let config = err.config
if(!config) return Promise.reject(err)
config._retryCount = config._retryCount || 0
//设置重连3次
if(config._retryCount>3)return Promise.reject(err)
config.retryCount++

//2s延迟发起新请求
let newReq= new Promise((rs)=>{
setTimeout(()=>{
rs()
},2000)
})
//返回新的axios请求
return newReq.then(function(){
return request(config)
})
}
)

说活对js/node的事件循环机制

这个问题出现频率还是挺高的,主要考察面试者对js这门语言认识的深度,理解它的执行机制。在github上已经有人回答得很具体了:

说说对Nodejs中事件循环机制的理解

说说你对事件循环的理解

new 具体做了什么,如何实现一个new?

这是答案

typescript

ts中函数重载的概念?应用场景?

ts函数重载就是同一个函数提供多个函数类型,它的意义在于让你清晰的知道,传入不同的参数得到不同的结果,这也是它的应用场景。

vue全家桶

vuex的原理,如何按需加载

vuex是vuejs专用状态管理工具,采用集中式存储管理所有组件的状态,并保证状态以一种可预测的方式改变。

它由以下几个部分组成:

state

单一存储状态,存储基本数据

getters

是计算属性,类似vue实例中的computed

mutations

state的更改在这里处理

actions

提交mutation去更改数据,往往包含异步操作。

module

分割模块,每个模块拥有独立的state、getters、mutantions、actions

除此之外,vuex还提供了mapState,mapGetters.mapMutation,mapActions等函数,使得开发者在vm中更加方便地处理store.

vuex和vue实例绑定是借助mixin,在初始化阶段之前把store注册到vue组件实例,并注册引用$store。借助vue的data实现state的响应,借助computed实现getters的实时监听。

vuex按需加载,有时候我们进入首页,并不需要所有的vuex里面维护的状态,只需要加载首页用到的状态即可,这个时候就可以利用vuex提供的registerModule来按需异步加载不同的vuex模块。

1
2
3
4
5
if(isNeedVuexModule){
import('./store/modules/'+moduleName).then(res=>{
this.$store.registerModule(moduleName,res.default)
})
}

vue中是如何监听数组的改变的

vue2在原因层面不支持用Object.defineProperty监听不存在的数组元素,并且通过一些能造成数组改变的方法也不能被监听到。vue2中重写了push,pop,shift,unshift,aplice.sort,reverse这七个数组方法,数组在执行这几个方法是手动触发响应页面的效果。至于为什么不去监听数组中已存在元素的变化,可参考:https://github.com/vuejs/vue/issues/8562

谈谈vue.extend的作用

Vue提供的一个构造器,用于创建一个子类,参数是一个包含组件选型的对象,

注意:Vue.extend创建的是一个构造器,而不是一个组件实例

1
2
3
4
5
6
7
8
9
10
11
12
13
//html
<div id='id'></div>
//js
//创建构造器
var Child = Vue.extend({
template:'<p>{{name}}</p>',
data(){
return{
name:'Brian'
}
}
})
new Child().$mount('#id')

前端架构

讲讲前端性能优化

1、减少http请求

2、使用http2

3、使用服务端渲染

4、静态资源使用cdn

5、css放在文件头部,JavaScript放在文件底部,还有async和deffer的使用

6、使用字体图标代替图片图标

7、善用缓存,不重复加载相同的内容,cache-control, service worker

8、压缩文件

9、图片优化

  • -图片延迟加载
  • -响应式图片,使用picture media
  • -调整图片大小
  • -降低图片质量
  • -尽可能利用css3效果代替图片
  • -使用webp格式的图片

10、代码按需加载

11、减少页面重排重绘

  • -修改样式时不直接写样式,而是通过替换class来改变样式
  • -先将dom元素脱离文档流,修改完之后再带回文档

12、避免页面卡顿

目前大多数设备的屏幕刷新率为 60 次/秒。因此,如果在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。 其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工作要做,因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。

13、使用requestAnimationFrame来实现视觉变化。

由于大部分设备刷新频率为60次/秒,每一帧的平均时间大概为16毫秒,js实现动画最好的情况就是在帧的开头开始执行,requestAnimationFrame正好可以满足这点

14、使用webworker线程处理耗时操作

说说微前端架构

微前端其实是借鉴了微服务的概念,微服务大家都知道:后端(根据业务层次)分拆代码并形成独立的库,每个代码库负责自己的业务逻辑,并公开api,独立部署,分由不同团队维护。微前端借鉴了这种做法,把前端项目按照业务分拆成更小的单元,这些单元分由不同人员或团队开发维护,单独部署,其核心思想是远程应用程序和运行时加载。

参考 深入解析微前端乾坤原理

打包/构建工具

关于webpack,如何编写一个loader和plugin

1、关于loader

由于webpack只能处理js文件,对于其他类型的文件需要专门的转换器处理一下再传入webpack,这个转换器就是loader。它对模块的源代码进行转换,将文件从不同的语言转换为JavaScript,也可将图像转换为data URL。loader其实就是一个函数,接受源模块,把处理结果传给下一个loader或webpack。

下面来看看如何编写一个loader:

1
2
3
4
5
6
//一个把alert转为console.log的简单的loader
/loaders/alertToConsole.js
mudule.exports = function(source){
//source是webpack传入的源代码
return source.replace('alert','console.log')//把alert转为console.log
}

然后在webpack中配置该loader

1
2
3
4
5
6
7
8
9
10
11
12
mudule:{
rules:[
{
test:/\.js/,
use:[
{
loader:path.resolve(_dirname,'./loaders/alertToConsole.js')
}
]
}
]
}

这样,项目中所有js文件中的alert就都会被转成console.log了。当然,如果要实现一些复杂的功能可能要先把源代码转为ast,也就是抽象语法树,可以将其看成树形js对象,通过遍历ast树可以做一些复杂的操作,最后再转化成目标代码返回。

2、关于plugin

webpack的生命周期中会广播出很多事件,plugin可以监听这些事件,在合适的时间通过webpack的api去改变输出结果。

一个简单的plugin的结构:

1
2
3
4
5
6
7
8
9
10
11
class myPlugin{
constructor(options){
console.log('my plugin options',options)
}
apply(compiler){
compiler.plugin('done',compilation=>{
do something
})
}
}
module.exports = myPlugin;

上面的例子中,可以把plugin看成带有apply方法的类。compiler代表webpack的整个生命周期,compilation代表依次新的编译过程。两者都暴露了钩子,可根据这些钩子编写一个满足实际需求的插件。

下面实现一个简单的需求:生成打包文件之后输出一个包含“文件打包完毕”的.txt文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/plugins/txtPlugin.js
class txtPlugin{
constructor(options){
this.options = options
}
apply(compiler){
compiler.plugin('done',(compilation,callback)=>{
let words '文件打包完毕'
compilation.assets['xxx.txt']={
source:function(){
return words
},
size:function(){
return words.length
}
}
callback()
})
}
}
module.exports = txtPlugin

使用

1
2
3
4
5
6
const txtPlugin = require('/plugins/txtPlugin.js')
module.exports = {
plugins:[
new txtPlugin()
]
}

3、loader和plugin的区别

loader遵循单一原则,只能做一件事,比如sass-loader,只能解析sass/scss文件,而plugin是针对整个流程的,在这个流程中可以广泛地执行任务。

http/https

http状态码

常见的http状态码:

  • 1表示消息 100临时相应,需要客户端继续发送请求 101 服务器根据客户端请求切换协议 主要用于websoket或http2升级
  • 2表示成功 200表示成功 201表示创建了新的资源 202 表示服务器已经接受了请求 但未处理 204服务器成功处理但不返回任何内容 206 表示处理了部分请求
  • 表示重定向 301/308永久重定向 302/307临时 304表示使用缓存
  • 4表示请求错误 400错误语法 401未授权 403服务器拒绝 404未找到 407需要代理 408超时
  • 5表示服务器错误 500服务器内部错误 502网关错误 504网关超时 505 http版本的问题

其他

路由/按钮权限控制是怎么做的

参考 https://juejin.cn/post/6844903933429678094

webapp如何做缓存

serviceWorker,ApplicationCache ,LocalStorage,indexedDb