前端面试官经常问到的面试题(持续更新中)

305 阅读8分钟

为什么组件保存数据的data是一个函数?

因为组件是用来复用的 ,为了保证每个实例上的data数据的独立性,规定了必须使用函数,而不是对象。

如果组件中的data使用对象的话,每个实例(组件)上使用的data数据是相互影响(对象是对于内存地址的引用,直接定义个对象的话组件之间都会使用这个对象,这样会造成组件之间数据相互影响。 )

如果组件中data选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的data属性值不会互相影响

详细说一下组件的生命周期钩子函数,以及各个声明周期函数可以做什么事情?

vue生命周期共分为四个阶段一:实例创建二:DOM渲染三:数据更新四:销毁实例

共有八个基本钩子函数:

beforeCreate:在vue实例化之前调用,在这个函数中无法获取数据,同时页面的真实dom节点也没有渲染出来为null

在此阶段可以做的事情:加loading事件

created:在vue实例化之后调用,可以获取到数据页面真实dom

在此阶段可以做的事情:解决loading,请求ajax数据,为mounted渲染做准备

beforeMount:挂载到DOM树之前调用(代表dom马上就要被渲染出来了,但是还没有真正的在页面中渲染出来)可以做ajax与初始化事件的绑定操作

在此阶段可以做的事情:可以做ajax与初始化事件的绑定操作

mounted:挂载到DOM树之后调用,真实的dom也已经渲染出来了

在此阶段可以做的事情:启动定时器、绑定自定义事件、订阅消息等【初始化操作】

beforeUpdate:数据更新之前调用

updated:数据更新之后调用

在此阶段可以做的事情:数据更新时,做一些处理(此处也可以用watch进行观测)

beforeDestroy:vue实例销毁前执行,此时仍然可以使用子组件的实例、methods、watch

在此阶段可以做的事情: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】

destroyed:vue实例销毁后执行,无法再使用子组件的实例,methods、watch

在此阶段可以做的事情: 组件销毁时进行提示

在被keep-alive包含的组件/路由中,会多出两个生命周期的钩子:activated 与 deactivated

activated :activated在组件第一次渲染时会被调用,之后在每次缓存组件被激活时调用。

activated调用时机:

第一次进入缓存路由/组件,在mounted后面,beforeRouteEnter守卫传给 next 的回调函数之前调用:

deactivated:组件被停用(离开路由)时调用

使用了keep-alive就不会调用beforeDestroy(组件销毁前钩子)和destroyed(组件销毁),因为组件没被销毁,被缓存起来了

vue中如何获取DOM对象?

方法一、

直接给相应的元素加id,然后再document.getElementById("id")获取,然后设置相应属性或样式

注意: 要在mounted中使用,因为只有在执行mounted的时候,vue已经渲染了dom节点,这个时候是可以获取dom节点的

方法二、

使用ref,给相应的元素加ref="name", 然后再this.$refs.name获取到该元素

data改变更新DOM是同步还是异步的?怎么访问到更新后的DOM呢?

异步

当data中某个属性改变时,这个值并不会立即渲染到页面上,而是先放到watcher队列上(异步),所以,导致改变的数据挂载到dom上有延迟,故而我们拿到的还是原来的数据

我们可以使用$nextTick:因为$nextTick 是在下次 DOM 更新循环结束之后执行延迟回调

还可以使用定时器setTimeout

webpack是什么?作用是什么? 默认的入口文件什么?

webpack是什么?

  • webpack的诞生之初主要是想解决代码的拆分问题。
  • webpack 是一个现代 JavaScript 应用程序的静态模块打包工具

作用是什么?

代码转换、文件优化、代码分割、模块合并、自动刷新、代码校验、自动发布

  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包,来保证我们项目结构的清晰和可读性。
  • 编译兼容。在以前,我们总是要手写一堆浏览器兼容代码,这让人很头皮发麻,而在今天这个问题被大大的弱化了,通过webpackLoader机制,对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展。通过webpackPlugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

默认的入口文件什么?

webpack 默认的入口文件是 src/index.js,我们可以通过在webpack.config.js中去配置 entry 属性来对入口文件进行修改。

module.exports = {
    entry: './public/index.js'
}

#默认入口文件
 ./src/index.js
#默认出口文件
./dist/main.js

webpack打包命令是什么? 打包的文件默认存在什么地方?

  • 打包命令npm run build

(这个命令会告诉webpack打包工具去执行package.json里面的scripts对象的build的脚本,也就是说实际执行的是webpack --config webpack.prod.js) 。

  • 打包的文件默认存在:根路径/dist/main.js

webpack的loader有哪些? 作用是什么? babel的作用是什么?

webpack的loader有哪些?以及作用

在页面开发过程中,我们经常性加载除了 js 文件以外的内容,这时候我们就需要配置响应的 loader 进行加载

常见的 loader 如下:

  • style-loader: 将 css 添加到 DOM 的内联样式标签 style 里
  • css-loader :允许将 css 文件通过 require 的方式引入,并返回 css 代码
  • less-loader: 处理 less
  • sass-loader: 处理 sass
  • postcss-loader: 用 postcss 来处理 CSS文件
  • autoprefixer-loader: 处理 CSS3 属性前缀,已被弃用,建议直接使用 postcss
  • file-loader: 分发文件到 output 目录并返回相对路径
  • url-loader: 和 file-loader 类似,但是当文件小于设定的 limit 时可以返回一个 Data Url
  • html-minify-loader: 压缩 HTML
  • babel-loader :用 babel 来转换 ES6 文件到 ES5

babel的作用是什么?

babel是一个node命令行工具,将es6语法转换为浏览器能解析的es5语法的一种工具

webpack的plugin有哪些(HtmlWebpackPlugin和webpack-dev-server)?作用是什么?

常见的plugin

BannerPlugin:在每个生成chunk顶部添加banner

CopyWebpackPlugin:将单个文件或整个目录复制到构建目录中

DefinePlugin:允许在编译时配置的全局变量

ContextReplacementPlugin:重写requier表达式的推断上下文

IgnorePlugin:从bundel中排出某些模块

HtmlWebpackPluginwebpack-dev-server的作用是什么?

HtmlWebpackPlugin的作用:简单创建HTML文件,用于服务器访问

webpack-dev-server的作用:

  • webpack-dev-server是webpack官方提供的一个小型node.js Express服务器。使用它可以为webpack打包生成的资源文件提供web服务
  • 主要提供两个功能: 为静态文件提供服务、自动刷新和热替换

通过配置proxy实现请求转发,即当接口域名发生改变时,我们不用一个一个修改接口,只要修改配置项的域名就可以了

通过配置historyFallback为true,解决请求不到页面问题,如果访问除根路径以外的地址,最终都会转向去请求根路径。

什么是渐进式框架? 优点是什么?

Vue是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用

  • *什么是渐进式框架:*就是我们想用或者能用的功能特性,我们不想用的部分功能可以先不用。VUE不强求我们一次性接受并使用它的全部功能特性。
  • 优点: 当我们的项目规模逐渐的变大,我们可能会逐渐用到前端路由、vuex、状态集中管理、并最终实现一个高度工程化的前端项目。这些功能特性我们可以逐步引入,当然不用也可以。

什么是前端工程化? 好处是什么?

前端工程化指的是:在企业级的前端项目开发中,把前端开发所需的工具、技术、流程、经验等进行规范化、 标准化。最终落实到细节上,就是实现前端的“4 个现代化”: 模块化、组件化、规范化、自动化。

前端工程化的好处主要体现在如下两方面:

① 前端工程化让前端开发能够“自成体系”,覆盖了前端项目从创建到部署的方方面面。 (极大提升开发效率 )

② 最大程度地提高了前端的开发效率,降低了技术选型、前后端联调等带来的协调沟通成本。 (降低大型项目的开发难度 )

单页面应用程序有什么优缺点? 缺点如何解决?

单页面应用:

单页面应用程序将所有的活动局限于一个Web页面中,仅在该Web页面初始化时加载相应的HTML、JavaScript 和 CSS。一旦页面加载完成了,SPA不会因为用户的操作而进行页面的重新加载或跳转。取而代之的是利用 JavaScript 动态的变换HTML的内容,从而实现UI与用户的交互。由于避免了页面的重新加载,SPA 可以提供较为流畅的用户体验。

优点:

  1. 用户体验好、快,内容的改变不需要重新加载整个页面
  2. 良好的前后端工作分离模式
  3. 减轻服务器压力
  4. 具有桌面应用的即时性、网站的可移植性和可访问性

缺点:

  1. 首屏加载慢

    解决办法:

    • Vue-router懒加载
    • 异步加载组件
    • 服务端渲染
    • 使用CDN加速
  2. 不利于SEO

    解决办法:

    • 服务端渲染
    • 路由采用h5 history模式
    • 页面预渲染
  3. 不适合开发大型项目

MVVM模式是什么?MVC模式是什么?

MVVM模式

MVVM模式是一种软件的架构模式,是(Model-View-ViewModel)的缩写,其核心是VM,VM是视图与模型之间的桥梁,它实现了视图与模型的相互映射。

在MVVM中模型的改变会引起视图的改变,视图的改变会引发模型的改变。

MVC模式

MVC模式是一种软件设计的典范,一种组织代码的方法。


M 是 model 模型
V 是 view 视图
C 是 control 控制器

控制器是用来将不同的view和不同的model组织在一起。

key如果用索引会出现什么问题


key值用index可能会造成数据错乱

什么情况下,数组或者对象的数据产生变动, 视图不更新? 解决方案是什么?(2种)

数组变动视图不更新是:

1、当利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue;

2、当修改数组的长度时,例如:vm.items.length = newLength;

对象变动视图不更新是:

3、向响应式对象添加属性;

4、向响应式对象删除属性;

5、修改对象某一个属性;

解决方案:

  • 创建新的数组替换原有数组值
  • 使用vue自带的 vue.set(object , key , value );?向响应式对象添加属性
  • 使用vue自带的 vue.delete(object , key );?向响应式对象删除属性;使用 $forceUpdate() 方法强制更新;
1.使用 $set() 方法手动设置;
​
    $set(targetObj,key,value)//接受三个参数,(目标对象,属性名/index,属性值)
    eg:$set(list,0,'aaa')//把数组第0项修改为'aaa'
    
2.使用 $forceUpdate() 方法强制更新; 
​
    eg: list[0]='aaa';
        $forceUpdate()
​

Vue能监测到数组变化的方法:

push() - pop() - shift() - unshift() - splice() - sort() - reverse()

虚拟DOM是什么?diff算法如何比较新旧虚拟DOM(从根元素说起)?

虚拟DOM就是一个普通的JavaScript对象,包含了tagpropschildren三个属性。本质是保存DOM关键信息的JS对象

虚拟DOM好处?

  1. 提高DOM更新的性能, 不频繁操作真实DOM, 在内存中找到变化部分, 再更新真实DOM(打补丁)

diff算法如何比较新旧虚拟DOM?

  • 同级比较
  • 如果根元素变化:删除重新建立整个DOM树
  • 根元素未变,属性改变:DOM复用,只更新属性
  • 根元素未变,子元素内容改变:看有无key值1.无key-就地更新 2.有key-按key比较

v-for的key值有什么讲究?作用是什么?key用index可能会造成数据错乱

讲究:v-for中key属性的值应唯一,有id用id, 无id用索引

好处:可以配合虚拟DOM提高更新的性能 ,起到身份认证的作用,避免v-for“就地更新”策略导致的问题。

key的作用主要是为了更高效的对比虚拟DOM中每个节点是否是相同节点;

官方说法:v-for中key 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key的变化重新排列元素顺序,并且会移除 key 不存在的元素。有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

v-model的本质是什么?

v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。

语法糖简单来说就是便捷写法,


v-model="foo" 等价于 :value="foo" 加上 @input="foo = $event"
​
​
:value='num' 和 @input="num = $event.target.value"

v-model不仅是语法糖还有一定的副作用

副作用如下:如果 v-model 绑定的是响应式对象上某个不存在的属性,那么 vue 会悄悄地增加这个属性,并让它响应式。

v-model单向数据流:子组件不能改变父组件传递给它的 prop 属性,推荐的做法是它抛出事件,通知父组件自行改变绑定的值。

计算属性computed和侦听属性watch的相同点和不同点是什么?

watch 和 computed 相同点

watch 和 computed 都是以函数为基础的,它们都是通过监听自身依赖的数据在变化时触发相关的函数去实现自身数据的变动,(都具有监听属性变化的功能

watch 和 computed 不同点

  1. 功能上:computed是计算属性,watch是监听一个值的变化,然后执行对应的回调。

  2. 是否调用缓存:computed中的函数所依赖的属性如果没有发生变化,那么调用当前的函数会从缓存中读取,而watch在每次监听的值发生变化的时候都会执行回调。

  3. 是否调用return:computed中的函数必须要用return返回,watch中的函数不是必须要用return

  4. 加载顺序:computed 是在 HTML,DOM 加载后马上执行的,如赋值 watch它用于观察 Vue 实例上的数据变动。

  5. computed不支持异步,watch支持异步(为什么computed里不支持异步,因为computed里面需要return,是同步代码,所以不能有异步代码)

  6. computed可监听多个属性变化,多对一; watch监听一个属性,并接受两个入参,第一个是新值,第二个是旧值;

  7. computed的完整写法:(想要给计算属性赋值, 需要使用set方法,用get方法返回 )

    
    computed: {
        "属性名": {
            set(){
                
            },
            get() {
                return "值"
            }
        }
    }
    
  8. watch的完整写法: (只要有对象, 必须加上deep:true ,立即执行需要加上immediate)

    
    watch: {
     obj: {
       deep: true,
       handler(newValue) {
        console.log(....)
       }
     }
    }
    
  9. watch擅长处理的场景:一个数据影响多个数据 -------搜索框。

  10. computed擅长处理的场景:一个数据受多个数据影响 -- 使用场景:当一个值受多个属性影响的时候--------购物车商品结算

组件中style标签的scoped作用是什么? 什么原理?

作用:实现组件的私有化,不对全局造成样式污染,表示当前style属性只属于当前模块。 (是H5新属性,是布尔属性)

原理:scoped会在DOM结构及css样式上加上唯一性的标记【data-v-something】属性(hash值),从而达到样式私有化,不污染全局的作用。

组件传值的方式?

一、父组件向子组件传值

即父组件通过属性的方式向子组件传值,子组件通过 props 来接收。 8

  • 在父组件的子组件标签中绑定自定义属性
  • 在子组件中使用props(可以是数组也可以是对象)接收即可。可以传多个属性。

二、子组件向父组件传值

  • ①子组件绑定一个事件,通过this.$emit()来触发 ,在父组件中定义并绑定事件

  • ②通过 $parent / $children 或 $refs 访问组件实例

    • 这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。
    • 注意:这种方式的组件通信不能跨级。
  • ③通过$attrs / $listeners

    • this.$attes拿的是父组件除了子组件props定义之外的属性和值
    • this.$listeners拿到的是父组件传递的原生事件,用的时候需要在子组件对应的DOM标签上添加v-on="$listeners"

三、兄弟组件之间传值

  • 1.还是通过 $emit 和 props 结合的方式在父组件中给要传值的两个兄弟组件都绑定要传的变量,并定义事件

  • 2.通过一个 空 vue 实例 创建一个 EventBus.js 文件,暴露一个 vue 实例

    步骤:

    
    import Vue from 'Vue'  
    ​
    export default new Vue()
    ​
    在要传值的文件里导入这个空 vue 实例,绑定事件并通过 $emit 触发事件函数
    ​
    (也可以在 main.js 中全局引入该 js 文件,我一般在需要使用到的组件中引入)
    ​
    在接收传值的组件中也导入 vue 实例,通过 $on 监听回调,回调函数接收所有触发事件时传入的参数
    ​
    
  • 3.使用 vuex

四、多层父子组件通信


有时需要实现通信的两个组件不是直接的父子组件,而是祖父和孙子,或者是跨越了更多层级的父子组件
这时就需要用到 vue 提供的更高阶的方法:provide/inject。(依赖注入)
允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深
简单来说就是在父组件中通过provide来提供变量,然后在子组件中通过inject来注入变量,不管组件层级有多深,在父组件生效的生命周期内,这个变量就一直有效。
// 父组件
export default {
  provide: { // 它的作用就是将 **name** 这个变量提供给它的所有子组件。
    name: 'Jack'
  }
}
// 子组件
export default {
  inject: ['name'], // 注入了从父组件中提供的name变量
  mounted () {
    console.log(this.name);  // Jack
  }
}
​
​
注意:provide 和 inject 绑定并不是可响应的。即父组件的属性名name变化后,子组件不会跟着变。(单向绑定,非响应式)

总结:

  • 父子通信: 父向子传递数据是通过 props,子向父是通过 $emit;通过 $parent / $children 通信;$ref也可以
  • 访问组件实例;provide / inject ; $attrs / $listeners;
  • 兄弟通信: EventBusVuex
  • 跨级通信:EventBusVuexprovide / inject ; $attrs / $listeners

在公司的项目中封装过组件吗? 说一说封装以及使用的过程?

肯定封装过呀

在工作中封装的组件比较多,主要说一下怎么去用组件的:

  • 我们一般用脚手架开发项目,每个 .vue单文件就是一个组件。在另一组件import导入,并在components中注册,子组件需要数据,可以在props中接受定义。而子组件修改好数据后,想把数据传递给父组件。可以采用$emit方法。

我们可以在组件中去使用插槽:

  • 匿名插槽:

    子组件定义 slot 插槽,但并未具名,因此也可以说是默认插槽。只要在父元素中插入内容,就回默认加入到这个插槽中去。 😵

    
    // 子组件
    ​
    <template>
      <div>
        hello <slot>你好</slot>
      </div>
    </template>
    

    这里定义了一个默认插槽,只要往里头丢东西,就会被加入到这个插槽里面

    ⚠️ slot 元素里面可以加入一系列后备内容,一旦父元素没有插入任何信息,那么就会渲染后备内容。

    
    //父组件
    //在父组件中使用这个子组件,并插入字符串
    <my-comp>
        好好好
    </my-comp>
    

    但若在父组件中使用这个子组件,并插入字符串,效果如下:

    
    <div>
    hello 好好好
    </div>
    
  • 具名插槽:

    具名插槽可以出现在不同的地方,不限制出现的次数。只要匹配了 name 那么这些内容就会被插入到这个 name 的插槽中去。 🐼

    父组件中即可使用 slot 属性插入到对应的插槽中 slot="name"

  • 作用域插槽:

    我们需要获取到子组件提供的一些数据,那么作用域插槽就排上用场了。 🤓

    在子组件中创建 slot 并通过 v-bind 绑定数据 prop 的形式传入数据:

    
    <slot :data="data"></slot>
    

    在子组件 data 中创建数据:

    
    export default {
      name: 'MyComp',
      data () {
        return {
          data: { // 内部状态
            username: 'oli'
          }
        }
      }
    }
    

    在父组件中使用:v-slot="scope"就可以访问到子组件里的username---'oli'了

    
    <template v-slot="scope">{{scope.data.username}}</template>
    

响应拦截器: 数据过滤, 全局的错误提示

我们使用响应拦截器可以做:无感刷新、统一处理错误、统一对数据进行过滤 。

响应拦截器的第一个方法参数response接收的是axios包装后的服务器返回的数据,每次请求都需要解构,为了提高我们的开发效率,我们使用响应拦截器统一对数据进行过滤。

返回数据:这个需要看自己的公司-------如果后端返回的数据有success这个字段名,需要添加判断,看这个success到底是true还是false,true的话我们就return 数据 false的话就new一个错误提示对象

响应拦截器第二个方法参数是error,用于处理报错信息:

我们把该抛出的错误继续往后抛,可以被后面的异步请求里面的catch捕获

我们可以使用element-ui组件库提供的错误信息组件,去配置全局的错误信息提示

这里的error是网络错误,有status,有message

axios访问流程:

axios发送请求--→axios请求拦截器--→服务器--→axios响应拦截器--→then方法处理响应结果

请求拦截器用两个方法作为参数,若果没有错误,则运行第一个方法,如果中途抛出错误则运行第二个方法。(1)请求拦截器第一个方法参数是config,用于处理请求的配置。(2)请求拦截器第二个方法参数是error,用于处理报错信息。响应拦截器也用两个方法作为参数,如果没有错误,则运行第一个方法,如果中途抛出错误则运行第二个方法。(1)响应拦截器第一个方法参数是response,用于处理响应结果,起返回结果作为axios的then方法的response回调参数。(2)响应拦截器第二个方法参数是error,用于处理报错信息如果有多个请求拦截器和响应拦截器,则请求拦截器运行顺序为从后面至前面,响应拦截器运行顺序为从前面到后面。

配置路由权限(对token拦截处理)

我们配置路由权限的目的是:

判断用户跳转路由的时候是否有权限进入某个目标路由

核心为:

我们需要路由的钩子函数:全局前置守卫 router.beforeEach()

因为:在每次每一个路由改变的时候全局前置守卫都得执行一遍。

按道理这个全局前置守卫应该写在main.js中,我们需要把他抽离到一个叫permission.js的文件里----->注册全局前置导航守卫

并配合token来实现路由跳转关系 需要vuex来配合业务逻辑

  • 先判断有没有token
  • 有token再接着判断是否去登录页如果去登录页则跳转到主页next('/')
  • 不去登录页直接放行next() 想去哪就去哪
  • 没有token时,我们需要定义一个白名单
  • 如果去的页面在白名单里我们则放行next()
  • 如果不在白名单里,则需要跳到登录页next('/login')

\