前端面试-css、js、vue相关

111 阅读30分钟

CSS部分

W3C标准:

World Wide Web Consortium,中文是万维网联盟,也是web标准,就是我们所说的代码规范 对于结构的要求:1:标签字母小写、2:标签需要闭合、3:标签不能随意嵌套

对于CSS和JS的要求 使用外链css和js,使结构、表现、行为分为三个部分符合规范,便于维护,同时提高页面渲染速度,提高用户体验

尽量减少行内样式,使结构与表现分离,id与class命名要见文知义,标签越少加载越快,方便改版,有利于用户体验

是一系列标准的集合,因网页由三个部分组成,分别是结构层(HTML)、表现层(CSS)、行为层(JS)

HTML层叠等级

盒模型:

作用:页面渲染时,dom元素所采用的布局模型。 可通过box-sizing进行设置。根据计算宽高的区域可分为
border-box(IE盒模型)----- width = border+padding+content
content-box(W3C标准盒模型)----- width = content

问:怎么样才算是选择了‘W3C标准’的盒模型呢? 在网页的顶部加上 doctype 声明。假如不加 doctype 声明,那么各个浏览器会根据自己的行为去理解网页,加上了 doctype 声明,那么所有浏览器都会采用标准 w3c 盒子模型去解释你的盒子,网页就能在各个浏览器中显示一致了

解决浏览器兼容的问题 blog.csdn.net/weixin_3853…

实现居中的几种方式:
水平居中:
行内元素:text-align:center
块元素:
margin:0 auto
父元素:position:relative,子元素:position:absolute ,left:50%, transform:transaleX(-50%)
父元素:display:flex , justify-content:center

垂直居中: 行内元素:line-height:父元素的高度
块元素: 父元素:display:table,子元素:display:table-cell,vertical-align:middle
父元素:position:relative,子元素:position:absolute,top:50%,transform: translateY(-50%)
父元素:display:flex,align-items:center

水平垂直居中 display:flex,justify-content:center,align-items:center
父元素:position:relative,子元素:position:absolute,top:50%,left:50%,transform:translate(-50%,-50%)
position:absolute, left:0;top:0;right:0;bottom:0;margin:auto

CSS优先级:
!important > 行内 > head内部的style标签样式 > 外链CSS

选择器优先级 #id > .class > tag标签 > * > 默认

link 与 @import 的区别
link兼容性比@import好,@import需要IE5以上才能兼容
link可以用JS动态引用,@import不行
link的功能要比@import多,可以定义 RSS,定义 Rel 等作用,@import只能用于加载 css

CSS预处理器(Sass/Less/PostCss)
原理:将类CSS通过webpack编译成浏览器可读的真正的CSS格式。在这层编译之上,css有了更强大的功能:
常用功能:
嵌套
变量
循环语句
条件语句
自动前缀 单位转换
mixin复用
CSS动画
animation
transition

BFC---Block Formatting Context
blog.csdn.net/sinat_36422…
BFC是一个独立的布局环境,其中的元素布局是不受外界的影响,并且在一个BFC中,块盒与行盒(行盒由一行中所有的内联元素所组成)都会垂直的沿着其父元素的边框排列
formatting context页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用
布局的规则:
内部的box会垂直方向一个接一个的放置
Box垂直方向的距离是由margin决定。同一个BFC内的两个相邻的box的margin会重叠
每个盒子的margin box的左边会和border box的左边相接触,即使是浮动也是如此
BFC的区域不会与float box重叠
BFC是一个独立的布局空间,内部的子元素不会受外部影响
BFC计算高度会包含浮动的部分

触发的方法:
position不是static和relative
overflow不是visible
float不是none
display:inline-block / table

使用场景: 避免margin重叠
自适应两栏布局
包含浮动元素,清除内部的浮动
Flex布局:阮一峰老师的Flex布局教程:语法篇

JS部分

栈内存与堆内存
栈内存: 存储基本数据类型的变量,如:Number,Boolean,String,Null,Undefined
大小已知,或范围有上限
值传递
速度快
堆内存:
存储Object
大小未知,几乎无限制
引用传递
速度没有栈内存快
问:为什么基本数据类型保存在栈内存中,而饮用数据类型保存在堆内存中
答:因为基本数据类型稳定,且相对内存占用小,并且栈内存的查找速度要比堆内存快,两者存放优选栈内存,但因为引用数据类型的大小不固定,且堆内存的大小无限制,故此堆内存中

1、原型、构造函数、实例
原型(prototype):
一个简单的对象,用于实现对象的属性继承。可以简单的理解为对象的父亲
每一个JS对象都拥有一个_proto_的属性,可以通过obj._proto_来获取该对象的原型
原型对象的好处是可以让所有对象实例共享它所包含的属性和方法

构造函数
一个可以通过new来新建对象的函数
实例:可以通过构造函数和New新建的对象叫做实例。实例通过_ptoyo_指向原型,原型通过constructer指向构造函数
三者的关系:
构造函数.prototype ----> 原型
实例.-proto- ----> 原型
原型.constructor ----> 构造函数
构造函数.proto == Function.prototype

prototype和_proto_的区别: prototype是每个函数都有的一个属性,对象没有,通过bind绑定的没有这个属性,通过这个属性来指向原型
_proto_是每一个JS对象有的(函数也是对象(null除外)),通过obj._proto_来指向该对象原型

2、原型链
原型链是由原型对象组成,每个对象都有一个_proto_属性,指向创建该对象的构造函数的原型,_proto_将对象链接起来组成了原型链。是一个用来实现继承和属性共享的对象链
属性查找机制
当查找对象的属性时,会先从实例对象上去找,如果实例对象上不存在这个属性会沿着原型链向上查找,找到输出,找不到继续找,直到找到Obj.prototype上,如果还是没有,则输出undefined 属性修改机制:
当要修改对象的属性时,如果实例对象上存在这个属性则直接修改,如果不存在则会创建这个属性。 如果要修改原型的属性时,可以通过obj.prototype.XXX来修改,但是会造成所有继承于这个对象的实例的属性值发生改变

检验属性时存在于实例上还是存在于原型上:hasOwnProperty 属性只有存在于实例中才会返回true 例:obj.hasOwnProperty('name') Obj.prototype没有原型,obj.prototype.proto 为null

3、在new过程中都经历了哪些 创建一个新的对象
将这个新对象的_proto_属性指向构造函数的原型
构造函数的内部this指向这个新对象
将这个新对象返回(如果构造函数有返回值,则会返回该值)

4、执行上下文(EC)
包含的部分
变量对象
作用域
this
它的类型
全局执行上下文
函数执行上下文
eval执行上下文

代码执行过程
创建全局执行上下文
全剧执行上下文,开始自上而下的执行,遇到函数时,函数执行上下文被推到执行栈顶层
函数执行上下文被启动后,开始执行函数中的代码,函数执行上下文被挂起
函数执行完成后,会被移除执行栈,控制权交还给全局执行上下文,继续执行

5、变量对象: 每一个执行环境中都会有一个变量对象,用于存储当前执行环境的所有变量和函数,不包含函数表达式 执行上下文的一部分,可以抽象理解为数据作用域。存储着执行上下文中的所有的变量和函数声明(不包含函数表达式)
函数声明:var fn = function(){}
函数表达式:function fn(){}

6、作用域
一个变量或者函数的可访问的范围,控制着变量和函数的可见性与周期,包括全局作用域、函数作用域和ES6新增的块级作用域。块级作用域就是{}之间的作用域
通过var 在函数外部定义的变量是全局作用域,在函数内部和函数传过来的参数的作用域只在函数内部,window对象的所有属性拥有全局作用域

7、作用域链
在执行上下文中可以访问到父级变量甚至全局变量,这就是作用域链的功劳。它是一组对象列表,包含父级和自身的变量对象,因此我们能通过作用域链访问到父级里声明的变量和函数

8、闭包:
有权限访问上一层父级作用域的变量的函数。当父函数被销毁时,被return 回来的子函数[[scope]]中仍保留着父函数的变量和作用域,因此仍然可以访问父级的变量(立即执行函数其实就是个闭包)

优点:
拥有独立的作用域,避免变量污染

缺点:
外部函数无法访问这个独立作用域内的变量和函数
内部所有函数都指向一个父级函数,父级变量被修改时会影响所有子级函数

解决方案:
通过传参数的形式来获取内部变量、window对象上挂 ----- 对应缺点 1
通过setTimeout进行包裹,通过第三个参数传入 ----- 对应缺点 1
使用块级作用域,避免变量共享 ----- 对应缺点 2

9、script的引入方式
HTML静态引入
JS 动态引入

var script = document.createElment('script') 	
script.type = 'text/script' 	
script.url="app.js"

延迟加载
<script src="app.js" defer></script>
按顺序加载,但是页面渲染完成后,DOMContentLoaded之前执行,

异步加载

<script src="app.js" async></script>

页面渲染的过程中同时加载js文件 延迟加载和异步加载都是异步的

10、对象的拷贝(深拷贝和浅拷贝)---->只针对引用数据类型
浅拷贝:
通过赋值的形式拷贝引用对象,但仍指向堆内存中的同一个地址,新对象修改时原对象也会受影响
1、 使用Object.assgin({},obj) 实现浅拷贝

let newObj1 = {
      name:'LiSi',
      age:'22',
      innerObj:{
          sex:'gril'
      }
   }
let newObj2 = Object.assign({},newObj1)

2、展开运算符(...obj)

   let _obj1 = { name:{name:'张三'} ,age:21}
   let _obj2 = {sex:'girl',class:5}
   let _obj = {..._obj1, ..._obj2}

深拷贝:创造一个一摸一样的新对象,新对象和原对象不共享内存,新对象修改时原对象不会受影响

1、递归实现深拷贝 方法一:

    function isObject(obj){
      return Object.prototype.toString.call(obj) === '[object Object]' ||
              Object.prototype.toString.call(obj) === '[object Array]'
    }
    function deepCopy(source, hash = new WeakMap()){
      if(!isObject(source)) return source
      if(hash.has(source)) return hash.get(source)
      let res = Array.isArray(source) ? [] : {}
      hash.set(source, res)
      for(let key in source){
        if(Object.prototype.hasOwnProperty.call(source, key)){
          if(isObject(source[key])){
            res[key] = deepCopy(source[key], hash)
          }else{
            res[key] = source[key]
          }
        }
      }
      return res
    }

方法二: //实现深拷贝函数

function deepClone(data) {
    const type = this.judgeType(data);
    let obj = null;
    if (type == 'array') {
        obj = [];
        for (let i = 0; i < data.length; i++) {
            obj.push(this.deepClone(data[i]));
        }
    } else if (type == 'object') {
        obj = {}
        for (let key in data) {
            if (data.hasOwnProperty(key)) {
                obj[key] = this.deepClone(data[key]);
            }
        }
    } else {
        return data;
    }
    return obj;
}

function judgeType(obj) {
    // tostring会返回对应不同的标签的构造函数
    const toString = Object.prototype.toString;
    const map = {
        '[object Boolean]': 'boolean',
        '[object Number]': 'number',
        '[object String]': 'string',
        '[object Function]': 'function',
        '[object Array]': 'array',
        '[object Date]': 'date',
        '[object RegExp]': 'regExp',
        '[object Undefined]': 'undefined',
        '[object Null]': 'null',
        '[object Object]': 'object',
    };
    if (obj instanceof Element) {
        return 'element';
    }
    return map[toString.call(obj)];
}

2、JSON.stringify+JSON.parse 实现深拷贝

function deepCopy(obj){
    let _obj = JSON.stringify(obj)
    let _newObj = JSON.parse(_obj)
    return _newObj
}

JSON.stringify+JSON.parse的弊端

  1. 如果被拷贝的对象里有时间对象,被拷贝的对象的时间将只是字符串的形式,不再是时间对象
  2. 如果被拷贝的对象中有RegExp、Error对象,将会只得到空对象
  3. 如果被拷贝的对象中有函数、undefined,序列化的结果会把函数和undefined丢失
  4. 如果被拷贝的对象中有NaN、Infinity和-InfiniTy,序列化的结果会变成null
  5. 如果被拷贝的对象是由构造函数生成的,序列化后会丢失constructor(JSON.Stringify只能序列化对象的可枚举的自有属性)
  6. 如果被拷贝的对象有循环引用的情况下,无法实现正确的深拷贝

11、instanceof原理
在实例的原型链中是否能找到该构造函数的protoType所指向的原型,是为true

// L intanceof R
function intense_of(L,R){
    let O = R.prototype  // 构造函数拥有protoType属性
    let L = L._proto_ // 实例对象拥有_proto_属性
    while(true){
        if(L === null){
            return false
        }
        if(O === L){
            return true
        }
        let L = L._proto_ 
    }
}

12、代码的复用
当代码开始写第二遍时,就要考虑到代码的复用

  • 方法:
  • 函数封装
  • 继承
  • 复制 extend
  • 借用 call/apply
  • call和apply是Function.prototype的方法,作用是用来改变this指向
  • call传参需要逐一列出,而apply接受一个数组
  • 混入 mixin

13、继承(9种模式) ----> 一般是指原型链继承,通过原型链继承指定原型的属性和方法。
继承的方法:
原型链继承----->将父类的实例指向子类的原型

son.protoType = new Parent()

构造函数继承 -----> 复制父类的实例属性给子类

Parent.callthis

组合继承(原型链继承+构造函数继承)

function Son(){
    Parent.call(this)
}
Son.prototype = new Animal()

缺点:调用了两次父级构造函数,生成了两个实例,子类实例屏蔽了子类原型上的实例

实例继承 ----> 给父类实例添加属性,作为子类返回

function Son(args){
    var instance = new Parent(args)
    instance.sleep = function(){}
    return instance
}
var son = new Son({ name:'Tom' })

原型式继承---->借助原型基于已有的对象创建新的对象
方法一:let Son = Object.create(Parent) 方法二:

function object( O ){	// 传递一个字面量函数
    function F(){}	// 创建一个构造函数
    F.prototype = O     // 将字面量函数赋值给构造函数的原型
    return new F()	// 返回构造函数的实例
}

寄生式继承(基于原型式继承+工厂模式) -- 创建一个用于封装继承过程的函数,在函数内部增强对象, 最后返回这个对象

function object(o){
    function F(){}
    F.prototype = o
    return new F()
}
function createAnother(original){
    var clone = object(original)  // 调用函数创建一个新的对象
    clone.fn = function(){} 	// 增强这个对象
    return clone	// 返回这个对象
}

寄生组合式继承--> 借用构造函数+建立子类与父类的链接

function SuperType(name){
    this.name = name
    this.colors = ['red','green','yellow']
}
SuperType.prototype.sleep = function(){ alert('prototype') }
function SubType(name,age){
    SuperType.call(this,name)
    this.age = age
}
function inherit(SubType,SuperType){
    var proto = SuperType.prototype
    proto.constructor = SubType
    SubType.prototype = proto
}
inherit(SubType,SuperType)

圣杯模式

let inherit = (function(){
    function F(){} //函数是一个闭包,仅用做一个缓冲而不做他用
    return function(son,parent){
    F.prototype = parent.prototype
    // son.prototype = F.prototype; /* 不能这么写,因为这样写就相当于对象son、parent和 F 共享原型了 */
    son.prototype = new F() // 使对象targetSon试图修改自身属性时仅仅是以buffer函数作为对象进行修改,而不会影响到其他对象
    son.prototype.uber = parent.prototype 
    son.prototype.constructor = son 
    }
})()

es6语法糖 class / extends
注:一定要有super,否则新建实例时会报错。因为子类没有自己的this对象,需要继承父类的this对象,如果没有super子类就得不到父类的this对象

class Parent{
    constructor(props){
        this.name = props.name || 'Unknown'
    }
    eat(){
        console.log(this.name + '会来吃东西')
    }
    }
    class Son extends Parent{
        constructor(props,myAttribute){
        super(props)
        this.type = props.type
        this.attr = myAtrribute
    }
    play(){  console.log('子类的私有方法')  }
    myattr(){  console.log('子类的自由方法二')  }
}

ES5的构造函数与es6的class的区别:

  • es6的class不存在变量提升
  • es6中的class使用严格模式,引入一个未定义的变量会报错
  • es6中class所有的方法是不可枚举的
  • es6中class的调用必须使用new
  • class无法重写类名
  • class有两条继承链:
  • 子类的—proto—指向父类
  • 子类prototype属性的—proto—指向父类的prototype
  • es6子类通过-proto-找到父类,es5字类通过-proto-找到Function.prototype
  • class与构造函数生成this的顺序不同
  • 构造函数先建立子类实例对象的this,再调用父类构造函数修饰子类实例
  • es6中class是先建立父类实例对象的this,在调用子类的构造函数修饰this。所以在子类的constructor中必须要有super,这样才能正确的使用this
  • 因为生成this的顺序不同,所有es5不能直接继承原生构造函数,而es6可以

14、类型转换

  • -、*、/、%:全部转化成数值后计算
  • 数字 + 字符串 = 字符串
  • 数字 + 对象,优先调用对象valueOf -> toString
  • 数字 + boolean/null = 数字
  • 数字 + undefined = NaN
  • [1].toString === '1'
  • {}.toString() === '[Object Object]'
  • NaN !== NaN、+undefined为NaN

15、类型判断

  • 判断目标类型,typeOf并不能完全满足。这不是bug只是JS中万物皆对象的理论,需要我们区别对待
  • 基本类型-->null:String( null )
  • 基本类型-->String\Number\Boolean\undefined\function 可以用typeOf来判断
  • 引用类型-->Array\Date\Regxp Error 调用toString后根据[oject XXX]来判断
  • 引用类型通过Object.prototype.toString.call(被判断的对象),不使用toString是因为toString被数组重写过,会直接将数组转成字符串,失去类型判断的功能

判断数组类型的多种方法:

  • array instanceOf Array
  • array.proto == Array.prototype
  • arr.constructor == Array
  • Array.isArray(arr)
  • Object.prototype.toString.call(arr) === '[Object Array]'

16、模块化
模块化现在是前端开发很重要的思想,它提高了项目的可维护、可拓展、可协作性。通常在浏览器中使用ES6的模块化支持,在node使用commmonJS的模块化支持

  • ES6: import / export
  • commonJS:require / module.exports / exports(同步)
  • AMD:require / defind(异步)
  • CMD:seaJs
  • UMD:兼容CommonJS和AMD,目前多数模块的封装,既可以在 Node.js 环境又可以在 Web 环境运行,所以一般会采用 UMD 的规范 require与import的区别
  • require支持动态导入,import不支持,正在提案 (babel 下可支持)
  • require是 同步 导入,import属于 异步 导入
  • require是 值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化

17、函数执行改变this
this的概念:由于JS的设计原理,在函数中,可以引用运行环境中的变量,因此就需要一个机制让我们能在函数内部获取当前的运行环境,这便是this。要想弄明白this,就需要知道函数的运行环境,通俗一点就是谁调用了函数
例如:obj.fn() 那么运行环境就是obj,this指向的是obj
fn()可以理解为window.fn() 那么运行环境就是window,this指向的是window
因为这个机制不能完全满足我们的需求,因此提供了三种手动修改this指向的方法
call:fn.call(this,arg1,arg2,arg3)
apply:fn.apply(this,[arg1,arg2,arg3])
bind :fn.bind(this)(1,2)
call、apply是会直接运行的,而bind不会运行,而是但会一个新的方法

18、事件机制
事件委托:www.cnblogs.com/liugang-vip…
事件捕获:document --> html --> body --> div
事件冒泡:div --> body --> html --> document(IE浏览器只支持冒泡事件,attachEvent没有第三个参数)
事件绑定的类型:
div.onclick = function(){} 。
删除事件div.onclick = null / div.onclick = false
IE9+:target.addEventListener(type, fn, true<捕获>/false<冒泡>)、 target.removeEventListener()来绑定和移除事件
IE8之前:div.attachEvent()、div.detachEvent()

三种类型的区别:

  • addEventListener可以为一个事件添加多个相应的函数,彼此之间不会覆盖,而onclick则不可以
  • onclick支持所有的浏览器,而addEventListener不支持IE浏览器,IE浏览器的事件添加需要用attachEvent,由于IE浏览器只支持冒泡,所以attachEvent没有第三个参数
  • onclick在注销时,只需要将属性值设置为null即可,用addEventListener注册的事件注销时需用removeEventListener,且参数要与addEventListener是一样的,所以addEventListner指定的函数不能是一个匿名函数,否则注销不了。虽然函数内容一样,但是函数对象是不一样的
  • attachEvent回调函数中的this是window,而onclick回调函数中的this是当前捕获事件的对象
  • 浏览器事件兼容性处理

document加载事件回调
DOMContentLoaded:在dom树加载完成之后被触发,不等待css、js、img等下载< 兼容性较差 >
onload:等待页面的内容都加载完毕之后被触发

函数柯里化: 在一个函数中,填充几个参数后返回一个新的函数的技术。

babel的编译原理:

  1. Babylon 先将ES6、ES7的语法转成AST
  2. babel-traverse 将AST进行编译转换,生成新的AST
  3. babel-generator将新的AST转成ES5语法

19、数组(Array)

  • foreach
  • map
  • every
  • some
  • filter
  • find
  • reduce
  • push后增 / pop后删 返回添加或删除的元素,改变原数组
  • unshift 前增 / shift 前删
  • sort 排序 / reverse 倒叙
  • slice(start, end) 截取 -1代表最后一项,-2最后一项的前一项
  • splice(index,howmany,item1,…… ,itemN) 删除、新增、替换
  • concat
  • indexOf 20、创建对象的方式
var obj = new Object()
var obj = {}
function F(){}    var obj = new F()
Object.create()

21、实现AJAX的基本步骤 www.cnblogs.com/Ann-web-1/p…

  1. 创建xmlHttpRequest对象,也就是创建一个异步调用对象
  2. 创建一个新的http请求,并指定该http请求的方法,URL以及验证信息
  3. 设置相应http请求状态变化的函数
  4. 发送http请求
  5. 获取异步调用返回的数据
  6. 使用JS和DOM完成局部刷新

23、JS防抖与节流(性能优化)
防抖:将高频率操作优化成只在最后一次执行。比如用户输入内容完成之后做一次校验即可
节流:将高频率操作转化成低频率操作。比如鼠标的滚动事件、滑动事件,可以将它们处理成每隔一段时间后执行一次

24、JS的DOM和BOM
JS的三大部分:BOM、DOM、ECMAScript
BOM:浏览器对象模型

  • 最顶层的对象是window
  • BOM提供了很多方法来操作或者访问浏览器
  • 比如获取浏览器的宽度高度、新增关闭窗口、获取URL、跳转页面、setTimeout、弹窗(alert、confirm)
    DOM:文档对象模型
  • 最顶层的元素是document
  • 存储在页面树形结构中

阮一峰老师的generator相关章节

//Promise的then(resolve, reject)和catch()  
//Promise状态从pending到resolve之后肯定会走then的第一个参数,resolve函数
//Promise状态从pending到reject之后,如果then有第二个参数reject,则执行reject,如果没有第二个参数reject则执行cathch。reject的优先级高于catch
//如果是网络异常(比如断网),会直接进入catch而不会进入then的第二个回调reject
class Promsie {
    constructor(fn) {
        //三个状态
        this.status = 'pending',
        this.resolve = undefined;
        this.reject = undefined;
        let resolve = value => {
            if (this.status === 'pending') {
                 this.status = 'resolved';
                 this.resolve = value;
            }
        };
        let reject = value => {
            if (this.status === 'pending') {
                this.status = 'rejected';
                this.reject = value;
            }
        }
        try {
            fn(resolve, reject)
        } catch (e) {
            reject(e)
        }
    }
    then(onResolved, onRejected) {
        switch (this.status) {
            case 'resolved': onResolved(this.resolve); break;
            case 'rejected': onRejected(this.resolve); break;
            default:
        }
    }
}
Promise.race = function(promises) {
    if (!Array.isArray(promises)) {
        throw new Error("promises must be an array")
    }
    return new Promise(function (resolve, reject) {
        promises.forEach(p =>
            Promise.resolve(p).then(data => {
                resolve(data)
            }, err => {
                reject(err)
            })
        )
    })
}

25、什么异步?

  • 异步编程的4种方法
  • 回调函数
  • 事件监听
  • 发布、订阅
  • promise promise实现: blog.csdn.net/qq_40619263…
    Generator 可以终止执行流程
    async和await
    async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。是generator的语法糖

JS设计中支持回调,回调产生了闭包,闭包就是把自己的变量暴露给其他的对象,但是其他对象什么时候执行他并不知道,因此产生了异步。
因此异步其实就是在使用回调时所产生的一个结果

为什么异步会被大量的使用:
首先执行时间长不代表资源消耗就多。比如ajax,它会发送一个请求,然后等待请求的响应。请求的过程中时间消耗比较大,但是对于JS的执行环境而言,并没有任何的资源消耗,所以其实这时候的主线程是闲置的状态。如果不用异步的话,主线程所做的事情就是等待请求的响应。那么,其实是可以把等待这个过程的线程利用起来的,让他去做别的事情,等请求响应以后再来做这个事情。这就形成了异步

异步看似和多线程相似,都是在前面程序没有执行完的时候就做了其他的事情,但是其实是有本质区别的。如果不是这种等待的操作,而是运算比较大的那种,就会造成线程的阻塞。这个时候就需要利用多线程来解决这个问题
回过来说,JS是怎么利用这个等待资源的呢,这和JS的运行机制有关系,JS的运行机制会把所有需要执行的放在一个执行队列中去,像这样需要等待的程序,它会在执行了以后开始等待的时候把它放到执行队列的后面去,直到等待的过程结束后,再来执行它的回调

26、回流和重绘
juejin.cn/post/684490…
27、for in和for of的区别

  • for in 遍历数组
  • 1、遍历的返回值是index,且是字符串的形式,不能直接进行几何运算
  • 2、顺序有可能不是array的实际内部顺序
  • 3、会遍历出数组所有的可枚举属性,包括原型
  • 4、for in更加适合遍历对象

for of

  • 可以遍历有迭代器对象的对象,比如数组、字符串等。不能遍历普通对象,因为普通对象没有迭代器对象,如果要遍历对象的话可以内置Object.keys(obj)
  • for……in 遍历对象返回key值,遍历数组返回下标index
  • for……of 不能遍历对象,如果遍历需要使用 Object.keys(obj),返回key,遍历数组返回对应的项 28、事件循环 juejin.cn/post/684490…
    浏览器(多进程)包含了Browser进程(浏览器的主进程)、第三方插件进程和GPU进程(浏览器渲染进程)
    其中GPU进程(多线程)和Web前端密切相关,包含以下线程:
  • GUI渲染线程
  • JS引擎线程
  • 事件触发线程(和EventLoop密切相关)
  • 定时触发器线程
  • 异步HTTP请求线程 GUI渲染线程和JS引擎线程是互斥的,为了防止DOM渲染的不一致性,其中一个线程执行时另一个线程会被挂起。

JS引擎线程和事件触发线程 浏览器页面初次渲染完毕后,JS引擎线程结合事件触发线程的工作流程如下:
(1)同步任务在JS引擎线程(主线程)上执行,形成执行栈(Execution Context Stack)。
(2)主线程之外,事件触发线程管理着一个任务队列(Task Queue)。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
(3)执行栈中的同步任务执行完毕,系统就会读取任务队列,如果有异步任务需要执行,将其加到主线程的执行栈并执行相应的异步任务。

事件循环机制(Event Loop)
事件触发线程管理的任务队列是如何产生的呢?事实上这些任务就是从JS引擎线程本身产生的,主线程在运行时会产生执行栈,栈中的代码调用某些异步API时会在任务队列中添加事件,栈中的代码执行完毕后,就会读取任务队列中的事件,去执行事件对应的回调函数,如此循环往复,形成事件循环机制

任务类型
JS中有两种任务类型: 微任务(microtask)和宏任务(macrotask),在ES6中,microtask称为 jobs,macrotask称为 task。
宏任务: script (主代码块)、setTimeout 、setInterval 、setImmediate 、I/O 、UI rendering
微任务:process.nextTick(Nodejs) 、promise 、Object.observe 、MutationObserver
这里要重点说明一下,宏任务并非全是异步任务,主代码块就是属于宏任务的一种(Promises/A+规范)。
它们之间区别如下:

  • 宏任务是每次执行栈执行的代码(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
  • 浏览器为了能够使得JS引擎线程与GUI渲染线程有序切换,会在当前宏任务结束之后,下一个宏任务执行开始之前,对页面进行重新渲染(宏任务 > 渲染 > 宏任务 > ...)
  • 微任务是在当前宏任务执行结束之后立即执行的任务(在当前 宏任务执行之后,UI渲染之前执行的任务)。微任务的响应速度相比setTimeout(下一个宏任务)会更快,因为无需等待UI渲染。
  • 当前宏任务执行后,会将在它执行期间产生的所有微任务都执行一遍。

根据事件循环机制,重新梳理一下流程:

  • 执行一个宏任务(首次执行的主代码块或者任务队列中的回调函数)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有任务(依次执行)
  • GUI线程渲染完毕后挂起,JS引擎线程执行任务队列中的下一个宏任务

箭头函数与普通函数的区别
blog.csdn.net/z591102/art…

  • 箭头函数没有prototype,所以就没有this指向
  • 箭头函数没有自己的this,箭头函数在定义时继承自外层第一个普通函数的this。所以箭头函数的this,在它被定义时就已经确定了,不会被改变
  • call | apply | bind不能改变箭头函数的this指向
  • 箭头函数不能作为构造函数使用
  • 箭头函数不绑定arguments,用( ...rest)来代替arguments访问函数的参数列表
  • 箭头函数不能用作generator,不能使用yeild关键字

VUE

v-show 与 v-if的区别
v-if是通过创建和销毁DOM来实现的。可以说是真正的条件渲染,因为他会确保条件块内的事件监听和子组件适当的销毁和创建。如果初始条件为假则不创建,直到条件变为真时才开始渲染代码块
v-show是通过CSS的display来实现的,不管初始条件是真是假,都会创建代码块,然后通过改变display的属性进行切换
注:v-if适合使用在很少改变条件,不频繁切换的场景中,而v-show则适用于非常频繁切换条件的场景 v-if 和v-for

css和style是怎么绑定的 CSS绑定: 对象绑定-->

<div :class="{isShow:isShowFlag,'text-style':isHasText}"></div>

数组绑定-->

<div :class="[isActive ? activeClass : '', errorClass]"></div>

Style绑定: 对象绑定-->

<div :style="{color:activeColor , fontSize: fontsize + 'px'}"></div>

数组绑定-->

<div :style="[styleColor,styleSize]"></div>

怎样理解VUE的单项数据流
所有的prop使其父子prop之间形成了一个单向下行绑定。父级prop的更新会向下流动到子组件中。但是反过来则不行。这样可以防止子组件的意外操作改变了父组件的状态,使我们的应用的数据流向变得难以理解。
父级组件每次更新时,子组件的prop会被自动更新为最新的值,这意味着不应该在子组件内部修改prop。如果这样做了就会报错。子组件要想修改只能通过$emit派发一个自定义事件,父组件接到后,由父组件完成修改

computed和watch的区别及使用场景
segmentfault.com/a/119000001…

  • computed:计算属性,依赖于其他的值,并且computed的值有缓存,只有它依赖的属性值发生改变时,下一次获取的computed值的时候才会重新计算computed的值
  • watch:更多的是「观察」的作用,类似某些数据的监听回调,每当监听的数据发生改变时都会执行回调来进行后续操作 场景的区别:
  • computed:当我们需要数值计算时,且依赖于其他数据时,可以用computed。可以利用它的缓存特性,来避免每次取值时,都要重新计算
  • watch:当我们需要在数据变化时进行一些异步操作时,应该使用watch。

直接给一个数组项赋值,VUE能检测到变化嘛
vue不能检测到以下两种情况的数组变动
当利用索引直接设置一个数组项时
vm.items[indexOfItem] = newValue 当改变数组长度时
vm.items.length = newLength

为解决问题一,vue提供了以下解决方案:

Vue.set( vm.items, indexOfItem, newValue )
vm.$set( vm.items, indexOfItem, newValue)
vm.items.splice( indexOfItem, 1, newValue )

为解决问题二,vue提供以下操作方法

vm.items.splice( newLength )

生命周期
生命周期的定义
VUE实例有一个完整的生命周期。从开始创建实例,初始化数据,编译模板,挂载DOM-->渲染、更新DOM-->渲染,销毁等一系列的过程,称之为生命周期

各个生命周期的作用

  • beforeCreate 组件创建之初,此时组件的属性和事件都是默认的,其他的东西还没有创建,data和methods未被初始化
  • created 组件创建完成,此时data和methods已经初始化完成,可以调用methods或操作data
  • beforeMount 模板已经被编译完成,但只存在在内存中,还未挂载至页面上
  • mounted 模板已经被挂载到页面上,此时整个VUE实例初始化已完成
  • beforeUpdate data中的数据已经被更新,但是页面上的数据还是旧数据
  • updated 组件中data数据已经和页面同步
  • activated keep-alive专属,组件被激活时调用
  • deactivated keep-alive专属,组件被销毁时调用
  • beforeDestroy 组件销毁前调用
  • destroyed 组件销毁后调用

c. 生命周期示意图

vue.png vue的父组件和子组件生命周期函数的执行顺序
父子组件生命周期钩子函数执行顺序可以分为以下四类:

  • 创建,加载渲染过程
    父组件befroeCreate --> 父组件created --> 父组件beforeMount --> 子组件beforeCreate --> 子组件created --> 子组件beforeMount --> 子组件mounted --> 父组件mounted
  • 子组件更新 父组件beforeUpdate --> 子组件beforeUpdate --> 子组件updated --> 父组件updated
  • 父组件更新 父组件beforeUpdate --> 父组件updated
  • 销毁 父组件beforeDestroy --> 子组件beforeDestroy --> 子组件destroyed --> 父组件destroyeda

可以在哪个生命周期中发送异步请求
可以在create、before Mount、mounted中进行异步请求,因为在这三个函数中,data已经初始化完成,可以用来处理服务端返回的数据。但是最好还是在created中发送异步请求,因为:
1、能更快的获取到服务端返回的数据,减少页面的loading时间
2、SSR不支持beforeMount、mounted

父组件可以监听到子组件的生命周期吗? 可以。如果父组件监听到子组件挂载mounted后需要做一些处理,可以通过以下两种方式完成:
通过子组件暴露$emit

// Parent
<Child @mounted='doSomething' />
// child
mounted(){
this.$emit('mounted')
}

通过@hook

// Parent.vue 
<Child @hook:mounted="doSomething" ></Child> 
doSomething() { console.log('父组件监听到 mounted 钩子函数 ...'); }
// Child.vue 
mounted(){ console.log('子组件触发 mounted 钩子函数 ...'); },

谈谈你对keep-alive的理解
keep-alive是vue内置的组件,可以使被包含的组件保留状态,避免重新渲染:
结合路由动态,用于缓存组件
提供了include和exclude两个属性,两者都支持字符串和正则表达式

  • include:被匹配的组件会被缓存,不需要重新渲染
  • exclude:被匹配的组件不会被缓存,需要重新渲染,exclude优先级大于include 对应两个钩子函数
  • activated:组件被激活时触发
  • deactivated:组件被销毁时触发

组件中的data为什么是一个函数
因为组件是用来被复用的,且JS中对象是引用关系。如果data是一个对象,那么每个组件之间的作用域就没有被隔离,子组件中的data属性值会被相互影响。但当它是一个函数的时候,每个组件都有一个可独立维护的作用域,可以避免组件之间data属性值的相互影响

v-model的原理
在项目中我们主要是用v-model指令在表单input、textarea、select、checkbox、radio上创建双向绑定。
v-model其实只是一个语法糖,v-model在内部为不用的元素使用不同的属性抛出不同的事件

  • textare、text、input使用value属性+input事件
  • checkbox和radio使用checked属性+change事件
  • select使用将value作为prop,并使用change事件
<input  v-model='somethings'>

相当于

 <input v-bind:value='somethings' v-on:input='somethings = $event.target.value'>

vue-router路由模式有几种
hash
使用URL Hash值来做路由,支持所有浏览器。包括不支持HTML5 History API的浏览器
history
依赖HTML History API和服务器配置,history.
abstract
支持所有的javascript运行环境,如nodejs服务器。如果发现没有浏览器API,路由将会强制进入这个模式
v-router中常用的hash和history的实现原理
hash的实现原理:
早期前端的路由主要是通过location.hash来实现的。原理也很简单,location.hash的值就是URL中‘#’后面的内容

hash实现原理主要基于以下几个特性:

  • hash是客户端的一个状态,因此在向服务器端发送请求时,hash不会被发送
  • hash值的改变,会在浏览器访问记录中新增一条记录,因为可以通过浏览器的前进、后退按钮来完成hash的切换
  • 可以通过a标签并设置href属性,当用户点击这个标签时会改变hash的值。或者可以通过JavaScript对location.hash进行赋值,来完成hash值的改变
  • 可以通过hashchange来监听hash值的改变,从而完成页面的切换 history的实现原理:
    依赖于HTML5中的History API来实现URL的改变。其中最主要的两个API是history.pushState 和history.replaceState。这两个API可以在不进行刷新的情况下,操作浏览器的历史记录。不同的是前者是新增一条历史记录,后者是直接替换成当前的历史记录
    history实现原理主要基于以下几个特性:
  • pushState和replaceState两个API来实现URL的改变
  • 可以通过popstate事件来监听URL的改变,从而进行切换
  • history.pushState 和history.replaceState不能触发popstate,因此需要我们手动触发页面跳转 vue是如何实现数据双向绑定的?
    采用数据劫持结合发布-订阅设计模式,通过object.defineProperty劫持各个属性的getter和setter,在属性值发生变化时发送消息给订阅者,触发相应的监听回调

具体如下:

  • 实现一个监听器Observer,用来劫持和监听各个属性,属性值一旦发生变化,就会发消息给订阅者
  • 实现一个解析器Compile,用来解析VUE的模板指令,将模板中的变量转化成数据,初始化页面视图。并将指令对应的节点绑定更新函数,添加用于监听数据的订阅者,一旦数据发生变化,收到通知后,触发更新函数更新数据
  • 实现一个订阅者watcher,它是Observer和Compile的桥梁,主要是订阅Observer属性值变化的消息,当收到属性值变化的消息后,会触发compile的更新函数
  • 实现一个订阅器Dep,它是发布-订阅的设计模式,用来收集订阅者watcher,将watcher和observer进行统一管理

Vue框架怎么实现对象和数组的监听?
www.jb51.net/article/107…
Vue将data中的数组的一些方法进行了重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化
vue包含了一组观察数组变化的方法push、pop、unshift、shift、sort、reverse、splice方法进行了重写。在使用这些方法的时候也会触发视图的更新。
新建一个以Array.protoType为原型的对象。把这个对象本身附加属性,再将这个新建的对象作为原型或者作为属性传给Observer的value中去,来达到监听变化的目的。
判断当前的执行环境是否支持proto,支持则直接指向这组方法,如果不支持,则遍历这组方法,添加到对象中

Proxy和Object.defineProperty的优劣势

  • proxy能直接监听对象,而不是属性
  • proxy能直接监听数组
  • proxy返回的是一个新对象,我们可以直接操作新的对象达到目的,而object.defineProperty只能遍历对象属性进行修改
  • proxy有很多种拦截方法,但是object.defineProperty不具备
  • object.defineProperty的兼容性比proxy好一些
    虚拟DOM的实现原理(juejin.cn/post/684490…
    用JavaScript对象来模拟真实的DOM树,对真实的DOM树进行抽象
    diff算法 ----> 比较两个虚拟DOM树的差异
    pach算法 ----> 将两个虚拟DOM对象的差异应用到真正的DOM树
    减少真实DOM的创建,避免真实DOM的对比,取而代之的是通过对两个虚拟DOM对象的对比,对象的对比性能要远远强于DOM的对比

虚拟DOM的优缺点
优点:

  • 保证性能的下限:框架的虚拟DOM需要适配上层所有API的操作,因此虚拟DOM操作的实现必须是普适的,所以它的性能也并不是最优的,但是相对于粗暴的DOM操作性能还是要好很多。因此虚拟DOM可以保证我们在不需要手动优化的情况下,提供不错的性能
  • 不需要手动操作DOM:有了虚拟DOM之后,我们不再需要手动的去操作DOM,只需要维护view-model的交互逻辑即可。虚拟DOM和数据双向绑定,足以帮我们以预期的方式更新页面,大大的提高了开发的效率
  • 跨平台:虚拟DOM的本质就是JavaScript对象,而真实的DOM和平台具有强相关,所以虚拟DOM可以更方便的完成跨平台操作 缺点:
    不能极致优化:
    虽然虚拟DOM+合理的优化足以应对绝大多部分的应用的性能,但是对于性能要求极高的应用,虚拟DOM无法完成极致优化

VUE中的key有什么作用
www.zhihu.com/question/61…
概念:
key是vue中vnode的唯一标记,通过这个key,我们的diff操作可以更准确,更快速
当用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key的作用主要是为了高效的更新虚拟DOM。
虚拟DOM中diff是一层层的对比,这个对比的过程中需要有一个名字相对应,这样才能更精准的知道对比的是同一个元素。key值不用index,因为当我们的数组发生变化时,index的值也会变化,最好使用id这类唯一且稳定的标识

routeroute 和router 的区别:

  • $route 是‘路由信息对象’,包含params、hash、path、name、query等
  • $router是路由实例,包含钩子函数和跳转方法 路由的钩子函数
    首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。
    beforeEach主要有3个参数to,from,next:
  • to:route即将进入的目标路由对象,
  • from:route当前导航正要离开的路由
  • next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转。

next Tick
当你修改了data的值然后马上获取这个dom元素的值,是不能获取到更新后的值的。需要使用$nextTick这个回调,让修改后的data值渲染更新到dom元素之后在获取,才能成功

原理
根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout

VUE3相对于VUE2做了哪些事情:

  • proxy替换object.defineProperty
  • 虚拟DOM的瓶颈,静态节点标记
  • 用户应该能够在构建时删除未使用的框架功能的代码-也称为“Tree Shaking”
  • 语言切换至typeScript
  • 整体体积更小
  • composition API
  • VUE3.0相比VUE2.x在diff上做了哪些改动
  • Vue3.0在创建vnode的时候就确定其类型,已经在mount/patch的过程中采用位算法来判断一个VNode的类型,在这个基础上配合原来的diff核心算法,性能上有了很大的提升

VUE3.0的数据响应的原理?

  • Proxy只代理对象的第一层,如何深度观测?如果防止监测数据时多次set、get
  • proxy完成数据响应
  • 判断Reflect.get的返回值是不是引用类型,如果是的话则使用 .reacitve进行深度检测
  • 判断key值是否为当前被代理对象的target的自身属性或判断新值和旧的值是否相等,满足其中之一才可能执行trigger

VUE的性能优化

  • 保证key的稳定性和唯一性,避免使用index
  • 减少data中的数据,data中的数据都会增加getter和setter,收集对应的watcher
  • v-if和v-for避免同时使用
  • 尽量用v-if去代替v-show
  • 采用keep-alive进行页面缓存(可以减少虚拟DOM的对比产生的性能损耗)
  • 第三方模块按需引入
  • 服务端渲染SSR(将标签渲染成HTML的过程交给服务端去处理)
  • 防抖和节流
  • 代码压缩
  • 使用cdn加载第三方模块
  • sourceMap优化