前端面试

933 阅读16分钟

前言

最近受疫情的影响, 许多公司都暂停了招人的计划, 但是现在疫情都控制的差不多了, 所以公司对人才的需求肯定又掀起一片浪潮, 由于本人也是对前端有着强烈的兴趣, 所以, 总结一下以往所遇见的面试题, 希望能够对大家有所帮助.

HTML/CSS

1、div垂直居中的方式

父元素相对定位, 子元素绝对定位(margin: auto)

  <div class="parent">
    <div class="child"></div>
  </div>
-------------
.parent{
    width: 500px;
    height: 500px;
    background-color: royalblue;
    position: relative;
}
.child {
    width: 200px;
    height: 200px;
    background-color: red;
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    margin: auto;
}

父元素相对定位, 子元素绝对定位(定位子元素上和左50%, 相对自身偏移X、Y各-50%)

  <div class="parent">
    <div class="child"></div>
  </div>
-------------
.parent{
    width: 500px;
    height: 500px;
    background-color: royalblue;
    position: relative;
}
.child {
    width: 200px;
    height: 200px;
    background-color: red;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

利用flex布局

  <div class="parent">
    <div class="child"></div>
  </div>
-------------
 .parent{
      width: 500px;
      height: 500px;
      background-color: royalblue;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .child {
      width: 200px;
      height: 200px;
      background-color: red;
    }

2、rem布局

rem布局就是通过js来控制屏幕的等份,把屏幕分成N等份,然后每一等份则就是1rem,font-size就是屏幕的物理像素/N

3、圣杯布局和双飞翼布局

参考链接

4、flex布局

参考链接

5、用CSS绘制一个三角形

.child {
      width: 0;
      height: 0;
      border-right: 100px solid red;
      border-left: 100px solid transparent;;
      border-top: 100px solid red;
      border-bottom: 100px solid transparent;
    }

JS

1、闭包

面试官问到这个部分一般都是考察应聘者对变量作用域的,本人对闭包的理解呢就是一个有外部变量的函数就是闭包.

function init(parameter){
    var a = parameter
    function closure(){
      console.log(a)
    }
    closure();
}
init(1);

优点: 1. 可以在内存中存放一个长期变量。 2. 避免全局变量污染。 3. 存在私有成员。

缺点: 1. 增加内存使用量 2. 使用不当容易造成内存泄漏

2、防抖和节流

  • 防抖

防抖指的是当持续触发事件时,一定时间内没有再触发事件,事件处理函数才会执行一次,在设定时间内,有一次触发事件,就重新计算时间. 下面是封装的一个防抖函数, 可接受一个函数和响应时间

  function debounce(fn, time){
    var timeout = null
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
            fn()
        }, time)
    }  
  }
  • 节流

节流指的持续触发事件时,保证在一定时间内只调用一次事件处理函数,相当于定时器. 下面是封装的一个节流函数, 可接受一个函数和响应时间

function throttle(fn, time){
    var execute = true 
    return function () {
        if(!execute) return
        execute = false
        setTimeout(() => {
            fn()
            execute = true 
        }, time)
    }
}

3、URL从开始到展示

参考链接

4、深拷贝

  • JSON.parse(JSON.stringify())
let obj = {
      a: 1,
      b: { c: 2 }
    }
    let obj2 = JSON.parse(JSON.stringify(obj))
    console.log(obj2)
  • 递归方法
    var sourceObj = {
      a: 1,
      b: { 
          c: {
            d: 2
          },
          e: [1,2,3]
        }
    }
    function deepClone(targetObj, sourceObj){
      var objNames = Object.getOwnPropertyNames(sourceObj) // 获取源对象所有属性
      for(var i=0; i < objNames.length; i++){
        var objValue = Object.getOwnPropertyDescriptor(sourceObj, objNames[i]) // 获取源对象下的所有值
        if(typeof objValue.value === 'object' && objValue.value !== null) { // 如果不做此判断已经完成浅拷贝
          var newObj = Array.isArray(objValue.value) ? [] : {}; 
          deepClone(newObj, objValue.value); // 将新对象newObj作为引用对象带入递归函数中
          Object.defineProperty(targetObj, objNames[i], {
            enumerable: objValue.enumerable,
            writable: objValue.writable,
            configurable: objValue.configurable,
            value: newObj
          });
          continue;
        }
        Object.defineProperty(targetObj, objNames[i], objValue); // 将源对象下的值赋给目标对象
      }
      return targetObj
    }
    var targetObj = deepClone({}, sourceObj);
    sourceObj.b.c.d = 10
    console.log(targetObj.b.c.d) // 2
  • 第三方的JS库, 如lodash

lodash的属性cloneDeep也可以完成深拷贝

5、浅拷贝

  • Object.assign()
var sourceObj = {
      a: 1,
      b: { 
          c: {
            d: 2
          },
          e: [1,2,3]
        }
    }
var targetObj = Object.assign({}, sourceObj);
targetObj.b.c.d = 3;
console.log(targetObj.b.c.d); // 3

注: 若对象只有一层, 则此方法是深拷贝

6、跨域

由于浏览器的同源策略导致了跨域, 那同源策略是什么呢, 同源策略是一种约定, 是浏览为了防止受到XSS、CSFR等攻击, 如果两个通讯地址的协议、域名、端口号相同, 那么这两个地址通讯将被浏览器视为不安全的.那如何解决跨域呢

  • JSONP

利用script标签不受同源策略限制的特性, 缺点只限get请求

  • iframe + document.domain

页面通过js强制设置document.domain为基础主域,就实现了同域。

  • postMessage跨域

postMessage事件可以解决页面和其打开的新窗口的数据传递、多窗口之间消息传递等问题,可解决不同域的通信

  • 服务器代理

常见服务器代理, nginx

7、call, apply, bind

这三个都是改变函数的执行时的this指向, call()方法可以接收一个参数列表, apply()方法可以接收一个数组,bind()方法是将this绑定在这个函数上,但是不会立即执行

    let a = {
      b: 1
    }
    function init(item) {
        console.log(item)
        console.log(this.b)
    }
    init.call(a, 'string')
    init.apply(a, ['arr'])

区别: 1、接收参数不同, call是接收参数列表, apply是接收一个数组 2、call、apply与bind, 前两个是立即执行, 而bind是只是改变函数的this指向

8、存储方式

  • cookie

一般是由服务器生成, 可以设置过期时间, 可携带到请求中, 储存大小为4k

  • localStorage

储存于浏览器中, 若非手动删除, 则一直存在, 储存大小为5M

  • sessionStorage

储存在浏览器中, 关闭页面则清除, 储存大小为5M

  • indexDB

可一直存在, 储存大小不限, 还可以indexDB做页面缓存, 后期会专门出一遍关于页面缓存的文章

9、promise与setTimeout

    setTimeout(function() {
      console.log(1)
    }, 0);
    new Promise(function executor(resolve) {
      console.log(2);
      for( var i=0 ; i<10000 ; i++ ) {
        i == 9999 && resolve();
      }
      console.log(3);
    }).then(function() {
      console.log(4);
    });
    console.log(5);
    // 结果 2 3 5 4 1

这是在笔试经常会遇到的题, 首先你要知道的是事件的执行顺序, 同步大于异步, 而在这道题中, promise > .then > setTimeout. 根据这个我们来分析一下这道题.

  1. 整个函数的执行是同步的, 那么就是从上执行到下的, 遇到setTimeout, 将setTimeout放入异步队列中, 等待执行, 然后遇到promise(promise本身是同步的, 但是它的then和catch方法是异步的), 所以首先会在控制台输出2 .
  2. 函数内部都是从上到下执行的, 但是then方法是异步的, 所以暂时又把then()放入异步队列中, 继续向下执行, 然后输出3.
  3. 这个时候函数的执行已经完成, 但是外部的同步继续向下执行, 然后输出5,
  4. 这个时候同步已经执行完成, 执行异步函数, 由于then>setTimeout, 所以, 先执行then(), 输出4,
  5. 最后执行setTimeout, 输出1

有关promise方面的知识,后期会专门出一篇有关于promise的文章

VUE

1、vue常用的一些指令

v-text、v-html、v-if、v-else、v-else-if、v-for、v-show、v-on、v-bind、v-model等具体用法可参考VUE官网

2、v-model的原理

v-model其实就是vue一语法糖,主要使用于表单元素的双向绑定, 在子组件中可自动获取value和change函数, 类似antd中的form.

3、怎么理解MVVM

MVVM是Model–View–ViewModel的简称, Model代表数据模块, 主要是处理数据和业务逻辑, View代表视图, 主要是视图的展示, ViewModel相当于连接Model与View的桥梁, 在MVVM的框架下,View和Model是没有关联的, 主要是ViewModel将两者相结合, 所以ViewModel是双向的,Model的变化会影响View的展示, 而View的的数据变化也会同步在Model中,如下图所示.

MVVM的好处就是View与Model都是相对独立存在的, 开发和设计人员可分开做属于自己的模块. 可以将视图逻辑绑定在ViewModel, 多次渲染. 在ViewModel可绑定多个不同的View, 当View发生变化时, Model可不变, Model变化时, View也可以不变.

4、vue有哪些生命周期

  • 各个生命周期

beforeCreate: 组件实例创建之前, data与props还不能使用

creted: 组件实例完全被创建, Dom还未创建, 但data和props生效

beforeMount: Dom被挂载之前, render被调用

mounted: 实例初始化完成, Dom完全被挂载

beforeUpdata: 组件更新之前, data更新, 但是并没有同步页面

updataed: 组件更新完成, 同步页面

beforeDestroy: 销毁实例之前, 此时实例仍然完全可用

destroyed: 销毁实例之后, 实例完全被销毁

  • 生命周期示意图

5、谈谈Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式的管理方式储存应用程序的各种状态.主要有以下几个模块

  • State:单一状态树, 里面包含了所有的状态的初始状态
  • Getter:就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
  • Vuex示意图

REACT

1、props与state的区别

props是不可改变的,在每个组件都可以使用,通过redux传递或者父子组件传递,state是组件内的状态,在组件创建完毕后,可以通过this.setState改变其状态。

2、this.setState同步和异步的特性

this.setState在React中是个异步的API, 主要是用来改变State的状态, 因为这个API是异步的, 所以说在函数中, 如果调用了这个API, 不会立马得到改变后的值.

  constructor(props){
    super(props)
    this.state = {
      value: 0
    }
    this.getValue = this.getValue.bind(this)
  }
  getValue(){
    const { value } = this.state
    console.log(value) // 0
    this.setState({ value: value + 1 })
    console.log(value) // 0
  }

那如果在函数中多次调用会怎么样呢, 结果是一样的, 还是没办法立即获取到state的值, 即使多次调用this.setState, 也只是执行一次, 而且执行是最后一次.

  getValue(){
    const { value } = this.state
    console.log(value) // 0
    this.setState({ value: value + 1 })
    this.setState({ value: value + 2 })
    this.setState({ value: value + 3 })
    console.log(value) // 0
  }

那this.setState改变state的值后,能不能直接读取更新后的state值呢, 答案是肯定的, 因为this.setState可以接收回调函数, 所以我们可以在this.setState中传回调函数.

  getValue(){
    const { value } = this.state
    console.log(value) // 0
    this.setState((prevState) => ({ value: prevState.value + 2 }), () => {
      console.log(this.state.value) // 2
    })
    console.log(value) // 0
  }

3、react的生命周期

实在是有点多, 而且更新16版本以后生命周期有变化, 所以这个地方分享给大家一篇比较详细的文章吧. 参考文档

4、redux的工作原理

Redux是react的状态管理库, 这玩意说起来确实有点多, 我给大家分享一篇吧, 这对redux有比较详细的解释. 参考文档

5、redux与mobx的区别

Redux和Mobx都可以做为React的状态管理库, 那两者有什么区别的, 优缺点又如何呢

  1. Redux遵循函数型编程思想, Mobx遵循面向对象编程思想.
  2. Redux是单一状态管理库, Mobx是多个独立的状态管理库.
  3. Redux需要手动设置监听状态的变化, Mobx可自动监听.
  4. Redux改变状态需在原来状态的基础上返回新的状态, Mobx可直接更改状态.
  5. Redux对typescript支持困难, Mobx完美支持typescript

6、react的通信方式

通信方式无非就是父子通信, 兄弟通信, 可通过props/context/redux/mobx等实现通信, 因为这个属于比较基础一点的东西, 而且比较简单,不仔细说了. 参考文档

7、谈谈Hooks

Hooks是react推出来的新特性.

  • 优点:
  1. 面向函数型编程
  2. 不用面对生命周期
  3. 不用在为This而烦恼.
import React, { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

8、函数型组件和类组件有什么区别

  1. 函数型组件面对Function编程, 类组件面对Class编程.
  2. 函数型组件没有维护本地状态的变量(但是Hooks有), 类组件有维护本地状态的变量(state).
  3. 函数型组件没有生命周期(Hooks有, useEffect), 类组件有生命周期,可在不同周期做不同的操作
  4. 函数型组件注重UI的渲染, 类组件注重逻辑与显示.
  5. 函数型组件不需要声明类和绑定this, 只接收一个props, 类组件需要声明类和绑定this的作用域.

9、hooks怎么优化

Hooks的优化可以分为三类, useMemo、useCallback、memo

  1. useMemo

优化变量, 当依赖变量没有发生变化时, 不会触发更新

  • 当没有使用useMemo时
- 父组件
export default () => {
  const [value, setValue] = useState(0)
  const [testValue, setTestValue] = useState(1)
  let obj = {
    value,
    testValue
  }
  const change = () => {
    setTestValue((prev) => {
      let count = prev + 1
      console.log(count, '这是testValue')
      return count
    })
  }
  return (
    <div>
      <Hooks parameter={obj} /> // 传递变量时没有使用useMemo
      <Button onClick={change} >改变testValue</Button>
    </div>
  );
}
- 子组件
export default function Hooks(props) {
  const { parameter } = props
  console.log(parameter)
  return (
    <div></div>
  )
}

可以看出每次点击时, 都会响应式的更新子组件的数据.

  • 使用useMemo时
export default () => {
  const [value, setValue] = useState(0)
  const [testValue, setTestValue] = useState(1)
  let obj = {
    value,
    testValue
  }
  const change = () => {
    setTestValue((prev) => {
      let count = prev + 1
      console.log(count, '这是testValue')
      return count
    })
  }
  return (
    <div>
      <Hooks parameter={useMemo(() => (obj), [value])} />
      <Button onClick={change} >改变testValue</Button>
    </div>
  );
}
- 子组件
export default function Hooks(props) {
  const { parameter } = props
  console.log(parameter)
  return (
    <div></div>
  )
}

使用value作为依赖变量, 每次点击更新testValue时, 并不会更新子组件中的值, 此时需依赖value的变化才会更新传入的parameter

2. useCallback

优化函数, 只初始化一次函数, 依靠变量的状态改变决定是否重新渲染, 与useMemo原理相同

3. memo

优化组件, 与类组件的PureComponent相似, 但memo只适用于函数型组件, 如子组件的变量并没有更新, 那将不会重新渲染子组件. 例子如上诉使用useMemo时的父组件, 但子组件需要做出更新

- 父组件
export default () => {
  const [value, setValue] = useState(0)
  const [testValue, setTestValue] = useState(1)
  let obj = {
    value,
    testValue
  }
  const change = () => {
    setTestValue((prev) => {
      let count = prev + 1
      console.log(count, '这是testValue')
      return count
    })
  }
  return (
    <div>
      <Hooks parameter={useMemo(() => (obj), [value])} />
      <Button onClick={change} >改变testValue</Button>
    </div>
  );
}
- 子组件
function Hooks(props) {
  const { parameter } = props
  console.log(parameter)
  return (
    <div></div>
  )
}
export default memo(Hooks) // 使用memo包裹起来

结果, 与使用useMemo时的父组件打印出的结果对比, 子组件只打印一次, 所以, 子组件的变量并没有更新时, 不会再次渲染.

10、虚拟DOM与真实DOM

虚拟dom是为了用来提升性能的,它最大优势是diff算法,首先,一个普通的对象,包含tag,props,children,把一段html 代码转化成一个对象,就是虚拟dom,虚拟dom提升性能的点就是在于dom发生变化的时候,可以通过diff算法对比JavaScript 原生对象,然后只针对变化的dom进行操作,而不是更新整个视图 子节点做比对.

key的产生:如果子节点只是变换了位置,那么diff算法就会替换掉所有的子节点,重新将子节点的虚拟dom转换成真实的dom,十分消耗性能,所以对子节点增加key,通过key的对比,来判断子节点是否移动了,具体过程是,首先对新的节点进行重排,先进行相同节点的diff,最后把子节点按照新的节点重新排序。

基础算法

1、冒泡排序

原理:从第一个元素开始找,与每相邻元素做比较, 若前者比后者大, 则交换位置, 每遍历一次可找到这次遍历的最大值.

    function bubbleSort(arr){
      var len = arr.length
      for (var i = 0; i < len - 1; i++){
        for (var j = 0; j < len - i - 1 ; j++){
          if (arr[j] > arr[j + 1]){
            var temp = arr[j]
            arr[j] = arr[j+1]
            arr[j+1] = temp
          }
        }
      }
      return arr;
    }
    console.log(bubbleSort([1,45,64,52,32,2,6,78])) // [1, 2, 6, 32, 45, 52, 64, 78]

2、快速排序

原理: 选择一个数作为基准,可选择第一个元素,也可以选择中间元素,将比这个数大的放在右边, 比这个数小的放在左边, 再对左右两边重复比较, 直到不能只剩一个元素.

   function quickSort(arr) {
      if (arr.length < 2) return arr // 只剩1个元素,不能分割
      let left = [], right=[], mid=arr.splice(Math.floor(arr.length/2),1)
      for(var i=0;i<arr.length;i++){
        if(arr[i]<mid){
          left.push(arr[i])
        }else{
          right.push(arr[i])
        }
      }
      return quickSort(left).concat(mid, quickSort(right)) // 采用递归继续调用
    }
    console.log(quickSort([1,45,32,64,52,78,2,32,6,6,78])) // [1, 2, 6, 6, 32, 32, 45, 52, 64, 78, 78]

提示: 排序算法还包括有:希尔排序, 插入排序, 归并排序等,但面试中一般对以上两种考察的多.

3、将对象转成一个树型结构

这是我之前遇到的一个面试题, 题意是,将对象转成树型结构(pid为父级id, 为0表示顶级), 形式如{id:1,pid:0,children:[...]}

let arr = [
    {id: 1, pid: 0},
    {id: 2, pid: 1},
    {id: 3, pid: 2},
    {id: 4, pid: 4},
    {id: 5, pid: 1},
    {id: 6, pid: 3},
    {id: 7, pid: 3},
    {id: 8, pid: 0}
  ]
  function tree(data){
  // 定义一个临时对象
    let temp = {};
    let newData = []
    data.forEach(item => {
      // 将数组转化为对象
      temp[item.id] = item;
    });
    data.forEach(item => {
      let parent = temp[item.pid];
      if(parent) {
        (parent.children || (parent.children = [])).push(item);
      } else {
        newData.push(item);
      }
    });
    return newData
  }
  console.log(tree(arr))

4、数组去重

数组去重的方法有很多啊, 同样, 封装一个函数分享给大家, 这个可以普通的数组去重, 但是如果数组内有引用类型, 想针对引用类型去重, 则需要指定key去重

function uniqueArr(arr = [], key){
    if(key) {
      // 如果数组为引用类型数组, 可以指定key去重
      let hash = {}
      return arr.reduce((prev, next) => {
        !hash[next[key]] && (hash[next[key]] = true) && prev.push(next)
        return prev
      }, [])
    }else {
      return [...new Set(arr)]
    }
  }
  let arr = [
    { name: 'huxiaolei', age: '24' },
    { name: 'huxiaolei', age: '25' },
    { name: 'huxiaolei', age: '26' }
  ]
  console.log(uniqueArr(arr, 'huxiaolei')) // 如果数组为引用类型数组, 可以指定key去重
  // [{name:'huxiaolei',age:'24'}]
  console.log(uniqueArr([1,45,32,64,52,78,2,32,6,6,78])) // 普通数组去重
  // [1, 45, 32, 64, 52, 78, 2, 6]

5、递归

递归的原理就是在本函数中调用本函数, 但是需要结束条件, 这个部分本人在上述的深拷贝、快速排序中都有实现, 所以就不多叙述了

最后

本人是第一次写文章, 若文中有任何不足, 欢迎指正. 同时也祝愿各位正在面试的同学早日找到工作, 疫情很快就会过去, 也祝愿我们的祖国越来越繁荣昌盛

优秀文章推荐