HTML+CSS

  1. 说一说盒模型?

    盒模型分为w3c的标准盒模型(content-box)、IE盒模型(border-box)

    盒模型:分为内容(content)、填充(padding)、边界(margin)、边框(boder)四个部分

    两种盒模型的区别:

    1. w3c盒模型的width、height只包含内容content,不包含boder和padding
    2. IE盒模型的width、height包含content、boder和padding
  2. 说一说CSS的选择器有哪些?

    选择器的权重分别为:行内:1000;Id选择器:100;类选择器、伪类选择器、属性选择器:10;元素选择器,伪元素选择器:1

    1. ID选择器
    2. 类选择器
    3. 标签选择器
    4. 后代选择器 (div p)
    5. 相邻后代选择器(div > p)
    6. 兄弟选择器(li~a)
    7. 相邻兄弟选择器(li+a)
    8. 属性选择器(a[rel=””])
    9. 伪类选择器
    10. 伪元素选择器
    11. 通配符选择器
  3. 说一说你常用的伪类选择器以及他们的用法?

    1、父级下的所有子元素: :nth-child、:nth-last-child、first-child、:last-child、:only-child

    2、父级下同一类型的所有子元素::nth-of-child、:nth-last-of-type、:first-of-type、:last-of-type、:only-of-type

     <div class="parent">
            <div class="child d">d1</div>
            <div class="child d">d2</div>
            <p class="child p">p1</p>
            <p class="child p">p2</p>
    </div>
    
  • 第一类:父级下的所有子元素

    • :nth-child()

      .parent .child:nth-child(1) {
          color: #f00; //d1红
      }
    • :nth-last-child()

      .parent .child:nth-last-child(1) {
          color: #f00; //p2红
      }
    • first-child

      .parent .child:first-child {
          color: #f00; //d1红
      }
    • only-child

  • 父级下同一类型的所有子元素

    • :nth-of-type

      .parent .child:nth-of-type(1) {
          color: #f00; //d1 p1红
      }
    • :nth-of-last-type

      .parent .child:nth-of-type(1) {
          color: #f00; //d2 p2红
      }
    • :first-of-type

      .parent .child:first-of-type {
          color: #f00; //d1 p1红
      }
  1. CSS中哪些属性可以继承?

    1. 字体系列属性

      font、font-family、font-weight、font-size、font-style、font-variant、font-stretch

    2. 文本系列属性

      text-indent、text-align、text-shadow、line-height、word-spacing、letter-spacing、text-transform、direction、color

    3. 列表属性

      list-style-type、list-style-image、list-style-position、list-style

    4. 光标属性

      cursor

    5. 元素可见性

      visibility

  2. 如何居中一个元素?

    1. 固定宽高
      1. 绝对定位+transform
      2. 绝对定位+left、top、right、bottom+margin
      3. flex布局
      4. grid布局
    2. 不定宽高
      1. 绝对定位+transform
      2. flex布局
      3. grid布局
      4. flex+margin
  3. display有哪些值?分别说说作用?

    1. block:块类型。默认宽度为父元素宽度,可设置宽高,换行显示。
    2. none:元素不显示,并从文档流中移除。
    3. inline:行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示。
    4. inline-block:默认宽度为内容宽度,可以设置宽高,同行显示。
    5. list-item:像块类型元素一样显示,并添加样式列表标记。
    6. table:此元素会作为块级表格来显示。
    7. inherit:规定应该从父元素继承display属性的值。
  4. position有哪些值?分别说说作用?

    1. relative:生成相对定位的元素,相对于其元素本身所在正常位置进行定位。
    2. absolute:生成绝对定位的元素,相对于值不为static的第一个父元素的paddingbox进行定位
    3. sticky:粘性定位
    4. static:默认值。没有定位,元素出现在正常的流中(忽略top,bottom,left,right,z-index声明)。
    5. fixed:生成绝对定位的元素,相对于浏览器窗口进行定位。
    6. inherit:规定从父元素继承position属性的值。
  5. 讲讲BFC?

    BFC 即 Block Formatting Contexts (块级格式化上下文),它属于上述定位方案的普通流。

    具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。

    触发BFC的方式

    1. 父元素为body
    2. 浮动元素:float除了none之外的属性
    3. 绝对定位:position:absolute、fixed
    4. display:inline-block、table-cells、flex
    5. overflow:hiden、auto、scroll

    使用场景

    1. margin重叠
    2. BFC区域与float区域重叠
    3. 浮动清除
  6. 页面隐藏方式的比较

    • display:元素不再占据页面空间;触发回流,进而触发重绘
    • visibility:直接隐藏元素,但仍占据页面空间;触发重绘;绑定的事件不能被触发
    • opacity:使元素变为透明,但仍占据页面空间;触发重绘;绑定的事件仍能被触发
  7. 两栏布局

    • 左边定宽,右边自适应
      • 左边左浮动,固定宽度;右边设置 margin-left:200px
      • 左边绝对定位,固定宽度;右边设置 margin-left:200px
      • 左边右边均为绝对定位,左侧 topbottomleftright 均设置为0;右侧 topbottomright 均设置为0,left设置为200px,width:100%
      • flex布局,父级 display:flex 左边定宽;右边设置flex:1
      • grid布局,父级 display:grid;grid-template-columns:200px 1fr
    • 左边自适应,右边定宽(同上)
    • 一列不定,一列自适应
      • 左侧左浮动,右侧触发BFC添加overflow:hidden
      • flex布局,父级 display:flex,左侧不设宽度,右侧flex:1fr
      • grid布局,父级 display:grid;grid-template-columns:auto 1fr
  8. 说一说js动画和css动画的区别?

    • 代码复杂度方面,简单动画,css 代码实现会简单一些,js 复杂一些。复杂动画的话,css 代码就会变得冗长,js实现起来更优。
    • 动画运行时,对动画的控制程度上,js 比较灵活,能控制动画暂停,取消,终止等,css动画不能添加事件,只能设置固定节点进行什么样的过渡动画。
    • 兼容方面,css 有浏览器兼容问题,js 大多情况下是没有的。
    • 性能方面,css动画相对于优一些,css 动画通过GUI解析,js 动画需要经过j s 引擎代码解析,然后再进行 GUI 解析渲染。
  9. 如何对超出文本框的文字进行省略操作?

     .p2{
         font-size: 30px;
         overflow: hidden; /* 超出文本隐藏 */
         text-overflow: ellipsis; /* clip:修剪文本;ellipsis:显示省略符号来代表被修剪的文本;string:使用给定的字符串来代表被修剪的文本。 */
         white-space: nowrap; 
    }
  10. 说一说meta属性?

    meta主要有四个属性:name、content、http-equiv、charset

    charset

    <!-- 定义网页文档的字符集 -->
    <meta charset="utf-8" />

    name+content

    <!-- 网页作者 -->
    <meta name="author" content="开源技术团队"/>
    <!-- 网页地址 -->
    <meta name="website" content="https://sanyuan0704.github.io/frontend_daily_question/"/>
    <!-- 网页版权信息 -->
     <meta name="copyright" content="2018-2019 demo.com"/>
    <!-- 网页关键字, 用于SEO -->
    <meta name="keywords" content="meta,html"/>
    <!-- 网页描述 -->
    <meta name="description" content="网页描述"/>
    <!-- 搜索引擎索引方式,一般为all,不用深究 -->
    <meta name="robots" content="all" />
    <!-- 移动端常用视口设置 -->
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0, user-scalable=no"/>
    <!-- 
      viewport参数详解:
      width:宽度(数值 / device-width)(默认为980 像素)
      height:高度(数值 / device-height)
      initial-scale:初始的缩放比例 (范围从>0 到10)
      minimum-scale:允许用户缩放到的最小比例
      maximum-scale:允许用户缩放到的最大比例
      user-scalable:用户是否可以手动缩 (no,yes)
     -->

    http-equiv

    <!-- expires指定网页的过期时间。一旦网页过期,必须从服务器上下载。 -->
    <meta http-equiv="expires" content="Fri, 12 Jan 2020 18:18:18 GMT"/>
    <!-- 等待一定的时间刷新或跳转到其他url。下面1表示1秒 -->
    <meta http-equiv="refresh" content="1; url=https://www.baidu.com"/>
    <!-- 禁止浏览器从本地缓存中读取网页,即浏览器一旦离开网页在无法连接网络的情况下就无法访问到页面。 -->
    <meta http-equiv="pragma" content="no-cache"/>
    <!-- 也是设置cookie的一种方式,并且可以指定过期时间 -->
    <meta http-equiv="set-cookie" content="name=value expires=Fri, 12 Jan 2001 18:18:18 GMT,path=/"/>
    <!-- 使用浏览器版本 -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <!-- 针对WebApp全屏模式,隐藏状态栏/设置状态栏颜色,content的值为default | black | black-translucent -->
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
  11. link标签的rel的属性值

    image-20200726112514719

  12. 说一说html首部声明Doctype html的作用?不加会出现什么情况?

    ​ DOCTYPE是document type(文档类型)的简写,用来说明你用的XHTML或者HTML是什么版本。

    ​ 要建立符合标准的网页,DOCTYPE声明是必不可少的关键组成部分;除非你的XHTML确定了一个正确的DOCTYPE,否则你的标识和CSS都不会生效。

  13. css触发硬件加速的方法有哪些?

    /**使用3d效果来开启硬件加速--需要transform变换**/
    .speed-up {
       -webkit-transform: translate3d(250px,250px,250px)
       rotate3d(250px,250px,250px,-120deg)
       scale3d(0.5, 0.5, 0.5);
    }
    /**原理上还是使用3d效果来开启硬件加速--不需要transform变换**/
    .speed-up{
       -webkit-transform: translateZ(0);
       -moz-transform: translateZ(0);
       -ms-transform: translateZ(0);
       -o-transform: translateZ(0);
       transform: translateZ(0);
    }

    注:硬件加速最好只用在animation或者transform上。不要滥用硬件加速,因为这样会增加性能的消耗,如果滥用反而会使动画变得更加卡,这样就得不偿失了。

  14. h5的新特性

    1. 语义化标签
    2. 本地存储-sessionStorage、localStorage
    3. 表单标签
    4. 地理定位
  15. animation和transition的区别

    transition 用 property 去设置过渡效果的属性名称,duration 设置过渡效果的周期,timing-function 规定速度效果的速度曲线,delay 设定过渡效果什么时候开始;

    animation 可以用 name 设置动画的名称,用 duration 设置动画完成的周期,用 timing-function 设置动画的速度曲线,delay 设置动画什么时候开始,iteration-count 设置动画播放的次数,direction 规定下一个周期是否逆向的播放,play-state 动画是否正在进行或者暂停,fill-mode 设置动画停了之后位置什么状态

    区别:

    1、transition 是过渡,是样式值的变化的过程,只有开始和结束;animation 其实也叫关键帧,通过和 keyframe 结合可以设置中间帧的一个状态;

    2、animation 配合 @keyframe 可以不触发时间就触发这个过程,而 transition 需要通过 hover 或者 js 事件来配合触发;

    3、animation 可以设置很多的属性,比如循环次数,动画结束的状态等等,transition 只能触发一次;

    4、animation 可以结合 keyframe 设置每一帧,但是 transition 只有两帧;

    5、在性能方面:浏览器有一个主线程和排版线程;主线程一般是对 js 运行的、页面布局、生成位图等等,然后把生成好的位图传递给排版线程,而排版线程会通过 GPU 将位图绘制到页面上,也会向主线程请求位图等等;我们在用使用 aniamtion 的时候这样就可以改变很多属性,像我们改变了 width、height、postion 等等这些改变文档流的属性的时候就会引起,页面的回流和重绘,对性能影响就比较大,但是我们用 transition 的时候一般会结合 tansfrom 来进行旋转和缩放等不会生成新的位图,当然也就不会引起页面的重排了;

  16. img、input等一些是行内元素,为什么还能设置宽高?

    因为img这些元素的显示是通过属性来控制的,所以我们称这类元素为:可置换元素,所以可以设置他们的宽高。

  17. 如何解决盒子垂直margin上的边界叠加问题?

    margin重叠示例:

    image-20200805213916737

    解决方案:

    • 触发BFC
    • 外元素使用padding
    • 内层元素透明边框
    • 内层元素绝对定位
    • 内层元素浮动
  18. @import和link的区别

    • 老祖宗的差别,link属于XHTML标签,而@import完全是css提供的一种方式。link标签除了可以加载css外,还可以做很多其他的事情,比如定义RSS,定义rel连接属性等,@import只能加载CSS。
    • 加载顺序的差别:当一个页面被夹在的时候(就是被浏览者浏览的时候),link引用的CSS会同时被加载,而@import引用的CSS会等到页面全部被下载完再加载。所以有时候浏览@import加载CSS的页面时会没有样式(就是闪烁),网速慢的时候还挺明显。
    • 兼容性的差别。由于@import是CSS2.1提出的所以老的浏览器不支持,@import只有在IE5以上的才能识别,而link标签无此问题,完全兼容。
    • 使用dom控制样式时的差别。当时用JavaScript控制dom去改变样式的时候,只能使用link标签,因为@import不是dom可以控制的
  19. mouseover和mouseenter的区别?

    mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡过程。对应的移除事件是mouseout

    mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave

  20. 如何取消冒泡?如何取消默认事件?如何取消其他事件?

    取消冒泡:stoppropagation(一般)、cancelBubble(IE)

    取消默认事件:prentdefault

    取消其他事件:stopimmediatepropagation

  21. 说说async和defer的区别?

    image-20200810221407642

  22. css阻止默认事件的属性?

    point-event:none

Javascript

  1. 说一说js有哪些基本类型?

    • string
    • number
    • symbol
    • null
    • undefined
    • boolean
  2. 说一说你对原型和原型链的理解?

    1. prototype: 每个通过 function 关键字创建的函数都有一个 prototype 的属性,这个属性是一个指针,指向一个对象。这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。简单来说:该函数实例化的所有对象的 __proto__ 的属性指向这个对象,他是该函数所有实例化对象的原型。Person.prototype === person.__proto__
      image-20200518160711483

    2. constructor:当函数创建,prototype 属性指向一个原型对象时,在默认情况下,这个原型对象将会获得一个 constructor 属性,这个属性是一个指针,指向prototype 所在的函数对象。简单来说:Person,prototype.constructor 就指向 Person 对象。Person.prototype.constructor === Person
      image-20200518161254319

    3. proto当我们调用构造函数创建一个新的实例后,在这个实例的内部将包含一个执政,指向构造函数的原型属性。Person.prototype === person.__proto__
      image-20200518161511889

    4. 原型属性访问
      每当代码读对象的某个属性时,首先会在对象本身搜索这个属性,如果找到该属性就返回该属性的值,如果没有找到,则继续搜索该对象对于的原型对象,以此类推下去。
      因为这样的搜索过程,因此我们在如果实例中添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性,因为在实例中搜索到该属性后就不会再往后搜索了。

    5. 原型属性判断
      通过 hasOwnProperty() 方法来判断一个属性是否存在实例中。如果属性存在实例中,方法返回True

      function Person() {};
      
      Person.prototype.name = "laker" ;
      
      var student = new Person();
      
      console.log(student.name); // laker
      console.log(student.hasOwnProperty("name")); // false
      
      student.name = "xiaoming";
      console.log(student.name); //xiaoming 屏蔽了原型对象中的 name 属性
      console.log(student.hasOwnProperty("name")); // true
  3. for-in 循环
    for-in 返回的是所有能够通过对象访问的、可枚举的属性,其中包括实例中的属性,也包括了存在原型中的属性,也包括了存在于原型中的属性。

  4. isPrototypeOf() :用于测试一个对象是否存在于另一个对象的原型链上。
    console.log(Person.prototype.isPrototypeOf(student)); // true

  5. Object.getPrototypeOf()
    console.log(Object.getPrototypeOf(student) === Person.prototype); // true

  6. 原型链:原型链是实现继承的主要方法,基本思想是利用一个引用类型继承另一个引用类型的属性和方法。

        function Super() {
    
        };
    
       function Middle() {
    
       };
    
       function Sub() {
    
       };
    
       Middle.prototype = new Super();
       Sub.prototype = new Middle();
       var suber = new Sub();
    
       console.log((new Middle()).__proto__ === Middle.prototype);//true
       console.log(Sub.prototype.__proto__ === Middle.prototype); //true
       console.log(Middle.prototype.__proto__ === Super.prototype); //true

image-20200518171250627

  1. 默认的原型:其实我们上面这个原型链是不完整的,还记得我们以前说过所有的引用类型都继承了 Object 吗?这个继承就是通过原型链来实现的。我们一定要记住,所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.Prototype 。这也正是所有的自定义类型都会继承 toString() 、valueOf() 等默认方法的根本原因。
  1. {} 和 [] 的valueOf和toString的结果是什么?

    {} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"
    
    [] 的 valueOf 结果为 [] ,toString 的结果为 ""
  2. 说一说Js的继承?

    • 原型链继承

      基本思想:利用原型链来实现继承父类的一个实例作为子类的原型

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

      优点:

      • 简单明了
      • 实例是子类的实例,实际上也是父类的实例
      • 父类新增原型方法/原型属性,子类都能访问到

      缺点:

      • 所有子类的实例的原型都共享同一个超类实例的属性和方法
      • 无法实现多继承
    • 构造继承

      基本思想:通过使用call、apply方法可以在新创建的对象上执行构造函数,用父类的构造函数来增加子类的实例

      function Parent(name){
          this.name = name
      }
      Parent.prototype.say = function(){
          console.log(this.name)
      }
      
      function Child(name){
          //继承属性
          Parent.call(this,name)
      }
      

      优点:

      • 简单明了,直接继承父类构造函数的属性和方法。

      缺点:

      • 无法继承原型链上的属性和方法
    • 组合继承

      基本思想:利用构造继承和原型链组合

      function Parent(name){
          this.name = name
      }
      Parent.prototype.say = function(){
          console.log(this.name)
      }
      function Child(name){
          //继承属性
          Parent.call(this,name)
      }
      Child.prototype = Parent.prototype
      Child.prototype.constructor = Child

      优点:

      • 解决了构造继承和原型链继承的两个问题

      缺点:

      • 实际上子类上会拥有父类的两份属性,只是子类属性覆盖了父类的属性
    • 原型式继承:

      基本思想:基于已有的对象创建新的对象

      function object(o){
          function F(){};
          F.prototype = o;
          return new F();
      }

      其实这种方式就是 ES5Object.create 方法,使用方式如下:

      使用方法如下:

      function Parent(name){
          this.name = name
      }
      Parent.prototype.say = function(){
          console.log(this.name)
      }
      function Child(name){
          //code
      }
      Child.prototype = object(Parent)

      优点:

      • 可以实现基于一个对象的简单继承,不比构建构造函数

      缺点:

      • 与原型链中提到的缺点相同,一个是传参的问题,一个是属性共享的问题。
    • 寄生继承

      基本思想:依托一个对象而生的一种继承方法,因此称之为寄生

      const juejinUserSample = {
          username: 'ulivz',
          password: 'xxx'
      }
      function JuejinUser(obj) {
          var o = Object.create(obj)
           o.prototype.readArticle = function () {
              console.log('Read article')
          }
          return o;
      }
    • 寄生组合继承

      function Person(name){
        this.name=name;
      }
      Person.prototype.sayName=function(){
        console.log(this.name+' '+this.gender+' '+this.age);
      }
      function Female(name,gender,age){
        Person.call(this,name);//第一次调用父类构造函数             
        this.age=age;
        this.gender=gender;
      }
      function inheritPrototype(Female,Person){ 
        var protoType=Object.create(Person.prototype);
        protoType.constructor=Female;
        Female.prototype=protoType;
      }
      inheritPrototype(Female,Person);
      Female.prototype.sayAge=function(){
      console.log(this.name+' '+this.age);
      }
       var fm=new Female('skila','female',19);
       fm.sayName();//skila female 19
       fm.sayAge();skila  19
  1. 谈一谈变量提升?

    当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。

    b() // call b
    console.log(a) // undefined
    
    var a = 'Hello world'
    
    function b() {
        console.log('call b')
    }

    在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。

    在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

    b() // call b second
    
    function b() {
        console.log('call b fist')
    }
    function b() {
        console.log('call b second')
    }
    var b = 'Hello world'

    var 会产生很多错误,所以在 ES6中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用。

  2. 说一说什么是 Event Loop

    由于js是一种单线程的语言,执行一些异步操作就需要用到js的事件循环机制,也就是 Event Loop

    Event Loop 分为三个部分:调用栈(call stack)、消息队列(Message Queue)、微任务队列(Microtask Queue)

    Event Loop 开始时,会从全局栈开始,一行一行执行,遇到函数调用会函数压入调用栈内,被压入的函数叫做帧(Frame),函数返回后,会从调用栈弹出。

    function func1(){
        console.log(1)
    }
    function func2(){
        console.log(2)
        func1()
        console.log(3)
    }
    func2()
    //输出2 1 3

    js中的异步操作:fecthsetIntervalsetTimeout中的回调函数,会入队到消息队列中成为消息。当调用栈清空的时候,会将消息队列的消息压入调用栈中,执行并弹出。注:下面的代码setTimeout 延时虽然为0,其实还是异步。着是因为HTML5标准规定这个函数的第二个参数不得小于4毫秒,不足会自动增加。

    function func1(){
        console.log(1)
    }
    function func2(){
        setTimeout(()=>{
            console.log(2)
        },0)
        func1()
        console.log(3)
    }
    func2()
    //输出1 3 2

    js中的 promiseasyncawait创建的异步操作,会加入到微任务队列中,在调用栈被清空的时候立即执行,并且处理期间新加入的微任务也会一并执行。

    var a = new Promise(resolve=>{
        console.log(4)
        resolve(5)
    })
    function func1(){
        console.log(1)
    }
    function func2(){
        setTimeout(()=>{
            console.log(2)
        },0)
        func1()
        consoe.log(3)
        p.then(res=>{
            console.log(res)
        }).then(()=>{
            console.log(6)
        })
    }
    func2()
    // 4 1 3 5 6 2
  3. 看一看这道题的隐式类型转换?

    • 隐式类型转换规则

      • 转成string类型+(字符串连接符)
      • 转成number类型:++/–(自增自减运算符) + - * /(算数运算符)(关系运算符)
      • 转成boolen!(逻辑非运算符)
    • 坑一:字符串连接符与算术运算符隐式转换规则混淆

      //字符串连接符 String(1)+"true"
      console.log(1+"true")//1true
      //算术运算符1+Number(true)
      console.log(1+true) //2
      //算术运算符1+Number(undefined)=1+NaN = NaN
      console.log(1+undefined) //NaN
      //算术运算符1+Number(null)=1+0=1
      console.log(1+null) //1
    • 坑二:关系运算符:会把其他数据类型转换成number之后再比较关系

      //当关系运算符两边有一边是字符串时,会将其他数据类型使用Number()转换,然后比较关系 Number("2")>10 = 2>10 = false
      console.log("2">10)//false
      //党关系运算符两边都是字符串的时候,此时同时转成number然后比较关系;此时并不是按照Number()的形式转换为数字,而是按照字符串对应的unicode编码来转成数字
      console.log("2">'10')//true
      console.log("abc">'b'//false
      console.log("abc">"aad")//true
      //特殊情况NaN与任何数据比较都是false
      console.log(NaN==NaN)//false
      //Boolean(undefined)==Boolean(null)
      console.log(undefined==null)//true
    • 坑三:复杂数据类型(数组和对象)在隐式转换时会先转成String,然后再转成Number运算

      image-20200621134556764

      image-20200621134610183

      复杂类型转number顺序如下:

      • 先使用valueOf()方法获取其原始值,如果原始值不是number类型,则使用toString()方法转为string
      • 再将string转成number运算
      var a ={
          i:0,
          valueOf:function(){
              return ++a.i
          }
      }
      if(a==1&&a==2&&a==3){
          console.log("1")
      }
    • 坑四:逻辑非隐式转换与关系运算符隐式转换搞混淆

      //[].toString()='';boolean('') = false
      console.log([]==0)//true
      //!运算符的优先级高 boolean([]) = true
      console.log(![]==0)//true
      //[].toString()=空字符串 ![]= false
      console.log([]==![])//true
      //引用类型数据存在堆中,栈中存的是地址,两个数组的地址不同
      console.log([]==[])//false
      //同上
      console.log({}=={})//false
  4. 说一说typeof运算符?

    typeof的作用如下:

    • 识别所有值类型

    • 识别函数

    • 判断是否为引用类型(不可再细分)

      let a;    typeof a // undefined
      const str = 'abc';    typeof str //string
      const n = 100;    typeof n //number
      const b = true;    typeof b // boolean
      const s = Symbol('s'); typeof s //symbol
      typeof console.log //function
      typeof function(){} //function
      typeof null //object
      typeof [1,3] //object
      typeof {} //object
  5. 说一说闭包?

    简单来说:闭包的产生是由于函数作用域造成的,在函数B内访问函数A内的定义的变量,则称函数B为闭包,通常有两种表现:

    • 函数作为返回值

      function create() {
          let a = 10
          return function(){
              console.log(a)
          }
      }
      let fn = create()
      a = 100
      fn()
      //100
    • 函数作为参数被传递

      function print(fn) {
          let a = 200
          fn()
      }
      let a = 100
      function fn1(){
          console.log(a)
      }
      print(fn1)
      //100

    闭包的应用场景?

    • 在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。

    • 可以已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

    闭包的缺点:

    • 闭包使函数内部的变量不能被内存释放,这些变量就会占用内存,内存消耗大,可能导致内存泄露
  6. 看下下面这道题说一说输出是什么?(this指向问题)

    this的取值,是在函数执行确认的,不是在定义时确认的;分别有以下这几种情况

    看看下面这些题:this的题

    image-20200621151909285

  7. 说一说深拷贝和浅拷贝?

    浅拷贝的方法:

    • Object.assign()
    • ...
    • Array.prototype.slice()
    • Array.prototype.concat()

    深拷贝方法:

    • JSON.parse(JSON.stringify())

      这种方法无法处理对象内含有函数和正则(ps:函数会变为null,正则会变为空对象)

  8. 说一说JS的模块化?

    js的模块化有:AMD/CMD、CommonJs、Es moudule

    CommonJs:

    // a.js
    module.exports = {
        a: 1
    }
    // or 
    exports.a = 1
    
    // b.js
    var module = require('./a.js')
    module.a // -> log 1

    Es moudule:

    import XXX from './a.js'
    import { XXX } from './a.js'
    // 导出模块 API
    export function a() {}
    export default function() {}
  9. 说一说es6里面的promise吧
    promise文章

  10. 说一说var、let、const的区别?

    var 声明的变量,在定义时会被提升到当前作用域的顶部,例如:

    console.log(a) //undefined
    var a = 1

    会被解析为:

    var a
    console.log(a)
    a = 1

    不仅变量会提升,函数也可以被提升并且提升顺序比var 定义的还要高

    console.log(a) //f a(){}
    function a(){
    
    }
    var a = 10

    letconst 定义的变量会存在 暂时性死区,不能在声明前使用。

  11. 说一说下面的这段代码的输出?

    var a = {n:1}
    var b = a
    a.x = a = {n:2}
    console.log(a.x)// undefined
    console.log(b.x)// {n:2}

    原因:

    这道题考察的是js的连等的情况,连等时,运算会从右到左进行运算,但是在赋值完成前,会对前面的赋值进行缓存,所以在进行a={n:2}时,这个时候a的指向并不是立刻在{n:2}内存空间,不过此时,运算未完成,a还是指向的是{n:1}这一块内存,并且b的指向仍然为{n:1}所以最后a的指向为{n:2},b的指向为{x:{n:2},n:1}

  12. 说一说箭头函数和普通函数的区别?

    • arguments属性

      箭头函数没有arguments属性,可用..rest来代替

    • this指向

      箭头函数的this指向是通过上下文的this指向确定的

    • 箭头函数不能通过call,apply,bind等方法改变this

    • 箭头函数没有原型属性

    • 箭头函数是匿名函数,不能使用new 方法

  13. 说一说事件流?

    js的事件流分为3个阶段:

    • 捕获阶段
    • 目标阶段
    • 冒泡阶段
  14. js的给函数加上默认参数时,会按照es6来进行解析,es6是有块级作用域的所以下面的输出是12

       function side(arr) {
         arr[0] = arr[2];
       }
       function a(a, b, c = 3) {
         c = 10;
         side(arguments);
         return a + b + c;
       }
       a(1, 1, 1);

    上面的代码如果把默认参数去掉,答案就是21

  15. 说一说立即执行函数?他有什么作用?

    立即执行函数:声明一个匿名函数并马上调用就叫做立即执行函数

    image-20200725123039091

    立即执行函数的作用:

    • 不为函数命名,避免污染全局变量
    • 立即执行函数内部形成一个单独的作用域,可以封装一些外部无法读取的私有变量
    • 封装变量
  16. 说一说CommonJs和Es6模块引入的区别?

    CommonJs模块输出的是值的拷贝(浅拷贝),也就是说,一旦输出一个值,模块内部的变化不会影响到这个值。

    ES6模块是动态引用,并且不会缓存,模块里面的便令绑定其所在的模块,而是动态地去加载值,并且不能重新复制

  17. webpack编译流程

    初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;

    开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;

    确定入口:根据配置中的 entry 找出所有的入口文件;

    编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;

    完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;

    输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;

    输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

  18. 说一说js内的连等?

    首先要明白一点,对于连等赋值,在申明变量的时候,无论你是使用let或者const,后者的变量都会变成全局变量,因为在执行语句的时候,是从右到左执行赋值。如:

       let a = b = 2;
       // 相当于
       b = 2; // 未申明自动提升为全局变量
       let a = b;
    由此可见,使用连等赋值,会增加全局变量,增加了变量冲突的风险,在使用的时候,应该慎重。可以看看下面的例子:
       var a = b =10;   // a,b都是全局变量且值都是10
            (function(){
                var a = b = 20;  // a为局部变量,值为20;b为全局变量,值为20,会影响外面的变量b的值
             })();
       console.log(a);   // 10
       console.log(b);   // 20
  1. null和undefined的区别

    null 表示没有对象,即该处不应该有值

    1) 作为函数的参数,表示该函数的参数不是对象

    2) 作为对象原型链的终点

    undefined 表示缺少值,即此处应该有值,但没有定义

    1)定义了形参,没有传实参,显示undefined

    2)对象属性名不存在时,显示undefined

    3)函数没有写返回值,即没有写return,拿到的是undefined

    4)写了return,但没有赋值,拿到的是undefined

    在给定默认参数的函数中,如果传入null时,不会使用默认参数。

  2. 前端加密的方法?

    1. base64
    2. md5

Vue

  1. 能说一说MVVM吗?

    MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

  2. 说说你对spa单页面的理解,他的优缺点分别是什么?

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

    优点:

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

    缺点:

    • 初次加载耗时多;为实现单页Web应用功能及显示效果,需要在加载页面的时候将js,css同意加载,部分页面按需加载。
    • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理。
    • SEO难度大:由于所有的内容都在一个页面中动态替换显示,所以在SEO上其有着天然的弱势。
  3. v-show和v-if有什么区别?

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

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

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

  4. class与style如何动态绑定?

    class可以通过对象语法和数组语法进行动态绑定:

    • 对象语法:

      <div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
      
      data: {
        isActive: true,
        hasError: false
      }
    • 数组语法:

      <div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
      
      data: {
        activeClass: 'active',
        errorClass: 'text-danger'
      }
      

    style也可以通过对象语法和数组语法进行动态绑定:

    • 对象语法:

      <div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
      
      data: {
        activeColor: 'red',
        fontSize: 30
      }
    • 数组语法:

      <div v-bind:style="[styleColor, styleSize]"></div>
      
      data: {
        styleColor: {
           color: 'red'
         },
        styleSize:{
           fontSize:'23px'
        }
      }
      
  5. 怎样理解Vue的单向数据流?

    所有的prop都使得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

    额外的,父级组件发生更新时,子组件中所有的prop都将会刷新诶最新的值。这意味着你不应该在一个子组件内部改变prop。如果这样做了,Vue会在浏览器控制台中发出警告。子组件想修改时,只能通过$emit派发一个自定义事件,父组件接收到后,由父组件修改。

    这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:

    props: ['initialCounter'],
    data: function () {
      return {
        counter: this.initialCounter
      }
    }
    

    这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性:

    props: ['size'],
    computed: {
      normalizedSize: function () {
        return this.size.trim().toLowerCase()
      }
    }
  6. computed和watch的区别和运用的场景?

    computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

    watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

    运用场景:

    • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
    • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
  7. 直接给一个数组项赋值,Vue能检测到变化吗?

    由于Js的限制,Vue不能检测到以下数组的变动:

    • 当你利用索引直接设置一个数组项时,vm.items[indexOfItem] = newValue
    • 当你修改数组的长度时,例如:vm.items.length = newLength

    为了解决第一个问题,Vue 提供了以下操作方法:

    // Vue.set
    Vue.set(vm.items, indexOfItem, newValue)
    // vm.$set,Vue.set的一个别名
    vm.$set(vm.items, indexOfItem, newValue)
    // Array.prototype.splice
    vm.items.splice(indexOfItem, 1, newValue)
    

    为了解决第二个问题,Vue 提供了以下操作方法:

    // Array.prototype.splice
    vm.items.splice(newLength)
  8. 在哪个生命周期内调用异步请求?

    可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

    • 能更快获取到服务端数据,减少页面 loading 时间;
    • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
  9. 父组件可以监听到子组件的生命周期吗?

    • 子组件使用$emit
    • 父组件使用@hook:mounted
  10. 谈谈你对keep-alive的了解?

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

    • 一般结合路由和动态组件,用于缓存组件

    • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;

    • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

      处。

  11. 组件中data为什么是一个函数?

    为什么组件中的data必须是一个函数,然后return一个对象,而new Vue实例里,data可以直接是一个对象?

    // data
    data() {
      return {
        message: "子组件",
        childName:this.name
      }
    }
    
    // new Vue
    new Vue({
      el: '#app',
      router,
      template: '<App/>',
      components: {App}
    })
    

    因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。

  12. v-model的原理

    我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

    • text 和 textarea 元素使用 value 属性和 input 事件;
    • checkbox 和 radio 使用 checked 属性和 change 事件;
    • select 字段将 value 作为 prop 并将 change 作为事件。

    以input表单元素为例

    <input v-model='something'>
    
    相当于
    
    <input v-bind:value="something" v-on:input="something = $event.target.value">
    

    如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:

    父组件:
    <ModelChild v-model="message"></ModelChild>
    
    子组件:
    <div>{{value}}</div>
    
    props:{
        value: String
    },
    methods: {
      test1(){
         this.$emit('input', '小红')
      },
    },
    
  13. Vue组件间通信有哪几种方式?

    • props/$emit适用父子组件通信

    • ref$parent/$children适用父子组件通信

      ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

      $parent / $children:访问父 / 子实例

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

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

    • $attrs/$listeners适用于隔代组件通信

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

      $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

    • provide / inject 适用于 隔代组件通信

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

    • Vuex 适用于 父子、隔代、兄弟组件通信

  14. 说一说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 且同时保存在单一的状态树中。
  15. 说一说Vuex的工作流程。

    vue由state,getter,mutation,action组成,state用来存储数据,在组件内使用时,可以使用this.$store.state.属性名来使用(如果含有模块的话,在$store后添加模块名即可)还可以使用Vuex提供的语法糖mapState,这种方法会将vuex所有模块都注册在全局无需添加模块名,其他的语法糖也是如此。组件在获取数据时,可能会对数据进行一些操作,就可以使用getter,类似于计算属性。我们在修改state数据时,只能通过触发commit,从而触发mutation的方法,从而改变state内的数据。如果有异步操作时,则需要触发dispatch,从而触发action,再触发mutation改变state。

  16. vue-router路由有几种?

    • hash:使用URL hash值来做路由。支持所有浏览器,包括不支持HTML5 History Api的浏览器;
    • history: 依赖HTML5 history API和服务器配置。具体可以查看HTML5 History模式;
  17. 能说下hash和history路由的实现原理吗?

    (1) hash模式的实现原理

    早期的前端路由的实现就是基于location.hash来实现的。 其实现原理很简单,location.hash的值就是URL中#后面的内容,比如下面这个网站,它的location.hash的值为”#search”

    https://www.word.com#search

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

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

    (2)history 模式的实现原理

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

    window.history.pushState(null, null, path);
    window.history.replaceState(null, null, path);
  • pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;
    • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
    • history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。
  1. Vue是如何实现数据双向绑定的?

    Vue数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图:

    image-20200626153808948

    • 输入框内容变化时,Data中的数据同步变化。即View=>Data的变化。
    • Data中的数据变化时,文本节点的内容同步变化。即Data=> Data的变化

    其中,View变化更新Data,可以通过事件监听的方式来实现,所以Vue的数据双向绑定的工作主要是如何根据Data变化更新View。

    Vue主要通过以下4个步骤来实现数据双向绑定的:

    • 实现一个监听器Observer:对数据对象进行遍历,包括子属性对象的属性,利用Object.defineProperty()对属性都加上setter和getter。这样的化,给这个对象的某个值赋值,就会触发setter,那么就能监听到数据变化。
    • 实现一个解析器Compile:解析Vue模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对于的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知调用更新函数进行数据更新。
    • 实现一个订阅者Watcher:Watcher订阅者是Observer和Compile之间通信的桥梁,主要的任务是订阅Observer中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Compile中对应的更新函数
    • 实现一个订阅器Dep:订阅器采用发布-订阅设计模式,用来收集订阅者Watcher,对监听器Observer和订阅者Watcher进行统一的管理
  2. Vue怎么实现的对象和数组监听?

    通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。

  3. Proxy与Object.defineProperty的优劣对比

    Proxy的优势如下:

    • Proxy可以直接监听对象而非属性
    • Proxy可以直接监听数组的变化
    • Proxy有13种拦截方法,不限于apply、ownKeys、deleteProperty、has等
    • Proxy返回的是一个新对象,可以之操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。
    • Proxy作为新标准就受到浏览器厂商持续的性能优化,也就是传说中的新标准的性能红利。

    Object.defineProperty的优势

    兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

  4. 虚拟DOM的优缺点?

    其他Vue原理相关参考文章

    优点:

    • 操作性能下限:框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
    • 无需手动操作DOM:我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
    • 跨平台:虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

    缺点:

    • 无法进行极致优化:虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
  5. 为什么Vuex种会使用action?

    加action是因为要区分可追踪,不可追踪。mutation绝对可以追踪,action不追踪。action中最后必须调用mutation的commit方法;

    action的作用:

    1. 告诉你这个不可追踪
  6. 这个是异步 至于异步竞争/顺序,这些自己控制

  7. 一个父组件和一个子组件他们的生命周期钩子函数执行顺序。

    • 加载渲染过程

      父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

    • 子组件更新过程

      父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

    • 父组件更新过程

      父 beforeUpdate -> 父 updated

    • 销毁过程

      父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

React

  1. React中的异步加载

    • improt

      在使用 import * from '' 是在编译时就加载了,import())是在运行时再加载所需文件

    • React.lazy

      语法const Demo = React.lazy(()=>import("./Demo"))

    • React.Suspense

      在加载异步组件时,可以使用 React.Suspense传入一个fallback来显示加载页面

  2. shouldComponentUpdate

    shouldComponentUpdate是一个组件的生命周期函数,用于拦截组件渲染

    应用场景:在一个界面中,可能有很多个组件,可是在页面的更新时,只对其中部分组件更新,如果不对未更新的其他组件做处理,他们也会重新渲染。所以我们可以使用shouldComponentUpdate来比较该组件的propsstate是否变化,来确定该组件是否重新渲染。shouldComponentUpdate默认返回 true

  3. PureComponent

    PureComponent是优化React重要方法之一,易于实施,只要把继承类从Component改为PureComponent即可,可以减少不必要的render次数,从而提高性能,而且可以减少书写shouldComponentUpdate函数

  4. React.memo()

    上面两种方法都是对应类组件来优化的,我们知道函数组件也是React不可分割的一部分,所以我们对函数式组件可以通过使用React.memo()来进行性能的优化,类似于就是函数式的 PureComponent

    const Funcomponent = ()=> {
        return (
            <div>
                Hiya!! I am a Funtional component
            </div>
        )
    }
    const MemodFuncComponent = React.memo(FunComponent)
  5. 受控组件和非受控组件

    在React中,所谓的受控组件和非受控组件是针对表单而言的。

    • 受控组件

      受控组件的特点

      • 表单元素依赖于状态,表单元素需要默认值实时映射到状态的时候,就是受控组件,和Vue中的双向绑定类似。
      • 受控组件,表单元素的修改会实时映射到状态值上,此时就可以对输入的内容进行校验。
      • 受控组件只有继承React.Component才会有状态。
      • 受控组件必须要在表单上使用onChange时间来绑定对应的事件。
    • 非受控组件
      非受控组件的特点

      • 不受状态的控制,获取数据就是相当于操作DOM
      • 好处是很容易和第三方组件结合

      定义非受控组件的两种方式:

      方式一:函数,通过在虚拟DOM节点上使用ref,并使用函数,将函数的参数挂载到实例的属性上,如下:

      handleSubmit = (e) => {
          // 阻止原生默认事件的触发
          e.preventDefault();
          console.log(this.username.value);
      }
      render() {
          return (
              <form onSubmit={this.handleSubmit}>
                  {/* 将真实的DOM,username是输入框中输入的值赋值给组件实例上
                      这样,在页面表单提交的时候就可以通过this.username.value获取到输入框输入的值
                  */}
                  用户名<input
                      name="username"
                      type="text"
                      ref={username=>this.username=username}
                  /><br />
              </form>
          )
      }

      方式二:通过构造函数声明的方式

      constructor(){
          super();
          // 在构造函数中创建一个引用
          this.second=React.createRef();
      }
      handleSubmit = (e) => {
          // 阻止原生默认事件的触发
          e.preventDefault();
          console.log(this.second.current.value);
      }
      render() {
          return (
              <form onSubmit={this.handleSubmit}>
                  {/* 自动将输入框中输入的值放在实例的second属性上 */}
                  密码<input
                      name="password"
                      type="text"
                      ref={this.second}
                  /><br />
              </form>
          )
      }
  6. React Portals

    React提供了一个了在页面任何DOM节点都可渲染组件的方法-Protals

    使用方法: ReactDOM.createPortal(child, container)

  7. React Context

    解决层级过于复杂组件的通信不用通过一层一层props传递

    Context API

    • React.createContext 这个方法会返回一个带有 ProviderConsumer的对象。
    • 使用 Provider 组件包裹在组件数的最外层,并接受一个vaule 属性。
    • 使用 Consumer 组件,在组件树中 Provider 组件内部的任何地方都能获取到状态的子集。
    import React, {Component} from 'react'
    const ThemeContext = React.createContext('light')
    
    class App extends Component {
      render () {
        return (
          <ThemeContext.Provider value="dark">
            <Toolbar />
          </ThemeContext.Provider>
        )
      }
    }
    
    function Toolbar () {
      return (
        <div>
          <ThemeButton />
        </div>
      )
    }
    
    function ThemeButton (props) {
      return (
        <ThemeContext.Consumer>
          { theme => <button {...props} theme={theme}>{theme}</button> }
        </ThemeContext.Consumer>
      )
    }

    可以看到ThemeButton中直接获取到了theme属性,并没有通过Toolbar来传递props,这正是Context做的事。

浏览器/性能优化

  1. 浏览器的渲染原理

    1. 解析文档,根据文档构建一颗DOM树,DOM树是由DOM元素及属性节点组成
    2. 解析CSS,构建CSSOM树
    3. 根据DOM树和CSSOM树构建渲染树。渲染树的节点称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和DOM元素相对应,但这种对应关系不是一对一的,不可见的DOM元素不会被插入渲染树。
    4. 当渲染对象被创建并添加到树种,他们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。
    5. 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染树并调用渲染对象的paint方法将他们的内容显示在屏幕上,绘制使用UI基础组件。
  2. 什么情况阻塞渲染

    1. 渲染的前提是生成渲染树,所以HTML和CSS肯定会阻塞渲染。如果想渲染的越快,越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。
    2. 当浏览器在遇到 script 标签时,会暂停构建DOM,完成后才会从暂停的地方重新开始,也就是说,想要首屏渲染越快,就越不应该在首屏就加载JS文件,这也是建议将 script 标签放在 body 标签底部的原因。
    3. 现在也可以给 script 标签添加 defer 或者 async 属性。
      1. defer JS文件会并行下载,但是会放到HTML解析完成后顺序执行,所以对于这种情况可以把 script 标签放在任意位置。
      2. 对于没有任何依赖的JS文件可以加上 async 属性,表示JS文件下载和解析不会阻塞渲染。
  3. 说一说页面渲染过程中触发的事件?

    • document.readystatechange() —— 正在加载
    • document.DomContentLoaded() —— DOM树渲染完成,此时可能还在加载外部资源
    • window.load() —— 所有资源均加载完成
  4. 说一说回流和重绘?

    1. 重绘:当节点需要更改外观而不会影响布局的,比如改变color就叫称为重绘,如:
      image-20200517093431038
    2. 回流:是布局或者几何属性需要改变就称为回流
      image-20200517093442989
  5. 如何减少回流?

    • 使用transform代替top
    • 不要把节点的属性值放在一个循环里当成循环里的变量
    • 不要使用table布局,可能很小的一个小改动会造成整个table的重新布局
    • 把DOM离线后修改。如:使用documentFragment对象在内存里操作DOM
    • 不要一条一条修改DOM的样式。最好预先定义好css的class,然后修改DOM的className
  6. 说一说V8如何进行垃圾内存回收?

    js不像c/c++,让程序员自己开辟和释放内存,而是类似java,采用自己的一套垃圾回收算法进行自动的内存管理。

    • 内存限制
      在其他后端语言中,如java/go,对于内存使用没有什么限制,但是js不一样,v8只能使用系统的一部分内存,具体来说,在64位系统下,v8最多只能分配1.4G,在32位系统中最多只能分配0.7G。

      对于栈内存而言,当ESP(堆栈)指针下移,也就是上下文切换后,栈顶的空间会自动被回收。但对于堆内存而言就比较复杂了。所有的引用类型的数据在js中都是通过堆进行空间分配的。当我们构造一个对象进行赋值操作的时候,其实相应的内存已经分配到了堆上,可以不断这样创建对象,让V8为他分配空间,直到堆的大小达到上限。

      为什么V8会给他设置上限?

      原因有两个:一个是JS单线程的执行机制,另一个是Js垃圾回收机制

      首先JS是单线程运行的,这意味一旦进入到垃圾回收,其他各种运行逻辑都要暂停;另一个方面垃圾回收其实是非常耗时的操作。

    • 新生代的内存回收
      V8把堆内存分成了两部分进行处理——新生代和老生代内存。顾名思义,新生代就是临时分配的内存,存活事件短,老生代是常驻内存,存活时间长。V8的堆内存,也就是两个内存之和。

      image-20200621003430559

    • 根据这两种不同种类的堆内存,V8采用了不同的回收策略,来根据不同的场景做针对性的优化。

      新生代的内存默认限制是多少?在64位和32位系统下分别位32MB和16MB。为什么会这么小呢?因为在新生代中的变量存活时间短,来了马上就走,不容易产生太大的内存负担,因此可以将他设的足够小。

新生代的垃圾回收的过程?

  • 首先将新生代内存一分为二:

    image-20200621102418685

    其中from部分表示正在使用的内存,to是目前限制的内存。

    当进行垃圾回收时,V8将From部分的对象检查一遍,如果是存活的对象那么复制到To内存中(在To内存中按照顺序从头放置的),如果是非存活对象直接回收即可。

    当所有的From中的存活对象按照顺序进入到To内存之后,From和To两者的角色对调,From现在被闲置,To为正在使用,如此循环。

    思考:如果这样,直接将非存活兑现回收不就行了吗,为什么还有后面的一系列操作?

    原因:在To内存中按照顺序从头放置的,这是为了应对这样的场景:

    image-20200621103327819

    深色的小方块代表存活存活的对象,白色部分表示待分配的内存,由于堆内存是连续分配的,这样零零散散的空间可能会导致稍微大一点的对象没有办法进行空间分配,这种零散的空间也叫做内存碎片。刚刚介绍的新生代垃圾回收算法也叫Scavenge算法。

    scavenge算法主要就是解决内存碎片的问题,在进行一顿复制之后,To空间就编程了这样子

    image-20200621103911882

    这样就大大方便了后续连续空间的分配

      可scavenge算法的劣势也非常明显,就是内存只能使用新生代内存的一半,但是他只存放生命周期短的对象,这种对象一般很少,因此时间性能非常优秀
    • 老生代内存回收
      刚刚介绍了新生代的回收方式,那么新生代中的变量如果经过多次回收后依然存在,那么就会被放入老生代内存,这种现象就叫做晋升

      发生晋升其实不只是这一种原因,我们来梳理一下会有哪些情况触发晋升:

      • 已经经历过一次Scavenge回收
  • To(闲置)空间的内存占用超过25%

    老生代垃圾回收流程:

    • 进行标记-清除;这个过程首先会遍历堆中所有对象,他们做上标记,然后对代码环境中使用的变量以及被强引用的变量取消标记,剩下的就是要删除的变量了,在随后的消除阶段对其空间的回收。

    • 整理内存碎片;v8的解决方式非常简单出包,在清除阶段后,把存活的对象全部往一段靠拢。

image-20200621131144577
由于是移动对象,它的执行速度不可能很快,事实上也是整个过程最耗时间的部分。

  1. 从输入URL到展示页面发生了什么?

    • 在浏览器地址栏输入URL
    • 首先判断是否有永久重定向(301)
    • 浏览器查看资源是否有强缓存,若命中强缓存就直接从缓存中使用,若协商缓存则需要到服务器进行校验资源是否可用。
      • HTTP1.0提供expires,值为一个绝对时间表示缓存过期时间
      • HTTP1.1增加了一个cache-control:max-age,值为以秒为单位的最大缓存过期时间。还有no-cache,no-store,分别表示可以缓存但会立即失效和不能缓存。
    • 浏览器解析URL获取协议,主机,端口,path。
    • 浏览器组装一个HTTP(GET)请求报文。
    • DNS解析,查找过程如下:
      • 首先查找浏览器缓存;
      • 第二查找本机缓存
      • 第三查找hosts文件
      • 第四路由器缓存
      • 第五ISP DNS缓存
      • 第六DNS查询(递归查询、迭代查询)
    • 端口建立TCP连接,三次握手:
      1. 客户端发送一个TCP的SYN=1,seq=x的连接请求到服务器端。
      2. 服务器端返回SYN=1,ACK=x+1,Seq=Y的确认连接字段。
      3. 客户端收到确认连接字段后发生ACK=Y+1,seq=z
    • TCP连接建立后发起HTTP请求
    • 服务器接收到请求并解析,将请求转发到服务程序。
    • 服务器检查HTTP请求头部是否有缓存验证信息,如果缓存未过期,返回304状态码
    • 处理程序读取完整请求并准备HTTP响应,同时可能需要查询数据库等操作
    • 服务器将相应报文通过TCP连接发送会浏览器。
    • 浏览器接收到HTTP响应,根据情况选择是否关闭TCP连接,若关闭的话四次挥手。
      1. 浏览器端首先向服务器端发送数据已发送完毕的报文
      2. 服务器端接收到之后,返回确认报文同意浏览器停止发送数据,但此时服务器端依旧可以接收未接收完的数据,并且可以返回数据给浏览器端。
      3. 服务器端将所有数据返回给客户端之后,向客户端发送请求连接释放报文。
      4. 客户端收到连接释放报文后,向服务器端返回确认连接释放报文。
    • 浏览器接收到服务端的响应报文后,会检查响应状态码,根据不同的状态码处理不同的情况。
    • 如果资源可以缓存,进行缓存。
    • 将响应内容进行解码。
    • 根据资源类型决定如何处理。
    • 若资源类型为html类型,解析html文档,构建DOM树,并下载相关资源,构造CSSOM树,执行js脚本。
    • 首先构造DOM树
    • 解析过程中如果遇到图片、样式表、js文件便启动下载
    • 构建CSSOM树
    • 根据DOM树和CSSOM树构建渲染树:
      1. 从DOM树的根节点遍历所有可见节点。
      2. 对每一个节点找到恰当的CSSOM规则并应用
      3. 发布可视节点的内容和计算样式
    • js脚本解析:
      1. 浏览器创建document对象并解析html,将解析的文本和节点添加到文档中。
      2. html解析器遇到没有defer和async属性的script时,将他们添加到文档中,然后去执行脚本语句。在脚本下载和执行时html解析器会暂停。直到script下载和执行完毕
      3. 当解析器遇到async属性的script时,开始下载脚本但会继续解析文档,当脚本下载完毕时就会立刻回过头去执行该脚本,但是解析器不会停下来等它下载。
      4. 当解析器遇到defer属性的script时,defer脚本会在文档解析完毕时按照顺序执行,并且可以访问完整文档。
      5. 浏览器在document对象上触发DOMContentLoaded事件
      6. 此时文档完全解析完成,浏览器可能还在等待图片等内容加载,等待这些内容完成载入并且所有异步脚本完成载入和执行之后,window会触发loaded事件
    • 显示页面(html解析过程中会逐步显示页面)。
  2. 说说通过webpack构建的项目如何优化?

    webpack性能优化

  3. 客户反馈首屏加载过慢,如何优化?

    • 网络传输性能优化

      • 浏览器缓存优化

        我们知道,在浏览器向服务器发送请求之前,首先会查找本地是否有相同的文件,如果有,就会直接拉起本地缓存。

        浏览器的缓存分为强缓存和协商缓存,当强缓存未命中时,会发起协商缓存。具体缓存的方式在这儿就不做过多赘述了。

      • 资源打包压缩

        上面所提及的浏览器缓存优化是针对于第二次访问网站的优化,那么如何解决首屏加载过慢呢?

        现在大多数的前端项目几乎都是通过 webpack 进行构建的,所以这里介绍一些 webpack 打包资源的压缩。

        1. js 压缩
        2. html 压缩
        3. 提取公共资源
        4. css 压缩
        5. 开启 gzip 压缩
      • 图片资源优化

        • 不要在 200X200 的容器放 400X400 的图片
        • 使用雪碧图(减少请求资源)
        • 使用字体图标
        • 使用webp 图片格式,减少图片体积
      • 使用cdn

    • 页面渲染优化

      • CSS属性读写分离
      • 通过切换class或者使用元素的style.csstext属性去批量操作元素样式。
      • DOM元素的离线更新:当对DOM进行相关操作时,例、appendChild等都可以使用Document Fragment对象进行离线操作,带元素“组装”完成后再一次插入页面,或者使用display:none 对元素隐藏,在元素“消失”后进行相关操作。
      • 将没用的元素设为不可见:visibility:hidden,这样可以减少重绘的压力,必要时再将元素显示
      • 压缩DOM的深度,一个渲染层内不要有过深的子元素,少用DOM完成页面样式,多使用伪元素或者box-shadow取代。
      • 图片在渲染前指定大小:因为img元素是内联元素,所以在加载图片后会改变宽高,严重的情况会导致整个页面重排,所以最好在渲染前就指定其大小,或者让其脱离文档流。
      • 对页面中可能发生大量重排重绘的元素单独触发渲染层,使用GPU分担CPU压力。(这项策略需要慎用,得着重考量以牺牲GPU占用率为代价能否换来可期的性能优化,毕竟页面中存在太多的渲染层对于GPU而言也是一种不必要的压力,通常情况下,我们会对动画元素采取硬件加速。)
  4. 谈谈你对重绘和回流的理解。

    • 回流

      • 触发条件

        简单来说,就是当我们对DOM结构的修改引发DOM几何尺寸变化的时候,会发生回流的过程。

        • 一个DOM元素的几何属性变化,常见的集合属性有width、height、padding、margin、left、top、border等等
        • 使DOM节点发生增减或者移动
        • 读写offset、scroll、client属性时,浏览器为了获取这些值,需要进行回流
        • 调用window.getComputedStyle方法
      • 回流过程

        image-20200701213632809

        按照上面的渲染流水线,触发回流的时候,如果DOM结构发生改变,则重新渲染DOM树,然后讲后面的流程全走一遍。

        相当于是将解析和合成的过程重新又走了一遍,开销非常大。

    • 重绘

      • 触发条件

        当DOM的修改导致样式的变化,并且没有影响几何属性的时候,会导致重绘

      • 重绘过程

        由于没有导致 DOM 几何属性的变化,因此元素的位置信息不需要更新,从而省去布局的过程。流程如下:

        image-20200701220643622

        可以看到,重绘不一定导致回流,但回流一定发生了重绘。

  5. webpack优化

    1. exclude/include

      可以通过 excludeinclude 配置来确保转译尽可能少的文件。顾名思义,exclude指定要排除的文件,include指定包含的文件。

      exclude`的优先级高于 `include`,在 `include`和`exclude`中使用绝对路径数组,尽量避免 `exclude`,更倾向于使用 `include
      //webpack.config.js
      const path = require('path');
      module.exports = {
          //...
          module: {
              rules: [
                  {
                      test: /\.js[x]?$/,
                      use: ['babel-loader'],
                      include: [path.resolve(__dirname, 'src')]
                  }
              ]
          },
      }
    2. cache-loader

      在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。

      安装依赖: npm install cache-loader -D

      cache-loader 的配置很简单,放在其他loader 之前即可。修改 webpack的配置如下:

      module.exports = {
          //...
      
          module: {
              rules: [
                  {
                      test: /\.jsx?$/,
                      use: ['cache-loader','babel-loader']
                  }
              ]
          }
      }
    3. happypack

      有大量文件需要解析和处理,构建是文件读写和计算密集型的操作,特别是当文件数量变多后,webpack 构建慢的问题会显得严重。文件读写和计算操作是无法避免的,那能不能让 webpack 同一时刻处理多个任务,发挥多核CPU电脑的威力?

      happypack 就能让 webpack做到这点,它将任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。

      安装 happypacknpm install happypack -D

      修改配置文件:

      const Happypack = require('happypack');
      module.exports = {
          //...
          module: {
              rules: [
                  {
                      test: /\.js[x]?$/,
                      use: 'Happypack/loader?id=js',
                      include: [path.resolve(__dirname, 'src')]
                  },
                  {
                      test: /\.css$/,
                      use: 'Happypack/loader?id=css',
                      include: [
                          path.resolve(__dirname, 'src'),
                          path.resolve(__dirname, 'node_modules', 'bootstrap', 'dist')
                      ]
                  }
              ]
          },
          plugins: [
              new Happypack({
                  id: 'js', //和rule中的id=js对应
                  //将之前 rule 中的 loader 在此配置
                  use: ['babel-loader'] //必须是数组
              }),
              new Happypack({
                  id: 'css',//和rule中的id=css对应
                  use: ['style-loader', 'css-loader','postcss-loader'],
              })
          ]
      }
    4. thread-loader

      除了使用 happypack 外,我们也可以使用 thread-loader, 把 thread-loader 放置在其他 loader之前,那么放置在这个loader 之后的 loader 就会在一个单独的 worker 池中运行。

      worker池中运行的loader 是受到限制的。

      • 这些loader 不能产生新的文件
      • 这些loader 不能使用定制的 loader API
      • 这些loader 无法获取webpack的选项配置

      安装依赖:
      npm install thread-loader -D

      module.exports = {
          module: {
              rules: [
                  {
                      test: /\.jsx?$/,
                      use: ['thread-loader', 'cache-loader', 'babel-loader']
                  }
              ]
          }
      }
    5. 开启JS多进程压缩

    6. HardSourceWebpackPlugin

      HardSourceWebpackPlugin 为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source

      配置 hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。

      安装依赖:

      npm install hard-source-webpack-plugin -D
      //webpack.config.js
      var HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
      module.exports = {
          //...
          plugins: [
              new HardSourceWebpackPlugin()
          ]
      }
    7. resolve

      resolve 配置 webpack 如何寻找模块对应的文件,假设我们确定模块都从根目录下的 node_modules中查找可以这样配置

      const path = require('path');
      module.exports = {
          //...
          resolve: {
              modules: [path.resolve(__dirname, 'node_modules')],
          }
    8. 页面上渲染10000个标签,会发生什么情况?如何优化?

      最普通的操作方法就是操作DOM,直接插入:

      <script>
          var container = document.getElementById('container')
          for(let i = 0; i < 10000; i++){
              let li = document.createElement('li')
              li.innerHTML = 'hello world'
              container.appendChild(li);
          }
      </script>

      使用这种方法效率会非常低,甚至会在页面出现闪屏、卡屏的现象。因为每次调用appendChild()方法时,浏览器都会重新渲染页面。如果大量的更新DOM节点,则会非常消耗性能,影响用户体验。

      解决方法:使用Fragment文档片段

      <script>
          var container = document.getElementById('container')
          var fragment = document.createDocumentFragment()
          for(let i = 0; i < 10000; i++){
              let li = document.createElement('li')
              li.innerHTML = 'hello world'
              fragment.appendChild(li)
          }
          container.appendChild(fragment);
      </script>

      JavaScript提供了一个文档片段DocumentFragment的机制。把所有要构造的节点都放在文档片段中执行,这样可以不影响文档树,也就不会造成页面渲染。当节点都构造完成后,再将文档片段对象添加到页面中,这时所有的节点都会一次性渲染出来,这样就能减少浏览器负担,提高页面渲染速度。

计算机网络

  1. GET 和 POST 有什么区别?

    • 从缓存的角度,get请求会被浏览器主动缓存下来,留下历史记录,而post默认不会。
    • 从编码的角度,get只能进行url编码,只能接受ASCII字符,而post没有限制。
    • 从参数的角度,get一般放在url中,因此不安全,post放在请求体中,更适合传输敏感信息。
    • 从幂等性的角度,get是幂等的,而post不是。(幂等表示执行相同的操作,结果也是相同的)
    • 从TCP的角度,get请求会吧请求报文一次性发出去,而post会分为两个tcp数据包,首先发出header部分,如果服务器响应100(continue),然后发body部分。
    • 从传参长度来看,get由于是从url提交数据,不同浏览器对url的长度限制不同。post由服务器的设置和内存大小有关。
  2. 如何理解HTTP状态码?

    HTTP的状态码分为五类:

    • 1xx:表示目前是协议处理的中间状态,还需后续操作
    • 2xx:表示成功状态
    • 3xx:重定向状态,资源位置发生变动,需要重新请求
    • 4xx:请求报文有误
    • 5xx:服务器端发生错误

    1xx

    101(Switching Protocols)。在HTTP升级为WebSocket的时候,如果服务器同意变更,就会发送状态码 101。

    2xx

    200 OK是见得最多的成功状态码。通常在响应体中放有数据。

    204 No Content含义与 200 相同,但响应头后没有 body 数据。

    206 Partial Content顾名思义,表示部分内容,它的使用场景为 HTTP 分块下载和断点续传,当然也会带上相应的响应头字段Content-Range

    3xx

    301 Moved Permanently即永久重定向,对应着302 Found,即临时重定向。

    比如你的网站从 HTTP 升级到了 HTTPS 了,以前的站点再也不用了,应当返回301,这个时候浏览器默认会做缓存优化,在第二次访问的时候自动访问重定向的那个地址。

    而如果只是暂时不可用,那么直接返回302即可,和301不同的是,浏览器并不会做缓存优化。

    304 Not Modified: 当协商缓存命中时会返回这个状态码。

    4xx

    400 Bad Request: 开发者经常看到一头雾水,只是笼统地提示了一下错误,并不知道哪里出错了。

    403 Forbidden: 这实际上并不是请求报文出错,而是服务器禁止访问,原因有很多,比如法律禁止、信息敏感。

    404 Not Found: 资源未找到,表示没在服务器上找到相应的资源。

    405 Method Not Allowed: 请求方法不被服务器端允许。

    406 Not Acceptable: 资源无法满足客户端的条件。

    408 Request Timeout: 服务器等待了太长时间。

    409 Conflict: 多个请求发生了冲突。

    413 Request Entity Too Large: 请求体的数据过大。

    414 Request-URI Too Long: 请求行里的 URI 太大。

    429 Too Many Request: 客户端发送的请求过多。

    431 Request Header Fields Too Large请求头的字段内容太大。

    5xx

    500 Internal Server Error: 仅仅告诉你服务器出错了,出了啥错咱也不知道。

    501 Not Implemented: 表示客户端请求的功能还不支持。

    502 Bad Gateway: 服务器自身是正常的,但访问的时候出错了,啥错误咱也不知道。

    503 Service Unavailable: 表示服务器当前很忙,暂时无法响应服务。

  3. 说一说HTTP的特点?HTTP有哪些缺点?

    特点:

    • 灵活可扩展,主要体现在两个方面。一个是语义上的自由,之规定了基本格式,比如空格分隔单词,换行分隔字段,其他的各个部分都没有严格的语法限制。另一个是传输形式的多样性,不仅仅可以传输文本,还能传输图片,视频等任意数据。
    • 可靠性,HTTP基于TCP/IP,因此把这一特性继承了下来。
    • 请求-应答。一发一收、有来有回。
    • 无状态。这里的状态是指通行过程的上下文信息,而每次http请求都是独立、无关的,默认不需要保留状态信息。

    缺点:

    • 无状态
    • 明文传输
    • 队头阻塞问题
  4. 说一说你知道的Accept系列的字段?

    • 数据格式

      http支持许多数据格式,因此http通过Content-Type这个字段来标记报文body部分的数据类型,这是对于发送端而言的,接收端想要收到特定类型的数据,也可以用Accept字段

      具体而言。这两个字段的取值可以分为下面几类:

      • text: text/html, text/plain, text/css 等
      • image: image/gif, image/jpeg, image/png 等
      • audio/video: audio/mpeg, video/mp4 等
      • application: application/json, application/javascript, application/pdf, application/octet-stream
    • 压缩方式

      当然一般这些数据都是会进行编码压缩的,采取什么样的压缩方式就体现在了发送方的Content-Encoding字段上, 同样的,接收什么样的压缩方式体现在了接受方的Accept-Encoding字段上。这个字段的取值有下面几种:

      • gzip: 当今最流行的压缩格式
      • deflate: 另外一种著名的压缩格式
      • br: 一种专门为 HTTP 发明的压缩算法
    • 支持语言

      对于发送方而言,还有一个Content-Language字段,在需要实现国际化的方案当中,可以用来指定支持的语言,在接受方对应的字段为Accept-Language。如:

      // 发送端
      Content-Language: zh-CN, zh, en
      // 接收端
      Accept-Language: zh-CN, zh, en
    • 字符集

      最后是一个比较特殊的字段, 在接收端对应为Accept-Charset,指定可以接受的字符集,而在发送端并没有对应的Content-Charset, 而是直接放在了Content-Type中,以charset属性指定。如:

      // 发送端
      Content-Type: text/html; charset=utf-8
      // 接收端
      Accept-Charset: charset=utf-8
      复
  5. HTTP如何处理大文件传输?

    对于大文件来说,一口气全部传输显然是不现实的,会有大量的等待时间。http通过使用范围请求的允许客户端仅仅请求一个资源的一部分。

    • 如何支持

      使用范围请求,前提是服务器要支持范围请求,要支持这个功能就必须加上响应头:

      Accept-Range:none

      用来告知客户端这边是支持范围请求的

    • Range字段拆解

      对于客户端而言,需要指定请求的哪一部分,通过Range这个请求头字段确定,格式为bytes=x-y。

      • 0-499表示从开始到第 499 个字节。
      • 500- 表示从第 500 字节到文件终点。
      • -100表示文件的最后100个字节。

      服务器收到请求后,首先验证范围是否合法,如果越界了那么返回416错误码,否则读取相应片段,返回206状态码

      同时,服务器需要添加Content-Range字段,这个字段的格式根据请求头中的Range字段的不同有所差异。

      具体来说,请求单段数据和请求多段数据响应头是不一样的。

      // 单段数据
      Range: bytes=0-9
      // 多段数据
      Range: bytes=0-9, 30-39
    • 单段数据

      对于单端数据的请求,返回的响应如下:

      HTTP/1.1 206 Partial Content
      Content-Length: 10
      Accept-Ranges: bytes
      Content-Range: bytes 0-9/100
      
      i am xxxxx

      值得注意的是Content-Range字段,0-9表示请求的返回,100表示资源的总大小,很好理解。

    • 多段数据

      HTTP/1.1 206 Partial Content
      Content-Type: multipart/byteranges; boundary=00000010101
      Content-Length: 189
      Connection: keep-alive
      Accept-Ranges: bytes
      
      --00000010101
      Content-Type: text/plain
      Content-Range: bytes 0-9/96
      
      i am xxxxx
      --00000010101
      Content-Type: text/plain
      Content-Range: bytes 20-29/96
      
      eex jspy e
      --00000010101--

这个时候出现了一个非常关键的字段Content-Type: multipart/byteranges;boundary=00000010101,它代表了信息量是这样的:

  • 请求一定是多段数据请求
    • 响应体中的分隔符是 00000010101

因此,在响应体中各段数据之间会由这里指定的分隔符分开,而且在最后的分隔末尾添上--表示结束。

  1. 说一说HTTP1.1如何解决HTTP的队头阻塞问题

    什么是队头阻塞?

    http传输是基于请求-应答的模式进行的,报文必须是一发一收的,但值得注意的是,里面的任务被放在一个任务队列中串行执行,一旦队首的请求处理太慢,就会阻塞后面请求的处理。这就是HTTP队头阻塞问题

    并发连接

    对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其他所有任务。在RFC2616规定客户端最多并发2两个连接,不过事实上在现在浏览器标准中,这个上线要多很多,chrome中是6个。

    域名分片

    把一个主域名分为多个子域名,这样就可以并发的长连接更多了,事实上也更好地解决了队头阻塞问题。

  2. 说一说cookie吧?

    HTTP 是一个无状态的协议,每次 http 请求都是独立、无关的,默认不需要保留状态信息。但有时候需要保存一些状态,怎么办呢?

    HTTP 为此引入了 Cookie。Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储(在chrome开发者面板的Application这一栏可以看到)。向同一个域名下发送请求,都会携带相同的 Cookie,服务器拿到 Cookie 进行解析,便能拿到客户端的状态。而服务端可以通过响应头中的Set-Cookie字段来对客户端写入Cookie

    Cookie的属性

    • 生命周期

      Cookie的有效期可以通过Expires和Max-Age两个属性来设置

      • expires即过期时间
      • Max-Age用的是一段时间间隔,单位是秒,从浏览器收到报文开始计算。

      若Cookie过期,则这个Cookie会被删除,并不会发给服务端。

    • 作用域

      Domain和path,给Cookie绑定了域名和路径,在发送之前,发现域名或者路径和这两个属性不匹配,那么就不会带上Cookie。path为/时,表示域名下的任意路径都允许使用Cookie

    • 安全相关

      如果带上Secure,说明只能通过HTTP传输cookie

      如果cookie字段带上httponly,说明只能通过HTTP协议传输,不能通过JS访问,这也是预防XSS攻击的重要手段

      相应的,对于CSRF攻击的预防,也有samesite属性

      samesite可以设置三个值,strict、lax、none

      a.Strict模式下,浏览器完全禁止第三方请求携带Cookie。比如请求sanyuan.com网站只能在sanyuan.com域名当中请求才能携带 Cookie,在其他网站请求都不能。

      b.Lax模式,就宽松一点了,但是只能在 get 方法提交表单况或者a 标签发送 get 请求的情况下可以携带 Cookie,其他情况均不能。

      c.None模式下,也就是默认模式,请求会自动携带上 Cookie。

    Cookie的缺点

    • 容量缺陷。Cookie 的体积上限只有4KB,只能用来存储少量的信息。
    • 性能缺陷。Cookie 紧跟域名,不管域名下面的某一个地址需不需要这个 Cookie ,请求都会携带上完整的 Cookie,这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了很多不必要的内容。但可以通过DomainPath指定作用域来解决。
    • 安全缺陷。由于 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改,在 Cookie 的有效期内重新发送给服务器,这是相当危险的。另外,在HttpOnly为 false 的情况下,Cookie 信息能直接通过 JS 脚本来读取。
  3. 说一说http缓存?

    http缓存分为两种缓存:强缓存和协商缓存

    • 强缓存

      浏览器中的缓存作用分为两种情况,一种是需要发送http请求的,一种是不需要发送。

      首先是检查强缓存,这个阶段不需要发送http请求。

      在http1.0和http1.1中,这个字段是不一样的。在http1.0使用的是expires,而http1.1使用的是Cache-Control。

      • Expires即过期时间,存在服务器端返回的响应头中,告诉浏览器在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。

        Expires: Wed, 22 Nov 2019 08:41:00 GMT

        这种方式,实际上并不可取,因为服务器的时间和浏览器的时间可能并不一致,那服务器返回的这个过期时间可能就是不准确的。因此在HTTP1.1中新增了Cache-Control

      • Cache-Control是一个具体的过期时间段,通过采用过期时长来控制缓存,对应的字段是max-age。

        Cache-Control:max-age=3600

        代表这个响应返回后在3600秒,也就是一个小时之内可以直接使用缓存。

        Cache-Control还有其他一些属性:

        • public:客户端和代理服务器都可以缓存。
        • private:这种情况就只有浏览器能缓存,中间代理服务器不能缓存。
        • no-cache:跳过当前的强缓存,发送HTTP请求,即直接进入协商缓存阶段
        • no-store:不进行任何形式的缓存
        • s-maxage:这个和max-age长得比较像,但是区别在于s-maxage是针对代理服务器缓存时间。
        • must-revalidate:是缓存就有过期的时候,加上这个字段一旦缓存过期,就必须到源服务器验证
    • 协商缓存

      强缓存失效之后,浏览器在请求头中携带相应的缓存tag来向服务器发请求,由服务器根据这个tag,来决定是否使用缓存,这就是协商缓存

      具体来说,这样的缓存tag分为两种: Last-ModifiedETag。这两者各有优劣,并不存在谁对谁有绝对的优势,跟上面强缓存的两个 tag 不一样。

      • Last-Modified

        最后修改时间,在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。浏览器接收到后,如果再次请求,会在请求头中携带 if-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间。

        服务器拿到请求头的 If-Modified-Since的字段后,其实会和这个服务器中 该资源的最后修改时间 对比。

        • 如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
        • 否则返回304,告诉浏览器直接用缓存。
      • ETag

        ETag 是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头把这个值给浏览器。

        浏览器接收到ETag的值,会在下次请求时,将这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。

        服务器接收到If-None-Match后,会跟服务器上该资源的ETag进行比对:

        • 如果两者不一样,说明要更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
        • 否则返回304,告诉浏览器直接用缓存
    • 缓存位置

      当强缓存和协商缓存中服务器返回304的时候,我们直接从缓存中获取资源。那这些资源究竟缓存在什么位置?

      浏览器中的缓存位置一共有4中,按优先级从高到底排列分别是:

      • Service Worker
      • Memory Cache
      • Disk Cache
      • Push Cache

      Service Worker

      Service Worker 借鉴了 Web Worker的 思路,即让 JS 运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问DOM。虽然如此,但它仍然能帮助我们完成很多有用的功能,比如离线缓存消息推送网络代理等功能。其中的离线缓存就是 Service Worker Cache

      Service Worker 同时也是 PWA 的重要实现机制,关于它的细节和特性,我们将会在后面的 PWA 的分享中详细介绍。

 **Memory Cache**

 **Memory Cache**指的是内存缓存,从效率上讲它是最快的。但是从存活时间来讲又是最短的,当渲染进程结束后,内存缓存也就不存在了。



 **Disk Cache**

 **Disk Cache**就是存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,但是他的优势在于存储容量和存储时长。稍微有些计算机基础的应该很好理解,就不展开了。

 好,现在问题来了,既然两者各有优劣,那浏览器如何决定将资源放进内存还是硬盘呢?主要策略如下:

 - 比较大的JS、CSS文件会直接被丢进磁盘,反之丢进内存
 - 内存使用率比较高的时候,文件优先进入磁盘



 **Push Cache**

 即推送缓存,这是浏览器缓存的最后一道防线。它是 `HTTP/2` 中的内容,虽然现在应用的并不广泛,但随着 HTTP/2 的推广,它的应用越来越广泛。
  1. 什么是跨域?浏览器如何拦截响应?如何解决跨域?

    浏览器遵循同源策略(scheme(协议)host(主机)port(端口)都相同则为同源)。非同源站点有这样一些限制:

    • 不能读取和修改对方的DOM
    • 不能访问对方的Cookie、indexDB、LocalStorage
    • 限制XMLHttprequest请求

    当浏览器向目标URI发请求时,只要当前URL和目标URL不同源,则产生跨域。

    解决跨域常见的有以下几种方法:

    • CORS

      CORS 其实是 W3C 的一个标准,全称是跨域资源共享。它需要浏览器和服务器的共同支持,具体来说,非 IE 和 IE10 以上支持CORS,服务器需要附加特定的响应头,后面具体拆解。不过在弄清楚 CORS 的原理之前,我们需要清楚两个概念: 简单请求非简单请求

      浏览器根据请求方法和请求头的特定字段,将请求做了一下分类,具体来说规则是这样,凡是满足下面条件的属于简单请求:

      • 请求方法为 GET、POST 或者 HEAD
      • 请求头的取值范围: Accept、Accept-Language、Content-Language、Content-Type(只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain)

      浏览器画了这样一个圈,在这个圈里面的就是简单请求, 圈外面的就是非简单请求,然后针对这两种不同的请求进行不同的处理。

      • 简单请求

        它会自动在请求头当中,添加一个Origin字段,用来说明请求来自哪个。服务器拿到请求之后,在回应时对应地添加Access-Control-Allow-Origin字段,如果Origin不在这个字段的范围中,那么浏览器就会将响应拦截。

        因此,Access-Control-Allow-Origin字段是服务器用来决定浏览器是否拦截这个响应,这是必需的字段。与此同时,其它一些可选的功能性的字段,用来描述如果不会拦截,这些字段将会发挥各自的作用。

        Access-Control-Allow-Credentials。这个字段是一个布尔值,表示是否允许发送 Cookie,对于跨域请求,浏览器对这个字段默认值设为 false,而如果需要拿到浏览器的 Cookie,需要添加这个响应头并设为true, 并且在前端也需要设置withCredentials属性:

        let xhr = new XMLHttpRequest();
        xhr.withCredentials = true;

        Access-Control-Expose-Headers。这个字段是给 XMLHttpRequest 对象赋能,让它不仅可以拿到基本的 6 个响应头字段(包括Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma), 还能拿到这个字段声明的响应头字段。比如这样设置:

        Access-Control-Expose-Headers: aaa

        那么在前端可以通过 XMLHttpRequest.getResponseHeader('aaa') 拿到 aaa 这个字段的值。

      • 非简单请求

        非简单请求相对而言会有些不同,体现在两个方面: 预检请求响应字段

        我们以 PUT 方法为例。

        var url = 'http://xxx.com';
        var xhr = new XMLHttpRequest();
        xhr.open('PUT', url, true);
        xhr.setRequestHeader('X-Custom-Header', 'xxx');
        xhr.send();
        

        当这段代码执行后,首先会发送预检请求。这个预检请求的请求行和请求体是下面这个格式:

        OPTIONS / HTTP/1.1
        Origin: 当前地址
        Host: xxx.com
        Access-Control-Request-Method: PUT
        Access-Control-Request-Headers: X-Custom-Header
        

        预检请求的方法是OPTIONS,同时会加上Origin源地址和Host目标地址,这很简单。同时也会加上两个关键的字段:

        • Access-Control-Request-Method, 列出 CORS 请求用到哪个HTTP方法
        • Access-Control-Request-Headers,指定 CORS 请求将要加上什么请求头

        这是预检请求。接下来是响应字段,响应字段也分为两部分,一部分是对于预检请求的响应,一部分是对于 CORS 请求的响应。

        预检请求的响应。如下面的格式:

        HTTP/1.1 200 OK
        Access-Control-Allow-Origin: *
        Access-Control-Allow-Methods: GET, POST, PUT
        Access-Control-Allow-Headers: X-Custom-Header
        Access-Control-Allow-Credentials: true
        Access-Control-Max-Age: 1728000
        Content-Type: text/html; charset=utf-8
        Content-Encoding: gzip
        Content-Length: 0

        其中有这样几个关键的响应头字段:

        • Access-Control-Allow-Origin: 表示可以允许请求的源,可以填具体的源名,也可以填*表示允许任意源请求。
        • Access-Control-Allow-Methods: 表示允许的请求方法列表。
        • Access-Control-Allow-Credentials: 简单请求中已经介绍。
        • Access-Control-Allow-Headers: 表示允许发送的请求头字段
        • Access-Control-Max-Age: 预检请求的有效期,在此期间,不用发出另外一条预检请求。

        在预检请求的响应返回后,如果请求不满足响应头的条件,则触发XMLHttpRequestonerror方法,当然后面真正的CORS请求也不会发出去了。

        CORS 请求的响应。绕了这么一大转,到了真正的 CORS 请求就容易多了,现在它和简单请求的情况是一样的。浏览器自动加上Origin字段,服务端响应头返回Access-Control-Allow-Origin。可以参考以上简单请求部分的内容。

    • JSONP

      虽然XMLHttpRequest对象遵循同源政策,但是script标签不一样,它可以通过 src 填上目标地址从而发出 GET 请求,实现跨域请求并拿到响应。这也就是 JSONP 的原理,接下来我们就来封装一个 JSONP:

      const jsonp = ({ url, params, callbackName }) => {
        const generateURL = () => {
          let dataStr = '';
          for(let key in params) {
            dataStr += `${key}=${params[key]}&`;
          }
          dataStr += `callback=${callbackName}`;
          return `${url}?${dataStr}`;
        };
        return new Promise((resolve, reject) => {
          // 初始化回调函数名称
          callbackName = callbackName || Math.random().toString.replace(',', ''); 
          // 创建 script 元素并加入到当前文档中
          let scriptEle = document.createElement('script');
          scriptEle.src = generateURL();
          document.body.appendChild(scriptEle);
          // 绑定到 window 上,为了后面调用
          window[callbackName] = (data) => {
            resolve(data);
            // script 执行完了,成为无用元素,需要清除
            document.body.removeChild(scriptEle);
          }
        });
      }
      

      CORS相比,JSONP 最大的优势在于兼容性好,IE 低版本不能使用 CORS 但可以使用 JSONP,缺点也很明显,请求方法单一,只支持 GET 请求。

    • Nginx

  2. 说一说XSS攻击?

    • 什么是XSS攻击?

      XSS 全称是 Cross Site Scripting(即跨站脚本),为了和 CSS 区分,故叫它XSS。XSS 攻击是指浏览器中执行恶意脚本(无论是跨域还是同域),从而拿到用户的信息并进行操作。

      这些操作一般可以完成下面这些事情:

      1. 窃取Cookie
      2. 监听用户行为,比如输入账号密码后直接发送到黑客服务器。
      3. 修改 DOM 伪造登录表单。
      4. 在页面中生成浮窗广告。

      通常情况,XSS 攻击的实现有三种方式——存储型反射型文档型。原理都比较简单,先来一一介绍一下。

      • 存储型

        顾名思义就是将恶意脚本存储了起来,确实,存储型的 XSS 将脚本存储到了服务端的数据库,然后在客户端执行这些脚本,从而达到攻击的效果。

        常见的场景是留言评论区提交一段脚本代码,如果前后端没有做好转义的工作,那评论内容存到了数据库,在页面渲染过程中直接执行, 相当于执行一段未知逻辑的 JS 代码,是非常恐怖的。这就是存储型的 XSS 攻击。

      • 反射性

        指的是恶意脚本作为网络请求的一部分。比如我输入:

        http://sanyuan.com?q=<script>alert("你完蛋了")</script>

        这样,在服务器端会拿到q参数,然后将内容返回给浏览器端,浏览器将这些内容作为HTML的一部分解析,发现是一个脚本,直接执行,这样就被攻击了。

        之所以叫它反射型, 是因为恶意脚本是通过作为网络请求的参数,经过服务器,然后再反射到HTML文档中,执行解析。和存储型不一样的是,服务器并不会存储这些恶意脚本。

      • 文档型

        文档型的 XSS 攻击并不会经过服务端,而是作为中间人的角色,在数据传输过程劫持到网络数据包,然后修改里面的 html 文档

        这样的劫持方式包括WIFI路由器劫持或者本地恶意软件等。

    • 防范措施

      • 一个信念: 不要相信用户的输入,对输入内容转码或者过滤,让其不可执行。
      • 两个利用: 利用 CSP,利用 Cookie 的 HttpOnly 属性。
  3. 说一说CSRF攻击?

    CSRF(Cross-site request forgery), 即跨站请求伪造,指的是黑客诱导用户点击链接,打开黑客的网站,然后黑客利用用户目前的登录状态发起跨站请求。

    举个例子, 你在某个论坛点击了黑客精心挑选的小姐姐图片,你点击后,进入了一个新的页面。

    那么恭喜你,被攻击了:)

    你可能会比较好奇,怎么突然就被攻击了呢?接下来我们就来拆解一下当你点击了链接之后,黑客在背后做了哪些事情。

    • 自动发 GET 请求

    黑客网页里面可能有一段这样的代码:

    <img src="https://xxx.com/info?user=hhh&count=100"></img>

    进入页面后自动发送 get 请求,值得注意的是,这个请求会自动带上关于 xxx.com 的 cookie 信息(这里是假定你已经在 xxx.com 中登录过)。

    假如服务器端没有相应的验证机制,它可能认为发请求的是一个正常的用户,因为携带了相应的 cookie,然后进行相应的各种操作,可以是转账汇款以及其他的恶意操作。

    • 自动发 POST 请求

    黑客可能自己填了一个表单,写了一段自动提交的脚本。

    <form id='hacker-form' action="https://xxx.com/info" method="POST">
      <input type="hidden" name="user" value="hhh" />
      <input type="hidden" name="count" value="100" />
    </form>
    <script>document.getElementById('hacker-form').submit();</script>

    同样也会携带相应的用户 cookie 信息,让服务器误以为是一个正常的用户在操作,让各种恶意的操作变为可能。

    • 诱导点击发送 GET 请求

    在黑客的网站上,可能会放上一个链接,驱使你来点击:

    <a href="https://xxx/info?user=hhh&count=100" taget="_blank">点击进入修仙世界</a>

    点击后,自动发送 get 请求,接下来和自动发 GET 请求部分同理。

    这就是CSRF攻击的原理。和XSS攻击对比,CSRF 攻击并不需要将恶意代码注入用户当前页面的html文档中,而是跳转到新的页面,利用服务器的验证漏洞用户之前的登录状态来模拟用户进行操作。

    • 防范措施

      • 利用Cookie的SameSite属性

      • 验证来源站点
        请求头中的Origin和Referer

        Origin只包含域名信息,而Referer包含了具体的 URL 路径。

      • CSRF Token

  4. 说一说osi七层模型?

    • 物理层
    • 数据链路层
    • 网络层
    • 传输层
    • 会话层
    • 表示层
    • 应用层
  5. 说一说DNS解析的具体流程

    DNS解析的就是通过域名系统查询对应的IP。具体过程为首先当用户在url中输入一个网址后,操作系统首先会在本地缓存中查询,若本地缓存中查询到就向用户返回对应的IP,若没有则去系统配置的DNS服务器中查询,本地服务器向根域名服务器中查询,根域名服务器返回顶级域名服务器的ip地址的列表,本地服务器向顶级域名服务器发送请求,顶级域名服务器返回权威域名服务器的IP地址列表,然后本地服务器再向权威域名服务器发送请求,最后权威域名服务器返回一个与主机名对应的IP地址列表。

    递归查询指的是当我们发起一次请求后,域名服务器代为向下一级服务器发起请求,最终返回一个结果给我们,用户只需发起一次请求即可。而迭代查询指的是用户发起一次请求之后,域名服务器返回单次查询的结果,下一次域名服务器还需要用户自己发起请求查询,迭代查询需要发起多次查询才会返回最终的查询结果。

    那么我们向本地DNS服务器发起请求的方式就是递归请求,因为我们只需发起一次请求,等待本地服务器返回最终结果给我们。而本地DNS服务器向域名服务器发起请求属于迭代请求,需要本地服务器一次次的向不同级域名服务器发起请求才会返回最终的结果。

  6. 说一说tcp的三次握手

    • 第一次握手:SYN=1,seq=x。客户端向服务器端发起连接请求报文段,该报文中包含自身的数据通讯初始序号,请求发送后,客户端进入SYN-SENT状态。
    • 第二次握手:SYN=1,ACK=1,确认序号=x+1,seq=y。服务器端收到连接请求之后,如果同意连接则会发送一个应答,该应答中也会包含自身的数据通讯初始号,发送完成后便进入SYN-RECEIVED状态。
    • 第三次握手:ACK=1,确认序号=y+1,seq=x+1。客户端收到连接同意的应答后,还要向服务器端发送一个确认报文,客户端发送完这个报文段后便进入到ESTABLISHED状态,服务器端收到这个应答后也进入ESTABLISHED状态,此时连接建立成功。

    为什么不用两次握手?

    1. 如果只有两次握手,那么服务器端不会知道是否客户端能够建立连接,会对网络造成性能上的浪费。
    2. 如果只有两次握手,当第一次客户端向服务器端发送的连接请求若没有丢失而是被阻塞在网络中会产生错误。这样客户端长时间没有接受到服务器端传递过来的连接确认报文会再次发起请求连接报文给服务器,那么等到阻塞的报文畅通后,服务器端会响应给客户端,因为只有两次握手这样就又会建立起连接,但这个请求已经报废了,会重复连接对网络资源的浪费。那若有三次握手,则客户端即使收到服务器的确认报文也会认为这是报废的请求,而不会再次发起确认给服务器就不会建立连接,由于服务器收不到确认,就知道客户端没有请求连接。
  7. 说一说tcp的四次挥手

    • 第一次挥手:如果客户端认为数据发送完成,会向服务器端发送数据发送完成的请求字段。
    • 第二次挥手:服务器收到连接释放请求,会告诉应用层要释放TCP连接,然后会发送ACK包给客户端,进入CLOSE_WAIT状态,表示客户端到服务器的连接已经释放,不接受客户端发送的数据了。但TCP连接是双向的,所以服务器依旧可以发送数据给客户端。
    • 第三次挥手:服务器端如果还有数据没发送完毕会继续发送,完毕后会向客户端发送连接释放请求,然服务器端进入LAST-ACK状态。
    • 第四次挥手:客户端收到释放请求后,会向服务器发送确认应答,此时客户端进入TIME-WAIT阶段,这个状态会持续2MSL(最大生存期,指报文段在网络中生存的时间,超时会被抛弃)时间,若在该时间内没有服务器的重发请求的话,就进入CLOSED状态,当服务器端接收到确认应答后也进入CLOSED状态。

    为什么第四次挥手中客户端进入TIME-WAIT状态要持续2MSL时间后再进入CLOSED状态?

    为了保证服务器能收到客户端的确认应答,若客户端发完确认应答之后直接进入CLOSED状态,若果客户端的确认应答因为网络问题一直没有到达,那么就会造成服务器端不能正常关闭。若服务器端没有收到应答信息,会向客户端发送超时重传,此时客户端在2MSL时间内再次收到服务器的连接释放请求,便回再向服务器端重新发送应答信息。

  8. 说一说TCP和UDP的区别?

    TCP是一个面向连接的、可靠的、基于字节流的传输层协议。

    UDP是一个面向无连接的传输层协议。(就这么简单,其它TCP的特性也就没有了)。

    具体来分析,和 UDP 相比,TCP 有三大核心特性:

    1. 面向连接。所谓的连接,指的是客户端和服务器的连接,在双方互相通信之前,TCP 需要三次握手建立连接,而 UDP 没有相应建立连接的过程。

    2. 可靠性。TCP 花了非常多的功夫保证连接的可靠,这个可靠性体现在哪些方面呢?一个是有状态,另一个是可控制。

      TCP 会精准记录哪些数据发送了,哪些数据被对方接收了,哪些没有被接收到,而且保证数据包按序到达,不允许半点差错。这是有状态

      当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发。这是可控制

      相应的,UDP 就是无状态, 不可控的。

    3. 面向字节流。UDP 的数据传输是基于数据报的,这是因为仅仅只是继承了 IP 层的特性,而 TCP 为了维护状态,将一个个 IP 包变成了字节流。

  9. tcp如何保证可靠性

    • 校检和
    • 确认应答与序列号
    • 超时重传
    • 连接管理
    • 流量控制
    • 拥塞控制
  10. 说一说cookie和session的区别?

    • 存储位置的不同

      cookie存放在浏览器,session存放在服务端

    • 编码方式不同

      Cookie 只能保存ASCII字符串,如果是Unicode字符或者二进制数据需要先进行编码。

      ession能够存取很多类型的数据,包括String、Integer、List、Map等,Session中也可以保存Java对象。

    • 域的支持不同

      比方说a.com的Cookie在a.com下都能用,而a.com的Session在api.a.com下都不能用,解决这个问题的办法是JSONP或者跨域资源共享。

    • 存储大小不同

      单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,而session则存储与服务端,浏览器对其没有限制。

  11. 说一说http报文?

    HTTP报文分为:报文首部、报文主体,分隔使用一个空行

    报文首部又可以分为:请求行/响应行、请求首部字段/响应首部字段、通用首部字段、实体首部字段。

    常见的请求首部字段有:

    image-20200711172811864

    响应首部字段有:

    image-20200711172840554

    通用首部字段:

    image-20200711172911679

    实体首部字段:

    image-20200711172931618

  12. osi 7层协议中各层的传输基本单位?

    image-20200715155416761

手撕代码

  1. 手写apply

    Function.prototype.MyApply = function (context) {
        context = context || window
        let args = Array.from(arguments).slice(1) || []
        const key = Symbol()
        context[key] = this
        const result = context[key](...args)
        delete context[key]
        return result
    }
  1. 手写call

    Function.prototype.MyCall = function (context, ...args) {
        context = context || window
        args = args || []
        let key = Symbol()
        context[key] = this
        let result = context[key](...args)
        delete context[key]
        return result
    }
    
  1. 手写bind

    Function.prototype.MyBind = function (context) {
        context = context || window
        var self = this
        var args = Array.from(arguments).slice(1)
        return function () {
            return self.apply(context, args)
        }
    }
  1. 手写ajax

    class Ajax {
      get(url, fn) {
        let xhr = new XMLHttpRequest()
        xhr.open("get", url, false)
        xhr.onreadystatechange = function () {
          if (xhr.readyState === 4) {
            if (xhr.status === 200 || xhr.status === 304) {
              fn.call(xhr.responseText)
            }
          }
        }
        xhr.send()
      }
      post(url, data, fn) {
        let xhr = new XMLHttpRequest()
        xhr.open('post', url, false)
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
        xhr.onreadystatechange = function () {
          if (xhr.readyState === 4) {
            if (xhr.status === 200 || xhr.status === 304) {
              fn.call(xhr.responseText)
            }
          }
        }
        xhr.send(data)
      }
    }
  1. 手写防抖

    function debounce(fn, delay = 500) {
        let timer = null
        return function () {
            if (timer) clearTimeout(timer)
            timer = setTimeout(() => {
                fn.apply(this, arguments)
                timer = null
            }, delay)
        }
    }
  1. 手写节流

    function throttle(fn, delay = 500) {
        let timer = null
        return function () {
            let args = arguments
            if(!timer){
                timer = setTimeOut(()=>{
                    timer = null
                    fn.call(this,args)
                },delay)
            }
        }
    }
  1. 数组扁平化

    function flat1(arr) {
        return arr.flat(Infinity)
    }
    
    function flat2(arr) {
        return arr.reduce((result, item) => {
            return result.concat(Array.isArray(item) ? flatten(item) : item);
        }, []);
    }
    
    function flat3(arr) {
        return arr.toString().split(',').map(function (item) {
            return Number(item);
        })
    }
    
    function flat4(arr) {
        return arr.join(',').split(',').map(function (item) {
            return parseInt(item);
        })
    }
    
    function flat5(arr) {
        while (arr.some(item => Array.isArray(item))) {
            arr = [].concat(...arr);
        }
        return arr;
    }
  1. 手写instanceof

    function _instanceof(left, right) {
        var O = right.prototype;// 取B的显示原型
        left = left.__proto__;// 取A的隐式原型
        while (true) {
            //Object.prototype.__proto__ === null
            if (left === null)
                return false;
            if (O === left)// 这里重点:当 O 严格等于 A 时,返回 true
                return true;
            left = left.__proto__;
        }
    }
    
  2. 手写new

    function myNew(fn, ...args) {
        let instance = Object.create(fn.prototype);
        let result = fn.call(instance, ...args)
        return typeof result === 'object' ? result : instance;
    }
  1. 手写深拷贝

    function deepClone(obj) {
        if (typeof obj !== 'object' || obj == null) {
            return obj
        }
        let result
        if (Array.isArray(obj)) {
            result = []
        } else {
            result = {}
        }
        for (let key in obj) {
            if (obj.hasOwnProperty(key)) {
                result[key] = deepClone(obj[key])
            } else {
                result[key] = obj[key]
            }
        }
        return result
    }
  2. 手写promise

    class MyPromise {
        constructor(executor) {
            //状态
            this.state = "pending"
            //成功的值
            this.value = null
            //失败的值
            this.reason = null
            //异步回调
            this.OnRejectedCallBacks = []
            this.OnResolvedCallBacks = []
            const resolve = (value) => {
                if (this.state === 'pending') {
                    this.value = value
                    this.state = 'resolved'
                    this.OnResolvedCallBacks.forEach(fn => fn())
                }
            }
            const reject = (reason) => {
                if (this.state === 'pending') {
                    this.reason = reason
                    this.state = 'rejected'
                    this.OnRejectedCallBacks.forEach(fn => fn())
                }
            }
            executor(resolve, reject)
        }
        then(OnResolve, OnRejected) {
            const p2 = new MyPromise((resolve, reject) => {
                if (this.state === 'resolved') {
                    setTimeout(() => {
                        let x = OnResolve(this.value)
                        resolvePromise(p2, x, resolve, reject)
                    }, 0)
                } else if (this.state === 'rejected') {
                    setTimeout(() => {
                        let x = OnRejected(this.reason)
                        resolvePromise(p2, x, resolve, reject)
                    }, 0)
                } else if (this.state === 'pending') {
                    this.OnResolvedCallBacks.push(() => {
                        setTimeout(() => {
                            let x = OnResolve(this.value)
                            resolvePromise(p2, x, resolve, reject)
                        }, 0)
                    })
                    this.OnRejectedCallBacks.push(() => {
                        setTimeout(() => {
                            let x = OnRejected(this.reason)
                            resolvePromise(p2, x, resolve, reject)
                        }, 0)
                    })
                }
            })
            return p2
        }
    }
    const resolvePromise = (p2, x, resolve, reject) => {
        if (p2 === x) {
            return new TypeError("循环引用")
        }
        if (typeof x === 'function' || (typeof x === 'object' && x != 'null')) {
            try {
                const then = x.then;
                if (typeof then === 'function') {
                    then.call(x, y => {
                        resolvePromise(p2, y, resolve, reject)
                    }, r => {
                        reject(r)
                    })
                }
            }
            catch (err) {
                reject(err)
            }
        } else {
            resolve(x)
        }
    }
    
  3. 快速排序

    function quickSort(arr, L, R) {
        if (L >= R) return
        let left = L
        let right = R
        let pivot = arr[left]
        while (left < right) {
            while (left < right && pivot <= arr[right]) right--
            if (left < right) arr[left] = arr[right]
            while (left < right && pivot >= arr[left]) left++
            if (left < right) arr[right] = arr[left]
            if (left >= right) arr[left] = pivot
        }
        quickSort(arr, L, right - 1)
        quickSort(arr, right + 1, R)
    }
    
  4. 手写Promise.all

    Promise.all = function(promises) {
      return new Promise((resolve, reject) => {
        let result = [];
        let len = promises.length;
        if(len === 0) {
          resolve(result);
          return;
        }
        const handleData = (data, index) => {
          result[index] = data;
          if(index == len - 1) resolve(result);
        }
        for(let i = 0; i < len; i++) {
          Promise.resolve(promise[i]).then(data => {
            handleData(data, i);
          }).catch(err => {
            reject(err);
          })
        }
      })
    }

其他

  1. Git的基本操作
    • git add . 提交文件至暂存区
    • git diff 查看修改的内容
    • git status 查看状态,有无文件需要提交
    • git commit -m 'xxx' 提交文件
    • git push origin master 推到远程分支
    • git pull origin master 拉起远程分支
    • git branch 查看分支
    • git checkout -b xxx/git checkout xx 新建分支并进入分支 / 切换分支
    • git merge xxx 合并分支
    • get fetch 拉取所有分支

最后更新: 2020年09月21日 15:46

原始链接: http://www.kweku.top/2020/06/25/05.前端面试/