前端 | 面试题汇总 | 看完涨薪5k+

5,013 阅读40分钟

这里把前些天做的《每日五道面试题》系列里面的题目做一个汇总,方便一次性查看

1、 HTML语义化

根据内容的结构化(内容语义化),选择合适的标签(代码语义化)便于开发者阅读和写出更优雅的代码的同时让浏览器的爬虫和机器很好地解析。

常见的语义化标签:

title:页面主体内容。

hn:h1~h6,分级标题,h1 与 title协调有利于搜索引擎优化。

ul:无序列表。

ol:有序列表。

header:页眉通常包括网站标志、主导航、全站链接以及搜索框。

nav:标记导航,仅对文档中重要的链接群使用。

main:页面主要内容,一个页面只能使用一次。如果是web应用,则包围其主要功能。

article:定义外部的内容,其中的内容独立于文档的其余部分。

section:定义文档中的节(section、区段)。比如章节、页眉、页脚或文档中的其他部分。

aside:定义其所处内容之外的内容。如侧栏、文章的一组链接、广告、友情链接、相关产品列表等。

footer:页脚,只有当父级是body时,才是整个页面的页脚。

small:呈现小号字体效果,指定细则,输入免责声明、注解、署名、版权。

strong:和 em 标签一样,用于强调文本,但它强调的程度更强一些。

em:将其中的文本表示为强调的内容,表现为斜体。

mark:使用黄色突出显示部分文本。

figure:规定独立的流内容(图像、图表、照片、代码等等)(默认有40px左右margin)。

figcaption:定义 figure 元素的标题,应该被置于 figure 元素的第一个或最后一个子元素的位置。

cite:表示所包含的文本对某个参考文献的引用,比如书籍或者杂志的标题。

blockquoto:定义块引用,块引用拥有它们自己的空间。

q:短的引述(跨浏览器问题,尽量避免使用)。

time:datetime属性遵循特定格式,如果忽略此属性,文本内容必须是合法的日期或者时间格式。

abbr:简称或缩写。

dfn:定义术语元素,与定义必须紧挨着,可以在描述列表dl元素中使用。

del:移除的内容。

ins:添加的内容。

code:标记代码。

meter:定义已知范围或分数值内的标量测量。(Internet Explorer 不支持 meter 标签)

progress:定义运行中的进度(进程)。

2、盒模型

页面渲染时,dom 元素所采用的 布局模型。可通过box-sizing进行设置,分为W3C盒子模型(标准盒模型)IE盒子模型(怪异盒模型)

区别:

标准盒模型:width/height 只是内容高度,不包含 padding 和 border 值

IE盒子模型: width/height 包含了 padding 和 border 值

//设置标准模型 
box-sizing: content-box; 
//设置IE模型 
box-sizing: border-box; 
复制代码

3、BFC

块级格式化上下文(Block Formatting Context,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。

IE下为 Layout,可通过 zoom:1 触发

  • 触发条件:

    • 根元素
    • position: absolute/fixed
    • display: inline-block / table
    • float 元素
    • ovevflow !== visible
  • 规则:

    • 属于同一个 BFC 的两个相邻 Box 垂直排列
    • 属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠
    • BFC 中子元素的 margin box 的左边, 与包含块 (BFC) border box的左边相接触 (子元素 absolute 除外)
    • BFC 的区域不会与 float 的元素区域重叠
    • 计算 BFC 的高度时,浮动子元素也参与计算
    • 文字层不会被浮动层覆盖,环绕于周围
  • 应用:

    • 阻止margin重叠
    • 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个div都位于同一个 BFC 区域之中)
    • 自适应两栏布局
    • 可以阻止元素被浮动元素覆盖

4、 居中布局

  • 水平居中

    • 行内元素: text-align: center
    • 块级元素: margin: 0 auto
    • absolute + transform
    • flex + justify-content: center
  • 垂直居中

    • line-height: height
    • absolute + transform
    • flex + align-items: center
    • table
  • 水平垂直居中

    • absolute + transform
    • flex + justify-content + align-items
    • absolute+负margin
    • absolute + auto margin
    • absolute + calc
    • table
    • flex
    • grid

5. 选择器优先级

!important > 行内样式 > #id > .class > tag > * > 继承 > 默认

6、HTTP 状态码

第一个数字定义了响应的类别,且有五种可能取值。

  • 1xx:指示信息--表示请求已接收,继续处理。
  • 2xx:成功--表示请求已被成功接收、理解、接受。
  • 3xx:重定向--要完成请求必须进行更进一步的操作。
  • 4xx:客户端错误--请求有语法错误或请求无法实现。
  • 5xx:服务器端错误--服务器未能实现合法的请求。

常见状态代码、状态描述的说明如下。

  • 200 OK:客户端请求成功。
  • 400 Bad Request:客户端请求有语法错误,不能被服务器所理解。
  • 401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
  • 403 Forbidden:服务器收到请求,但是拒绝提供服务。
  • 404 Not Found:请求资源不存在,举个例子:输入了错误的URL。
  • 500 Internal Server Error:服务器发生不可预期的错误。
  • 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常,举个例子:HTTP/1.1 200 OK(CRLF)。

7、浏览器输入 url 之后发生了什么

  • 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
  • 建立TCP连接(三次握手);
  • 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
  • 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
  • 释放 TCP连接(四次挥手);
  • 浏览器将该 html 文本并显示内容;

8、同源策略和跨域通信

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
协议、域名、端口只要有一个不一样,就是不同的源。

跨域通信

  • JSONP:在页面上引入不同域上的js脚本文件不受同源策略限制。因此在js文件载入完毕之后,触发回调,可以将需要的data作为参数传入。 优点:兼容性好(兼容低版本IE) 缺点:JSONP只支持GET请求
  • CORS:根据请求头的Origin值和响应头的Access-Control-Request-HeadersAccess-Control-Request-Method的值进行比对,通过了就可以请求成功,没通过就请求失败
  • postMessage (H5中新增的),window.postMessage(message,targetOrigin) 方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源
  • node反向代理:如果我们用的是node起的前端服务,那我们可以使用node来直接进行反向代理,引入一个处理代理的插件,然后配置一个target
  • Nginx反向代理:主要就是用了nginx.conf内的proxy_pass http://xxx.xxx.xxx,会把所有请求代理到那个域名
  • WebSocket
  • Hash(window.location.hash + iframe)

9、Cookie、sessionStorage、localStorage区别

共同点:都是保存在浏览器端,且同源的。

区别:

(1)cookie数据始终在同源的http请求中携带,即cookie在浏览器和服务器间来回传递。

sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。

(2)cookie数据不能超过4k(适合保存小数据)。

sessionStorage和localStorage容量较大,

(3)数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭前有效。

localStorage:始终有效,窗口或浏览器关闭也一直保存,需手动清除;

cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。

(4)作用域不同。 sessionStorage不在不同的浏览器窗口中共享;

localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。

应用场景:

localStorage:常用于长期登录(+判断用户是否已登录),适合长期保存在本地的数据。

sessionStorage :敏感账号一次性登录;

cookies与服务器交互。

10、GET和POST的区别

#这个问题。我相信只要你说你做过接口测试,基本上都被问到过。

  简单来说:GET产生一个TCP数据包,POST产生两个TCP数据包

  严格的说:对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

  而对于POST请求。浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)

  • GET请求的参数是放在请求的URL中,而POST方法是放在请求体中

  • GET请求在URL中传递参数时会有长度限制,而POST无限制(不是绝对的,只是相对来说)

  • GET请求会被浏览器主动缓存,而POST不会

  • GET请求的参数会保存在浏览器中,而POST的参数不会保存在浏览器中

11、JS有几种数据类型,其中基本数据类型有哪些!

七种数据类型

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (ECMAScript 6 新定义)
  • Object

(ES6之前)其中5种为基本类型:string,number,boolean,null,undefined,

ES6出来的Symbol也是原始数据类型 ,表示独一无二的值

Object 为引用类型(范围挺大),也包括数组、函数

ES2020(即 ES11),又增加了新类型:**BigInt。**这里能答出来8种类型更加加分

12、原型和原型链

创建一个函数就会为其创建一个prototype属性,指向这个函数的原型对象,原型对象会自动获得constructor属性,指向prototype属性所在函数。

当一个对象调用某个方法或者属性的时候,先在自身查找,如果找到就调用,如果没有就顺着__proto__到原型对象中查找,如果还没有就继续去原型的原型中查找,一直到null,这样形成一条链叫做原型链。如果还没有找到就返回undefined。

13、继承

(1)原型链继承

(2)构造函数继承

(3)组合继承

14、作用域和闭包

作用域是可访问的变量的集合。在JavaScript中,作用域为可访问变量,对象,函数的集合,它分为局部作用域和全局作用域。作用域决定了这些变量的可访问性(可见性)

闭包属于一种特殊的作用域,称为 静态作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数仍然可以继续访问到父级的变量对象,这样的函数称为闭包。

  • 闭包会产生一个很经典的问题:

    • 多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
  • 解决:

    • 变量可以通过 函数参数的形式 传入,避免使用默认的[[scope]]向上查找
    • 使用setTimeout包裹,通过第三个参数传入
    • 使用 块级作用域,让变量成为自己上下文的属性,避免共享

15、call和apply、bind

首先要了解this的指向问题:

(1)this的指向不是在函数定义时确定的,而是在函数调用时确定,this默认情况下指向window,严格模式下为undefined

(2)使用new 调用构造函数时,构造函数内的this 指向新创建的对象

(3)通过 出call/apply/bind方法显式调用函数时,函数内this 指向指定的对象(第一个参数)

(4)通过上下文对象A调用函数时,函数内this指向对象A

var obj = {
  name: '橘子君',
  fn: function () {
    console.log(this);
  }
}
obj.fn();// 输出:{name: "橘子君", fn: ƒ}

(5)箭头函数本身并不存在this箭头函数的this的指向由它的外层作用域来决定的(指向外层作用域的this)

再来看call和apply、bind

作用:在函数调用时改变函数的执行上下文也就是this的值指向

区别:call采用不定长的参数列表,而apply使用一个参数数组。

  • call: fn.call(target, 1, 2)
  • apply: fn.apply(target, [1, 2])
  • bind: fn.bind(target)(1,2)

16、为什么 JavaScript 是单线程

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

17、浅拷贝、深拷贝

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。基本类型不存在这个问题。

简单来说,有两个对象 A 和 B,B = A,当你修改 A的属性或者方法 时,B 的值也跟着发生了变化,这时候就叫浅拷贝。如果不发生变化,就叫深拷贝。在引用数据类型中,会产生浅拷贝的问题。

如何实现深拷贝?

(1)使用递归的方式实现深拷贝

(2)用**JSON.parse( JSON.stringify(obj) )**来完成深拷贝,但是该方法不能解决属性为函数,undefined,循环引用的的情况

(3)用Object.assign()来完成深拷贝,newObj = Object.assign({}, obj),这种方法对于一层对象来说是没有问题的,但是如果对象的属性对应的还是对象或者数组时,就是浅拷贝。

var obj = { a: {a: "kobe", b: 39} };
var initalObj = Object.assign({}, obj);
initalObj.a.a = "wade";
console.log(obj.a.a); // wade 原对象也改变了,是浅拷贝

一层的情况

let obj = {
   username: 'kobe'
};
let obj2 = Object.assign({},obj);
obj2.username = 'wade';
console.log(obj);//{username: "kobe"} 原对象没有被改变,是深拷贝

18、数组去重

方法1:定义一个新数组,并存放原数组的第一个元素,然后将元素组一一和新数组的元素对比,若不同则存放在新数组中

方法2:先将原数组排序,在与相邻的进行比较,如果不同则存入新数组。

方法3:利用对象属性存在的特性,如果没有该属性则存入新数组。

Array.prototype.unique = function () {
    var arr = this, obj = {}, result = [];
    for (var i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) { //如果能查找到,证明数组元素重复了
            obj[arr[i]] = 1;
            result.push(arr[i]);
        }
    }
    return result;
};
var a = [1, 2, 3, 1, 2, 3];
var b = a.unique();
console.log(b); //打印结果:(3) [1, 2, 3]

方法4(最常用):使用es6 set,Set数据结构,它类似于数组,其成员的值都是唯一的

let arr= [1, 2, 3, 3, 5, 7, 2, 6, 8];
console.log([...new Set(arr)]);

方法5:使用filter过滤函数去重。

var arr = [1, 2, 3, 1, 2, 3];
console.log(arr.filter((v, i, arr) => arr.indexOf(v) === i))//打印结果:(3) [1, 2, 3]

19、防抖、节流

防抖:触发高频函数事件后,n秒内函数只能执行一次,如果在n秒内这个事件再次被触发的话,那么会重新计算时间。通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可。
节流:所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可

//防抖
function debounce(func, wait){
  let timeout;
  return function(){
    if(timeout){
      clearTimeout(timeout)
    }
    timeout = setTimeout(() => {
      func.apply(this, arguments)
    }, wait)
  }}
//节流
function throttle(func, wait){
  let timeout;
  return function(){
    if(!timeout){
      timeout = setTimeout(() => {
        timeout = null;
        func.apply(this, arguments)
      }, wait)
    }
  }}

20、数组操作

  • map【常用】: 遍历数组,返回回调返回值组成的新数组

  • forEach【常用】: 无法break,可以用try/catchthrow new Error来停止

  • filter【常用】: 过滤

  • some: 有一项返回true,则整体为true

  • every: 有一项返回false,则整体为false

  • join【常用】: 通过指定连接符生成字符串

  • push / pop: 末尾推入和弹出,改变原数组, push 返回数组长度, pop 返回原数组最后一项;

  • unshift / shift: 头部推入和弹出,改变原数组,unshift 返回数组长度,shift 返回原数组第一项 ;

  • sort(fn) / reverse【常用】: 排序与反转,改变原数组

  • concat【常用】: 连接数组,不影响原数组, 浅拷贝

  • slice(start, end): 返回截断后的新数组,不改变原数组

  • splice(start, number, value...)【常用】: 返回删除元素组成的数组,value 为插入项,改变原数组

  • indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标

  • reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执行,prev 为上次化简函数的return值,cur 为当前值

    • 当传入 defaultPrev 时,从第一项开始;
    • 当未传入时,则为第二项

21、解释一下变量提升

  • 所有的声明都会提升到作用域的最顶上去。

  • 函数声明的优先级高于变量声明的优先级,并且函数声明和函数定义的部分一起被提升。

举例说明一下:

(1)变量提升

    console.log(a);  //undefined
    var a = 123; 

因为变量a的声明被提到了作用域顶端。上面代码编译后应该是下面这个样子

    var a;
    console.log(a)
    a = 123
    //所以输出内容为 undeifend

(2)函数提升

具名函数的声明有两种方式:1. 函数声明式 2. 函数字面量式

//函数声明式
function bar () {}
//变量形式声明; 
var foo = function () {}

函数 变量形式声明 和普通变量一样 提升的 只是一个没有值的变量。

函数声明式的提升现象和变量提升略有不同,函数声明式会提升到作用域最前边,并且将声明内容一起提升到最上边。

看例子:

bar()

var bar = function() {
  console.log(1);
}
// 报错:TypeError: bar is not a function


bar()
function bar() {
  console.log(1);
}
//输出结果1

22、ES6+增加了哪些新特性

  • 声明 let / const

  • 解构赋值

  • class / extend: 类声明与继承

  • Set / Map: 新的数据结构

  • 箭头函数

  • promise

  • async/await

  • 数组的扩展

  • 对象的扩展

23、let和const区别

  • let / const: 块级作用域、不存在变量提升、暂时性死区、不允许重复声明
  • const: 声明常量,无法修改(基本类型不可修改,引用类型如对象和数组,可以修改)

24、async和await的用途?

async函数是generator函数的语法糖 ,async函数始终返回一个Promise,await可以实现一个"等待"的功能,async/await被称为异步编程的终极解决方案,即用同步的形式书写异步代码,并且能够更优雅的实现异步代码顺序执行以及在发生异步的错误时提供更精准的错误信息

25、对promise的理解,手写promise,或者问如何解决回调地狱

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。

有三个状态:

  1. 等待中(pending)
  2. 完成了(resolved)
  3. 拒绝了(rejected)

Promise缺点:

  • 首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

解决了什么问题:

Promise 的出现解决了 之前的回调地狱问题,并且Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise , 并且是一个全新的Promise 。是因为Promise 的状态不可变。如果你在then中使用了return ,那么 return 的值会被 Promise .resolve 包装。

手写promise【重点】

//newPromise.js

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class NewPromise {
    constructor(executor) {
        executor(this.resolve,this.reject)
    }
    //promise状态
    status = PENDING
    value = undefined
    reason = undefined
    //值默认没有
    resolve = value => {
        //如果状态不是等待,阻止程序向下执行
        if(this.status !== PENDING)return
        this.status = FULFILLED
        this.value = value
    }
    //使用箭头函数的原因:直接调用一个普通函数,函数里面的this指向是undefined的
    reject = reason => {
        if(this.status !== PENDING)return
        //将状态更改为失败
        this.status = REJECTED
        this.reason = reason
    }
    //判断promise的状态,返回回调函数,需要传递value和reason
    then(successCallback,failCallback){
        //判断状态
        if(this.status === FULFILLED){
            successCallback(this.value)
        }else if(this.status === REJECTED){
            failCallback(this.reason)
        }
    }
}

module.exports = NewPromise

一个名叫“格子啊”的网友写的promise,大家看看有毛病吗

class promise {
  constructor(fn) {
    this.state = 'pending'
    this.resolveCbs = []
    this.rejectCbs = []
    this.resolve = this.resolve.bind(this)
    this.reject = this.reject.bind(this)
    fn(this.resolve, this.reject)
  }

  resolve(data) {
    this.successData = data
    this.state = 'resolve'
    this.resolveCbs.forEach(cb => {
      cb && cb(this.successData)
    })
  }

  reject(data) {
    this.failData = data
    this.state = 'reject'
    this.rejectCbs.forEach(cb => {
      cb && cb(this.failData)
    })
  }

  then(cb) {
    cb && this.resolveCbs.push(cb)
    return this
  }

  catch(cb) {
    cb && this.rejectCbs.push(cb)
    return this
  }
}

26、SPA是什么,有什么优缺点

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

27、谈谈对MVVM的理解

MVVM 是Model-View-ModelView的缩写,是一种脱胎于 MVC 模式的设计模式
Model 代表数据层,负责存放业务相关的数据;
View 代表视图层,负责在页面上展示数据;
**ViewModel 是的作用是同步 View 和 Model 之间的关联。**数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

回答以上部分面试一般够用了,下面是扩展阅读:

viewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。

MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们只需要处理和维护 ViewModel,视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel处理后 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。

28、Vue2.x双向绑定原理和Vue3.x双向绑定原理

Vue2.x双向绑定原理

Vue 采用数据劫持+订阅发布模式实现双向绑定。通过 Object.defineProperty()方法来为组件中 data 的每个属性添加 get 和 set 方法,在数据变动时,触发 set 里相应的监听回调函数,将变动信息发布给订阅者。

回答以上即可,以下为详细解读:

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

Vue3.x双向绑定原理

Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

Vue3.x是通过proxy(代理)实现的数据的双向绑定

区别:

  • Object.defineProperty一次性只能给对象的一个属性添加get&set方法,因此无法监听对象属性的变化,无法监听数组length的变化,只能通过一些数组方法变现实现。
  • Proxy:一次性给对象所有属性都设置get&set方法,可以监听对象属性变化和数组length变化

29、虚拟 DOM

虚拟DOM是将状态映射成视图的众多解决方案中的一种,是通过状态生成一个虚拟节点树,然后使用虚拟节点树进行渲染生成真实DOM,在渲染之前,会使用新生成的虚拟节点树和上一次虚拟节点树进行对比,只渲染不同的部分。

简单理解就是: 虚拟DOM就是js对象,该js对象模拟真实DOM的结构,有tagprops,chidren等属性分别保存了节点的名称,属性,子节点等。

为什么要使用虚拟DOM?

虚拟 DOM 就是为了解决浏览器性能问题而被设计出来的,前面所说的大量操作真实DOM会消耗浏览器的性能,是因为每次的DOM变化都会重新渲染页面,而虚拟DOM不会立即操作DOM,而是通过diff算法,把这些变化,保存到一个js对象中,最终将这个js对象,一次性attch 到 DOM 树上,再进行后续操作,避免大量无谓的计算量。即:把页面的更新全部保存到js对象中,等更新完成再一次性渲染成真实的DOM,最终交给浏览器绘制

虚拟dom的好处

  • 减少dom操作次数,虚拟dom将需要创建的元素存放在数组中,之后再一次性的插入到节点,同时借助dom diff 一层层比较dom变化来得到patch再更新dom;
  • 跨平台支持

vue中的虚拟DOM

在vue.js中,虚拟dom是用VNode这个class去定义和初始化虚拟dom的属性和方法。

30、Vue的生命周期

  • beforeCreate(创建前) 在数据观测和初始化事件还未开始

  • created(创建后) 完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来

  • beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。

  • mounted(载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。

  • beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。

  • updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。

  • beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。

  • destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。 官方图解:

31、为什么vue组件中的 data 必须是一个函数,return 一个对象

因为组件是用来复用的,且 JS 里对象是引用类型,如果组件中 data 是一个对象,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,每个实例可以维护一份被返回对象的独立拷贝,组件实例之间的 data 属性值不会互相影响

追问:new Vue 实例里,data 可以直接是一个对象?

new Vue() 是根组件,是不会被复用的,因此不存在引用对象的问题。

32、v-if和v-show的用途和区别

v-if真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

33、v-for循环为什么要加key值,key用什么合适

key特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

总结一下:key 是为Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。

在列表中一般使用当前项的id作为key,而不要使用index,因为列表增删操作是导致index值的变化,增加了跟新dom的数量,性能低下。

34、计算属性computed 和侦听器watch 的区别和运用的场景?

计算属性 computed
(1)支持缓存,只有依赖数据发生变化时,才会重新进行计算函数;
(2)计算属性内不支持异步操作
(3)计算属性的函数中都有一个 get(默认具有,获取计算属性)和 set(手动添加,设置计算属性)方法;
(4)计算属性是自动监听依赖值的变化,从而动态返回内容。

侦听属性 watch
(1)不支持缓存,只要数据发生变化,就会执行侦听函数;
(2)侦听属性内支持异步操作
(3)侦听属性的值可以是一个对象,接收 handler 回调,deep,immediate 三个属性
(3)监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些其他事情

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

35、如何动态更新对象或数组的值?

由于Object.defineProperty() 只能对属性进行数据劫持,Vue 无法监听到对象或数组内部某个属性值的变化,因此在直接设置以上两类数据的值时,页面不会实时更新。此时可以通过 this.$set 方法来解决。

this.$set(数组/对象,要改变的index/属性名,要改成的value) 

除了$set外,对于数组Vue提供了一些可以触发视图更新的方法

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

36、组件之间的通信(父子组件、兄弟组件、跨级组件)

【几乎必问题目】

下面我们分别介绍每种通信方式:

(1)props / $emit 适用 父子组件通信

父传子props,子传父 $on、$emit

(2)ref$parent / $children 适用 父子组件通信

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
  • $parent / $children:访问父 / 子实例

(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信

这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

(4)$attrs/$listeners 适用于 隔代组件通信

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

(5)provide / inject 适用于 隔代组件通信

祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

(6) vuex:终极解决方案,适用于 父子、隔代、兄弟组件通信

37、介绍一下 Vuex 吧

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。store 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

主要包括以下几个模块:

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
  • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

38、$nextTick 是什么?

vue实现响应式并不是数据发生变化后dom立即变化,而是按照一定的策略来进行dom更新。

nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用nextTick,则可以在回调中获取更新后的 DOM

39、SSR是什么?

SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端

SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreatecreated两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。还有就是服务器会有更大的负载需求。

40、做过哪些Vue的性能优化

优化是个很大的话题,涉及多个方面:

编码阶段

  • 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
  • v-if和v-for不能连用
  • 如果需要使用v-for给每项元素绑定事件时使用事件代理
  • SPA 页面采用keep-alive缓存组件
  • 在更多的情况下,使用v-if替代v-show
  • key保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载

SEO优化

  • 预渲染
  • 服务端渲染SSR

打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用cdn加载第三方模块
  • 多线程打包happypack
  • splitChunks抽离公共文件
  • sourceMap优化

用户体验

  • 骨架屏
  • PWA

41、vue-router 中 hash 和 history 路由模式实现原理

“大家最常用的却也是最容易大家忽视的问题”

(1)hash 模式的实现原理

实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search':

https://www.demo.com#search

hash 路由模式的实现主要是基于下面几个特性:

  • URL 中 hash 值只是客户端的一种状态,当向服务器端发出请求时,hash 部分不会被发送;
  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;
  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。

(2)history 模式的实现原理

history 模式使用了HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:

使用方法
pushState(state, title, url)
repalceState(state, title, url)
参数说明
state: 可以通过history.state读取
title: 可选参数,暂时没有用,建议传个短标题
url: 改变过后的url地址、

history 路由模式的实现主要基于存在下面几个特性:

  • pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;
  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
  • history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

42、keep-alive是什么

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:

  • 一般结合路由和动态组件一起使用,用于缓存组件;
  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

43、在哪个生命周期内调用异步请求?

本题每人都能轻易答出来,关键的关键是答出SSR部分,为自己加分

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

每天5道面试题(8)中提到SSR服务器端渲染只支持beforeCreatecreated两个钩子,所以SSR的异步请求要放在created钩子中

44、介绍一下Vue中的Diff算法

  • 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换。
  • 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)。
  • 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
  • 匹配时,找到相同的子节点,递归比较子节点。

在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n^3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

45、Vue模版编译原理

Vue的编译过程就是将template转化为render函数的过程。会经历以下阶段:

  • 生成AST树

  • 优化

  • codegen

首先解析模版,生成AST语法树(一种用JavaScript对象的形式来描述整个模板)。

使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。

Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。

编译的最后一步是将优化后的AST树转换为可执行的代码。

46、重排、重绘

重排(Reflow)是什么?

  • 定义:DOM中各个元素都有自己的盒子模型,需要浏览器根据样式进行计算,并根据计算结果将元素放到特定位置,这就是Reflow
  • 触发Reflow的条件
    • 增、删、改、移DOM
    • 修改CSS样式
    • Resize窗口
    • 页面滚动
    • 修改网页的默认字体

重绘(Repaint)是什么?

  • 定义:当各种盒子的位置、大小以及其他属性改变时,浏览器需要把这些元素都按照各自的特性绘制一遍,这个过程称为Repaint。
  • 触发Repaint的条件:
    • DOM改动
    • CSS改动

如何减少重绘、避免重排?

本质上,就是合并修改。具体的措施有:

  • DOM层面:DocumentFragment本质上是一个占位符,真正插入页面的是它的所有子孙节点,所以,将需要变动的DOM节点先汇总到DocumentFragment,然后一次性插入,可以减少DOM操作的次数。
  • CSS层面:操作多个样式时,可以先汇总到一个类中,然后一次性修改

47、css3动画常用的属性

  • transition动画:

transition-property:    参与过渡的属性

transition-duration:    过渡的持续时间

transition-delay:     延迟过渡的时间

transition-timing-function:过渡的动画类型

复合写法:

transition: 属性值1  属性值2  属性值3  属性值4
  • animation关键帧动画:

animation-name:设置对象所应用的动画名称

animation-duration:设置对象动画的持续时间
animation-timing-function:设置对象动画的过渡类型
animation-delay:设置对象动画延迟的时间
animation-iteration-count:设置对象动画的循环次数(默认情况下循环1次)
animation-direction:设置对象动画在循环中是否反向运动
animation-play-state:设置对象动画的状态

常常结合transform使用:

平移:transfrom:translate(X,Y)

旋转:transform: rotate()

斜切:ransform:skew(X,Y)

缩放:transform:scale(X,Y)

例如:

.box{
    height:200px;
    width: 200px;
    position: relative;
    margin:200px auto;
    transform-style:preserve-3d;
    -webkit-transform-style:preserve-3d;
    transform:rotateX(20deg) rotateY(20deg);
    animation:one 5s linear infinite;
    -webkit-animation:one 5s linear infinite;
}
@keyframes one{
    from{
        transform:rotateX(-20deg) rotateY(0deg) ;
    }
    to{
        transform:rotateX(340deg) rotateY(360deg) ;
    }
}

48、事件委托

事件捕获和冒泡允许我们实现一种被称为 事件委托 的强大的事件处理模式。

这个想法是,如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。

在处理程序中,我们获取 event.target 以查看事件实际发生的位置并进行处理。

49、JS哪些操作会造成内存泄露

内存泄漏是指一块被分配的内存既不能使用,也不能回收,直到浏览器进程结束。

  • 意外的全局变量
  • 闭包
  • 没有清理的dom元素 dom元素赋值给变量,又通过removeChild移除dom元素。但是dom元素的引用还在内存中
  • 被遗忘的定时器或者回调

50、说几条写JavaScript的基本规范?

  • 1、不要在同一行声明多个变量

  • 2、使用===或!==来比较

  • 3、使用字面量的方式来创建对象、数组,替代new Array这种形式

  • 4、不要使用全局函数

  • 5、switch语句必须要带default分支

  • 6、函数不应该有的时候有return,有的时候没有return

  • 7、fon-in循环中的变量,用var关键字说明作用域,防止变量污染

  • 8、变量的声明遵循驼峰命名法,声明构造函数时首字母大写,定义常量的时候尽量用大写字母,用_分割

  • 9、三元表达式可以替代if语句

  • 10、&&和||是可以短路的,使用&&时如果前面一个值是错的,那么后面的值不用判断,使用||时,如果前面一个值是对的,那么后面的值不用判断

  • 11、比较数据类型以下6种情况是false,其他都是true:false、""、0、null、NaN、undefined

  • 12、数据类型检测用typeof,对象类型检测用instanceof

  • 13、异步加载第三方的内容

  • 14、单行注释//,多行注释/**/

  • 15、使用命名空间解决变量名冲突

  • 16、多人协作开发,新建一个js文件,const声明常量,在js文件中引用,用常量名替代方法名,这样做可以防止命名冲突

本着能为找工作中的前端帮一点小忙忙,借鉴了掘金里面很多大咖的文章,如有侵权请告知