高级Web前端必会面试题知识点,大厂面试必备

高级Web前端工程师必会面试题,这里只是整理一些范围知识点,并没有特别具体的面试题目,只要把这些知识点搞明白了,面试题都不是问题。

另外整理了一套 前端面试集锦,内容涉及前端各个方面,并且每道题目基本都有对应得答案解析,是你面试必备宝典。
骚年,加油!高薪等你。

一、HTML+CSS 系列

布局的三种模式

参考:https://www.cnblogs.com/Renyi-Fan/p/12173366.html#_label3

  • 弹性盒布局 flex
    盒子是并列的,可以设置指定宽度,轻松实现两栏,三栏布局,经典的圣杯布局和双飞翼布局就是通过flex来实现的。
    但是,flexbox 布局方式对浏览器的支持不太友好,有一些兼容性问题,但是,这应该是未来发展的趋势。

  • 浮动 float浮动
    float布局是目前各大网站用的最多的一种布局方式了。
    通过给元素设置float属性来实现元素浮动,浮动的元素是脱离文档流的,但是不脱离文本流。
    特点:
    1) 对自身:a、float元素可以形成块,可以让行内元素变成块元素,也拥有宽和高;b、浮动元素的位置尽量靠上;c、设置float:left 或 float:right,如果这一行满足不了浮动元素的宽度,则会被挤到下一行。
    2) 对兄弟:a、不影响其他块元素的位置;b、影响其他块元素的文本
    3) 对父元素:高度塌陷
    高度塌陷:(解决方案)

  • 响应式
    最简单的方式是加上一个 meta 标签, ,其中 width = device-width 这一句的意思是让页面的宽度等于屏幕的宽度。
    rem 是指html的font-size的大小, 根据rem来计算各个元素的宽高,然后在配合media query 就可以实现自适应
    @media query 语法

 @media screen and (max-width: 360px) {
     
  	html {
      font-size: 12px; }
 }
移动端布局适配设备的方案

参考https://www.cnblogs.com/liuXiaoDi/p/12261100.html

  • rem
    rem 以根元素字体大小作为参照的布局方式,可以实现等比缩放布局,不管内容是多大的,显示的内容是一样的。
  • %
    页面宽度为100%,高度固定,当屏幕越大,显示的内容也就越多。
    控件弹性,图片等比例缩放,文字流式
1px边框的解决方案
  • 原理:
    在高分辨率的显示屏中,像素比为2或者3,1px边框看起来比真实的1px边框看起来更宽
  • 方案
    1. 使用伪类添加transform:元素本身不定义边框,给伪元素定义一个 1px 边框,并且根据像素比值(dpr => device-pixel-ratio)设置缩放比例
    当 dpr > 2.5 时设置 scale(0.333),
    当 2.49 > dpr > 1.5 时设置 scale(0.5),
    当 dpr < 1.49 时设置 scale(1)
    2. 图片 border-image
BFC(Block Formatting Contexts)

文档:day0220–BFC.note
链接:http://note.youdao.com/noteshare?id=f0a95534319d2cd8f49dbe9b6f5240eb&sub=B77453B2104943B09697AF70C4CE68A5

  • 概念
    BFC:块级格式化上下文
  • 触发条件
    1. 根元素 html
    2. float的值不为none
    3. overflow的值不为visible
    4. display的值为inline-block/table-cell/table-caption/flex/inline-flex
    5. position 的值为 absolute 或者 fixed
  • 特性
    1. box垂直方向的距离由margin决定,属于同一个BFC的两个相邻box的margin会发生重叠
      解释margin上下会重叠,以及解决方法的原因
      解决方法:给第二个box添加一个父元素,并且添加overflow: hidden;属性,使这两个box分属两个不同的BFC,即可解决margin重叠的问题。
    2. 计算BFC高度时,浮动元素也参与计算
      解析高度塌陷 可以用 overflow: hidden解决
    3. BFC的区域不会与float box发生重叠
    4. BFC内部的box会在垂直方向一个接一个的放置
    5. 每个元素的margin的左边会与包含块border的左边相接触,即使存在浮动也会如此
    6. BFC就是页面上的一个独立容器,容器里面的元素不会影响到外面的元素
BootStrap布局原理
  • Bootstrap 是一个用于快速构建Web应用程序和网站的前端框架,是基于html、css、js。
  • 栅格布局:
    Bootstrap 内置了一套响应式、移动设备优先的流式栅格系统,会随着屏幕设备或者视口(Viewport)尺寸的增加,系统会自动分为最多12列。
    网格系统的实现原理也是非常简单的,只需要通过定义容器的大小,然后平分12份(也有24,32的,但是12是最常见的),再调整内外边距,最后结合媒体查询,就可以实现了这种栅格布局系统。
  • 优点:自适应布局,友好的移动端布局
  • 缺点:不兼容IE,需要修改bootstrap的样式
  • 相关问题:
    • 分辨率:xs(超小屏幕手机 < 780px)、sm(小屏幕平板 >= 780px)、md(中等屏幕桌面显示器 >= 992px)、lg(大屏幕大桌面显示器 >= 1200px)
    • bootstrap 栅格还有24格的 要怎么设置:分别在十二的基础上嵌套,每个嵌套一个!
    • container 与 container-fluid 有什么区别:container是文本居中、container-fluid占整行
Sass/Less/Stylus/StyledComponents 区别,联系,在什么场景下使用
  • CSS预处理器(Sass/less/stylus):就是一种专门的编程语言,为css增加一些编程的特性,将css作为目标生成文件,然后就可以使用这种语言进行编程。可以让代码更加简洁、适应性更强、可读性更强、易于代码的维护。
  • 样式化组件(StyledComponents):
  • 区别与联系:
    1. Sass与Less语法较为严谨,Stylus语法比较散漫,Less上手比较快
    2. Sass和Less相互影响比较大,Sass已经全面兼容css
    3. Sass和Less都有第三方工具提供转译,特别是Sass和Compass是绝配
    4. Sass、Less和Stylus都具有变量、作用域、混含、嵌套、继承、运算符、颜色函数、导入和注释等基本特性,各自特性实现功能基本类似,只是使用规则有所不同
    5. Sass和Stylus具有类似的语言处理能力,比如条件语句、循环语句等,而Less需要通过when等关键词模拟这些功能
  • 使用场景:
    • Sass:在编写代码时,需要用到变量、嵌套、混合、导入等高级操作时,可以更好的组织管理样式文件,更高效的开发项目。
    • Less:它更适用于皮肤、模板等整体框架固定死的网站制作,比如论坛、空间
    • Stylus:可编程
    • StyledComponents:在React中使用,编写实际的css代码来设计组件样式,不需要组件和样式之间的映射,创建成功后就是一个React组件

二、JavaScript 系列

变量对象+作用域链+this(执行上下文)
  • 变量对象:如果变量与执行上下文相关,那变量自己应该知道它的数据存储在哪里,并且知道如何访问,这种机制就是变量对象(VO)。
    变量对象是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的变量、函数声明、函数形参等内容。
  • 全局对象:是在进入任何执行上下文之前就已经创建了的对象。
    全局对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。
  • 作用域链与原型链:
    • 区别:
      1.作用域链是对于变量而言,原型链是对于对象的属性。
      2.作用域链顶层是window,原型链顶层是Object
    • 联系:从链表开始查找,直到找到为止。
  • this指向:取决于被调用的方式
    1. 如果普通的函数调用,非严格模式下,this指向window,严格模式下,this是undefined
    2. 如果是对象调用的方式,this指向该对象
    3. 如果是call()、apply()或者bind()方式调用,this指向被绑定的对象
    4. 如果是构造函数调用方式,this指向实例化出来的新对象
    5. 如果是箭头函数,是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定
闭包
  • 闭包:就是能够读取其他函数内部变量的函数
  • 变量的作用域分为全局变量和局部变量两种。而js中的函数内部可以直接读取全局变量,但是函数外部无法读取函数内部的局部变量。如果想要取得函数内部的变量,就要在函数的内部再定义一个函数,将函数作为返回值返回,就可以在函数的外部读取他的内部变量了。
  • 闭包的作用:
    1. 可以读取函数内部的变量
    2. 让这些变量的值始终保存在内存中 => 可能导致内存泄漏
  • 闭包的this指向:外部函数的this指向调用他的对象,内部函数的this指向了全局对象。
原型
  • 原型(__proto__对象属性):原型为同一个构造函数new出来的实例对象提供了一个公共的区域来存放共同的属性和方法。
    js规定,每一个函数都有一个prototype对象属性,指向另一个对象,prototype的所有属性和方法,都会被构造函数的实例继承。这就意味着,我们可以把那些不变(公共)的属性和方法,直接定义在 prototype对象属性上。prototype就是调用构造函数所创建的那个实例对象的原型。
    prototype可以让所有的对象实例共享它所包含的属性和方法。也就是说,不必再构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。
  • 为什么要使用原型:可以节省一定的内存,特别是需要构建多个实例的时候。
  • 原型链:实例对象与原型之间的连接,叫做原型链。
    js在创建对象的时候,都有一个叫做proto的内置属性,用于指向创建它的函数对象的原型对象prototype。
  • 内部原型(proto)和构造器原型(prototype)
    1. 每个对象都有一个proto属性,原型链上的对象正是依赖这个属性连结在一起。
    2. 作为一个对象,当访问其中的一个属性或者方法的时候,如果这个对象中没有这个方法或属性,那么js引擎将会访问这个对象的proto属性所指向的上一个对象,并在那个对象中查找指定的方法或属性,如果不能找到,那就会继续通过这个对象的proto属性指向的对象进行向上查找,直到这个链表结束。
继承
  • 前提:提供父类(继承谁,提供谁的属性)
  • 分类:
  1. 原型链继承:可以让新实例的原型等于父类的实例
    • 特点:实例可继承的属性有:实例的构造函数的属性、父类构造函数属性、父类原型的属性。(新实例不会继承父类实例的属性)
    • 缺点:1. 新实例无法向父类构造函数传参
      2. 继承单一
      3. 所有新实例都会共享父类实例的属性(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改)
  2. 构造函数继承:用.call().apply()将父类构造函数引入子类函数(在子类函数中做了父类的复制)
    • 特点:1. 只继承了父类构造函数的属性,没有继承父类原型的属性
      2. 解决了原型链继承的缺点
      3. 可以继承多个构造函数属性(call多个)
      4. 在子实例中可以向父实例传参
    • 缺点:1. 只能继承父类构造函数的属性
      2. 无法实现构造函数的复用(每次用都要重新调用)
      3. 每个新实例都有父类构造函数的副本
  3. 组合继承(原型链继承+构造函数继承):结合了两种模式的优点,传参和复用
    • 特点:1. 可以继承父类原型上的属性,可以传参,可复用
      2. 每个新实例引入的构造函数属性是私有的
    • 缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
  4. 原型式继承:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了一个可以随意增添属性的实例或对象,Object.create()就是这个原理
    • 特点:类似于复制一个对象,用函数来包装
    • 缺点:1. 所有实例都会继承原型上的属性
      2. 无法实现复用(新实例属性都是后面添加的)
  5. 寄生式继承:就是给原型式继承外面套个壳子
    • 特点:没有创建自定义类型,因为只是套了个壳子返回对象,这个函数就成了创建的新对象。
    • 缺点:没有用到原型,无法复用
  6. 寄生组合继承:(常用)修复了组合继承的问题
    寄生:在函数内返回对象然后调用
    组合:1. 函数的原型等于另一个实例;2. 在函数中用apply或者call引用另一个构造函数,可传参
函数节流和防抖
  • 函数的节流和防抖是优化高频率执行js代码的一种手段,js中的一些事件在执行触发时,会不断调用 绑定在事件上的回调函数,极大的浪费资源,降低性能。为了优化体验,需要对这类事件进行调用次数的限制,此时引入函数节流防抖。
  • 节流(throttle):控制事件发生的频率,比如控制为1s发生一次,甚至1分钟发生一次。
    • 应用场景:
      1. scroll事件,滚动监听事件,每隔一段时间计算一次位置信息等
      2. 浏览器的播放事件,每隔1s计算一次进度信息
      3. input框实时搜索并发送请求展示下拉列表,每隔1s发送一次请求。(防抖也可以)
      4. 高频点击提交,表单重复提交
    • 代码实现:
 function throttle(fn, delay) {
     
     let timer;
     return function () {
     
         let _this = this;
         let args = arguments;
         if (timer) {
     
             return;
         }
         timer = setTimeout(function () {
     
             fn.apply(_this, args);
             timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
         }, delay)
     }
 }
  • 防抖(debounce):防止抖动,以免把一次事件误执行多次,影响性能。
    • 应用场景:
      1. 登录注册、发短信等按钮避免用户点击过快,导致多次发送请求,需要防抖
      2. 调整浏览器窗口大小时,resize次数过于频繁,造成计算过多,此时需要一次到位,需要防抖
      3. 文本编辑器实时保存,无任何更改操作一段时间后自动保存
      4. mousemovemouseover鼠标移动事件防抖
      5. 搜索框搜索输入,只需要用户最后一次输入完,在发送请求防抖
      6. 手机号、邮箱验证输入检测
    • 代码实现:
 function debounce(fn, delay) {
     
            let timer; // 维护一个 timer
            return function () {
     
                let args = arguments;
                if (timer) {
     
                    clearTimeout(timer);
                }
                timer = setTimeout(()=> {
     
                    fn.apply(this, args); // 用apply指向调用debounce的对象,相当于this.fn(args);
                }, delay);
            };
        }
  • 比较:
    • 相同点:
      1. 都可以通过使用 setTimeout 实现
      2. 目的都是为了降低回调执行频率,节省计算资源
    • 不同点:
      1. 函数防抖:在一段连续操作结束后,处理回调,利用 clearTimeoutsetTimeout 实现。
      2. 函数节流:在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用它来提高性能。
      3. 函数防抖关注一定时间连续触发的事件只在最后执行一次,而函数节流侧重于一段时间内只执行一次。
  • 总结:
    防抖:防止抖动,单位时间内事件触发会被重置,避免事件被误伤触发多次。代码实现重在清零 clearTimeout。防抖可以比作等电梯,只要有人进来,就需要再等一会。业务场景有避免触发按钮多次重复提交。
    节流:控制流量,单位时间内事件只能触发一次。代码实现重在开锁关锁 timer=timeout;timer=null。节流可以比作红绿灯,每等一个红灯时间就可以过一批。
函数柯里化与反柯里化
  • 柯里化(currying):是一种编程技术,主要就是把原本接收多个参数的函数变成只接受一个参数的函数,并且返回一个接收剩余参数的函数。
    • 应用场景:
      1. 封装一些含有环境判断的方法时,函数柯里化可以帮助我们减少判断条件执行的次数;
      2. 在封装函数节流、防抖、bind此类返回值是函数的方法时,会用到函数柯里化;
      3. 可用与 vue、react、小程序中的事件传参,在事件绑定时就执行回调函数传参,根据传参值返回真正的事件 callback函数。
  • 反柯里化:扩大方法的适用范围。
    1. 可以让任何对象拥有其他对象的方法(改变原来方法上下文)
    2. 增加被反柯里化方法接收的参数

设计模式

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

  1. 工厂模式
    工厂模式定义了一个用于创建对象的接口,用户只负责传递需要的参数,不需要关心内部的逻辑,隐藏了创建实例的复杂度,最后返回一个实例。
  2. 单例模式
    单例模式是指在内存中只会创建且仅创建一次对象的设计模式。单例模式很常用,比如全局缓存、全局状态管理等,这些只需要一个对象,就可以使用单例模式。比如 Redux、Vuex的store。
  3. 观察者模式(发布-订阅模式)
    观察者模式中一般都需要实现三个接口: subscribe()接收观察者,使其订阅;unsubscribe()取消订阅;fire()触发事件,通知到所有观察者。
  4. 装饰器模式
    装饰器模式不需要改变已有的接口,它的作用是给对象添加功能。比如 react里的高阶组件。
  5. 适配器模式
    适配器用来解决两个接口不兼容的问题,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。我们其实经常使用到适配器模式。比如父组件传递给子组件一个属性,组件内部需要使用 computed计算属性来做处理,这个过程就用到了适配器模式。
ES6新增API(Proxy, Reflect, Promise, Generator, async, Decorator, Class)
  • Proxy:用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。这个词的原理为代理,在这里可以表示由它来“代理”某些操作,译为“代理器”。

    • 基本用法:ES6原生提供给了Proxy构造函数,用来生成Proxy实例。
      var proxy = new Proxy(target, handler);
      
      Proxy对象的所有用法,都是通过这种形式。不同的只是handle参数的写法。其中new Proxy用来生成Proxy实例,target是表示所要拦截的对象,handle是用来定制拦截行为的对象。
  • Reflect:是一个全局的普通的对象,原型是Object。

    • 目的:
      1. 将Object对象的一些属于语言内部的方法放到Reflect对象上,从Reflect上能拿到语言内部的方法。如:Object.defineProperty;
      2. 修改某些object方法返回的结果。如:Object.defineProperty(obj, name, desc)在无法定义属性的时候会报错,而Reflect.defineProperty(obj, name, desc)则会返回false;
      3. 让Object的操作都变成函数行为。如object的命令式:name in obj和delete obj[name] 则与 Reflect.has(obj, name)、Reflect.deleteProperty(obj, name)相等;
      4. Reflect对象的方法与Proxy对象的方法一一对应,只要proxy对象上有的方法reflect也能找到。
  • Promise:是一个专门解决异步回调地狱的问题。

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

      • 特点:
      1. 对象的状态不受外界影响,promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
      2. 一旦状态改变就不会再变,任何时候都可以得到这个结果,promise对象的状态改变,只有两种可能:从 pending => fulfilled,从pending => rejected。这个就称为 resolved。如果改变已经发生,再对 promise 对象添加回调函数,也会立即得到这个结果,这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。
      • 用法:
      1. Promise 是一个构造函数,这个构造函数有两个参数,分别是 resolve(成功之后的回调函数)和 reject(失败之后的回调函数)
        因为 Promise 表示的是一个异步操作,每当我们 new 一个 promise 实例,就表示一个具体的异步操作,那么这个异步操作的结果就只能有两种状态:成功/失败,两者都需要回调函数 resolve/reject返回。所以内部拿到操作结果后,无法使用 return 把操作结果返回给调用者,这个时候只能用回调函数的形式来吧成功或者失败的结果返回给调用者。
      2. promise 实例生成以后,可以用 then方法分别指定 resolved 状态和 rejected 状态的回调函数,then方法可以接收两个回调函数作为参数,第一个回调函数是 promise 对象的状态变成 resolved 时调用,第二个回调函数是 promise 对象的状态变为 rejected 时调用。第二个回调函数是可选的,这两个函数都接受 promise 对象传出的值作为参数。
  • Generator:与平常的函数不同,它可以理解为是一个分布执行的函数,返回值是一个遍历器。

    • 用法:
      1. 外部可以通过next(),thow()和return()调用,只是调用的形式不同。
      2. 在应用方面主要是异步调用,不同于以前的回调函数和Promise(Promise算是对回调函数解决嵌套繁琐问题提出的)
      3. 它在每一个yield中部署自己的异步操作,等到需要执行的时候再调用。
      4. generator函数和Ajax可以一起进行同步操作。
      5. 它的分布执行的特性决定了它对耗时大的多步操作有很大的改进(generator如果你不执行,那之后的程序系统不会编译)。
      6. 部署Iterator接口:generator函数可以再任何对象上部署Iterator接口。
  • async/await:回调地狱的终极解决方案,使用它可以把异步代码写的看起来像同步代码。

    • await 是一个函数中的关键字,要求函数必须是 async 声明的函数。当 await 中的方法执行完毕或者返回后执行后续代码。
  • Decorator:修饰器,是一个函数,用来修饰类的行为。不过目前主流浏览器都没有很好的支持,我们需要用babel来转换为浏览器能识别的语言。

    • 装饰器在javascript 中仅仅可以修饰类和属性,不能修饰函数。
    • 装饰器对类的行为的改变,是代表编译时发生的,而不是在运行时。
    • 装饰器能在编译阶段运行代码。
    • 装饰器是经典的AOP模式的一种实现方式。
    • 执行顺序:同一处的多个装饰器是按照洋葱模型,由外到内进入,再由内到外执行。
  • Class:类,通过class关键字可以定义类。

    • class 关键字的出现使得其在对象写法上更加清晰,更像是一种面向对象的语言。
    • 注意:
      1. 在类中声明方法的时候,千万不要给该方法加上 function 关键字
      2. 方法之间不要用逗号分割,否则会报错
    • 用法:
      1. 类自身指向的就是构造函数,所以可以认为 ES6 中的类其实就是构造函数的另一种写法。
      2. 类的所有方法都定义在类的prototype属性上,也可以通过prototype属性对类添加方法。
      3. 可以通过 Object.assign() 来为对象动态增加方法。
      4. constructor方法是类的构造函数的默认方法,通过new命令生成对象实例时,自动调用该方法。
      5. constructor方法如果没有显式定义,会隐式生成一个constructor方法。所以即使你没有添加构造函数,构造函数也是存在的。constructor方法默认返回实例对象this,但是也可以指定constructor方法返回一个全新的对象,让返回的实例对象不是该类的实例。
      6. 类的所有实例共享一个原型对象,他们的原型都是 Person.prototype,所以proto属性是相等的。
      7. class不存在变量提升,所以需要先定义再使用。
简述ES6的新特性

参考文档

  • 不一样的变量声明:const和let
  • 模板字符串
  • 箭头函数(Arrow Functions)
  • 函数的参数默认值
  • Spread / Rest 操作符
  • 二进制和八进制字面量
  • 对象和数组解构
  • 对象超类
  • for…of 和 for…in
  • ES6中的类 Class
浏览器渲染页面的过程

https://www.jianshu.com/p/b6b42fd3f80e

  • 浏览器从服务器那收到的HTML,CSS,JavaScript等相关资源,然后经过一系列处理后渲染出来的web页面。

  • 过程:

    1. 浏览器将获取的HTML文档并解析成DOM树。
    2. 处理CSS标记,构成层叠样式表模型CSSOM(CSS Object Model)。
    3. 将DOM和CSSOM合并为渲染树(rendering tree)将会被创建,代表一系列将被渲染的对象。
    4. 渲染树的每个元素包含的内容都是计算过的,它被称之为布局layout。浏览器使用一种流式处理的方法,只需要一次pass绘制操作就可以布局所有的元素。
    5. 将渲染树的各个节点绘制到屏幕上,这一步被称为绘制painting。

    以上五个步骤并不一定一次性顺序完成,比如DOM或CSSOM被修改时,亦或是哪个过程会重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。而在实际情况中,JavaScript和CSS的某些操作往往会多次修改DOM或者CSSOM。

浏览器缓存(强缓存,协商缓存)
  • 缓存:就是一个资源副本,当我们向服务器请求资源后,会根据情况将资源copy一份副本存在本地,方便下次读取。缓存最根本的作用是减少没必要的请求,使用缓存可以减少时长,从而优化用户体验,减少流量消耗,减轻服务器的压力。
  • 强缓存:直接从本地副本比对读取,不去请求服务器,返回的状态码是 200(expires 和 cache-control)
  • 协商缓存:会去服务器比对,若没改变才直接读取本地缓存,返回的状态码是 304(last-modified 和 etag)
  • 获取缓存的流程:
    1. 先根据这个资源的 http header 判断它是否命中强缓存,如果命中,则直接从本地缓存中获取资源,不会则向服务器请求 资源。
    2. 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过另一些request header验证这个资源是否命中协商缓存,这个过程成为http再验证,如果命中,服务器直接返回请求而不返回资源,而是告诉客户端之间从缓存中获取,客户端收到返回后就直接从客户端获取资源。
    3. 强缓存和协商缓存的共同之处在于:如果命中缓存,服务器不会返回资源;区别是:强缓存不发送请求打服务器,但是协商缓存会发送请求到服务器。
    4. 当协商缓存没有命中时,服务器会返回资源给客户端。
    5. 当ctrl+F5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存。
    6. 当F5刷新页面时,跳过强缓存但会检查协商缓存
浏览器端 Event loop

https://segmentfault.com/a/1190000018181334

  • 事件循环(event-loop):主线程从"任务队列"中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。此机制具体如下:主线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查microtask队列是否为空(执行完一个任务的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去任务队列中取下一个任务执行。
  • 什么需要:因为JavaScript是单线程的。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。
路由实现原理
  • 早期的路由都是后端实现的,直接根据 url 来 reload 页面,页面变得越来越复杂服务器端压力变大,随着 ajax 的出现,页面实现非 reload 就能刷新数据,也给前端路由的出现奠定了基础。我们可以通过记录 url 来记录 ajax 的变化,从而实现前端路由。

  • History API(history.pushState 和 history.replaceState)

    • 这两个API都接收三个参数,分别是
      状态对象(state object) — 一个JavaScript对象,与用pushState()方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate事件都会被触发,并且事件对象的state属性都包含历史记录条目的状态对象的拷贝。
      标题(title) — FireFox浏览器目前会忽略该参数,虽然以后可能会用上。考虑到未来可能会对该方法进行修改,传一个空字符串会比较安全。或者,你也可以传入一个简短的标题,标明将要进入的状态。
      地址(URL) — 新的历史记录条目的地址。浏览器不会在调用pushState()方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的URL不一定是绝对路径;如果是相对路径,它将以当前URL为基准;传入的URL与当前URL应该是同源的,否则,pushState()会抛出异常。该参数是可选的;不指定的话则为文档当前URL。
    • 相同之处是这两个API都会操作浏览器的历史记录,不会引起页面的刷新。
    • 不同之处是pushState会增加一条新的历史记录,而replaceState会替换当前的历史记录。
  • hash

    • 我们经常在 url 中看到 #,这个 # 有两种情况,一个是我们所谓的锚点,比如典型的回到顶部按钮原理、Github 上各个标题之间的跳转等,路由里的 # 不叫锚点,我们称之为 hash,大型框架的路由系统大多都是哈希实现的。
    • hashchange事件:根据监听哈希变化触发的事件
      • 使用 window.location 处理哈希的改变时不会重新渲染页面,而是当作新页面加到历史记录中, 这样跳转页面就可以在 hashchange事件中注册 ajax 从而改变页面内容。
      • hashchange 在低版本 IE 需要通过轮询监听 url 变化来实现
session、cookie 与 token 认证

参考文档:Cookie、Session和Token认证详解

  • 用户通过浏览器登录一个网站,在该浏览器内打开网站其他页面时,不需要重新登录。而HTTP是无状态的协议,不同的网站,判断用户登录状态的方法都不一样。有的网站是通过session来验证用户的登录状态,有的网站是通过token来验证用户的登录状态,也有的网站是通过其他的方式来判断。
  • Cookie 是服务器发送给客户端的用于验证某一会话信息的数据,不同网站Cookie中字段是不一样的,是由服务器端设置的。Cookie中常放入session_id 或者 token 用来验证会话的登录状态。
  • session是保存在服务器端的经过加密的存储特定用户会话所需的属性及配置信息的数据。当我们打开浏览器访问某网站时,session建立,只要浏览器不关闭(也有时间限制,可以自己设置超时时间),这个网站就可以记录用户的状态,当浏览器关闭时,session结束。
  • Token是服务器端生成的用于验证用户登录状态的加密数据,和用session验证差不多。只不过Token验证服务器端不需要存储用户会话所需的配置等数据。只需要后端将Token进行验证签名,然后再发给浏览器。所以,使用Token进行验证,在一次会话中,Token值是不变化的,这和session一样。
跨域的解决方案

参考文档:前端常见浏览器跨域请求解决方案

  • jsonp 跨域
    动态创建script,再请求一个带参网址实现跨域通信(script 标签的 src 属性不受同源策略的限制 );
    缺点是只能实现 get 一种请求。
  • CORS(跨域资源共享)
    在后端添加允许访问的请求头
    js // 配置 cors 跨域 header("Access-Control-Allow-Origin:*"); header("Access-Control-Request-Methods:GET, POST, PUT, DELETE, OPTIONS"); header('Access-Control-Allow-Headers:x-requested-with,content-type,test-token,test-sessid');
  • nginx 代理(proxy)
    正向代理,反向代理
  • nodejs 中间件代理
    node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发
    使用 node + express + http-proxy-middleware 搭建一个proxy服务器。
Ajax (axios、fetch、原生xhr)
  • 原生xhr:XMLHttpRequest对象

    • 现代浏览器,最开始与服务器交换数据,都是通过XMLHttpRequest对象。
    • 优点:
      1. 不重新加载页面的情况下更新网页
      2. 在页面已加载后从服务器请求/接收数据
      3. 在后台向服务器发送数据。
    • 缺点:
      1. 使用起来也比较繁琐,需要设置很多值。
      2. 早期的IE浏览器有自己的实现,这样需要写兼容代码。
          // 兼容处理
          if (window.XMLHttpRequest) {
                // model browser
              xhr = new XMLHttpRequest()
          } else if (window.ActiveXObject) {
                // IE 6 and older
              xhr = new ActiveXObject('Microsoft.XMLHTTP')
          }
          xhr.open('POST', url, true)
          xhr.send(data)
          xhr.onreadystatechange = function () {
               
          try {
               
              // TODO 处理响应
              if (xhr.readyState === XMLHttpRequest.DONE) {
               
              // XMLHttpRequest.DONE 对应值是 4
              // Everything is good, the response was received.
              if (xhr.status === 200) {
               
                  // Perfect!
              } else {
               
                  // There was a problem with the request.
                  // For example, the response may hava a 404 (Not Found)
                  // or 500 (Internal Server Error) response code.
              }
              } else {
               
              // Not ready yet
              }
          } catch (e) {
               
              // 通信错误的事件中(例如服务器宕机)
              alert('Caught Exception: ' + e.description)
          }
          }
      
  • jQuery ajax: $.ajax

    • jQuery 里面的 AJAX 请求也兼容了各浏览器,可以有简单易用的方法 . g e t , .get, .get.post。简单点说,就是对XMLHttpRequest对象的封装。
    • 优点:
      1. 对原生XHR的封装,做了兼容处理,简化了使用。
      2. 增加了对JSONP的支持,可以简单处理部分跨域。
    • 缺点:
      1. 如果有多个请求,并且有依赖关系的话,容易形成回调地狱。
      2. 本身是针对MVC的编程,不符合现在前端MVVM的浪潮。
      3. ajax是jQuery中的一个方法。如果只是要使用ajax却要引入整个jQuery非常的不合理。
      $.ajax({
               
          type: 'POST',
          url: url, 
          data: data,
          dataType: dataType,
          success: function () {
               },
          error: function () {
               }
      })  
      
  • Axios

    • Axios 是一个基于 promise 的HTTP库,可以用在浏览器和 node.js 中。它本质也是对原生XMLHttpRequest的封装,只不过它是Promise的实现版本,符合最新的ES规范。
    • 优点:
      1. 从浏览器中创建XMLHttpRequests
      2. 从 node.js 创建 http 请求
      3. 支持 Promise API
      4. 拦截请求和响应
      5. 转换请求数据和响应数据
      6. 取消请求
      7. 自动转换 JSON 数据
      8. 客户端支持防御 XSRF
    • 缺点:
      只持现代代浏览器。
          axios({
               
              method: 'post',
              url: '/user/12345',
              data: {
               
              firstName: 'liu',
              lastName: 'weiqin'
              }
          })
          .then(res => console.log(res))
          .catch(err => console.log(err))
      
  • fetch

    • Fetch API提供了一个 JavaScript 接口,用于访问和操作HTTP管道的部分,例如请求和响应。它还提供了一个全局fetch()方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。
    • fetch是低层次的API,代替XHR,可以轻松处理各种格式,非文本化格式。可以很容易的被其他技术使用,例如Service Workers。但是想要很好的使用fetch,需要做一些封装处理。
    • 优点:
      在配置中,添加mode: 'no-cors’就可以跨域了
    • 问题:
      1. fetch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理
      2. fetch默认不会带cookie,需要添加配置项。
      3. fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费。
      4. fetch没有办法原生监测请求的进度,而XHR可以。
      fetch('http://example.com/movies.json')
      .then(function(response) {
               
          return response.json();
      })
      .then(function(myJson) {
               
          console.log(myJson);
      });
      
  • fetch规范与jQuery.ajax()主要有两种方式的不同:
    1. 当接收到一个代表错误的 HTTP 状态码时,从 fetch()返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。
    2. 默认情况下,fetch 不会从服务端发送或接收任何 cookies, 如果站点依赖于用户 session,则会导致未经认证的请求(要发送 cookies,必须设置 credentials 选项)。


三、webpack 系列

webpack 细节
  • webpack中在使用babel插件处理js代码的时候,为什么使用polyfill,如何使用polyfill?
    • 原因:因为在使用preset_env 处理js代码时,无法将所有的ES6的语法全部转换成ES5语法,就比如promise、array.from以及实例方法都无法转换,这个时候需要加入垫片。
    • 使用:
      1. 在入口文件引入@babel/polyfill ,会污染全局环境
      2. 在配置文件中的entry中写 ,也会污染全局环境
      3. 可以配置@babel/preset-env useBuiltIns接收3个参数
      entry:不管代码 有没有用到,只要目标浏览器不支持的都会引入对应的polyfill;自动引入polyfill模块;
      usage: 根据代码的使用情况,按需引入;自动引入polyfill模块;
      false:不会自动引入polyfill模块;
      4. corejs 3.0以后的版本; 如果参数为entry,则需要在入口文件中引入两个包
webpack 和 gulp 的区别
  • webpack是一个模块打包器,强调的是一个前端模块化方案,更侧重模块打包,我们可以把开发中的所有资源都看成是模块,通过loader和plugin对资源进行处理。
  • gulp是一个前端自动化构建工具,强调的是前端开发的工作流程,可以通过配置一系列的task,第一task处理的事情(如代码压缩,合并,编译以及浏览器实时更新等)。然后定义这些执行顺序,来让glup执行这些task,从而构建项目的整个开发流程。自动化构建工具并不能把所有的模块打包到一起,也不能构建不同模块之间的依赖关系。
webpack 从启动构建到输出结果经历了一系列过程
  1. 解析webpack配置参数,合并从shell传入和webpack.config.js文件里配置的参数,生产最后的配置结果。
  2. 注册所有配置的插件,好让插件监听webpack构建生命周期的事件节点,以做出对应的反应。
  3. 从配置的entry入口文件开始解析文件构建AST语法树,找出每个文件所依赖的文件,递归下去。
  4. 在解析文件递归的过程中根据文件类型和loader配置找出合适的loader用来对文件进行转换。
  5. 递归完后得到每个文件的最终结果,根据entry配置生成代码块chunk。
  6. 输出所有chunk到文件系统。
gulp基本使用

四、nodejs 系列

Node.js的 Event loop

在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程我们称为Tick。每个Tick的过程就是查看是否有事件待处理。如果有就取出事件及其相关的回调函数。然后进入下一个循环,如果不再有事件处理,就退出进程。

Node中间件
  • 中间件主要是指封装所有Http请求细节处理的方法。一次Http请求通常包含很多工作,如记录日志、ip过滤、查询字符串、请求体解析、Cookie处理、权限验证、参数验证、异常处理等,但对于Web应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。

  • Koa 与 Express 比较

    • 语法区别
      Express 异步使用 回调
      koa1 异步使用 generator + yeild
      koa2 异步使用 await/async
    • 中间件区别
      koa采用洋葱模型,进行顺序执行,出去反向执行,支持context传递数据
      Express本身无洋葱模型,需要引入插件,不支持context
      Express的中间件中执行异步函数,执行顺序不会按照洋葱模型,异步的执行结果有可能被放到最后,response之前。
      这是由于,其中间件执行机制,递归回调中没有等待中间件中的异步函数执行完毕,就是没有await中间件异步函数。
    • 集成度区别
      express 内置了很多中间件,集成度高,使用省心,
      koa 轻量简洁,容易定制
Node.js 模块(CommonJS、模块分类、模块导出、模块加载)
CommonJS 规范
  1. 一个js文件就是一个模块
  2. 模块内所有的变量均为局部变量,不会污染全局
  3. 模块中需要提供给其他模块使用的内容需要导出
  4. 导出使用exports.xxx = xxx或module.exports=xxx或this.xxx=xxx
  5. 其他模块可以使用require函数导入
    node实现了CommonJS规范,在编写模块时,都有require、exports、module三个预先定义好的变量可以使用。
    require{}函数的两个作用:
    1. 执行导入的模块中的代码;
    2. 返回导入模块中的接口;
模块分类

1. 核心模块,也叫内置模块、原生模块

  • 例如:fs,http,path,url 模块;
  • 所有内置模块,在安装node.js的时候,就已经编译成 二进制文件,可以直接加载运行(速度较快);
  • 部分内置模块,在node.exe这个进程启动的时候就已经默认加载了,所以可以直接使用。
    2. 文件模块
  • 按文件后缀来分
  • 如果加载时,没有指定文件后缀名,那么,就按照 .js .json .node顺序依次加载相应模块
    3. 自定义模块(第三方模块)
模块导出
// 方式一:module.exports 导出
module.exports = {
     
    say: sayName
}
// 方式二:exports 导出
exports.say = sayName
模块加载过程
  1. 看 require() 加载模块时传入的参数是否以 ‘/’ 或 ‘./’ 或 ‘./’ 等等这样的路径方式开头(相对路径或绝对路径都可以)
  2. 如果是,就会按照传入的路径直接去查询对应的模块
    (1)如果传入的为具体的文件名,例如:require(’./test.js’),
    直接根据给定的路径去加载模块,找到了就加载成功,找不到加载失败
    (2)如果传入的不是具体的文件名,例如:require(’./test’)
    第一步:根据给定的路径,依次添加文件后缀.js、.json、.node进行匹配,如果匹配不到,执行第二步
    第二步:查找是否有 test 目录(尝试寻找 test 包)
    找不到:加载失败
    找到了:依次在test目录下查找package.json文件(找到该文件后尝试找main字段中的入口文件)、index.js、index.json、index.node,找不到则加载失败
  3. 如果不是,那就认为传入的是“模块名称”,例如:require(‘http’)、require(‘fs’)
    (1)如果是核心模块,就直接加载核心模块
    (2)不是核心模块:从当前目录开始,依次递归查找所有父目录下的node_modules 目录中是否包含相应的包,如果查找完毕磁盘根目录依然没有,则加载失败

五、其他

http和应用编程相关的问题

参考文章

git及git工作流

数据库(MySQL、MongoDB)

前后端模板(artTemplate、ejs)


六、Vue 高级

Vue.js 动态组件与异步组件
  • 动态组件:keep-alive
    当在组件之间切换的时候,想要保持这些组件的状态,以避免反复渲染导致的性能问题。重新创建动态组件的行为通常是非常有用的,我们希望那些标签的组件实例能够被在他们第一次被创建的时候缓存下来,为了解决这个问题,可以使用 元素将其动态组件包裹起来。这样,失活的组件将会被缓存。
  • 异步组件:
    • 在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器上加载一个模块。为了简化, Vue允许以一个工厂函数的方式定义组件,这个工厂函数会异步解析这个组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
    • 这样工厂函数会收到一个 resolve 回调,这个回调函数会在从服务器得到组件定义的时候被调用,也可以调用 reject(reason) 来表示加载失败。
      Vue.component('async-webpack-example', function (resolve) {
               
          // 这个特殊的 `require` 语法将会告诉 webpack
          // 自动将你的构建代码切割成多个包,这些包
          // 会通过 Ajax 请求加载
          require(['./my-async-component'], resolve)
      })
      
    • 也可以在工厂函数中返回一个 Promise,使用动态导入
      Vue.component(
          'async-webpack-example',
          // 这个动态导入会返回一个 `Promise` 对象。
          () => import('./my-async-component')
      )
      
    • 当使用局部注册的时候,也可以之间提供一个返回 promise 的函数
      new Vue({
               
          components: {
               
              'my-component': () => import('./my-async-component')
          }
      })
      
  • 处理加载状态(2.3.0新增)
    const AsyncComponent = () => ({
           
        // 需要加载的组件 (应该是一个 `Promise` 对象)
        component: import('./MyComponent.vue'),
        // 异步组件加载时使用的组件
        loading: LoadingComponent,
        // 加载失败时使用的组件
        error: ErrorComponent,
        // 展示加载时组件的延时时间。默认值是 200 (毫秒)
        delay: 200,
        // 如果提供了超时时间且组件加载也超时了,
        // 则使用加载失败时使用的组件。默认值是:`Infinity`
        timeout: 3000
    })
    
处理边界情况($root, $parent, $refs, 依赖注入, 循环引用, $forceUpdate, v-once)
  • 访问根实例 $root
    所有的子组件都可以将这个实例作为一个全局 store 来访问或使用。
    this.$root.xxx 对于demo或者非常小型的有少量组件的应用来说比较方便,但是在绝大多数情况下,需要使用 Vuex 来管理应用的状态。

  • 访问父级组件实例 $parent
    可以用来从一个子组件访问父组件的实例。可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。
    在绝大多数情况下,会使得应用更难调试和理解,尤其是变更了父级组件的数据的时候。
    在需要向任意更深层级的组件提供上下文信息时推荐使用依赖注入。

  • 访问子组件实例或子元素 $refs
    给子组件添加一个 ref 属性,赋予一个ID引用,就可以使用$refs来访问这个子组件实例

     // 子组件定义ref属性
     <base-input ref="usernameInput"></base-input>
     // 允许父组件给子组件输入框获取焦点
     this.$refs.usernameInput.focus()
    

    refv-for 一起使用的时候,得到的ref会包含对应数据源的这些子组件的数组。
    $refs 会在组件渲染完成后生效,并且不是响应式的,应该避免在模板或计算属性中访问 $refs

  • 依赖注入:provideinject

    • provide:可以指定想要提供给后代组件的数据/方法
    provide: function () {
           
        return {
           
            getMap: this.getMap
        }
    }
    
    • inject:在任何后代组件中,通过 inject 选项接收指定的属性
    inject: ['getMap']
    
    • 相比 $parent:这种用法可以在任意后代组件中访问该定义的属性,而不需要暴露整个组件实例。同时这些组件之间的接口是始终明确定义的,和props一样。
    • 可以把依赖注入看作一部分“大范围有效的prop”,除了
      • 祖先组件不需要知道那些后代组件使用它提供的property
      • 后代组件不需要知道被注入的property来自哪里
    • 负面影响:
      • 将应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难;
      • 提供的property是非响应式的;
  • 程序化的事件侦听器:

    • $emit 可以被 v-on 侦听
    • 手动侦听事件:
      • $on(eventName , eventHandler) 侦听一个事件
      • $once(eventName , eventHandler) 一次性侦听一个事件
      • $off(eventName , eventHandler) 停止侦听一个事件
  • 循环引用

    • 递归组件
      组件是可以在自己的模板中调用自身,但是只能通过name选项来实现。
      当使用Vue.component全局注册一个组件时,这个全局的ID会自动设置为该组件的name选项。
      使用不当,递归组件就可能导致无限循环。所以,使用递归调用是有条件性的。
    • 组件之间的循环引用
  • 模板定义替代品

    • 内联模板
      inline-template:添加这个属性会将这个组件里面的内容作为模板,但是会让模板的作用域变得难以理解,在组件内优先选择 template选项 或者