记录近期面试题,面试总结

10,788 阅读12分钟

记录近期换工作时遇到的面试题和面试题答案

css 部分

盒模型

问题:说一下 css 的盒模型

盒模型分为标准模型和怪异盒模型(IE 盒模型)

标准盒模型:盒模型的宽高只是内容(content)的宽高

怪异盒模型:盒模型的宽高是内容(content)+填充(padding)+边框(border)的总宽高

问题:css 如何设置两种模型

/* 标准模型 */
box-sizing:content-box;

 /*IE模型*/
box-sizing:border-box;

问题:有没有遇到过边距重叠,怎么解决 边距重叠问题如下图所示

利用 BFC 去解决,下方详细说明 BFC

参考代码:利用 BFC 的特性,把另一个盒子放入另一个独立的 BFC 中,从而使得两个盒子之间互相不受影响

<section class="top">
    <h1>上</h1>
    margin-bottom:30px;
</section>
<div style="overflow: hidden;">
    <section class="bottom">
    <h1>下</h1>
    margin-top:50px;
    </section>
</div>

BFC

问题:说一下 BFC

  1. 什么是 BFC

    BFC(Block Formatting Context)格式化上下文,是 Web 页面中盒模型布局的 CSS 渲染模式,指一个独立的渲染区域或者说是一个隔离的独立容器。

  2. 形成 BFC 的条件

    • 浮动元素,float 除 none 以外的值
    • 定位元素,position(absolute,fixed)
    • display 为以下其中之一的值 inline-block,table-cell,table-caption
    • overflow 除了 visible 以外的值(hidden,auto,scroll)
  3. BFC 的特性

    • 内部的 Box 会在垂直方向上一个接一个的放置。
    • 垂直方向上的距离由 margin 决定
    • bfc 的区域不会与 float 的元素区域重叠。
    • 计算 bfc 的高度时,浮动元素也参与计算
    • bfc 就是页面上的一个独立容器,容器里面的子元素不会影响外面元素。

rem 原理

rem 布局的本质是等比缩放,一般是基于宽度,假设将屏幕宽度分为 100 份,每份宽度是 1rem,1rem 的宽度是屏幕宽度/100,,然后子元素设置 rem 单位的属性

通过改变 html 元素的字体大小,就可以设置子元素的实际大小。

比 rem 更好的方案(缺点兼容不好)

vw(1vw 是视口宽度的 1%,100vw 就是视口宽度),vh(100vh 就是视口高度)

实现三栏布局

(两侧定宽,中间自适应)

这里写出五种实现方式:

  1. flex 方式实现
/* css */
.box {
    display: flex;
    justify-content: center;
    height: 200px;
}
.left {
    width: 200px;
    background-color: red;
    height: 100%;
}
.content {
    background-color: yellow;
    flex: 1;
}
.right {
    width: 200px;
    background-color: green;
}

/* html */
<div class="box">
    <div class="left"></div>
    <div class="content"></div>
    <div class="right"></div>
</div>
  1. 浮动方式,此方式 content 必须放在最下边
/* css */
.box {
      height: 200px;
    }
.left {
    width: 200px;
    background-color: red;
    float: left;
    height: 100%;
}
.content {
    background-color: yellow;
    height: 100%;
}
.right {
    width: 200px;
    background-color: green;
    float: right;
    height: 100%;
}

/* html */
<div class="box">
    <div class="left"></div>
    <div class="right"></div>
    <div class="content"></div>
</div>
  1. 绝对定位方式实现
/* css */
.box {
    position: relative;
    height: 200px;
}
.left {
    width: 200px;
    background-color: red;
    left: 0;
    height: 100%;
    position: absolute;
}
.content {
    background-color: yellow;
    left: 200px;
    right: 200px;
    height: 100%;
    position: absolute;
}
.right {
    width: 200px;
    background-color: green;
    right: 0;
    height: 100%;
    position: absolute;
}

/* html */
<div class="box">
    <div class="left"></div>
    <div class="content"></div>
    <div class="right"></div>
</div>
  1. 表格布局实现方式
/* css */
.box {
    display: table;
    height: 200px;
}
.left {
    width: 200px;
    background-color: red;
    height: 100%;
    display: table-cell;
}
.content {
    background-color: yellow;
    height: 100%;
    display: table-cell;
}
.right {
    width: 200px;
    background-color: green;
    height: 100%;
    display: table-cell;
}

/* html */
<div class="box">
    <div class="left"></div>
    <div class="content"></div>
    <div class="right"></div>
</div>
  1. grid 网格布局
/* css */
.box {
    display: grid;
    grid-template-columns: 200px auto 200px;
    grid-template-rows: 200px;
}
.left {
    background-color: red;
}
.content {
    background-color: yellow;
}
.right {
    background-color: green;
}

/* html */
<div class="box">
    <div class="left"></div>
    <div class="content"></div>
    <div class="right"></div>
</div>

使一个盒子水平垂直居中

这里写出五种实现方式:

  1. 宽度和高度已知的
/* css */
#box{
    width: 400px;
    height: 200px;
    position: relative;
    background: red;
}
#box1{
    width: 200px;
    height: 100px;
    position: absolute;
    top: 50%;
    left: 50%;
    margin-left: -100px;
    margin-top: -50px;
    background: green;
}
/* html */
<div id="box">
    <div id="box1">

    </div>
</div>
  1. 宽度和高度未知
/* css */
 #box{
    width: 800px;
    height: 400px;
    position: relative;
    background: red;
}
#box1{
    width: 100px;
    height: 50px;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    margin: auto;
    background: green;
}
/* html */
<div id="box">
    <div id="box1">

    </div>
</div>
  1. flex 布局
/* css */
#box{
    width: 400px;
    height: 200px;
    background: #f99;
    display: flex;
    justify-content: center;//实现水平居中
    align-items: center;//实现垂直居中
}
#box1{
    width: 200px;
    height: 100px;
    background: green;
}
/* html */
<div id="box">
    <div id="box1">

    </div>
</div>
  1. 平移 定位+transform
/* css */
#box{
    width: 400px;
    height: 200px;
    background: red;
    position: relative;
}
#box1{
    width: 200px;
    height: 100px;
    background: #9ff;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}
/* html */
<div id="box">
    <div id="box1">

    </div>
</div>
  1. table-cell 布局
/* css */
#box{
    display: table-cell;
    vertical-align: middle
}
#box1{
    margin: 0 auto;
}
/* html */
<div id="box">
    <div id="box1">

    </div>
</div>

js 部分

什么是闭包,闭包的用途

能够读取其他函数内部变量的函数,或简单理解为定义在一个函数内部的函数,内部函数持有外部函数内变量的引用。

闭包的用途:

  • 读取其他函数内部的变量
  • 让变量的值始终保存在内存中
  • JavaScript中闭包的应用都有关键词return,引用 JavaScript 秘密花园中的一段话就是因为:

闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为 函数 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。

call, apply, bind 区别? 怎么实现 call,apply 方法

相似之处:

  1. 都是用来改变函数的 this 对象的指向的。
  2. 第一个参数都是 this 要指向的对象。
  3. 都可以利用后续参数传参。 区别:
  4. call 接受函数传参方式为:fn.call(this, 1, 2, 3)
  5. apply 接受函数传参方式为:fn.apply(this,[1, 2, 3])
  6. bind 的返回值为一个新的函数,需要再次调用: fn.bind(this)(1, 2, 3)

手动实现 call 方法:

Function.prototype.myCall = function(context = window, ...rest) {
    context.fn = this; //此处this是指调用myCall的function
    let result = context.fn(...rest);
    //将this指向销毁
    delete context.fn;
    return result;
};

手动实现 apply 方法:

Function.prototype.myCall = function(context = window, params = []) {
    context.fn = this; //此处this是指调用myCall的function
    let result
    if (params.length) {
        result = context.fn(...params)
    }else {
        result = context.fn()
    }
    //将this指向销毁
    delete context.fn;
    return result;
};

手动实现 bind 方法:

Function.prototype.myBind = function(oThis, ...rest) {
    let _this = this;
    let F = function() {}
    // 根据 bind 规定,如果使用 new 运算符构造 bind 的返回函数时,第一个参数绑定的 this 失效
    let resFn = function(...parmas) {
        return _this.apply(this instanceof resFn ? this : oThis, [
            ...rest,
            ...parmas
        ]);
    };
    // 继承原型
    if (this.prototype) {
        F.prototype = this.prototype;
        resFn.prototype = new F;
    }
    return resFn;
};

怎么理解原型和原型链

每个函数都有 prototype

每一个对象都有 __proto__

实例的 __proto__ 指向构造函数的 prototype

js 引擎会沿着 __proto__ -> ptototype 的顺序一直往上方查找,找到 Object.prototype 为止,Object 为原生底层对象,到这里就停止了查找,如果没有找到,就会报错或者返回 undefined

js 继承的几种方式

常用继承:组合继承,寄生组合继承

组合继承: 利用 call 继承父类上的属性,用子类的原型等于父类实例去继承父类的方法

缺点:调用两次父类,造成性能浪费

function Parent(name) {
    this.name = name;
}

Parent.prototype.say = function() {
    console.log(this.name);
};
function Child(name) {
    Parent.call(this, name)
}
Child.prototype = new Parent;
let c = new Child("YaoChangTuiQueDuan");
c.say()

寄生组合继承:利用 call 继承父类上的属性,用一个干净的函数的原型去等于父类原型,再用子类的原型等于干净函数的实例

function Parent(name) {
    this.name = name;
}

Parent.prototype.say = function() {
    console.log(this.name);
};

function ExtendMiddle() {}

function Child(name) {
    Parent.call(this, name)
}

ExtendMiddle.prototype = Parent.prototype;
Child.prototype = new ExtendMiddle

let c = new Child("YaoChangTuiQueDuan");
c.say()

eventloop

请参考文章 Eventloop 不可怕,可怕的是遇上 Promise

new 的过程中做了什么? 手动实现一个 new

  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象
function create(...rest) {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = rest.shift()
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
}

说一些常用的 es6

  • let/const
  • 模板字符串
  • 解构赋值
  • 块级作用域
  • Promise
  • Class
  • 函数默认参数
  • 模块化
  • 箭头函数
  • Set
  • Map
  • Array.map
  • 等等

Promise 的基本使用和原理,以及简易版的 Promise 实现

Promise 的几个特性:

  1. Promise 捕获错误与 try/catch 相同
  2. Promise 拥有状态变化,并且状态变化不可逆
  3. Promise 属于微任务
  4. Promise 中的.then 回调是异步的
  5. Promsie 中.then 每次返回的都是一个新的 Promise
  6. Promise 会存储返回值

Promise 的简单实现:

class MyPromise {
    constructor(fn) {
        this.resolvedCallbacks = [];
        this.rejectedCallbacks = [];
        this.state = "PADDING";
        this.value = "";
        fn(this.resolve.bind(this), this.reject.bind(this));
    }
    resolve(value) {
        if (this.state === "PADDING") {
            this.state = "RESOLVED";
            this.value = value;
            this.resolvedCallbacks.forEach(cb => cb());
        }
    }

    reject(value) {
        if (this.state === "PADDING") {
            this.state = "REJECTED";
            this.value = value;
            this.rejectedCallbacks.forEach(cb => cb());
        }
    }

    then(resolve = function() {}, reject = function() {}) {
        if (this.state === "PADDING") {
            this.resolvedCallbacks.push(resolve);
            this.rejectedCallbacks.push(reject);
        }
        if (this.state === "RESOLVED") {
            resolve(this.value);
        }
        if (this.state === "REJECTED") {
            reject(this.value);
        }
    }
}

有没有使用过 async/await 说一下和 Promise 的区别、联系

  1. 使用了Promise,并没有和Promise冲突
  2. 在写法上完全是同步,没有任何回调函数
  3. await 必须在 async 函数中使用
  4. 如果 await 后面的方法是一个 Promise,会返回 Promise 的处理结果,等待 Promise 处理成功后在去执行下面的代码,如果等待的不是 Promise 对象,则返回该值本身,然后去同步的执行下方的代码
  5. 写在 await 后面的代码,相当于是执行 Promise.resolve()
  6. 使用 try/catch 来捕获错误

如何实现在Object上使用for...of迭代器

可以在 Object 的 prototype 上挂载一个 Symbol.iterator 方法,该方法返回一个对象,对象中包含 next 方法, next 方法也会返回一个对象,对象中包含 valuedone。 value 表示 每次迭代完成的值,done 表示是否迭代完成,为 false 时会继续迭代,为 true 表示迭代完成,停止迭代

Object.prototype[Symbol.iterator] = function () {
  let values = Object.values(this);
  let keys = Object.keys(this);
  let len = 0;
  return {
    next() {
      return {
        value: {
          value: values[len],
          key: keys[len++]
        },
        done: len > values.length
      }
    }
  }
}

CommonJS 和 ES6 中的模块化的两者区别

  1. 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  2. 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  3. 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  4. 后者会编译成 require/exports 来执行的

module.exports 和 exports 有什么关系

为了方便,Node为每个模块提供一个 exports 变量,指向 module.exports,相当于 exports 是 module.exports 地址的引用

会产生的问题:如果将 exports 新赋值了一个对象,如: exports = {},这个时候,就会打断与 module.exports 的联系,会导致导出不成功

什么是防抖和节流?有什么区别?

  • 控制频繁调用事件的调用频率
  • 防抖和节流的作用都是防止函数多次调用。
  • 区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的情况会每隔一定时间(参数wait)调用函数。

深拷贝用来解决什么问题?如何实现

如何区分深拷贝与浅拷贝,简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,如果B没变,那就是深拷贝。

由于 js 中分为两种变量类型,基本数据类型和引用数据类型,基本数据类型包括,number,string,boolean,null,undefined,symbol。

引用数据类型(Object类)有常规名值对的无序对象 {a:1},数组 [1,2,3],以及函数等。

而这两类数据存储分别是这样的:

  • 基本类型--名值存储在栈内存中,例如let a=1;

    当你 b=a 复制时,栈内存会新开辟一个内存,例如这样:

    所以当你此时修改 a=2,对 b 并不会造成影响,因为此时的 b 已更换内存地址,不受 a 的影响了

  • 引用数据类型--名存在栈内存中,值存在于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值

    当 b=a 进行拷贝时,其实复制的是 a 的引用地址,而并非堆里面的值。

    而当我们 a[0]=1 时进行数组修改时,由于 a 与 b 指向的是同一个地址,所以自然 b 也受了影响,这就是所谓的浅拷贝了。

    那,如果我们需要实现深拷贝的效果,就需要在堆内存中也开辟一个新的的内存空间来存放 b 的值,如图:

深拷贝的实现方式:

  1. json.parse & json.stringify

    先将一个对象转为json对象。然后再解析这个json对象,但此方法有几个缺点:

    • 如果 obj 里面有时间对象,序列化后,时间将只是字符串的形式,而不是时间对象
    • 如果 obj 里有RegExp、Error对象,则序列化的结果将只得到空对象
    • 如果 obj 里有函数,undefined,则序列化的结果会把函数或 undefined 丢失
    • 如果 ob j里有 NaN、Infinity 和 -Infinity,则序列化的结果会变成 null
    • 只能序列化对象的可枚举的自有属性,如果 obj 中的对象是有构造函数生成的,则序列化后,会丢弃对象的 constructor
    • 如果对象中存在循环引用的情况也无法正确实现深拷贝
  2. 递归方式实现深拷贝(ps:这里只是简易版,完整版建议看 lodash 的 cloneDeep 方法源码)

源码地址:github.com/lodash/loda…

function deepClone(obj) {
    let objClone = Array.isArray(obj) ? [] : {};
    let toString = Object.prototype.toString;
    for (key in obj) {
        if (obj.hasOwnProperty(key)) {
            //判断ojb子元素是否为对象,如果是,递归复制
            if ( toString.call(obj[key]) === "[object Object]" || toString.call(obj[key]) === "[object Array]" ) {
                objClone[key] = deepClone(obj[key]);
            } else {
                //如果不是,简单复制
                objClone[key] = obj[key];
            }
        }
    }
    return objClone;
}

什么是堆,什么是栈

栈(stack):栈会自动分配内存空间,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间。 堆(heap):动态分配的内存,大小不定也不会自动释放,存放引用类型

为什么会有栈内存和堆内存之分?

通常与垃圾回收机制有关。为了使程序运行时占用的内存最小。

当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的;

当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。

插入几万个 DOM,如何实现页面不卡顿?

方案一:

分页,懒加载,把数据分页,然后每次接受一定的数据,避免一次性接收太多

方案二:

setInterval,setTimeout,requestAnimationFrame 分批渲染,让数据在不同帧内去做渲染

方案三:

使用 virtual-scroll,虚拟滚动。

这种方式是指根据容器元素的高度以及列表项元素的高度来显示长列表数据中的某一个部分,而不是去完整地渲染长列表,以提高无限滚动的性能

virtual-scroll原理:

在用户滚动时,改变列表在可视区域的渲染部分

  • 计算当前可见区域起始数据的 startIndex
  • 计算当前可见区域结束数据的 endIndex
  • 计算当前可见区域的数据,并渲染到页面中
  • 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上
  • 计算 endIndex 对应的数据相对于可滚动区域最底部的偏移位置 endOffset,并设置到列表上

如下图所示:

startOffset 和 endOffset 会撑开容器元素的内容高度,让其可持续的滚动;此外,还能保持滚动条处于一个正确的位置。

你知道的优化手段有哪些?

  1. 减少HTTP请求:使用 iconfont 字体图标,使用精灵图,合并js,合并 css
  2. 减少DNS查询
  3. 将css放在页面最上面,将js放在页面最下面
  4. 压缩js和css:减少文件体积,去除不必要的空白符、格式符、注释,移除重复,无用代码,使用 gzip
  5. 使用浏览器缓存
  6. 避免 css 选择器层级嵌套太深
  7. 高频触发事件使用防抖和节流
  8. 使 ajax 可缓存
  9. 使用 cdn
  10. 持续补充...

浏览器

在浏览器地址栏键入URL,按下回车之后会经历以下流程:

  1. 解析 url 到 dns 服务器
  2. dns 服务器返回 ip 地址到浏览器
  3. 跟随协议将 ip 发送到网络中
  4. 经过局域网到达服务器
  5. 进入服务器的 MVC 架构 Controller
  6. 经过逻辑处理,请求分发,调用 Model 层
  7. Model 和数据进行交互,读取数据库,将最终结果通过 view 层返回到网络回到浏览器
  8. 浏览器根据请求回来的 html 和关联的 css, js 进行渲染
  9. 在渲染的过程中,浏览器根据 html 生成 dom 树,根据 css 生成 css 树
  10. 将 dom 树和 css 树进行整合,最终知道 dom 节点的样式,在页面上进行样式渲染
  11. 浏览器去执行 js 脚本
  12. 最终展现页面

浏览器渲染的过程,大致可以分为五步:

  1. html代码转化为dom
  2. css代码转化为cssom
  3. 结合dom和cssom,生成一颗渲染树
  4. 生成布局,即将所有的渲染树的节点进行平面合成
  5. 将布局绘制在屏幕上

浏览器缓存

当浏览器再次访问一个已经访问过的资源时,它会这样做:

  1. 看看是否命中强缓存,如果命中,就直接使用缓存了。
  2. 如果没有命中强缓存,就发请求到服务器检查是否命中协商缓存。
  3. 如果命中协商缓存,服务器会返回 304 告诉浏览器使用本地缓存。
  4. 否则,请求网络返回最新的资源。

浏览器缓存的位置:

  1. Service Worker: 它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
  2. Memory Cache: 内存缓存,读取内存中的数据肯定比磁盘快。但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
  3. Disk Cache: Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。

缓存的实现: 强缓存和协商缓存都是根据 HTTP Header 来实现的

什么是重绘和回流

回流:布局或者几何属性需要改变就称为回流。 重绘:当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘

区别:

回流必将引起重绘,而重绘不一定会引起回流。比如:只有颜色改变的时候就只会发生重绘而不会引起回流 当页面布局和几何属性改变时就需要回流

比如:添加或者删除可见的DOM元素,元素位置改变,元素尺寸改变——边距、填充、边框、宽度和高度,内容改变

vue&react

说一下 MVVM

MVVM是双向数据绑定

  • M: Model 数据层
  • V: View 视图层
  • VM: ViewModel 视图层和数据层中间的桥,视图层和数据层通信的桥梁

view 层通过事件去绑定 Model 层, Model 层通过数据去绑定 View 层

什么是 Virtual DOM 为什么使用 Virtual DOM

在之前,渲染数据时,会直接替换掉 DOM 里的所有元素,换成新的数据,为了渲染无用 DOM 所造成的性能浪费,所以出现了 Virtual DOM, Virtual DOM 是虚拟 DOM,是用 js 对象表示的树结构,把 DOM 中的属性映射到 js 的对象属性中,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等。当数据改变时,重新渲染这个 js 的对象结构,找出真正需要更新的 DOM 节点,再去渲染真实 DOM。Virtual DOM 本质就是在 JS 和 DOM 之间做了一个缓存

为什么操作真实 dom 有性能问题

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

Vue 的响应式原理

Vue 内部使用了 Object.defineProperty() 来实现数据响应式,通过这个函数可以监听到 setget 的事件。

  1. 首先利用 Object.defineProperty() 给 data 中的属性去设置 set, get 事件
  2. 递归的去把 data 中的每一个属性注册为被观察者
  3. 解析模板时,在属性的 get 事件中去收集观察者依赖
  4. 当属性的值发生改变时,在 set 事件中去通知每一个观察者,做到全部更新

Vue 的模板如何被渲染成 HTML? 以及渲染过程

  1. vue 模板的本质是字符串,利用各种正则,把模板中的属性去变成 js 中的变量,vif,vshow,vfor 等指令变成 js 中的逻辑
  2. 模板最终会被转换为 render 函数
  3. render 函数执行返回 vNode
  4. 使用 vNode 的 path 方法把 vNode 渲染成真实 DOM

Vue 的整个实现流程

  1. 先把模板解析成 render 函数,把模板中的属性去变成 js 中的变量,vif,vshow,vfor 等指令变成 js 中的逻辑
  2. 执行 render 函数,在初次渲染执行 render 函数的过程中 绑定属性监听,收集依赖,最终得到 vNode,利用 vNode 的 Path 方法,把 vNode 渲染成真实的 DOM
  3. 在属性更新后,重新执行 render 函数,不过这个时候就不需要绑定属性和收集依赖了,最终生成新的 vNode
  4. 把新的 vNode 和 旧的 vNode 去做对比,找出真正需要更新的 DOM,渲染

什么是 diff 算法,或者是 diff 算法用来做什么

  • diff 是linux中的基础命令,可以用来做文件,内容之间的对比
  • vNode 中使用 diff 算法是为了找出需要更新的节点,避免造成不必要的更新

Vuex是什么

vuex 就像一个全局的仓库,公共的状态或者复杂组件交互的状态我们会抽离出来放进里面。

vuex的核心主要包括以下几个部分:

  • state:state里面就是存放的我们需要使用的状态,他是单向数据流,在 vue 中不允许直接对他进行修改,而是使用 mutations 去进行修改
  • mutations: mutations 就是存放如何更改状态的一些方法
  • actions: actions 是来做异步修改的,他可以等待异步结束后,再去使用 commit mutations 去修改状态
  • getters: 相当于是 state 的计算属性

使用:

  • 获取状态在组件内部 computed 中使用 this.$store.state 得到想要的状态
  • 修改的话可在组件中使用 this.$store.commit 方法去修改状态
  • 如果在一个组件中,方法,状态使用太多。 可以使用 mapState,mapMutations 辅助函数

Vue 中组件之间的通信

父子组件:父组件通过 Props 传递子组件,子组件通过 $emit 通知父组件 兄弟组件:可以使用 vuex 全局共享状态,或 eventBus

如何解决页面刷新后 Vuex 数据丢失的问题

可以通过插件 vuex-persistedstate 来解决 插件原理:利用 HTML5 的本地存储 + Vuex.Store 的方法,去同步本地和 store 中的数据,做到同步更新

Vue中插槽的用法

待补充...

Vue 的生命钩子函数,调用每个钩子的时候做了什么

  • beforeCreate: 组件实例刚被创建,在此时,组件内部的属性还不能使用
  • created: 组件实例创建完成,属性已经绑定,但 DOM 还没有创建完成,一般用来请求接口,不可操作 DOM
  • beforeMount: 模板挂载前
  • mounted: 模板挂载后,在这个钩子中,模板已经挂载完毕,可以去操作 DOM 了
  • beforeUpdate:组件更新前
  • update: 组件更新后
  • activated:在使用 keep-alive 时才有的钩子函数,组件被激活时调用
  • deactivated: 在使用 keep-alive 时才有的钩子函数,组件被移除时调用
  • beforeDestory: 在组件被移除前调用
  • destoryed: 组件被移除后调用,一般用来清除定时器
  • 如果有子组件的话,那么调用顺序为:父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

前端路由的两种实现原理

两种路由模式分为 hash 模式 和 history 模式

hash 模式:

hash 模式背后的原理是 onhashchange 事件,可以在window对象上监听这个事件,在 hash 变化时,浏览器会记录历史,并且触发事件回调,在 hash 模式中,地址栏中会带一个 #

history 模式:

前面的 hashchange,你只能改变 # 后面的 url 片段,而 history api 则给了前端完全的自由

history api可以分为两大部分,切换和修改,参考 MDN

使用 history 需要服务端的支持,在hash模式下,前端路由修改的是#中的信息,而浏览器请求时是不带着的,所以没有问题.但是在history下,你可以自由的修改path,当刷新时,如果服务器中没有相应的响应或者资源,会刷出一个404来

什么是ssr, ssr和之前的后台模板有什么区别

待补充...

Vue 中 provide 是做什么的?

待补充...

mixins 一般用来做什么?

待补充...

nextTick 是什么?

待补充...

算法

排序算法(快排,冒泡)

待补充...

介绍下深度优先遍历和广度优先遍历,如何实现?

待补充...

笔试算法题

已知如下数组:
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

方法一:
function handleArr(arr) {
  let flatten = (arr) => arr.push ? arr.reduce((pre, current) => [...pre, ...flatten(current)], []) : [arr];
  return [...new Set(flatten(arr))].sort((a, b) => a - b);
}

方法二:
[...new Set(arr.toString().split(",").map(Number))].sort((a,b)=> a - b)

方法三:
[...new Set(arr.flat(Infinity))].sort((a,b)=>{ return a - b})

笔试执行题

let test = (function (a) {
  this.a = a;
  return function (b) {
    console.log(this.a + b);
  }
})((function(a){
  return a;
})(1,2))

test(4)

答案:5

解析:我们把(function (a) { this.a = a; return function (b) { console.log(this.a + b); } })()自执行函数体叫做 z1

(function(a){ return a; })(1,2) 叫做 z2

function (b) { console.log(this.a + b); }叫做 n1

test 函数为匿名自执行函数z1的返回值,实际上 test 函数为 n1,函数 z1 接收 a 作为参数,这里的 a 实际上为自执行函数 z2 返回值为 1, 那么 n1 函数中的参数 a 就是 1,在函数 z1this.a = a 这里的 this 指向的是 window 所以在 window 上挂载了一个 a 的属性,值为 1

在 text(4) 执行时,传入了 b 为 4, 函数 n1 中的 this 指向的是 window,window 中有之前挂载的属性 a 为 1 所以函数执行结果为 5

以上面试题为个人理解,如有不对的地方,欢迎各位大佬在评论区指正,共勉!