前端面试--111

css常问的问题

1:css的盒模型

CSS中的盒子模型(Box model)分为两种:W3C标准盒子模型和IE标准盒子模型。
大多数的浏览器都采用W3C标准,而IE采用的是IE标准。而怪异模式是指“部分浏览器在支持W3C标准的同时还保留了原先的解析模式”,怪异模式主要表现在IE内核的浏览器中。
通过对比来分析标准模式和怪异模式对于块大小的定义

标准模式下,一个块的宽度 = width+padding(内边距)+border(边框)+margin(外边距);
怪异模式下,一个块的宽度 = width+margin(外边距) (即怪异模式下,width包含了border以及padding);

在具体的一个盒模型里面在标准模型中,盒模型的宽高只是内容的宽高
IE模型中盒模型的宽高是内容+填充+边框的总宽高

标准模型和IE模型 height 和 width 计算方式不同
标准模型:只包括 content
IE模型:content + padding + border

标准模型(默认):box-sizing: content-box
IE模型:box-sizing: border-box

2:css的BFC

一、何为BFC

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

二、形成BFC的条件

  1、浮动元素,float 除 none 以外的值; 
  2、定位元素,position(absolute,fixed); 
  3、display 为以下其中之一的值 inline-block,table-cell,table-caption; 
  4、overflow 除了 visible 以外的值(hidden,auto,scroll);

三、BFC的特性

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

1)BFC中的盒子对齐
特性的第一条是:内部的Box会在垂直方向上一个接一个的放置。

浮动的元素也是这样,box3浮动,他依然接着上一个盒子垂直排列。并且所有的盒子都左对齐。

(2)外边距折叠
特性的第二条:垂直方向上的距离由margin决定

在常规文档流中,两个兄弟盒子之间的垂直距离是由他们的外边距所决定的,但不是他们的两个外边距之和,而是以较大的为准。

(3)不被浮动元素覆盖

以常见的两栏布局为例。

左边固定宽度,右边不设宽,因此右边的宽度自适应,随浏览器窗口大小的变化而变化。
还有三栏布局。

左右两边固定宽度,中间不设宽,因此中间的宽度自适应,随浏览器的大小变化而变化。
那利用BFC如何实现一侧固定,一侧自适应:
在这里插入图片描述

flex布局如何实现一侧固定,一侧自适应:
在这里插入图片描述

(4)BFC包含浮动的块

这个是大家再熟悉不过的了,利用overflow:hidden清除浮动嘛,因为浮动的盒子无法撑出处于标准文档流的父盒子的height。这个就不过多解释了,相信大家都早已理解。

3:css清除浮动

1)添加额外标签

这是在学校老师就告诉我们的 一种方法,通过在浮动元素末尾添加一个空的标签例如

,其他标签br等亦可。

<div class="main left">.main{float:left;}</div>
<div class="side left">.side{float:right;}</div>
<div style="clear:both;"></div>
</div>
<div class="footer">.footer</div>

优点:通俗易懂,容易掌握

缺点:可以想象通过此方法,会添加多少无意义的空标签,有违结构与表现的分离,在后期维护中将是噩梦,这是坚决不能忍受的,所以你看了这篇文章之后还是建议不要用了吧。

2)父元素设置 overflow:hidden

通过设置父元素overflow值设置为hidden;在IE6中还需要触发 hasLayout ,例如 zoom:1;

<div class="wrap" id="float3" style="overflow:hidden; *zoom:1;">
<h2>3)父元素设置 overflow </h2>
<div class="main left">.main{float:left;}</div>
<div class="side left">.side{float:right;}</div>
</div>
<div class="footer">.footer</div>

优点:不存在结构和语义化问题,代码量极少

缺点:内容增多时候容易造成不会自动换行导致内容被隐藏掉,无法显示需要溢出的元素;04年POPO就发现overflow:hidden会导致中键失效,这是我作为一个多标签浏览控所不能接受的。所以还是不要使用.

3)父元素也设置浮动

优点:不存在结构和语义化问题,代码量极少

缺点:使得与父元素相邻的元素的布局会受到影响,不可能一直浮动到body,不推荐使用

上面的几种方法都有各种各样的问题,下面的推荐方法:

4)使用:after 伪元素

需要注意的是 :after是伪元素(Pseudo-Element),不是伪类(某些CSS手册里面称之为“伪对象”).

由于IE6-7不支持:after,使用 zoom:1触发 hasLayout。

该方法源自于: How To Clear Floats Without Structural Markup

原文全部代码如下:

<style type="text/css">
 .clearfix:after {  content: "."; display: block; height: 0; clear: both; visibility: hidden;  }  
.clearfix {display: inline-block;}  /* for IE/Mac */  
</style>
<!--[if IE]>
 <style type="text/css">
 .clearfix {zoom: 1;/* triggers hasLayout */  display: block;/* resets display for IE/Win */} </style>
  <![endif]-->

鉴于 IE/Mac的市场占有率极低,我们直接忽略掉,最后精简的代码如下:
.clearfix:after {content:"."; display:block; height:0; visibility:hidden; clear:both; }
.clearfix { *zoom:1; }

或者

通过增加before即可避免浏览器顶部崩溃,是一种推荐大家使用的方法!

补充:

.clearfix:after {content:"."; display:block; height:0; visibility:hidden; clear:both; }
.clearfix { *zoom:1; }

  1. display:block 使生成的元素以块级元素显示,占满剩余空间;

  2. height:0 避免生成内容破坏原有布局的高度。

  3. visibility:hidden 使生成的内容不可见,并允许可能被生成内容盖住的内容可以进行点击和交互;

4)通过 content:".“生成内容作为最后一个元素,至于content里面是点还是其他都是可以的,例如oocss里面就有经典的 content:“XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX”,有些版本可能content 里面内容为空,一丝冰凉是不推荐这样做的,firefox直到7.0 content:”” 仍然会产生额外的空隙;

5)zoom:1 触发IE hasLayout。

通过分析发现,除了clear:both用来闭合浮动的,其他代码无非都是为了隐藏掉content生成的内容,这也就是其他版本的闭合浮动为什么会有font-size:0,line-height:0。

4:display:none、visibility:hidden、opacity:0三者之间的区别

三者共同点都是隐藏。不同点:

一、是否占据空间

display:none,隐藏之后不占位置;visibility:hidden、opacity:0,隐藏后任然占据位置。

二、子元素是否继承

display:none—不会被子元素继承,父元素都不存在了,子元素也不会显示出。

visibility:hidden—会被子元素继承,通过设置子元素visibility:visible来显示子元素。

opacity:0—会被子元素继承,但是不能设置子元素opacity:0来重新显示。

三、事件绑定

display:none 的元素都已经不再页面存在了,因此无法触发它绑定的事件。

visibility:hidden 不会触发它上面绑定的事件。

opacity: 0元素上面绑定的事件是可以触发的。

四、过渡动画

transition对于display是无效的。
transition对于visibility是无效的。
transition对于opacity是有效。

5:css中的可以继承和不可以继承的属性

一、无继承性的属性

1、display:规定元素应该生成的框的类型

2、文本属性:

vertical-align:垂直文本对齐

text-decoration:规定添加到文本的装饰

text-shadow:文本阴影效果

white-space:空白符的处理

unicode-bidi:设置文本的方向

3、盒子模型的属性:width、height、margin 、margin-top、margin-right、margin-bottom

4、背景属性:background、background-color、background-image、background-repeat、background-position、background-attachment

5、定位属性:float、clear、position、top、right、bottom、left、min-width、min-height、max-width、max-height、overflow、clip、z-index

二、有继承性的属性

1、字体系列属性

font:组合字体

2、文本系列属性

text-indent:文本缩进

text-align:文本水平对齐

line-height:行高

color:文本颜色

3、元素可见性:visibility

三、所有元素可以继承的属性

1、元素可见性:visibility

2、光标属性:cursor

四、内联元素可以继承的属性

1、字体系列属性

2、除text-indent、text-align之外的文本系列属性

五、块级元素可以继承的属性

1、text-indent、text-align

6:css中的单行和多行的文本溢出

单行的文本溢出

overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;

多行的:

display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;

7:css垂直居中

单行行内元素 1.可以设置padding-top,padding-bottom 2.将height和line-height设为相等

多行行内元素 1.可以将元素转为table样式,再设置vertical-align:middle; 2.使用flex布局

块级元素

已知高度绝对定位负边距

未知高度transform: translateY(-50%);

flex布局
display: flex;
justify-content: center;
align-items: center;

8:CSS 选择符有哪些?哪些属性可以继承?优先级算法如何计算? CSS3新增伪类有那些?

选择符
<1>、id选择器(#myId);
<2>、类选择器(.myClassName);
<3>、标签选择器(div,p,h1);
<4>、相邻选择器(h1 + p);
<5>、子选择器(ul > li);
<6>、后代选择器(li a);
<7>、通配符选择器(*);
<8>、属性选择器(button[disabled=“true”]);
<9>、伪类选择器(a:hover,li:nth-child);表示一种状态
<10>、伪元素选择器(li:before、:after,:first-letter,:first-line,:selecton);表示文档某个部分的表现

优先级:
!important > 行内样式(比重1000) > id(比重100) > class/属性(比重10) > tag / 伪类(比重1);

伪类和伪元素区别:
1>、伪类:a:hover,li:nth-child;
2>、伪元素:li:before、:after,:first-letter,:first-line,:selecton;

9:flex布局

3.属性总结
flex-container的属性有flex-direction, flex-wrap, justify-content, align-items, align-content

flex-direction(主轴方向):
(1) row(布局为一行,从start开始排)
(2) row-reverse(布局为一行,从end开始排)

(3) column(布局为一列,从start开始排)

(4) column-reverse(布局为一列,从end开始排)

flex-wrap(一条轴线排不下如何换行):
(1) nowarp (不换行,在一行显示)

(2) wrap(内容超过后换行)

(3) warp-reverse(换行后有两条轴线,reverse就是把轴线排列的顺序倒置过来)

justify-content(主轴对齐方式):

  1. flex-start (start侧对齐,左对齐)

  2. flex-end(end侧对齐,右对齐)

  3. center(中心对齐)

  4. space-between(左右两侧没有间距,中间间距相同)

  5. space-around(左右两侧的间距为中间间距的一半)

align-items(交叉轴对齐方式):
1)align-items:stretch; (拉伸)

2)align-items:flex-start(start侧开始,上对齐)

3)align-items:flex-end(end侧开始,下对齐)

4)align-items :center (中心对齐)

5)align-items:baseline(基线对齐)

align-content(多根轴线对齐方式):
1)align-content :stretch (拉伸)

2)align-content :flex-start (start侧开始,上对齐)

3)align-content :flex-end(end侧开始,下对齐)

4)align-content :center (中心对齐)

5)align-content:space-between(上下没有间距,中间各子元素间距相同)

6)align-content:space-around (上下间距之和等于中间各个间距)

flex-item相关属性有order,flex-grow,flex-shrink,lex-basis,align-self

order(排列顺序)

flex-grow(放大比例,剩余空间怎么分配,如下图所示,剩余空间的分配比例是1:2:1)

flex-shrink (缩小比例,超出空间怎么压缩)

flex-basis (item所占主轴空间,优先级高于width)

align-self (对齐方式,覆盖align-items)

利用 flex: 1; 含义是什么?

flex: 1; === flex: 1 1 auto;

这是完整写法, 详见mdn, 它还有另外两种完整写法, 分别是 initial (0 1 auto) 和 none (0 0 auto)

第一个参数表示: flex-grow 定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大
第二个参数表示: flex-shrink 定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小
第三个参数表示: flex-basis给上面两个属性分配多余空间之前, 计算项目是否有多余空间, 默认值为 auto, 即项目本身的大小

10:css中的圣杯布局和双飞翼布局

圣杯布局和双飞翼布局基本上是一致的,都是两边固定宽度,中间自适应的三栏布局,其中,中间栏放到文档流前面,保证先行渲染。解决方案大体相同,都是三栏全部float:left浮动,区别在于解决中间栏div的内容不被遮挡上,圣杯布局是中间栏在添加相对定位,并配合left和right属性,效果上表现为三栏是单独分开的(如果可以看到空隙的话),而双飞翼布局是在中间栏的div中嵌套一个div,内容写在嵌套的div里,然后对嵌套的div设置margin-left和margin-right,效果上表现为左右两栏在中间栏的上面,中间栏还是100%宽度,只不过中间栏的内容通过margin的值显示在中间。

效果简图如下:
在这里插入图片描述
1、圣杯布局

注意middle写在前面就行了,dom结构如下:

DOM:
<body>
<div id="hd">header</div>
<div id="bd">
  <div id="middle">middle</div>
  <div id="left">left</div>
  <div id="right">right</div>
</div>
<div id="footer">footer</div>
</body>

相对应的CSS内容如下:

<style>
#hd{
    height:50px;
    background: #666;
    text-align: center;
}
#bd{
    /*左右栏通过添加负的margin放到正确的位置了,此段代码是为了摆正中间栏的位置*/
    padding:0 200px 0 180px;
    height:100px;
}
#middle{
    float:left;
    width:100%;/*左栏上去到第一行*/
    height:100px;
    background:blue;
}
#left{
    float:left;
    width:180px;
    height:100px;
    margin-left:-100%;
    background:#0c9;
    /*中间栏的位置摆正之后,左栏的位置也相应右移,通过相对定位的left恢复到正确位置*/
    position:relative;
    left:-180px;
}
#right{
    float:left;
    width:200px;
    height:100px;
    margin-left:-200px;
    background:#0c9;
    /*中间栏的位置摆正之后,右栏的位置也相应左移,通过相对定位的right恢复到正确位置*/
    position:relative;
    right:-200px;
}
#footer{
    height:50px;
    background: #666;
    text-align: center;
}
</style>

2、双飞翼布局

DOM代码如下:

<body>
<div id="hd">header</div> 
  <div id="middle">
    <div id="inside">middle</div>
  </div>
  <div id="left">left</div>
  <div id="right">right</div>
  <div id="footer">footer</div>
</body>

双飞翼布局是在middle的div里又插入一个div,通过调整内部div的margin值,实现中间栏自适应,内容写到内部div中。

CSS代码如下:

<style>
#hd{
    height:50px;
    background: #666;
    text-align: center;
}
#middle{
    float:left;
    width:100%;/*左栏上去到第一行*/     
    height:100px;
    background:blue;
}
#left{
    float:left;
    width:180px;
    height:100px;
    margin-left:-100%;
    background:#0c9;
}
#right{
    float:left;
    width:200px;
    height:100px;
    margin-left:-200px;
    background:#0c9;
}

/*给内部div添加margin,把内容放到中间栏,其实整个背景还是100%*/ 
#inside{
    margin:0 200px 0 180px;
    height:100px;
}
#footer{  
   clear:both; /*记得清楚浮动*/  
   height:50px;     
   background: #666;    
   text-align: center; 
} 
</style>

11:CSS布局——多列等高布局

1、利用css中的table属性实现

<div class="table">
    <div class="tableRow">
        <div class="tableCell cell1">
            <div>
            <p>left</p>
            <p>left</p>
            <p>left</p>
        </div>
    </div>

    <div class="tableCell cell2">
        <div>
        <p>center</p>
        <p>center</p>
        <p>center</p>
        <p>center</p>
        </div>
    </div>

    <div class="tableCell cell3">
        <div>
            <p>right</p>
            <p>right</p>
        </div>
    </div>

</div>
</div>

.table {
    width: 500px;
    display: table;
}

.tableRow {
    display: table-row;
}

.tableCell {
    display: table-cell;
}

.cell1 {
    background: #6ee0b6;
}

.cell2 {
    background: #f3777b;
}

.cell3 {
    background: #c3c3ff;
}

效果图如下:
在这里插入图片描述
这种方法可以实现任意多列等高! 兼容IE8+

2、利用 margin 和 padding 对冲实现
这种方法最简单,只需要将padding值设的足够大,然后用相同大的负的margin来对冲。

<div id="wrapper">
    <div class="column left">
        <p>left</p>
        <p>left</p>
    </div>
    <div class="column center">
        <p>center</p>
        <p>center</p>
        <p>center</p>
        <p>center</p>
    </div>
    <div class="column right">
        <p>right</p>
        <p>right</p>
    </div>
</div>

#wrapper {
    overflow: hidden;
}

.column {
    float: left;
    width: 200px;
    margin-bottom: -99999px;
    padding-bottom: 99999px;
}
.left {
    background: #6ee0b6;
}
.center {
    background: #f3777b;
}
.right {
    background: #c3c3ff;
}

效果图如下:
在这里插入图片描述
同样的,这种方法也可以实现任意多列等高! 兼容IE6+

js常考面试题

1:js的基本数据类型

js中有六种数据类型,包括五种基本数据类(Number,String,Boolean,Undefined,Null),和一种复杂数据类型(Object)。

2:js的闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域,将函数内部的变量和方法传递到外部。
通俗的意义上来讲就是当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。

闭包的用途是会让一个定义的数值不会被GC 回收,会一直保存在内存当中
Javascript 中的 GC 机制:在 Javascript 中,如果一个对象不再被引用,那么这个对象就会被 GC 回收,否则这个对象一直会保存在内存中。

先上段代码:


//函数a

function a()

{
    var i=0;
    //函数b
   function b()
    {
        alert(++i);
    }
    return b;
}
  //函数c
  var c = a();
  c();

代码特点:
1、函数b嵌套在函数a内部;
2、函数a返回函数b。

代码中函数a的内部函数b,被函数a外面的一个变量c引用的时候,这就叫创建了一个闭包。有时候函数b也可以用一个匿名函数代替来返回,即return function(){};

优点:1.保护函数内的变量安全,加强了封装性 2.在内存中维持一个变量(用的太多就变成了缺点,占内存)

闭包之所以会占用资源是当函数a执行结束后, 变量i不会因为函数a的结束而销毁, 因为b的执行需要依赖a中的变量。
不适合场景:返回闭包的函数是个非常大的函数

闭包的典型框架应该就是jquery了。
闭包是javascript语言的一大特点,主要应用闭包场合主要是为了:设计私有的方法和变量。

二、闭包的使用场景

1.setTimeout

原生的setTimeout传递的第一个函数不能带参数,通过闭包可以实现传参效果。

function f1(a) {
    function f2() {
        console.log(a);
    }
    return f2;
}
var fun = f1(1);
setTimeout(fun,1000);//一秒之后打印出1

2.封装私有变量

如下面代码:用js创建一个计数器

function f1() {
    var sum = 0;
    var obj = {
       inc:function () {
           sum++;
           return sum;
       }
};
    return obj;
}
let result = f1();
console.log(result.inc());//1
console.log(result.inc());//2
console.log(result.inc());//3

在返回的对象中,实现了一个闭包,该闭包携带了局部变量x,并且,从外部代码根本无法访问到变量x

这在做框架的时候体现更明显,有些方法和属性只是运算逻辑过程中的使用的,不想让外部修改这些属性,因此就可以设计一个闭包来只提供方法获取。

闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。

  1. 逻辑连续,当闭包作为另一个函数调用的参数时,避免你脱离当前逻辑而单独编写额外逻辑。
  2. 方便调用上下文的局部变量。
  3. 加强封装性,第2点的延伸,可以达到对变量的保护作用。

缺点:

闭包有一个非常严重的问题,那就是内存浪费问题,这个内存浪费不仅仅因为它常驻内存,更重要的是,对闭包的使用不当会造成无效内存的产生

在早期的IE浏览器中,像下面这样写代码,会造成内存泄漏,里面的a变量不会被回收掉。现在这个问题已经被修复掉了,所以闭包跟内存泄漏没有什么必然的联系。
当我们关闭一个页面的时候,页面中使用的内存会全部被回收掉,当页面不关闭的时候,有些内存也会被回收掉,是由垃圾回收器来做的。 怎么确定那块内寸是不需要的呢?垃圾回收器会按照固定的时间间隔周期性的执行。

有一种引用计数 方式

工作原理:跟踪记录每个值被引用的次数。

现代浏览器,会把垃圾回收器增强,标记-垃圾回收算法;

如果在跟对象张找不到obj,和obj1,就会回收掉。修复上面的bug。

增强方式–垃圾回收方式是标记清除。

工作原理:是当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。

立即执行函数和闭包的区别
立即执行函数和闭包没有关系,虽然两者会经常结合在一起使用,但两者有本质的不同

立即执行函数只是函数的一种调用方式,只是声明完之后立即执行,这类函数一般都只是调用一次(可用于单例对象上),调用完之后会立即销毁,不会占用内存

闭包则主要是让外部函数可以访问内部函数的作用域,也减少了全局变量的使用,保证了内部变量的安全,但因被引用的内部变量不能被销毁,增大了内存消耗,使用不当易造成内存泄露

3:sessionStorage 、localStorage 和 cookie 之间的区别

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

不同点:
传递方式

1:cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递
2:sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存

数据大小

1:cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下

2: 存储大小限制也不同,cookie数据不能超过4k,同时因为每次http请求都会携带cookie,所以cookie只适合保存很小的数据
3:sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大

数据有效期

1:sessionStorage:仅在浏览器窗口(或者标签页)关闭前有效(即:窗口关闭之前满足同源策略下都有效果)
所以你刷新这个页面前进后退(前进后退得保证同源策略)

2:localStorage:始终有效,窗口或标签页关闭也一直保存(除非主动删除数据),因此用作持久数据
3:cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭

作用域不同

1:sessionStorage每个窗口的值都是独立的(每个窗口都有自己的数据),它的数据会随着窗口的关闭而消失,窗口间的sessionStorage也是不可以共享的(就是,你再打开一个标签页,同样的地址,也不会共享)

2:localStorage 在所有同源页面中都是共享的(前提是相同浏览器,别一个是谷歌浏览器,一个火狐浏览器,然后打开同一个页面说不是共享的哈哈)也就是能跨页不能跨域
3: cookie也是在所有同源窗口中都是共享的

cookie的属性

属性名:name(String)
描述:该Cookie的名称。Cookie一旦创建,名称便不可更改

属性名:value(Object)
描述:该Cookie的值。

属性名:maxAge(int)
描述:该Cookie失效的时间,单位秒。

属性名:secure(boolean)
描述:该Cookie是否仅被使用安全协议传输。安全协议。

属性名:path(String)
描述:该Cookie的使用路径。

属性名:domain(String)
描述:可以访问该Cookie的域名

属性名:comment(String)
描述:该Cookie的用处说明。浏览器显示Cookie信息的时候显示该说明

属性名:version(int)
描述:该Cookie使用的版本号。

4:var和let的区别

ES6 新增了let命令,用来声明局部变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效,而且有暂时性死区的约束。

先看个var的常见变量提升的面试题目:

题目1:


var a = 99;            // 全局变量a
f();                   // f是函数,虽然定义在调用的后面,但是函数声明会提升到作用域的顶部。 
console.log(a);        // a=>99,  此时是全局变量的a
function f() {
  console.log(a);      // 当前的a变量是下面变量a声明提升后,默认值undefined
  var a = 10;
  console.log(a);      // a => 10
}

// 输出结果:
undefined
10
99

ES6可以用let定义块级作用域变量
在ES6之前,我们都是用var来声明变量,而且JS只有函数作用域和全局作用域,没有块级作用域,所以{}限定不了var声明变量的访问范围。
例如:

{ 
  var i = 9;
} 
console.log(i);  // 9

ES6新增的let,可以声明块级作用域的变量。

{ 
  let i = 9;     // i变量只在 花括号内有效!!!
} 
console.log(i);  // Uncaught ReferenceError: i is not defined

let 配合for循环的独特应用
let非常适合用于 for循环内部的块级作用域。JS中的for循环体比较特殊,每次执行都是一个全新的独立的块作用域,用let声明的变量传入到 for循环体的作用域后,不会发生改变,不受外界的影响。看一个常见的面试题目:

for (var i = 0; i <10; i++) {  
  setTimeout(function() {  // 同步注册回调函数到 异步的 宏任务队列。
    console.log(i);        // 执行此代码时,同步代码for循环已经执行完成
  }, 0);
}
// 输出结果
10   共10个

// 这里面的知识点: JS的事件循环机制,setTimeout的机制等
如果把 var改成 let声明:

// i虽然在全局作用域声明,但是在for循环体局部作用域中使用的时候,变量会被固定,不受外界干扰。

for (let i = 0; i < 10; i++) { 
  setTimeout(function() {
    console.log(i);    //  i 是循环体内局部作用域,不受外界影响。
  }, 0);
}
// 输出结果:
0  1  2  3  4  5  6  7  8 9

let没有变量提升与暂时性死区
用let声明的变量,不存在变量提升。而且要求必须 等let声明语句执行完之后,变量才能使用,不然会报Uncaught ReferenceError错误。
例如:

console.log(aicoder);    // 错误:Uncaught ReferenceError ...
let aicoder = 'aicoder.com';
// 这里就可以安全使用aicoder

ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

let变量不能重复声明
let不允许在相同作用域内,重复声明同一个变量。否则报错:Uncaught SyntaxError: Identifier ‘XXX’ has already been declared

例如:

let a = 0;
let a = 'sss';
// Uncaught SyntaxError: Identifier 'a' has already been declared

5:js中的Dom和Bom的区别是啥

BOM是浏览器对象模型,用来获取或设置浏览器的属性、行为,例如:新建窗口、获取屏幕分辨率、浏览器版本号等。
DOM是文档对象模型,用来获取或设置文档中标签的属性,例如获取或者设置input表单的value值。
BOM的内容不多,主要还是DOM。
由于DOM的操作对象是文档(Document),所以dom和浏览器没有直接关系。

7:es6中的const

在面试的时候问到了一个问题,const定义的对象属性是否可以改变。当时没有考虑就回答了不可以,面试官微笑着回答说错了。回来后查看了一下const的定义,明白了其中的原理。

const是用来定义常量的,而且定义的时候必须初始化,且定义后不可以修改。对于基本类型的数据来说,自然很好理解了,例如 const PI = 3.14。如果定义的时候不初始化值的话就会报错,错误内容就是没有初始化。具体的错误信息如下图:
在这里插入图片描述
如果我们修改const定义的常量也是会出现错误的,提示的错误如下图:
在这里插入图片描述
可见,const定义的基本数据类型的变量确实不能修改,那引用数据类型呢?
先看一个演示:
在这里插入图片描述
P对象的name属性确实被修改了,怎么理解这个现象呢?

因为对象是引用类型的,P中保存的仅是对象的指针,这就意味着,const仅保证指针不发生改变,修改对象的属性不会改变对象的指针,所以是被允许的。也就是说const定义的引用类型只要指针不发生改变,其他的不论如何改变都是允许的。

我们试着修改一下指针,让P指向一个新对象,结果如下图:
在这里插入图片描述
即使对象的内容没发生改变,指针改变也是不允许的。

9:如何判断数组和对象

1.typeof操作符
这种方法对于一些常用的类型来说那算是毫无压力,比如Function、String、Number、Undefined等,但是要是检测Array的对象就不起作用了。 利用typeof除了array和null判断为object外,其他的都可以正常判断

alert(typeof null); // "object"
alert(typeof function () {
return 1;
}); // "function"
alert(typeof '梦龙小站'); // "string"
alert(typeof 1); // "number"
alert(typeof a); // "undefined"
alert(typeof undefined); // "undefined"
alert(typeof []); // "object" 

2.instanceof操作符
这个操作符和JavaScript中面向对象有点关系,了解这个就先得了解JavaScript中的面向对象。因为这个操作符是检测对象的原型链是否指向构造函数的prototype对象的。

var arr = [1,2,3,1];
alert(arr instanceof Array); // true 

3.对象的constructor属性
除了instanceof,每个对象还有constructor的属性,利用它似乎也能进行Array的判断。

var arr = [1,2,3,1];
alert(arr.constructor === Array); // true 

第2种和第3种方法貌似无懈可击,但是实际上还是有些漏洞的,当你在多个frame中来回穿梭的时候,这两种方法就亚历山大了。由于每个iframe都有一套自己的执行环境,跨frame实例化的对象彼此是不共享原型链的,因此导致上述检测代码失效

var iframe = document.createElement('iframe'); //创建iframe
document.body.appendChild(iframe); //添加到body中
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // 声明数组[1,2,3]
alert(arr instanceof Array); // false
alert(arr.constructor === Array); // false 

检测数组类型方法
以上那些方法看上去无懈可击,但是终究会有些问题,接下来向大家提供一些比较不错的方法,可以说是无懈可击了。

1.Object.prototype.toString
Object.prototype.toString的行为:首先,取得对象的一个内部属性[[Class]],然后依据这个属性,返回一个类似于"[object Array]"的字符串作为结果(看过ECMA标准的应该都知道,[[]]用来表示语言内部用到的、外部不可直接访问的属性,称为“内部属性”)。利用这 个方法,再配合call,我们可以取得任何对象的内部属性[[Class]],然后把类型检测转化为字符串比较,以达到我们的目的。

function isArrayFn (o) {
return Object.prototype.toString.call(o) === '[object Array]';
}
var arr = [1,2,3,1];
alert(isArrayFn(arr));// true 

call改变toString的this引用为待检测的对象,返回此对象的字符串表示,然后对比此字符串是否是’[object Array]’,以判断其是否是Array的实例。为什么不直接o.toString()?嗯,虽然Array继承自Object,也会有 toString方法,但是这个方法有可能会被改写而达不到我们的要求,而Object.prototype则是老虎的屁股,很少有人敢去碰它的,所以能一定程度保证其“纯洁性”:)

JavaScript 标准文档中定义: [[Class]] 的值只可能是下面字符串中的一个: Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp, String.
这种方法在识别内置对象时往往十分有用,但对于自定义对象请不要使用这种方法。

2.Array.isArray()
ECMAScript5将Array.isArray()正式引入JavaScript,目的就是准确地检测一个值是否为数组。IE9+、 Firefox 4+、Safari 5+、Opera 10.5+和Chrome都实现了这个方法。但是在IE8之前的版本是不支持的。

3.较好参考
综合上面的几种方法,有一个当前的判断数组的最佳写法:

var arr = [1,2,3,1];
var arr2 = [{ abac : 1, abc : 2 }];
function isArrayFn(value){
if (typeof Array.isArray === "function") {
return Array.isArray(value);
}else{
return Object.prototype.toString.call(value) === "[object Array]";
}
}
alert(isArrayFn(arr));// true
alert(isArrayFn(arr2));// true

10:什么是作用域链

每一个函数都有一个作用域,比如我们创建了一个函数,函数里面又包含了一个函数,那么现在 就有三个作用域,这样就形成了一个作用域链。

作用域的特点就是,先在自己的变量范围中查找,如果找不到,就会沿着作用域链往上找。

每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链。作用域链是函数被创建的作用域中对象的集合。作用域链可以保证对执行环境有权访问的所有变量和函数的有序访问
作用域链的最前端始终是当前执行的代码所在环境的变量对象,下一个变量对象来自包含环境,下一个变量对象来自包含环境的包含环境,依次往上,直到全局执行环境的变量对象。全局执行环境的变量对象始终是作用域链中的最后一个对象

11:js实现倒计时的2种方式

这是第一种用setInterval

<html>
 2 <head>
 3 <meta charset="UTF-8">
 4 <title>简单时长倒计时</title>
 5 <SCRIPT type="text/javascript">        
 6             var maxtime = 60 * 60; //一个小时,按秒计算,自己调整!   
 7             function CountDown() {
 8                 if (maxtime >= 0) {
 9                     minutes = Math.floor(maxtime / 60);
10                     seconds = Math.floor(maxtime % 60);
11                     msg = "距离结束还有" + minutes + "分" + seconds + "秒";
12                     document.all["timer"].innerHTML = msg;
13                     if (maxtime == 5 * 60)alert("还剩5分钟");
14                         --maxtime;
15                 } else{
16                     clearInterval(timer);
17                     alert("时间到,结束!");
18                 }
19             }
20             timer = setInterval("CountDown()", 1000);                
21         </SCRIPT>
22 </head>
23 <body>
24 <div id="timer" style="color:red"></div>
25 <div id="warring" style="color:red"></div>
26 </body>
27 </html>

第二种是用的setTimeout递归的方式

<html>  
 2 <head>  
 3     <meta charset="UTF-8">  
 4     <title>js简单时分秒倒计时</title>  
 5     <script type="text/javascript">  
 6         function countTime() {  
 7             //获取当前时间  
 8             var date = new Date();  
 9             var now = date.getTime();  
10             //设置截止时间  
11             var str="2017/5/17 00:00:00";
12             var endDate = new Date(str); 
13             var end = endDate.getTime();  
14             
15             //时间差  
16             var leftTime = end-now; 
17             //定义变量 d,h,m,s保存倒计时的时间  
18             var d,h,m,s;  
19             if (leftTime>=0) {  
20                 d = Math.floor(leftTime/1000/60/60/24);  
21                 h = Math.floor(leftTime/1000/60/60%24);  
22                 m = Math.floor(leftTime/1000/60%60);  
23                 s = Math.floor(leftTime/1000%60);                     
24             }  
25             //将倒计时赋值到div中  
26             document.getElementById("_d").innerHTML = d+"天";  
27             document.getElementById("_h").innerHTML = h+"时";  
28             document.getElementById("_m").innerHTML = m+"分";  
29             document.getElementById("_s").innerHTML = s+"秒";  
30             //递归每秒调用countTime方法,显示动态时间效果  
31             setTimeout(countTime,1000);  
32   
33         }  
34     </script>  
35 </head >  
36 <body onload="countTime()" >  
37     <div>  
38         <span id="_d">00</span>  
39         <span id="_h">00</span>  
40         <span id="_m">00</span>  
41         <span id="_s">00</span>  
42     </div>  
43 </body>  
44 </html>

13:js中的bind和call还有apply的区别

bind 就是用来绑定上下文的,强制将函数的执行环境绑定到目标作用域中去。与 call 和 apply 其实有点类似,但是不同点在于,它不会立即执行,而是返回一个函数。因此我们要想自己实现一个 bind 函数,就必须要返回一个函数,而且这个函数会接收绑定的参数的上下文。

因为bind方法不会立即执行函数,需要返回一个待执行的函数(这里用到闭包,可以返回一个函数)return function(){}

实现一个bind

大部分高级浏览器都实现了内置的Function.prototype.bind,用来指定函数内部的this 指向,即使没有原生的Function.prototype.bind 实现,我们来模拟一个也不是难事,代码如下:

Function.prototype.bind = function( context ){
var self = this; // 保存原函数
return function(){ // 返回一个新的函数
        return self.apply( context, arguments ); // 执行新的函数的时候,会     把之前传入的context
    // 当作新函数体内的this
    }
};
var obj = {
    name: 'sven'
};
var func = function(){
    alert ( this.name ); // 输出:sven
}.bind( obj);
func();

我们通过Function.prototype.bind 来“包装”func 函数,并且传入一个对象context 当作参
数,这个context 对象就是我们想修正的this 对象。

在Function.prototype.bind 的内部实现中,我们先把func 函数的引用保存起来,然后返回一个新的函数。当我们在将来执行func 函数时,实际上先执行的是这个刚刚返回的新函数。在新函数内部,self.apply( context, arguments )这句代码才是执行原来的func 函数,并且指定context对象为func 函数体内的this。

这是一个简化版的Function.prototype.bind 实现,通常我们还会把它实现得稍微复杂一点,
使得可以往func 函数中预先填入一些参数:

Function.prototype.bind = function(){
    var self = this, // 保存原函数
    context = [].shift.call( arguments ), // 需要绑定的this 上下文
    args = [].slice.call( arguments ); // 剩余的参数转成数组
    return function(){ // 返回一个新的函数
        return self.apply( context, [].concat.call( args, [].slice.call(    arguments ) ) );
        // 执行新的函数的时候,会把之前传入的context 当作新函数体内的this
       // 并且组合两次分别传入的参数,作为新函数的参数
    }
};
var obj = {
    name: 'sven'
};
var func = function( a, b, c, d ){
    alert ( this.name ); // 输出:sven
    alert ( [ a, b, c, d ] ) // 输出:[ 1, 2, 3, 4 ]
}.bind( obj, 1, 2 );
func( 3, 4 );

作用域绑定,这里可以使用apply或者call方法来实现
这两个东西的相同点是:
这两个方法的作用是一样的。

都是在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域。
一般来说,this总是指向调用某个方法的对象,但是使用call()和apply()方法时,就会改变this的指向。

不同点是:

接收参数的方式不同。
apply()方法 接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
call()方法 第一个参数和apply()方法的一样,但是传递给函数的参数必须列举出来。

bind改变函数作用域的方式和call和apply的不同点在于,call和apply是改变作用域的同时也会执行函数。而bind改变作用域会生成一个新函数,是否执行可以根据具体需求设置。

14:JSON.stringify和JSON.parse的使用区别

JSON.stringify 将数组转换成 JSON 字符串,然后使用 JSON.parse 将该字符串还原成数组。

15:js中的深浅拷贝

对象的浅拷贝
对象的浅拷贝简单,就是将一个变量赋给另一个变量

var obj1 = {
    name: 'test name',
    age: 18
}

var obj2 = obj1;

上面的例子中 obj2 经过浅拷贝拥有了 obj1 的属性
封装浅拷贝方法

 var easyCopy = function ( extendObj ) {
        var newObj = extendObj.constructor === Array ? [] : {};
        if (typeof extendObj != 'object') return;
        for (var key in extendObj) {
            if (extendObj.hasOwnProperty(key)) {
                newObj[key] = extendObj[key];
            }
        }
        return newObj
    };

    var obj2 = {
        tall: 1.8,
        weight: 75
    }

    var obj1 = easyCopy( obj2 );

    console.log( obj1 );

浅拷贝存在的问题
我们知道引用类型的赋值其实是改变了变量的指向,那么如果在需要拷贝的对象中存在某个属性的值是引用类型,如数组或子对象,那么浅拷贝后的原对象的属性获得的也只是这个指向
所以如果改变被拷贝对象的属性值,那么原对象的相应属性也会跟着改变

var obj2 = {
names: [‘test0’, ‘test1’, ‘test3’]
}

obj1 = easyCopy( obj2 );

console.log( obj1, obj2 );

obj2.names[1] = 'test0';

console.log( obj1, obj2 );

// 打印结果为:obj1.name[1] 的值从原来的 'test1' 变成了 'test0'

日常项目中使用比较多的是浅拷贝,但是如果某些情况下使用了浅拷贝,可能会产生一些极不容易发现的bug,所以这时候就需要用到深拷贝了
对象的深拷贝
深拷贝其实就是将对象中的数组、子对象进行深度递归遍历,直到其不是引用类型位置再进行复制,这样即使改变了其中一个的值,也不会影响到另一个
深拷贝的封装

var deepCopy = function( extendObj ){
        var str, newObj = extendObj.constructor === Array ? [] : {};
        if(typeof extendObj !== 'object'){
            return;
        } else if(window.JSON){
            str = JSON.stringify(extendObj);
            newObj = JSON.parse(str);
        } else {
            for(var key in extendObj){
              if (!extendObj.hasOwnProperty(key)) return;
                newObj[key] = typeof extendObj[key] === 'object' ?
                        cloneObj(extendObj[key]) : extendObj[key];
            }
        }
        return newObj;
    };

    var obj2 = {
        names: ['test0', 'test1', 'test3']
    }

    var obj1 = deepCopy( obj2 );

    console.log( obj1, obj2 );

    obj2.names[1] = 'test0';

    console.log( obj1, obj2 );

深拷贝的缺点
虽然深拷贝能够避免浅拷贝出现的问题,但是却会带来性能上的问题,如果一个对象非常复杂或数据庞大,所消耗的性能将会是很可观的

深浅拷贝的不同解决方法

一、浅拷贝

1、Object.assign()

官方对这个函数的介绍是:Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。实际上就是会把属性中的简单数据类型直接复制,而对于对象属性,只会拷贝地址(指针),上边介绍区别时用的就是这个;

	
var people1 = Object.assign({}, people);

需要注意的是,如果对象没有子对象,Object.assign()实现的就是深拷贝。
2、展开运算符(ES6新增)

var people = {
  name: "小明",
  act: ["吃饭", "睡觉"]
}
var people1 = {...people};
people1.name = "小红";
people1.act[1] = "打游戏";
console.log(people.name);//小明
console.log(people.act);// ["吃饭", "打游戏"]

3、自己写

var people = {
 name: "小明",
 act: ["吃饭", "睡觉"]
}
var people1 = shallowCopy(people);
people1.name = "小红";
people1.act[1] = "打游戏";
console.log(people.name);//小明
console.log(people.act);// ["吃饭", "打游戏"]
function shallowCopy(obj) {
 var res = {};
 for (var index in obj) {
 if (obj.hasOwnProperty(index)) {//不复制原型链上的属性
  res[index] = obj[index];
 }
 }
 return res;
}

二、深拷贝

1、JSON.parse(JSON.stringify(obj))

上边介绍区别时用的就是这个:

var people1 = JSON.parse(JSON.stringify(people));//深拷贝

这个方法比较简便但也存在问题

1、不能复制对象中的函数。

2、会忽略对象中的undefind。

2、lodash函数

官方介绍是:lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库。官网是
www.lodashjs.com/ ,我推荐用其中的_.cloneDeep(value)方法。

ar objects = [{ "a": 1 }, { "b": 2 }];
  
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false

还有自己去写一个递归,但是需要考虑的东西较多,不再赘述,也有用jq的$.extend()方法实现的,但是性能不好,这里提一下。

16:es6中的箭头函数

箭头函数是匿名函数,不能作为构造函数,不能使用new
箭头函数不绑定arguments,取而代之用rest参数…解决
箭头函数不绑定this,会捕获其所在的上下文的this值,作为自己的this值
箭头函数通过 call() 或 apply() 方法调用一个函数时,只传入了一个参数,对 this 并没有影响。
箭头函数没有原型属性
箭头函数不能当做Generator函数,不能使用yield关键字

2、箭头函数中的 this

箭头函数内的this值继承自外围作用域。运行时它会首先到它的父作用域找,如果父作用域还是箭头函数,那么接着向上找,直到找到我们要的this指向。
我们先看一道经典的关于this的面试题:

var name = 'leo';
var teacher = {
    name: "大彬哥",
    showName: function () {
        function showTest() {
            alert(this.name);
        }
        showTest();
    }
};
teacher.showName();//结果是 leo,而我们期待的是大彬哥,这里this指向了window,我们期待指向teacher

大家知道,ES5中的this说好听了叫"灵活",说不好听就是瞎搞,特别容易出问题.而且面试还非常爱考,工作更不用说了,经常给我们开发捣乱,出现不好调试的bug,用E箭头函数解决这个问题就很得心应手了。

var name = 'leo';
var teacher = {
    name: "大彬哥",
    showName: function () {
        let showTest = ()=>alert(this.name);
        showTest();
    }
};
teacher.showName();

箭头函数中的this其实是父级作用域中的this。箭头函数引用了父级的变量词法作用域就是一个变量的作用在定义的时候就已经被定义好,当在本作用域中找不到变量,就会一直向父作用域中查找,直到找到为止。

由于this在箭头函数中已经按照词法作用域绑定了,所以,用call或者apply调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:

var obj = {
    birth: 1996,
    getAge: function (year) {
        var b = this.birth; // 1996
        var fn = (y) => y - this.birth; // this.birth仍是1996
        return fn.call({birth:1990}, year);
    }
};
obj.getAge(2018); // 22 ( 2018 - 1996)

由于this已经在词法层面完成了绑定,通过call或apply方法调用一个函数时,只是传入了参数而已,对this并没有什么影响 。因此,这个设计节省了开发者思考上下文绑定的时间。

3、箭头函数的特性

3.1 箭头函数没有 arguments

箭头函数不仅没有this,常用的arguments也没有。如果你能获取到arguments,那它

一定是来自父作用域的。

function foo() {
  return () => console.log(arguments)
}

foo(1, 2)(3, 4)  // 1,2

上例中如果箭头函数有arguments,就应该输出的是3,4而不是1,2。

箭头函数不绑定arguments,取而代之用rest参数…解决

var foo = (...args) => {
  return args
}

console.log(foo(1,3,56,36,634,6))    // [1, 3, 56, 36, 634, 6]

箭头函数要实现类似纯函数的效果,必须剔除外部状态。我们可以看出,箭头函数除了传入的参数之外,真的在普通函数里常见的this、arguments、caller是统统没有的!

如果你在箭头函数引用了this、arguments或者参数之外的变量,那它们一定不是箭头函数本身包含的,而是从父级作用域继承的。

3.2 箭头函数中不能使用 new

let Person = (name) => {
    this.name = name;
};
let one = new Person("galler");

运行该程序,则出现TypeError: Person is not a constructor

3.3 箭头函数可以与变量解构结合使用。

const full = ({ first, last }) => first + ' ' + last;

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}

 full({first: 1, last: 5}) // '1 5'

3.4 箭头函数没有原型属性

var foo = () => {};
console.log(foo.prototype) //undefined

由此可以看出箭头函数没有原型。

另一个错误是在原型上使用箭头函数,如:

function A() {
  this.foo = 1
}

A.prototype.bar = () => console.log(this.foo)

let a = new A()
a.bar()  //undefined

同样,箭头函数中的this不是指向A,而是根据变量查找规则回溯到了全局作用域。同样,使用普通函数就不存在问题。箭头函数中不可加new,也就是说箭头函数不能当构造函数进行使用。

3.5 箭头函数不能换行

var func = ()
           => 1; // SyntaxError: expected expression, got '=>'

如果开发中确实一行搞不定,逻辑很多,就加{},你就想怎么换行怎么换行了。

var func = ()=>{
    return '来啊!互相伤害啊!'; // 1.加{} 2.加return
}

4、箭头函数使用场景

JavaScript中this的故事已经是非常古老了,每一个函数都有自己的上下文。

以下例子的目的是使用jQuery来展示一个每秒都会更新的时钟:

$('.time').each(function () {
  setInterval(function () {
    $(this).text(Date.now());
  }, 1000);
});

当尝试在setInterval的回调中使用this来引用DOM元素时,很不幸,我们得到的只是一个属于回调函数自身

上下文的this。一个通常的解决办法是定义一个that或者self变量:

$('.time').each(function () {
  var self = this;
  setInterval(function () {
    $(self).text(Date.now());
  }, 1000);
});

但当使用箭头函数时,这个问题就不复存在了。因为它不产生属于它自己上下文的this:

$('.time').each(function () {
  setInterval(() => $(this).text(Date.now()), 1000);
});

箭头函数的另一个用处是简化回调函数。

// 正常函数写法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);

当然也可以在事件监听函数里使用:

document.body.addEventListener('click', event=>console.log(event, this)); 
// EventObject, BodyElement

5、总结
5.1 箭头函数优点

箭头函数是使用=>语法的函数简写形式。这在语法上与 C#、Java 8 、Python( lambda 函数)和 CoffeeScript 的

相关特性非常相似。

非常简洁的语法,使用箭头函数比普通函数少些动词,如:function或return。

() => { ... } // 零个参数用 () 表示。

x => { ... } // 一个参数可以省略 ()。

(x, y) => { ... } // 多参数不能省略 ()。

如果只有一个return,{}可以省略。

更直观的作用域和 this的绑定,它能让我们能很好的处理this的指向问题。箭头函数加上let关键字的使用,将会让我们javascript代码上一个层次。

5.2 箭头函数使用场景

箭头函数适合于无复杂逻辑或者无副作用的纯函数场景下,例如用在map、reduce、filter的回调函数定义

中,另外目前vue、react、node等库,都大量使用箭头函数,直接定义function的情况已经很少了。

各位同学在写新项目的时候,要不断的琢磨箭头函数使用场景、特点,享受使用箭头函数带来的便利,这样才能更快地成长。

17:web前端之跨域的几种方式

1:JSONP

1.JSONP原理
利用<script元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。

2.JSONP和AJAX对比
JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

3.JSONP优缺点
JSONP优点是兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性。

通常为了减轻 web 服务器的负载,我们会把 js、css、img 等静态资源分离到另一台独立域名的服务器上,在 html 页面中再通过相应的标签从不同域名下加载静态资源,这是被浏览器所允许的,借助此原理,我们可以通过动态创建 script,再请求一个带参数的网址实现跨域通信。
(1) 原生实现

<script>
	var script = document.createElement('script'); 
	script.type = 'text/javascript';
	// 传参并指定回调函数 onBack
    script.src = 'http://www.domain.com:8080/login?user=admin&callback=onBack';
    document.head.appendChild(script);
   // 执行回调函数
   function onBack(res){
     alert(JSON.stringify(res));
   };
</script>

服务端返回如下

onBack({"status":true,"user":"admin"});

(2) jquery ajax

$.ajax({
   url:'http://www.domian.com:8080/login',
   type:'get',
   dataType:'jsonp',                  // 请求方式为 jsonp
   jsonpCallback:'onBack',      // 自定义回调函数名
   data:{}
});

(3) vue

this.$http.jsonp('http://www.domian.com:8080/login',{
   params:{},
   jsonp:'onBack'
}).then((res) =>{
   console.log(res);
})

后台 node.js 代码实例:

var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

sever.on('request',function(req.res){
    var params = qs.parse(req.url.split('?')[1]);
    var fn = params.callback;

    res.writeHead(200,{'Content-Type':'text/javascript'});
    res.write(fn+'(''+ JSON.stringify(params)+ ')');

   res.end();
});
server.listen('8080');
console.log('server is running at port 8080 ......');

2:CORS
1.CORS原理
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

在响应头上添加Access-Control-Allow-Origin属性,指定同源策略的地址。同源策略默认地址是网页的本身。只要浏览器检测到响应头带上了CORS,并且允许的源包括了本网站,那么就不会拦截请求响应。

普通跨域请求:只服务端设置 Access-Control-Allow-Origin 即可,前端无需设置。
带 cookie 请求:前后端都需要设置字段,另外需要注意,所带的 cookie 为跨域请求接口所在域的 cookie,而非当前页。
目前,所有浏览器都支持该功能(IE8+:IE8/9 需要使用 XDomainRequest 对象来支持 CORS),CORS 也已经成为主流的跨域解决方案。

需要注意:我们采用CORS(跨域资源共享)来解决跨域请求,这需要前后端的配合来完成。在这一过程中,后端支持了CORS跨域请求后,前端的请求配置可能会调起CORS的preflight请求,也就是我们所说的预检请求

preflight请求,就是在发生cors请求时,浏览器检测到跨域请求,会自动发出一个OPTIONS请求来检测本次请求是否被服务器接受

一个OPTIONS请求一般会携带下面两个与CORS相关的头:

Access-Control-Request-Method : 本次预检请求的请求方法。
Access-Control-Request-Headers:本次请求所携带的自定义首部字段。这些字段是导致产生OPTIONS请求的一个原因。后面会讲到。

这样,服务端收到该预检请求后,会返回与CORS相关的响应头。主要会包括下面几个,但可能还会有其他的有关CORS字段:

Access-Control-Allow-Origin: 服务器允许的跨域请求源
Access-Control-Allow-Methods: 服务器允许的请求方法
Access-Control-Allow-Headers : 服务器允许的自定义的请求首部字段

服务器通过CORS跨域请求后,下面浏览器就会发生正式的数据请求。整个请求过程其实是发生了两次请求:一个预检请求,通过后的实际数据请求。这些都可以在浏览器网络请求中看到。可以参考下图:
在这里插入图片描述
需要注意的是:

1、在上面的两次请求中,预检请求只是一个检查的过程,它不会携带任何请求的参数;预检通过后的请求才会真正的携带请求参数与服务器进行数据通信。

2、若服务器对预检请求没有任何响应,那么浏览器不知道服务器是否支持CORS而不会发送后续的实际请求;或者服务器不支持当前的Origin跨域访问也不会发送后续请求。

发生preflight请求的条件
上面的预检请求并不是CORS请求的必须的请求过程,在一定的条件下并不需要发生预检请求。那么发生预检请求的条件是什么呢?根据HTTP访问控制(CORS)介绍,其实发生预检请求的条件:是否是简单请求。简单请求则直接发送具体的请求而不会产生预检请求。具体来说如下:

满足下面的所有条件就不会产生预检请求,也就是该请求是简单请求:

请求方法是GET、POST、HEAD其中任意一个

必须是下面定义对CORS安全的首部字段集合,不能是集合之外的其他首部字段。
Accept、Accept-Language、Content-Language、Content-Type、DPR、Downlink、Save-Data、Viewport-Width、Width。

Content-Type的值必须是text/plain、multipart/form-data、application/x-www-form-urlencoded中任意一个值

满足上面所有的条件才不会发送预检请求,在实际项目中我们的请求格式可能是application/json格式编码,或者使用自定义请求头都会触发CORS的预检请求。

所以,在项目中是否会触发CORS的预检请求要做到心中有数。

(1) 前端设置

原生 ajax

var xhr = new XMLHttpRequest();   // IE8/9 需用 window.XDomainRequest 兼容
// 前端设置是否带 cookie
xhr.withCredentials = true;

xhr.open('post','http://www.domain2.com:8080/login',true)
xhr.setRequestHeader('Content-Type','application/x-www-from-urlencoded');
xhr.send('user = admin');
xhr.onreadystatechange = function(){
    if(xhr.readyState == 4 && xhr.status == 200){
        alert(xhr.responseText);
    };
};

jQuery ajax

$.ajax({
    .....
     xhrFields:{
         withCredentials: true;    // 前端设置是否带 cookie
     },
     crossDomain: true,     // 会让请求投中包含跨域的额外信息,但不会包含 cookie
})

vue 框架在 vue-resource 封装的 ajax 组件中加入以下代码

Vue.http.options.credentials = true;

(2) 服务端设置

Nodejs 后台设置

var http = require('http'); 
var  server = http.createServer();
var  qs = require('querystring');

server.on('request',function(req,res){
    var postData = '';

    // 数据块接收中
    req.addListener('data',function(chunk){
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end',function(){
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200,{
            'Access-Control-Allow-Credentials':'true',   // 后端允许发送 cookie
            'Access-Control-Allow-Orign':'http://www.domain1.com',  // 允许访问的域
            'Set-Cookie':'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'    // HttpOnly : 脚本无法读取 cookie
        });

       res.write(JSON.stringify(postData));
       res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080....');

3:WebSocket
Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

(1) 前端代码

<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功的处理
socket.on('connect',function(){
    // 监听服务端消息
    socket.on('message',function(msg){
        console.log('data from server: --->' + msg);
    });

    // 监听服务端关闭
    socket.on('disconnect',function(){
        console.log('Server socket has closed');
    });
});

document.getElementsByTagName('input')[0].onblur = function(){
    scoket.send(this.value);
};
</script>

(2) Nodejs scoket 后台

var http = require('http');
var socket = require('socket.io');

// 启动 http 服务
var server = http.createServer(function(req,res){
    res.writeHead(200,{
        'Content-type':'text/html'
    });
    res.end();
}) ;

server.listen('8080');
console.log('Server is running at port 8080....');

// 监听 socket 连接
socket.listen(server).on('connection',function(client){
    // 接收信息
    client.on('message',function(msg){
        client.send('hello:' + msg);
        console.log('data from client: -->' + msg);
    });

    // 断开处理
    client.on('disconnect',function(){
        console.log('Client socket has closed');
    });
})

4:postMessage
如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多的可以进行跨域操作的 window 属性之一,它可用于解决以下方面的问题:
a、页面和其打开的新窗口的数据传递
b、多窗口之间的数据传递
c、页面与嵌套的 iframe 消息的传递
d、上面三个场景的跨域数据传递

用法:postMessage(data,origin) 方法接受两个参数。
data:HTML5 规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用 JSON.stringify() 序列化。
origin:协议 + 主机 + 端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

(1) a.html(www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="dispaly:none"></iframe>
<script>
   var iframe = document.getElementById("iframe");
   iframe.onload = function(){
       var data = {
          name:"aym"
       };
       // 向 domain2 中传送跨域数据
       iframe.contentWindow.postMessage(JSON.stringify(data),'http://www.domain2.com");
   };
   window.addEventListener('message',function(e){
       alert('data from domain2'+e.data)
   },false);
</script>

(2) b.html(www.domain2.com/b.html)

<script>
   window.addEventListener('message',function(e){
       alert('data from domain1' + e.data);
       var data = JSON.parse(e.data);
       if(data){
           data.number = 10;
           // 处理后在发回给 domain1
            window.parent.postMessage(JSON.stringify(data),'http://www.domain1.com');
       };
   },false);
</script>

19:promise的使用方法

Promise是一个构造函数,自己身上有all、reject、resolve这几个眼熟的方法,原型上有then、catch等同样很眼熟的方法。

那就new一个

var p = new Promise(function(resolve, reject){
    //做一些异步操作
    setTimeout(function(){
        console.log('执行完成');
        resolve('随便什么数据');
    }, 2000);
});

Promise的构造函数接收一个参数,是函数,并且传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。其实这里用“成功”和“失败”来描述并不准确,按照标准来讲,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected。不过在我们开始阶段可以先这么理解,后面再细究概念。

在上面的代码中,我们执行了一个异步操作,也就是setTimeout,2秒后,输出“执行完成”,并且调用resolve方法。

运行代码,会在2秒后输出“执行完成”。注意!我只是new了一个对象,并没有调用它,我们传进去的函数就已经执行了,这是需要注意的一个细节。所以我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:

function runAsync(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('执行完成');
            resolve('随便什么数据');
        }, 2000);
    });
    return p;            
}
runAsync()

这时候你应该有两个疑问:1.包装这么一个函数有毛线用?2.resolve(‘随便什么数据’);这是干毛的?

我们继续来讲。在我们包装好的函数最后,会return出Promise对象,也就是说,执行这个函数我们得到了一个Promise对象。还记得Promise对象上有then、catch方法吧?这就是强大之处了,看下面的代码:

runAsync().then(function(data){
    console.log(data);
    //后面可以用传过来的数据做些其他操作
    //......
});

在runAsync()的返回上直接调用then方法,then接收一个参数,是函数,并且会拿到我们在runAsync中调用resolve时传的的参数。运行这段代码,会在2秒后输出“执行完成”,紧接着输出“随便什么数据”。

这时候你应该有所领悟了,原来then里面的函数就跟我们平时的回调函数一个意思,能够在runAsync这个异步任务执行完成之后被执行。这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。

你可能会不屑一顾,那么牛逼轰轰的Promise就这点能耐?我把回调函数封装一下,给runAsync传进去不也一样吗,就像这样:

function runAsync(callback){
    setTimeout(function(){
        console.log('执行完成');
        callback('随便什么数据');
    }, 2000);
}

runAsync(function(data){
    console.log(data);
});

效果也是一样的,还费劲用Promise干嘛。那么问题来了,有多层回调该怎么办?如果callback也是一个异步操作,而且执行完后也需要有相应的回调函数,该怎么办呢?总不能再定义一个callback2,然后给callback传进去吧。而Promise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。

链式操作的用法
所以,从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的:

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return runAsync3();
})
.then(function(data){
    console.log(data);
});

这样能够按顺序,每隔两秒输出每个异步回调中的内容,在runAsync2中传给resolve的数据,能在接下来的then方法中拿到。运行结果如下:

猜猜runAsync1、runAsync2、runAsync3这三个函数都是如何定义的?没错,就是下面这样

function runAsync1(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务1执行完成');
            resolve('随便什么数据1');
        }, 1000);
    });
    return p;            
}
function runAsync2(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务2执行完成');
            resolve('随便什么数据2');
        }, 2000);
    });
    return p;            
}
function runAsync3(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务3执行完成');
            resolve('随便什么数据3');
        }, 2000);
    });
    return p;            
}

在then方法中,你也可以直接return数据而不是Promise对象,在后面的then中就可以接收到数据了,比如我们把上面的代码修改成这样:

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return '直接返回数据';  //这里直接返回数据
})
.then(function(data){
    console.log(data);
});

reject的用法
到这里,你应该对“Promise是什么玩意”有了最基本的了解。那么我们接着来看看ES6的Promise还有哪些功能。我们光用了resolve,还没用reject呢,它是做什么的呢?事实上,我们前面的例子都是只有“执行成功”的回调,还没有“失败”的情况,reject的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调。看下面的代码。

function getNumber(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            var num = Math.ceil(Math.random()*10); //生成1-10的随机数
            if(num<=5){
                resolve(num);
            }
            else{
                reject('数字太大了');
            }
        }, 2000);
    });
    return p;            
}

getNumber()
.then(
    function(data){
        console.log('resolved');
        console.log(data);
    }, 
    function(reason, data){
        console.log('rejected');
        console.log(reason);
    }
);

getNumber函数用来异步获取一个数字,2秒后执行完成,如果数字小于等于5,我们认为是“成功”了,调用resolve修改Promise的状态。否则我们认为是“失败”了,调用reject并传递一个参数,作为失败的原因。

运行getNumber并且在then中传了两个参数,then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据。多次运行这段代码,你会随机得到下面两种结果:
或者
catch的用法
我们知道Promise对象除了then方法,还有一个catch方法,它是做什么用的呢?其实它和then的第二个参数一样,用来指定reject的回调,用法是这样:

getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:

getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
    console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

在resolve的回调中,我们console.log(somedata);而somedata这个变量是没有被定义的。如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果:

也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能。
all的用法
Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。我们仍旧使用上面定义好的runAsync1、runAsync2、runAsync3这三个函数,看下面的例子:

Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    console.log(results);
});

用Promise.all来执行,all接收一个数组参数,里面的值最终都算返回Promise对象。这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。那么,三个异步操作返回的数据哪里去了呢?都在then里面呢,all会把所有异步操作的结果放进一个数组中传给then,就是上面的results。所以上面代码的输出结果就是:

有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,是不是很酷?有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。

race的用法
all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」,这就是race方法,这个词本来就是赛跑的意思。race的用法与all一样,我们把上面runAsync1的延时改为1秒来看一下:

Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    console.log(results);
});

这三个异步操作同样是并行执行的。结果你应该可以猜到,1秒后runAsync1已经执行完了,此时then里面的就执行了。结果是这样的:

你猜对了吗?不完全,是吧。在then里面的回调开始执行时,runAsync2()和runAsync3()并没有停止,仍旧再执行。于是再过1秒后,输出了他们结束的标志。

这个race有什么用呢?使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:

//请求某个图片资源
function requestImg(){
    var p = new Promise(function(resolve, reject){
        var img = new Image();
        img.onload = function(){
            resolve(img);
        }
        img.src = 'xxxxxx';
    });
    return p;
}

//延时函数,用于给请求计时
function timeout(){
    var p = new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('图片请求超时');
        }, 5000);
    });
    return p;
}

Promise
.race([requestImg(), timeout()])
.then(function(results){
    console.log(results);
})
.catch(function(reason){
    console.log(reason);
});

requestImg函数会异步请求一张图片,我把地址写为"xxxxxx",所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout就跑赢了,则进入catch,报出“图片请求超时”的信息。

20:js的宏任务和微任务的区别包括event loop的事件循环

Event Loop 是什么
JavaScript的事件分两种,宏任务(macro-task)和微任务(micro-task)

宏任务:包括整体代码script,setTimeout,setInterval
微任务:Promise.then(非new Promise),process.nextTick(node中)

事件的执行顺序,是先执行宏任务,然后执行微任务,这个是基础,任务可以有同步任务和异步任务,同步的进入主线程,异步的进入Event Table并注册函数,异步事件完成后,会将回调函数放入Event Queue中(宏任务和微任务是不同的Event Queue),同步任务执行完成后,会从Event Queue中读取事件放入主线程执行,回调函数中可能还会包含不同的任务,因此会循环执行上述操作。

setTimeout(() => {
    console.log('延时1秒');
},1000)
console.log("开始")
输出:
开始
延时1秒

上述代码,setTimeout函数是宏任务,且是异步任务,因此会将函数放入Event Table并注册函数,经过指定时间后,把要执行的任务加入到Event Queue中,等待同步任务console.log(“开始”)执行结束后,读取Event Queue中setTimeout的回调函数执行。

上述代码不包含微任务,接下来看包含微任务的代码:

setTimeout(function() {
    console.log('setTimeout');
},1000)

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');

首先setTimeout,放入Event Table中,1秒后将回调函数放入宏任务的Event Queue中
new Promise 同步代码,立即执行console.log(‘promise’),然后看到微任务then,因此将其放入微任务的Event Queue中
接下来执行同步代码console.log(‘console’)
主线程的宏任务,已经执行完毕,接下来要执行微任务,因此会执行Promise.then,到此,第一轮事件循环执行完毕
第二轮事件循环开始,先执行宏任务,即setTimeout的回调函数,然后查找是否有微任务,没有,时间循环结束

到此做个总结,事件循环,先执行宏任务,其中同步任务立即执行,异步任务,加载到对应的的Event Queue中(setTimeout等加入宏任务的Event Queue,Promise.then加入微任务的Event Queue),所有同步宏任务执行完毕后,如果发现微任务的Event Queue中有未执行的任务,会先执行其中的任务,这样算是完成了一次事件循环。接下来查看宏任务的Event Queue中是否有未执行的任务,有的话,就开始第二轮事件循环,依此类推。

上述例子只是简单的一层嵌套,接下来看一个稍微复杂了一点点的例子:

console.log('1');
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
输出:
1
2
4
3
5

1:宏任务同步代码console.log(‘1’),不多说
2:setTimeout,加入宏任务Event Queue,没有发现微任务,第一轮事件循环走完
3:第二轮事件循环开始,先执行宏任务,从宏任务Event Queue中独取出setTimeout的回调函数
4:同步代码console.log(‘2’),发现process.nextTick,加入微任务Event Queue
5:new Promise,同步执行console.log(‘4’),发现then,加入微任务Event Queue
6:宏任务执行完毕,接下来执行微任务,先执行process.nextTick,然后执行Promise.then
7:微任务执行完毕,第二轮事件循环走完,没有发现宏任务,事件循环结束

22:cookie和session和token(包括单点登录)的解释

Cookie和Session的区别:

1、cookie数据存放在客户的浏览器上,session数据放在服务器上。

2、cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。

3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。

4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。

5、所以个人建议:

将登陆信息等重要信息存放为session

其他信息如果需要保留,可以放在cookie中

综上所述:

1、Cookie:保存在客户端,不是很安全;
2、Session:保存在服务器端,并生成一个Session id保存在客户端。访问过多时会占用服务器的内存和性能;
3、Token:首次登陆后,服务器生成Token值,保存在数据库中,再将这个Token值返回给客户端。增加数据库的存储和查询压力;
4、JWT(Json Web Token):前后端分离项目中使用。根据算法生成,保存在本地。消耗服务器的计算压力。

23:浏览器缓存有哪些,通常缓存有哪几种方式

强缓存 强缓存如果命中,浏览器直接从自己的缓存中读取资源,不会发请求到服务器。

强缓存的特定是不需要询问服务器,它通过expires和cache-control来实现。cache-control的优先级高于expires,它们都用来表示过期时间,expires是存储时间戳,cache-control使用max-age来表示相对时间。
cache-control 的no-cache不询问浏览器,直接请求服务器(进行协商缓存)。
而no-store则不是不使用任何缓存策略。
s-maxage只在代理服务器中生效。
如果资源没有过期,则会直接使用该资源。

协商缓存 当强缓存没有命中的时候,浏览器一定会发送一个请求到服务器,通过服务器端依据资源的另外一些http header验证这个资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回(304),若未命中请求,则将资源返回客户端,并更新本地缓存数据(200)。

HTTP头信息控制缓存

Expires(强缓存)+过期时间 Expires是HTTP1.0提出的一个表示资源过期时间的header,它描述的是一个绝对时间

Cache-control(强缓存) 描述的是一个相对时间,在进行缓存命中的时候,都是利用客户端时间进行判断 管理更有效,安全一些 Cache-Control: max-age=3600

服务端返回头Last-Modified/ 客户端请求头If-Modified-Since(协商缓存) 标示这个响应资源的最后修改时间。Last-Modified是服务器相应给客户端的,If-Modified-Sinces是客户端发给服务器,服务器判断这个缓存时间是否是最新的,是的话拿缓存。

服务端返回头Etag/客户端请求头If-None-Match(协商缓存) etag和last-modified类似,他是发送一个字符串来标识版本。

强缓存不请求服务器,客户端判断 、协商缓存要请求服务器

Http Header Last-Modified与ETag的区别

Etag是在HTTP 1.1中引入的,为了解决一些Last-Modified无法解决的问题,比如:

1.网站中的某些文件会定期的更新,但是文件内容并为改变;

2.文件更新非常频繁,Last-Modified精确到秒不能满足需求;

3.部分服务器不支持精确时间;

24:XSS和CSRF攻击

XSS
首先说下最常见的 XSS 漏洞,XSS (Cross Site Script),跨站脚本攻击,因为缩写和 CSS (Cascading Style Sheets) 重叠,所以只能叫 XSS。
XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的。XSS 的攻击方式千变万化,但还是可以大致细分为几种类型。

非持久型 XSS
非持久型 XSS 漏洞,也叫反射型 XSS 漏洞,一般是通过给别人发送带有恶意脚本代码参数的 URL,当 URL 地址被打开时,特有的恶意代码参数被 HTML 解析、执行。
在这里插入图片描述
一个例子,比如你的 Web 页面中包含有以下代码:

<select>
    <script>
        document.write(''
            + '<option value=1>'
            +     location.href.substring(location.href.indexOf('default=') + 8)
            + '</option>'
        );
        document.write('<option value=2>English</option>');
    </script>
</select>

攻击者可以直接通过 URL (类似:https://xx.com/xx?default=) 注入可执行的脚本代码

非持久型 XSS 漏洞攻击有以下几点特征 :
1 . 即时性,不经过服务器存储,直接通过 HTTP 的 GET 和 POST 请求就能完成一次攻击,拿到用户隐私数据
2 . 攻击者需要诱骗点击
3 . 反馈率低,所以较难发现和响应修复
4 . 盗取用户敏感保密信息

为了防止出现非持久型 XSS 漏洞,需要确保这么几件事情 :
1 . Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。
2 . 尽量不要从 URL,document.referrer,document.forms 等这种 DOM API 中获取数据直接渲染。
3 . 尽量不要使用 eval, new Function(),document.write(),document.writeln(),window.setInterval(),window.setTimeout(),
innerHTML,document.creteElement() 等可执行字符串的方法。
4 . 如果做不到以上几点,也必须对涉及 DOM 渲染的方法传入的字符串参数做 escape 转义。
5 . 前端渲染的时候对任何的字段都需要做 escape 转义编码。

escape 转义的目的是将一些构成 HTML 标签的元素转义,比如 <,>,空格 等,转义成 <,>, 等显示转义字符。有很多开源的工具可以协助我们做 escape 转义。

持久型 XSS
持久型 XSS 漏洞,也被称为存储型 XSS 漏洞,一般存在于 Form 表单提交等交互功能,如发帖留言,提交文本信息等,黑客利用的 XSS 漏洞,将内容经正常功能提交进入数据库持久保存,当前端页面获得后端从数据库中读出的注入代码时,恰好将其渲染执行。
主要注入页面方式和非持久型 XSS 漏洞类似,只不过持久型的不是来源于 URL,refferer,forms 等,而是来源于后端从数据库中读出来的数据。持久型 XSS 攻击不需要诱骗点击,黑客只需要在提交表单的地方完成注入即可,但是这种 XSS 攻击的成本相对还是很高。

攻击成功需要同时满足以下几个条件 :
1 . POST 请求提交表单后端没做转义直接入库。
2 . 后端从数据库中取出数据没做转义直接输出给前端。
3 . 前端拿到后端数据没做转义直接渲染成 DOM。

持久型 XSS 有以下几个特点 :
1 . 持久性,植入在数据库中
2 . 危害面广,甚至可以让用户机器变成 DDoS 攻击的肉鸡。
3 . 盗取用户敏感私密信息

为了防止持久型 XSS 漏洞,需要前后端共同努力 :
1 . 后端在入库前应该选择不相信任何前端数据,将所有的字段统一进行转义处理。
2 . 后端在输出给前端数据统一进行转义处理。
3 . 前端在渲染页面 DOM 的时候应该选择不相信任何后端数据,任何字段都需要做转义处理。

基于字符集的 XSS
其实现在很多的浏览器以及各种开源的库都专门针对了 XSS 进行转义处理,尽量默认抵御绝大多数 XSS 攻击,但是还是有很多方式可以绕过转义规则,让人防不胜防。比如「基于字符集的 XSS 攻击」就是绕过这些转义处理的一种攻击方式,比如有些 Web 页面字符集不固定,用户输入非期望字符集的字符,有时会绕过转义过滤规则。

以基于 utf-7 的 XSS 为例
utf-7 是可以将所有的 unicode 通过 7bit 来表示的一种字符集 (但现在已经从 Unicode 规格中移除)。
这个字符集为了通过 7bit 来表示所有的文字, 除去数字和一部分的符号,其它的部分将都以 base64 编码为基础的方式呈现。

<script>alert("xss")</script>
可以被解释为:
+ADw-script+AD4-alert(+ACI-xss+ACI-)+ADw-/script+AD4-

可以形成「基于字符集的 XSS 攻击」的原因是由于浏览器在 meta 没有指定 charset 的时候有自动识别编码的机制,所以这类攻击通常就是发生在没有指定或者没来得及指定 meta 标签的 charset 的情况下。

所以我们有什么办法避免这种 XSS 呢 ?
1 . 记住指定
2 . XML 中不仅要指定字符集为 utf-8,而且标签要闭合
3 . 牛文推荐:http://drops.wooyun.org/papers/1327 (这个讲的很详细)

基于 Flash 的跨站 XSS
基于 Flash 的跨站 XSS 也是属于反射型 XSS 的一种,虽然现在开发 ActionScript 的产品线几乎没有了,但还是提一句吧,AS 脚本可以接受用户输入并操作 cookie,攻击者可以配合其他 XSS(持久型或者非持久型)方法将恶意 swf 文件嵌入页面中。主要是因为 AS 有时候需要和 JS 传参交互,攻击者会通过恶意的 XSS 注入篡改参数,窃取并操作cookie。

避免方法 :
1 . 严格管理 cookie 的读写权限
2 . 对 Flash 能接受用户输入的参数进行过滤 escape 转义处理

未经验证的跳转 XSS
有一些场景是后端需要对一个传进来的待跳转的 URL 参数进行一个 302 跳转,可能其中会带有一些用户的敏感(cookie)信息。如果服务器端做302 跳转,跳转的地址来自用户的输入,攻击者可以输入一个恶意的跳转地址来执行脚本。

这时候需要通过以下方式来防止这类漏洞 :
1 . 对待跳转的 URL 参数做白名单或者某种规则过滤
2 . 后端注意对敏感信息的保护, 比如 cookie 使用来源验证。

CSRF
CSRF(Cross-Site Request Forgery),中文名称:跨站请求伪造攻击
那么 CSRF 到底能够干嘛呢?你可以这样简单的理解:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过 QQ 等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使 Web 应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个 QQ 好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。

   所以遇到 CSRF 攻击时,将对终端用户的数据和操作指令构成严重的威胁。当受攻击的终端用户具有管理员帐户的时候,CSRF 攻击将危及整个 Web 应用程序。

CSRF 原理
下图大概描述了 CSRF 攻击的原理,可以理解为有一个小偷在你配钥匙的地方得到了你家的钥匙,然后拿着要是去你家想偷什么偷什么。
在这里插入图片描述
CSRF 攻击必须要有三个条件 :
1 . 用户已经登录了站点 A,并在本地记录了 cookie
2 . 在用户没有登出站点 A 的情况下(也就是 cookie 生效的情况下),访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点A)。
3 . 站点 A 没有做任何 CSRF 防御
你也许会问:「如果我不满足以上三个条件中的任意一个,就不会受到 CSRF 的攻击」。其实可以这么说的,但你不能保证以下情况不会发生 :
1 . 你不能保证你登录了一个网站后,不再打开一个 tab 页面并访问另外的网站,特别现在浏览器都是支持多 tab 的。
2 . 你不能保证你关闭浏览器了后,你本地的 cookie 立刻过期,你上次的会话已经结束。
3 . 上图中所谓的攻击网站 B,可能是一个存在其他漏洞的可信任的经常被人访问的网站。

预防 CSRF
CSRF 的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的 CSRF 防御也都在服务端进行。服务端的预防 CSRF 攻击的方式方法有多种,但思路上都是差不多的,主要从以下两个方面入手 :
1 . 正确使用 GET,POST 请求和 cookie
2 . 在非 GET 请求中增加 token

一般而言,普通的 Web 应用都是以 GET、POST 请求为主,还有一种请求是 cookie 方式。我们一般都是按照如下规则设计应用的请求:
1 . GET 请求常用在查看,列举,展示等不需要改变资源属性的时候(数据库 query 查询的时候)
2 . POST 请求常用在 From 表单提交,改变一个资源的属性或者做其他一些事情的时候(数据库有 insert、update、delete 的时候)

当正确的使用了 GET 和 POST 请求之后,剩下的就是在非 GET 方式的请求中增加随机数,这个大概有三种方式来进行:
1 . 为每个用户生成一个唯一的 cookie token,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者不能获得第三方的 cookie(理论上),所以表单中的数据也就构造失败,但是由于用户的 cookie 很容易由于网站的 XSS 漏洞而被盗取,所以这个方案必须要在没有 XSS 的情况下才安全。
2 . 每个 POST 请求使用验证码,这个方案算是比较完美的,但是需要用户多次输入验证码,用户体验比较差,所以不适合在业务中大量运用。
3 . 渲染表单的时候,为每一个表单包含一个 csrfToken,提交表单的时候,带上 csrfToken,然后在后端做 csrfToken 验证。

CSRF 的防御可以根据应用场景的不同自行选择。CSRF 的防御工作确实会在正常业务逻辑的基础上带来很多额外的开发量,但是这种工作量是值得的,毕竟用户隐私以及财产安全是产品最基础的根本。

SQL 注入
SQL 注入漏洞(SQL Injection)是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

而造成 SQL 注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

很多 Web 开发者没有意识到 SQL 查询是可以被篡改的,从而把 SQL 查询当作可信任的命令。殊不知,SQL 查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过 SQL 查询去运行主机系统级的命令。

25:JS中的事件冒泡和事件捕获

事件捕获阶段:事件从最上一级标签开始往下查找,直到捕获到事件目标(target)。

事件冒泡阶段:事件从事件目标(target)开始,往上冒泡直到页面的最上一级标签。

用图示表示如下:
在这里插入图片描述
1、冒泡事件:

事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发。通俗来讲就是,就是当设定了多个div的嵌套时;即建立了父子关系,当父div与子div共同加入了onclick事件时,当触发了子div的onclick事件后,子div进行相应的js操作,但是父div的onclick事件同样会被触发。

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试事件冒泡</title>
    <style>
        div{padding:40px;}
        #div1{background: #00B83F;}
        #div2{background: #2a6496}
        #div3{background: #93C3CF}
    </style>
    <script>
    window.onload=function (){
        var odiv1=document.getElementById("div1");
        var odiv2=document.getElementById("div2");
        var odiv3=document.getElementById("div3");

        function fdiv1(){
            alert("div1");
        }
        function fdiv2(){
            alert("div2");
        }
        function fdiv3(ev){ 
            alert("div3");
        }
        odiv1.onclick=fdiv1;
        odiv2.onclick=fdiv2;
        odiv3.onclick=fdiv3;
    }

    </script>

</head>
<body>
  <div id="div1">
      <div id="div2">
          <div id="div3"></div>
      </div>
  </div>
</body>
</html> 

在这里插入图片描述

测试结果:点击div3时,依次弹出div3,div2,div1

2.阻止事件冒泡

给div3的绑定事件改为。ev.canceBubble=true;

  function fdiv3(ev){
            var en=ev || event;
            en.cancelBubble=true;
            alert("div3");
        }

测试结果:点击div3时,只弹出div3

3、事件捕获:

从顶层元素到目标元素或者从目标元素到顶层元素,和事件冒泡是一个相反的过程。事件从最不精确的对象(document 对象)开始触发,然后到最精确(也可以在窗口级别捕获事件,不过必须由开发人员特别指定)。

代码更改如下:

<script>
    window.onload=function (){
        var odiv1=document.getElementById("div1");
        var odiv2=document.getElementById("div2");
        var odiv3=document.getElementById("div3");
 
        odiv1.addEventListener("click",function(){
            alert("div1");
        },true);
        odiv2.addEventListener("click",function(){
            alert("div2");
        },true);
        odiv3.addEventListener("click",function(){
            alert("div3");
        },true);
    }
</script>

测试结果:点击div3时,依次弹出div1,div2,div3

结论:绑定事件时通过addEventListener函数,它有三个参数,第三个参数若是true,则表示采用事件捕获,若是false,则表示采用事件冒泡

事件委托

var oUl = document.getElementById("ul1");
  oUl.onclick = function(ev){
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if(target.nodeName.toLowerCase() == 'li'){
            alert(123);
         alert(target.innerHTML);
    }
  }

但是如果你的li标签里嵌套了其他标签,比如:

<ul id="ul1">
    <li><span>111</span></li>
</ul>

那么当你点击“111”的时候上面的js写法就是无效的,因为你直接点击的并不是li标签。所以在这里我们还需要判断点击的标签的父元素是不是li,所以这里的代码中要加一个while循环:

var oUl = document.getElementById("ul1");

oUl.onclick = function(e){
    var e = e || window.event; 
    var target = e.target || e.srcElement;
    while(target.tagName !== 'LI'){
      target = target.parentNode;
      if(target === oUl){
        target = null;
        break;
      }
    }
    if(target){
      console.log('你点击了ul中的li');
    }else{
      console.log('你点击的不是ul中的li');
    }
}

25:函数的节流和防抖

防抖(debounce)
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

防抖函数分为非立即执行版和立即执行版。

非立即执行版:

function debounce(func, wait) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);
        
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait);
    }
}

非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

我们依旧使用上述绑定 mousemove 事件的例子,通过上面的防抖函数,我们可以这么使用

content.onmousemove = debounce(count,1000);

上述防抖函数的代码还需要注意的是 this 和 参数的传递

let context = this;
let args = arguments;

防抖函数的代码使用这两行代码来获取 this 和 参数,是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接受到 e 参数。

立即执行版:

function debounce(func,wait) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);

        let callNow = !timeout;
        timeout = setTimeout(() => {
            timeout = null;
        }, wait)

        if (callNow) func.apply(context, args)
    }
}

立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果

在开发过程中,我们需要根据不同的场景来决定我们需要使用哪一个版本的防抖函数,一般来讲上述的防抖函数都能满足大部分的场景需求。但我们也可以将非立即执行版和立即执行版的防抖函数结合起来,实现最终的双剑合璧版的防抖函数。
双剑合璧版:

/**
 * @desc 函数防抖
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param immediate true 表立即执行,false 表非立即执行
 */
function debounce(func,wait,immediate) {
    let timeout;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(() => {
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳版:

function throttle(func, wait) {
    let previous = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

使用方式如下

content.onmousemove = throttle(count,1000);

定时器版:

function throttle(func, wait) {
    let timeout;
    return function() {
        let context = this;
        let args = arguments;
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null;
                func.apply(context, args)
            }, wait)
        }

    }
}

我们应该可以很容易的发现,其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候。

同样地,我们也可以将时间戳版和定时器版的节流函数结合起来,实现双剑合璧版的节流函数。

双剑合璧版:

/**
 * @desc 函数节流
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param type 1 表时间戳版,2 表定时器版
 */
function throttle(func, wait ,type) {
    if(type===1){
        var previous = 0;
    }else if(type===2){
        var  timeout;
    }
    return function() {
        let context = this;
        let args = arguments;
        if(type===1){
            let now = Date.now();

            if (now - previous > wait) {
                func.apply(context, args);
                previous = now;
            }
        }else if(type===2){
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(context, args)
                }, wait)
            }
        }
    }
}

26:ES6中map和set用法

一、map
Map是一组键值对的结构,具有极快的查找速度。

举个例子,假设要根据同学的名字查找对应的成绩,如果用Array实现,需要两个Array:

var names = ['Michael', 'Bob', 'Tracy'];
var scores = [95, 75, 85];

给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,Array越长,耗时越长。

或者通过object键值对的方式来实现存储和查找:

var names = Object.create({'Michael':95,'Bob':75,'Tracy':85});

但是如果这个表十分庞大的时候,查询速度也是很感人的。

所以ES6提供了map方法来提高查询的速度,如果用Map实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。如果用Map实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。

var m=new Map();
m.set('Adam', 67); // 添加新的key-value
m.set('Bob', 59);
console.log(m);//Map { 'Adam' => 67, 'Bob' => 59 }
console.log(m.has('Adam')); // 是否存在key 'Adam': true
console.log(m.get('Adam')); // 67
console.log(m.delete('Adam')); // 删除key 'Adam':true
console.log(m.get('Adam')); // undefined

ES6中Map相对于Object对象有几个区别
1:Object对象有原型, 也就是说他有默认的key值在对象上面, 除非我们使用Object.create(null)创建一个没有原型的对象;

2:在Object对象中, 只能把String和Symbol作为key值, 但是在Map中,key值可以是任何基本类型(String, Number, Boolean, undefined, NaN….),或者对象(Map, Set, Object, Function , Symbol , null….);

3:通过Map中的size属性, 可以很方便地获取到Map长度, 要获取Object的长度, 你只能用别的方法了;
  Map实例对象的key值可以为一个数组或者一个对象,或者一个函数,比较随意 ,而且Map对象实例中数据的排序是根据用户push的顺序进行排序的, 而Object实例中key,value的顺序就是有些规律了, (他们会先排数字开头的key值,然后才是字符串开头的key值);

二、set
Set和Map类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。

要创建一个Set,需要提供一个Array作为输入:

var arr=[1,2,3,3,'3'];
var s1=new Set(arr);
console.log(s1);//Set { 1, 2, 3, '3' }
s1.add(4);//重复添加无用
console.log(s1);//Set { 1, 2, 3, '3', 4 }
s1.delete(4);
console.log(s1);//Set { 1, 2, 3, '3' }

感觉set的用法可以有两个地方
因为key不能重复,所以可以用来去掉array中的重复元素。

'use strict';
// var set = new Set([1,2,1,2,2,1]);
var arr = [1,2,1,2,2,1];
//new Set 数组去重
function unique(arr){
return Array.from(new Set(arr));
};
//使用ES6的方法可以去重.
console.log(unique(arr));

可以用来统计键(这个暂时还没想到应用场景)

Map和Set是ES6标准新增的数据类型,请根据浏览器的支持情况决定是否要使用。

26:深入理解async和await

学完了Promise,我们知道可以用then链来解决多层回调问题,但是这还不是最理想的操作,我们需要调用很多个then链才能达到要求,那么有没有一种更简便代码量更少的方式达到then链相同的结果呢?asynv和await就很好地解决了这个问题,首先用async声明一个异步函数,然后再用await等待异步结果,把以前then链的结果放到直接放在await,非常方便。

那么,async和await原理是什么呢?为什么可以用这样的语法来优化then链呢?

1. async/await是什么?
async/await其实是Promise的语法糖,它能实现的效果都能用then链来实现,这也和我们之前提到的一样,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await译为等待,所以我们很好理解async声明function是异步的,await等待某个操作完成。当然语法上强制规定await只能出现在asnyc函数中,我们先来看看async函数返回了什么:

async function testAsy(){
   return 'hello world';
}
let result = testAsy(); 
console.log(result)

这个async声明的异步函数把return后面直接量通过Promise.resolve()返回Promise对象,所以如果这个最外层没有用await调用的话,是可以用原来then链的方式来调用的:

async function testAsy(){
   return 'hello world'
}
let result = testAsy() 
console.log(result)
result.then(v=>{
    console.log(v)   //hello world
})

联想一下Promise特点——异步无等待,所以当没有await语句执行async函数,它就会立即执行,返回一个Promise对象,非阻塞,与普通的Promise对象函数一致。

重点就在await,它等待什么呢?

按照语法说明,await等待的是一个Promise对象,或者是其他值(也就是说可以等待任何值),如果等待的是Promise对象,则返回Promise的处理结果;如果是其他值,则返回该值本身。并且await会暂停当前async function的执行,等待Promise的处理完成。若Promise正常处理(fulfillded),其将回调的resolve函数参数作为await表达式的值,继续执行async function;若Promise处理异常(rejected),await表达式会把Promise异常原因抛出;另外如果await操作符后面的表达式不是一个Promise对象,则返回该值本身。

2. 深入理解async/await
我们来详细说明一下async/await的作用。await操作符后面可以是任意值,当是Promise对象的时候,会暂停async function执行。也就是说,必须得等待await后面的Promise处理完成才能继续:

 function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒钟之后出现hello world
}
testAwt();

await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

我们再把上面的代码修改一下,好好体会“阻塞”这个词

 function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒钟之后出现hello world
  console.log('tangj')   // 3秒钟之后出现tangj
}
testAwt();
console.log('tangSir')  //立即输出tangSir

这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以’tangSir’'最先输出,hello world’和‘tangj’是3秒钟后同时出现的。

3. async和await简单应用
上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

现在举例,用 setTimeout模拟耗时的异步操作,先来看看不用 async/await 会怎么写

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

takeLongTime().then(v => {
    console.log("got", v); //一秒钟后输出got long_time_value
});

如果改用 async/await 呢,会是这样

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);  // 一秒钟后输出long_time_value
}

test();

tankLongTime()本身就是返回的 Promise 对象,所以加不加 async结果都一样。

4. 处理then链
前面我们说了,async和await是处理then链的语法糖,现在我们来看看具体是怎么实现的:

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用setTimeout来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理

function doIt(){
    console.time('doIt');
    let time1 = 300;
    step1(time1)
        .then((time2) => step2(time2))
        .then((time3) => step3(time3))  
        .then((result) => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        })
}

doIt();

//执行结果为:
//step1 with 300
//step2 with 500
//step3 with 700
//result is 900
//doIt: 1510.2490234375ms

输出结果 result 是 step3() 的参数 700 + 200 = 900。doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样:

async function doIt() {
    console.time('doIt');
    let time1 = 300;
    let time2 = await step1(time1);//将Promise对象resolve(n+200)的值赋给time2
    let time3 = await step1(time2);
    let result = await step1(time3);
    console.log(`result is ${result}`);
    console.timeEnd('doIt');
}

doIt();

//执行结果为:
//step1 with 300
//step2 with 500
//step3 with 700
//result is 900
//doIt: 1512.904296875ms

显然我们用async/await简单多了。

5. Promise处理结果为rejected
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。

async function myFunction() {
    try {
        await somethingThatReturnAPromise();
    } catch (err){
        console.log(err);
    }
}

//另一种写法
async function myFunction() {
    await somethingThatReturnAPromise().catch(function(err) {
        console.log(err);
    })
}

27:js类数组变成数组的方法

所谓的类数组对象,JavaScript对它们定义为:它们看起来很像数组,只是具有部分和数组相同特性:

拥有length属性
元素保存在对象中,可以通过索引访问
但是没有数组的其他方法,例如:push、slice、indexOf等。

将类数组转成数组的方法:

slice

1 var arr = Array.prototype.slice.call(arguments)
2 //或者
3 var arr = [].slice.call(arguments)

Array.from()
es6语法

1 var arr = Array.from(arguments);

$.makeArray()

1 var arr = $.makeArray(arguments)

28:es6中的proxy

Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。这个词的原理为代理,在这里可以表示由它来“代理”某些操作,译为“代理器”。

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

上面代码对一个空对象架设了一层拦截,重新定义了属性的读取(get)和设置(set)行为。对设置了拦截行为的对象obj,去读写它的属性,用自己的定义覆盖了语言的原始定义,运行得到下面的结果。

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2

ES6原生提供了Proxy构造函数,用来生成Proxy实例。

var proxy = new Proxy(target, handler);

Proxy对象的所有用法,都是上面的这种形式。不同的只是handle参数的写法。其中new Proxy用来生成Proxy实例,target是表示所要拦截的对象,handle是用来定制拦截行为的对象。

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

上面是一个拦截读取属性的行为的例子。要使Proxy起作用,必须针对Proxy实例进行操作,而不是针对目标对象(target)进行操作。

如果handler没有设置任何拦截,那就等同于直接通向原对象,如下:



var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"


我们可以将Proxy对象,设置到object.proxy属性,从而可以在object对象上调用。

var object = { proxy: new Proxy(target, handler) };

Proxy对象也可以作为其它对象的原型对象

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35

上面代码中,proxy对象是obj的原型对象,obj本身并没有time属性,所以根据原型链,会在proxy对象上读取属性,从而被拦截。

同一个拦截函数,可以设置多个操作。

var handler = {
  get: function(target, name) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },

  apply: function(target, thisBinding, args) {
    return args[0];
  },

  construct: function(target, args) {
    return {value: args[1]};
  }
};

var fproxy = new Proxy(function(x, y) {
  return x + y;
}, handler);

fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true

Proxy支持拦截的操作,一共有13种:

1:get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
2:set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
3:has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
4:deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
5:ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
6:getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
7:defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
8:preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
9:getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
10:isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
11:setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
12:apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
13:construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。

29:es6中的generator

Generator函数是ES6引入的新型函数,用于异步编程,通过yield关键字,可以让函数的执行流挂起,那么便为改变执行流程提供了可能。
Generator函数的定义

1:使用*来定义 ,在function与函数名之间有一个星号
2: 函数体内部使用yield语句

let test = function *() {
        yield 'A' 
        yield 'B'
      return 'end'
    }
//或者
function* test(){
  yield 'A' 
  yield 'B'
 return 'end'
}

Generator函数的特性

1:调用Generator函数并不会执行函数体,而是返回了一个Generator Object
2:使用next()方法,调用方法体

next方法会执行函数体,直到遇到第一个yield语句,然后挂起函数执行,等待后续调用。
next会返回一个对象,这个对象有2个key,value表示yield语句后面的表达式的值(‘hello’),done是个布尔值,表示函数体是否已经执行结束。再次调用next时,执行流在挂起的地方继续执行,直到遇到第2个yield,依次类推。

var t = test ()
t.next()

t.next()返回一个对象{value: “A”, done: false},'A’就是test函数执行到第一个yield语句之后得到的值,false表示函数还没有执行完,只是在这挂起。
可以一直调用next(),如果done: true,标识函数运行完毕。

抽奖次数的案例

通过一个剩余次数,来深入一下Generator函数的用法:
功能:
1.定义一个函数draw,接收一个count,输出count的值
2.定义一个Generator函数,接收一个count参数,方法体内,调用draw
3.创建一个按钮,点击一次,执行一次Generator函数

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button onclick="starts()">抽奖</button>
<script>
    //定义一个普通函数,输出剩余次数
    let draw = function (count) {
        console.log(`剩余 ${count} 次`)
    }
//定义一个Generator函数,接收一个count参数,每次调用都减少1
    let resize = function* (count) {
        while (count > 0){
            count -- ;
            yield draw(count) //调用draw函数,输出count
        }
    }
  //调用resize 函数,初始化count为5,此时resize 函数并不会执行。
    let start = resize(5);

    function starts() {
        start.next()//通过点击按钮,调用resize 的next方法执行函数体,执行一次便暂停一次
    }
    }
</script>
</body>
</html>

实现一个长轮询

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    {
        //长轮训
        let ajax = function* () {
            yield new Promise(function (resolve, reject) {
                setTimeout(function () {
                  resolve({code:1})
                },500)
            })
        }

        let pull = function () {
            let genertaor = ajax();
            let step = genertaor.next();
            step.value.then(function (res) {
                if(res.code != 0){
                    console.log(res)
                    setTimeout(function () {
                        pull();
                    },1000)
                }else{
                    console.log(res)
                }
            })
        }

        pull();

    }
</script>

</body>
</html>

代码说明:
1:定义一个Generator函数ajax,yield 返回一个Promise对象, resolve({code:1})表示模拟的网络请求返回值。
2:定义一个pull 函数,函数体里调用genertaor.next();因为返回的是一个Promise,调用then方法获取结果,判断如果code不等于0,那么间隔1秒继续执行pull();
如果 code不等于0,函数就会一直执行下去,达到了一个长轮训的效果。

31:js中数组遍历的方法

1、for循环

let arr = [1,2,3,4]
for(let j = 0,len=arr.length; j < len; j++) {
    console.log(arr[j]);
}

在这里插入图片描述
2、forEach循环

//1 没有返回值
arr.forEach((item,index,array)=>{
    console.log(index+':'+arr[index]);
})
//参数:value数组中的当前项, index当前项的索引, array原始数组;
//数组中有几项,那么传递进去的匿名回调函数就需要执行几次;
//但是forEach也有一些局限,不能continue跳过或者break终止循环


在这里插入图片描述
3、map循环

map的回调函数中支持return返回值;
并不影响原来的数组,只是相当于把原数组克隆一份,把克隆的这一份的数组中的对应项改变了;

var ary = [12,23,24,42,1];
var res = ary.map(function (item,index,ary ) {
    return item*10;
})
console.log(res);//-->[120,230,240,420,10];  原数组拷贝了一份,并进行了修改
console.log(ary);//-->[12,23,24,42,1];  原数组并未发生变化

在这里插入图片描述4、for Of 遍历

可以调用break、continue和return语句

var myArray= [12,23,24,42,1];
for (var value of myArray) {
console.log(value);
}

在这里插入图片描述
5、filter遍历

不会改变原始数组,返回新数组

var arr = [
  { id: 1, value: 2 },
  { id: 2, value: 4 },
  { id: 2, value: 7 },
]
let newArr = arr.filter(item => item.value>2);
console.log(newArr ,arr )

在这里插入图片描述
6、every遍历

every()是对数组中的每一项运行给定函数,如果该函数对每一项返回true,则返回true。如果返回false,就退出遍历

var arr = [ 1, 2, 3, 4, 5, 6 ];
if(arr.every( function( item, index, array ){
        return item > 3;
   })){
        console.log("满足条件,每一个都大于3" );
    }else{
        console.log("不满足条件,不是每一个都大于3" );
    }

在这里插入图片描述
7、some遍历

some()是对数组中每一项运行指定函数,如果该函数对任一项满足条件,则返回true,就退出遍历;否则返回false

var arr = [ 1, 2, 3, 4, 5, 6 ];
if(arr.some( function( item, index, array ){
        return item > 3;
   })){
        console.log("");
    }else{
        console.log("不满足条件,没有大于3的" );
    }

在这里插入图片描述
8、reduce

reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,接受四个参数:初始值(或者上一次回调函数的返回值),当前元素值,当前索引,调用 reduce 的数组。

var total = [0,1,2,3,4].reduce((a, b)=>a + b); //10
console.log(total)

在这里插入图片描述

9:for in

for(var item in arr|obj){} 可以用于遍历数组和对象

遍历数组时,item表示索引值, arr表示当前索引值对应的元素 arr[item]
遍历对象时,item表示key值,arr表示key值对应的value值 obj[item]

for in一般循环遍历的都是对象的属性,遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性

var obj = {a:1, b:2, c:3};    
for (let item in obj) {
  console.log("obj." + item + " = " + obj[item]);
}
// obj.a = 1
// obj.b = 2
// obj.c = 3
var arr = ['a','b','c'];
for (var item in arr) {
    console.log(item) //0 1 2
    console.log(arr[item]) //a b c
}

结论:

推荐在循环对象属性的时候使用for in,在遍历数组的时候的时候使用for of;
for in循环出的是key,for of循环出的是value;
for of是ES6新引入的特性。修复了ES5的for in的不足;
for of不能循环普通的对象,需要通过和Object.keys()搭配使用。

跳出循环的方式有如下几种:
return 函数执行被终止;
break 循环被终止;
continue 循环被跳过。

31:深入理解 requestAnimationFrame

在Web应用中,实现动画效果的方法比较多,Javascript 中可以通过定时器 setTimeout 来实现,css3 可以使用 transition 和 animation 来实现,html5 中的 canvas 也可以实现。除此之外,html5 还提供一个专门用于请求动画的API,那就是 requestAnimationFrame,顾名思义就是请求动画帧。 为了深入理解 requestAnimationFrame 背后的原理,我们首先需要了解一下与之相关的几个概念:

1、屏幕刷新频率
即图像在屏幕上更新的速度,也即屏幕上的图像每秒钟出现的次数,它的单位是赫兹(Hz)。 对于一般笔记本电脑,这个频率大概是60Hz, 可以在桌面上右键->屏幕分辨率->高级设置->监视器 中查看和设置。这个值的设定受屏幕分辨率、屏幕尺寸和显卡的影响,原则上设置成让眼睛看着舒适的值都行。

市面上常见的显示器有两种,即CRT和LCD, CRT就是传统显示器,LCD就是我们常说的液晶显示器。

CRT是一种使用阴极射线管的显示器,屏幕上的图形图像是由一个个因电子束击打而发光的荧光点组成,由于显像管内荧光粉受到电子束击打后发光的时间很短,所以电子束必须不断击打荧光粉使其持续发光。电子束每秒击打荧光粉的次数就是屏幕刷新频率。

而对于LCD来说,则不存在刷新频率的问题,它根本就不需要刷新。因为LCD中每个像素都在持续不断地发光,直到不发光的电压改变并被送到控制器中,所以LCD不会有电子束击打荧光粉而引起的闪烁现象。

因此,当你对着电脑屏幕什么也不做的情况下,显示器也会以每秒60次的频率正在不断的更新屏幕上的图像。为什么你感觉不到这个变化? 那是因为人的眼睛有视觉停留效应,即前一副画面留在大脑的印象还没消失,紧接着后一副画面就跟上来了,这中间只间隔了16.7ms(1000/60≈16.7), 所以会让你误以为屏幕上的图像是静止不动的。而屏幕给你的这种感觉是对的,试想一下,如果刷新频率变成1次/秒,屏幕上的图像就会出现严重的闪烁,这样就很容易引起眼睛疲劳、酸痛和头晕目眩等症状。

2、动画原理
根据上面的原理我们知道,你眼前所看到图像正在以每秒60次的频率刷新,由于刷新频率很高,因此你感觉不到它在刷新。而动画本质就是要让人眼看到图像被刷新而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。 那怎么样才能做到这种效果呢?

刷新频率为60Hz的屏幕每16.7ms刷新一次,我们在屏幕每次刷新前,将图像的位置向左移动一个像素,即1px。这样一来,屏幕每次刷出来的图像位置都比前一个要差1px,因此你会看到图像在移动;由于我们人眼的视觉停留效应,当前位置的图像停留在大脑的印象还没消失,紧接着图像又被移到了下一个位置,因此你才会看到图像在流畅的移动,这就是视觉效果上形成的动画。

3、setTimeout
理解了上面的概念以后,我们不难发现,setTimeout 其实就是通过设置一个间隔时间来不断的改变图像的位置,从而达到动画效果的。但我们会发现,利用seTimeout实现的动画在某些低端机上会出现卡顿、抖动的现象。 这种现象的产生有两个原因:

setTimeout的执行时间并不是确定的。在Javascript中, setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,因此 setTimeout 的实际执行时间一般要比其设定的时间晚一些。

刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的屏幕刷新频率可能会不同,而 setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致,从而引起丢帧现象。 那为什么步调不一致就会引起丢帧呢?

首先要明白,setTimeout的执行只是在内存中对图像属性进行改变,这个变化必须要等到屏幕下次刷新时才会被更新到屏幕上。如果两者的步调不一致,就可能会导致中间某一帧的操作被跨越过去,而直接更新下一帧的图像。假设屏幕每隔16.7ms刷新一次,而setTimeout每隔10ms设置图像向左移动1px, 就会出现如下绘制过程:

第0ms: 屏幕未刷新,等待中,setTimeout也未执行,等待中;

第10ms: 屏幕未刷新,等待中,setTimeout开始执行并设置图像属性left=1px;

第16.7ms: 屏幕开始刷新,屏幕上的图像向左移动了1px, setTimeout 未执行,继续等待中;

第20ms: 屏幕未刷新,等待中,setTimeout开始执行并设置left=2px;

第30ms: 屏幕未刷新,等待中,setTimeout开始执行并设置left=3px;

第33.4ms:屏幕开始刷新,屏幕上的图像向左移动了3px, setTimeout未执行,继续等待中;

从上面的绘制过程中可以看出,屏幕没有更新left=2px的那一帧画面,图像直接从1px的位置跳到了3px的的位置,这就是丢帧现象,这种现象就会引起动画卡顿。

4、requestAnimationFrame
与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

这个API的调用很简单,如下所示:

var progress = 0;

//回调函数

function render() {

    progress += 1; //修改图像的位置


    if (progress < 100) {

           //在动画没有结束前,递归渲染

           window.requestAnimationFrame(render);

    }

}


//第一帧渲染

window.requestAnimationFrame(render);

除此之外,requestAnimationFrame还有以下两个优势:

CPU节能:使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。而requestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。

函数节流:在高频率事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。

5、优雅降级
由于requestAnimationFrame目前还存在兼容性问题,而且不同的浏览器还需要带不同的前缀。因此需要通过优雅降级的方式对requestAnimationFrame进行封装,优先使用高级特性,然后再根据不同浏览器的情况进行回退,直止只能使用setTimeout的情况。下面的代码就是有人在github上提供的polyfill,详细介绍请参考github代码 requestAnimationFrame(https://github.com/darius/requestAnimationFrame)

if (!Date.now)

    Date.now = function() { return new Date().getTime(); };


(function() {

    'use strict';

    

    var vendors = ['webkit', 'moz'];

    for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {

        var vp = vendors[i];

        window.requestAnimationFrame = window[vp+'RequestAnimationFrame'];

        window.cancelAnimationFrame = (window[vp+'CancelAnimationFrame']

                                   || window[vp+'CancelRequestAnimationFrame']);

    }

    if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) // iOS6 is buggy

        || !window.requestAnimationFrame || !window.cancelAnimationFrame) {

        var lastTime = 0;

        window.requestAnimationFrame = function(callback) {

            var now = Date.now();

            var nextTime = Math.max(lastTime + 16, now);

            return setTimeout(function() { callback(lastTime = nextTime); },

                              nextTime - now);

        };

        window.cancelAnimationFrame = clearTimeout;

    }

}());

31:微信扫码登陆的过程

浏览器输入:https://wx.qq.com/?lang=zh_CN
手机登录微信,利用“扫一扫”功能扫描网页上的二维码
手机扫描成功后,提示“登录网页版微信”;网页上显示“成功扫描 请在手机点击确认以登录”
手机端点击“登录网页版微信”,网页跳转到用户的微信操作界面

整个扫码登录的操作过程还是挺简单的,而且交互地实时性比较好,如果网络不是非常阻塞,整个过程还是非常快的。

扫码登录原理

扫码登录大概的思路是:微信手机客户端从网页二维码里面得到一些信息,然后发送给网页微信的服务器,网页服务器验证信息并响应。下面,我们借助火狐浏览器提供的Firebug工具看看,到底是怎么一回事儿吧!

1.每次打开微信网页版的时候,都会生成一个含有唯一uid的二维码,而且每次刷新后都会改变。这样可以保证一个uid只可以绑定一个账号和密码,确定登录用户的唯一性。可以通过手机上的UC浏览器提供的扫码功能查看二维码里面的信息,但并不会自动打开该地址。我刷新三次,扫描结果如下,其中最后面那串数字就是uid:

1: https://login.weixin.qq.com/l/48e24d66bdbc4f
2: https://login.weixin.qq.com/l/0787fb4fa7ad4c
3: https://login.weixin.qq.com/l/92781a4a7f1c47

通过查看网页源码,这个页面在加载完毕时,已经把很多登录后才需要的相关资源都预先加载进来了,所以登录用户得到确认后展示用户信息的速度很快。

2.除了返回唯一的uid,实际上打开这个页面的时候,浏览器跟服务器还创建了一个长连接,请求uid的扫描记录。如果没有,在特定时长后(目前是27秒左右)会接到状态码408(请求超时),表示应该继续下一次请求;如果接到状态码201(服务器创建新资源成功),表示客户端扫描了该二维码
在这里插入图片描述
在这里插入图片描述
长轮询代码结构:

function _poll(_asUUID) {  
  // ....  
  $.ajax({  
    type: "GET",  
    url: "https://login." + _sBaseHost + "/cgi-bin/mmwebwx-bin/login?uuid=" + _asUUID + "&tip=" + show_tip,  
    dataType: "script",  
    cache: false,  
    timeout: _nAjaxTimeout,  
    success: function(data, textStatus, jqXHR) {  
      switch (_aoWin.code) {  
      case 200:  
        // ....  
        break;  
      case 201:  
        // ....  
        break;  
      case 408:  
        // ....  
        break;  
      case 400:  
      case 500:  
        // ....  
        break;  
      }  
    },  
    error: function(jqXHR, textStatus, errorThrown) {  
        // ....  
    }  
  });  
} 

3.当用户使用登录后的微信扫描二维码的时候,会将uid和手机微信产生的token进行绑定,并上传到服务器。这个时候,浏览器通过长轮询查询到uid扫描记录,立即得到201响应码,然后通知服务器,客户端由此也进入一个新的页面(就是那个要你点确认的按钮)。在客户端点击确认后,获得服务器授信的令牌,进行随后的信息交互过程。

结语

总的来说,微信扫码登录核心过程应该是这样的:浏览器获得一个唯一的、临时的uid,通过长连接等待客户端扫描带有此uid的二维码后,从长连接中获得客户端上报给服务器的帐号信息进行展示。并在客户端点击确认后,获得服务器授信的令牌,进行随后的信息交互过程。 在超时、网络断开、其他设备上登录后,此前获得的令牌或丢失、或失效,对授权过程形成有效的安全防护。

31:ajax实现的5大步骤

1.创建对象

首先我们需要一个Ajax的对象。在这里我们需要注意的是,由于不同的浏览器内核问题,部分浏览器对Ajax对象的创建方式不一样。在以IE7以下版本为内核的浏览器中,没有提供XMLHttpRequest对象。目前,浏览器基本都是支持XMLHttpRequest对象,所以不再考虑版本问题。(浏览器版本的不同,区别仅仅是创建对象的不同,其他都是一样的)

var xmlhttp = new XMLHttpRequest();//获取对象

2.设置回调函数

设置当请求执行后,服务器返回请求的状态码,根据请求的状态码对浏览器做出相应的响应。

hxmlhttp.onreadystatechange = function(){//设置回调函数
	if(xmlhttp.readyState == 4)//这里的4是请求的状态码,代表请求已经完成
		if(xmlhttp.status == 200 || xmlhttp.status == 304)//这里是获得响应的状态码,200代表成功,304代表无修改可以直接从缓存中读取
			var result = xmlhttp.responseText;//这里获取的是响应文本,也可以获得响应xml或JSON
			alert("获取了响应文本" + result);
}

3.设置请求地址

设置请求地址,需要注意如果使用GET请求需要在url中绑定请求参数。

var url = "UserServlet.do?action=showUser";//POST请求
var data = "id="+id;//为POST请求绑定请求参数,需要以键值对形式绑定
var url = "UserServlet.do?action=showUser&id="+id;//GET请求

4.设置open()方法

open方法用来告诉XMLHttpRequest对象,发送请求方式是"POST"还是"GET";请求需要发送到指定的url;执行方式是同步还是异步,默认为true(异步的方式)。这里设置成false(同步方式)与刷新页面无异。

xmlhttp.open("POST",url);

5.设置请求头(GET请求可以忽略这一步)

xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded");

6.发送请求

xmlhttp.send(data);//POST请求
xmlhttp.send();//GET请求

GET请求和POST请求的区别与form表单中method属性中的GET与POST区别一致。

重点:那我们如何去实现一个axios呢,链接:https://www.jianshu.com/p/7db8a90badf9

32:JavaScript中0.1+0.2==0.3

JavaScript:
0.1+0.2 == 0.3 // false
0.7*180 === 125.99999999999999 // true
1000000000000000128 === 1000000000000000129 // true

在这里插入图片描述
Python也有这个问题
在这里插入图片描述

不仅是JavaScript和Python,所有遵循IEEE754标准的语言中,0.1+0.2 !== 0.3; 0.2+0.3 === 0.5

单精度,双精度
IEEE754中浮点数值的表示是:单精度(32位)和双精度(64位),JavaScript中采用双精度浮点数。

JavaScript如何表示数字
JavaScript不区分整数和浮点数,遵循IEEE754标准,64位(bit)双精度浮点数编码,所以JS中所有的数字都是浮点数。

浮点数的特点

1:浮点数可以表示的值的范围比同等位数的整数表示方法的值的范围大得多
2:浮点数无法精确的表示其范围内所有的数值;有符号和无符号整数可以表示其范围内的每一个值。
3:浮点数的个数是无限的,导致JavaScript不能精确表示所有的浮点数,只能是一个近似值。

一个字节===8位

数字在内存中的表示

在这里插入图片描述
说明:

1:0位:符号位,0代表正数,1代表负数
2:1-11位:存储指数部分(e)
3:12-63位,存储小数部分(有效数字部分)

js 的最大安全数:
整数部分只有52位,而最大安全数是(2的53次方-1),为什么?
因为:二进制表示有效数字时,以1.xxx…xxxx的形式表示,尾数部分在规定形式下第一位默认是1(省略不写,xxx…xxxx是尾数部分,最长为52位,)。因此,JavaScript有效数字最长是53位二进制位。

Number.MAX_SAFE_INTEGER === 9007199254740991
Math.pow(2,53)-1 === 9007199254740991
Number.MAX_SAFE_INTEGER === Math.pow(2,53)-1 // true

浮点数是怎么运算的

计算机不能对十进制的数字直接运算,要先按照IEEE754转成二进制,再对阶运算

转成二进制
0.1和0.2转换成二进制后会无限循环

0.1 => 0.0001100110011001100110011001100110011001100110011001101......(无限循环的)
0.2 => 0.001100110011001100110011001100110011001100110011001101......(无限循环的)

在这里插入图片描述
由于IEEE754尾数位数的限制,二进制后边的就会被截掉。所以说,在十进制转二进制的过程中,精度就已经损失了。

0.1为什么等于0.1
在这里插入图片描述
标准中规定尾数f的固定长度是52位,再加上省略的一位,这53位是JS精度范围。它最大可以表示2^53(9007199254740992), 长度是 16,所以可以使用 toPrecision(16) 来做精度运算,超过的精度会自动做凑整处理

0.10000000000000000555.toPrecision(16) // "0.1000000000000000"

0.1.toPrecision(16)  // "0.1000000000000000"
0.1.toPrecision(23)  // "0.10000000000000000555112"

所以,0.1===0.1是因为超过16位后自动凑整,导致他俩相等了。

对阶运算

对阶:指将两个进行运算的浮点数的阶码(小数点的位数是否对齐)对齐的操作。对阶的目的是为了使两个浮点数的尾数能够进行加减运算。

由于指数位数不同,运算时需要对阶运算,这个过程也有可能会损失精度。

0.1+0.2的二进制对阶之后计算得到的二进制:
0.0100110011001100110011001100110011001100110011001100 

转化成十进制:0.30000000000000004

如何解决0.1+0.2不等于0.3的问题
1.设置一个误差范围值
设置一个误差范围值,通常称为“机器精度”,对于JavaScript来说,这个误差范围是Math.pow(2,-52)
在ES6中,提供了一个属性,Number.EPSILON,这个值等于2的﹣52次方

Number.EPSILON === Math.pow(2,-52)  // true

所以,只要判断(0.1+0.2)与0.3的差小于Number.EPSILONG,就可以说明两者是相等的

/**
 * @description 比较两个值是否相等
 * @param {Number} a 
 * @param {Number} b 
 * @return 相差小于某个值,返回true,否则返回false
 */
function numberEqual(a,b){
  return Math.abs(a-b) < Number.EPSILON
}

numberEqual((0.1+0.2), 0.3) // true

但是这样就有了一个问题就是

js中浮点型运算 加减乘除

js中浮点型是如何运算的呢?

例如:var a=0.69;

我想得到6.9 直接这样写 var c=a*10;

alert©; 得到结果是:6.8999999999999995

到网上一搜,有网友说这是一个JS浮点数运算Bug,找了解决方法:

方法一:有js自定义函数

<script>
 
//加法函数,用来得到精确的加法结果
//说明:javascript的加法结果会有误差,在两个浮点数相加的时候会比较明显。这个函数返回较为精确的加法结果。
//调用:accAdd(arg1,arg2)
//返回值:arg1加上arg2的精确结果
function accAdd(arg1,arg2){
var r1,r2,m;
try{r1=arg1.toString().split(".")[1].length}catch(e){r1=0}
try{r2=arg2.toString().split(".")[1].length}catch(e){r2=0}
m=Math.pow(10,Math.max(r1,r2))
return (arg1*m+arg2*m)/m
}
//给Number类型增加一个add方法,调用起来更加方便。
Number.prototype.add = function (arg){
return accAdd(arg,this);
}
 
//加法函数,用来得到精确的加法结果
//说明:javascript的加法结果会有误差,在两个浮点数相加的时候会比较明显。这个函数返回较为精确的加法结果。
//调用:accAdd(arg1,arg2)
//返回值:arg1加上arg2的精确结果
function accSub(arg1,arg2){
    var r1,r2,m,n;
    try{r1=arg1.toString().split(".")[1].length}catch(e){r1=0}
    try{r2=arg2.toString().split(".")[1].length}catch(e){r2=0}
    m=Math.pow(10,Math.max(r1,r2));
    //last modify by deeka
    //动态控制精度长度
    n=(r1>=r2)?r1:r2;
    return ((arg1*m-arg2*m)/m).toFixed(n);
}
 
//除法函数,用来得到精确的除法结果
//说明:javascript的除法结果会有误差,在两个浮点数相除的时候会比较明显。这个函数返回较为精确的除法结果。
//调用:accDiv(arg1,arg2)
//返回值:arg1除以arg2的精确结果
function accDiv(arg1,arg2){
var t1=0,t2=0,r1,r2;
try{t1=arg1.toString().split(".")[1].length}catch(e){}
try{t2=arg2.toString().split(".")[1].length}catch(e){}
with(Math){
r1=Number(arg1.toString().replace(".",""))
r2=Number(arg2.toString().replace(".",""))
return (r1/r2)*pow(10,t2-t1);
}
}
//给Number类型增加一个div方法,调用起来更加方便。
Number.prototype.div = function (arg){
return accDiv(this, arg);
}
 
//乘法函数,用来得到精确的乘法结果
//说明:javascript的乘法结果会有误差,在两个浮点数相乘的时候会比较明显。这个函数返回较为精确的乘法结果。
//调用:accMul(arg1,arg2)
//返回值:arg1乘以arg2的精确结果
function accMul(arg1,arg2)
{
var m=0,s1=arg1.toString(),s2=arg2.toString();
try{m+=s1.split(".")[1].length}catch(e){}
try{m+=s2.split(".")[1].length}catch(e){}
return Number(s1.replace(".",""))*Number(s2.replace(".",""))/Math.pow(10,m)
}
//给Number类型增加一个mul方法,调用起来更加方便。
Number.prototype.mul = function (arg){
return accMul(arg, this);
}
 
var a=0.69;
var b=10;
alert(a*b);//6.8999999999999995
alert((a*100)/10);
</script>

直接调用函数就可以。

方法二:如果在知道小数位个数的前提下,可以考虑通过将浮点数放大倍数到整型(最后再除以相应倍数),再进行运算操作,这样就能得到正确的结果了

alert(11*22.9);//得到251.89999999999998

alert(11*(22.9*10)/10);//得到251.9

32:js中的new()到底做了些什么

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) ;
(3) 执行构造函数中的代码(为这个新对象添加属性) ;
(4) 返回新对象。

new 操作符
在有上面的基础概念的介绍之后,在加上new操作符,我们就能完成传统面向对象的class + new的方式创建对象,在JavaScript中,我们将这类方式成为Pseudoclassical。
基于上面的例子,我们执行如下代码

var obj = new Base();

这样代码的结果是什么,我们在Javascript引擎中看到的对象模型是:
在这里插入图片描述
new操作符具体干了什么呢?其实很简单,就干了三件事情。

var obj  = {};
obj.__proto__ = Base.prototype;
Base.call(obj);

第一行,我们创建了一个空对象obj
第二行,我们将这个空对象的__proto__成员指向了Base函数对象prototype成员对象
第三行,我们将Base函数对象的this指针替换成obj,然后再调用Base函数,于是我们就给obj对象赋值了一个id成员变量,这个成员变量的值是”base”,关于call函数的用法。

如果我们给Base.prototype的对象添加一些函数会有什么效果呢?
例如代码如下:

Base.prototype.toString = function() {
    return this.id;
}

那么当我们使用new创建一个新对象的时候,根据__proto__的特性,toString这个方法也可以做新对象的方法被访问到。于是我们看到了:
构造子中,我们来设置‘类’的成员变量(例如:例子中的id),构造子对象prototype中我们来设置‘类’的公共方法。于是通过函数对象和Javascript特有的__proto__与prototype成员及new操作符,模拟出类和类实例化的效果。

32:创建一个Event类,并创建on、off、trigger、once方法

一、创建一个Event.js

class Event {
    constructor() {
        this.handlers = { // 记录所有的事件和处理函数

        }
    }
    /* *
    * on 添加事件监听
    * @param type 事件类型
    * @param handler 事件回调
    * on('click', ()=>{})
    * */
    on(type, handler, once=false) {
        if (!this.handlers[type]) {
            this.handlers[type] = [];
        }
        if (!this.handlers[type].includes(handler)) {
            this.handlers[type].push(handler);
            handler.once = once;
        }
    }
    /* *
    * off 取消事件监听
    * 
    *  */
    off(type, handler) {
        if (this.handlers[type]) {
            if (handler === undefined) {
                this.handlers[type] = []
            } else {
                this.handlers[type] = this.handlers[type].filter((f)=>{
                    return f!=handler
                })
            }
        }
    }
    /* *
    * @param type 要执行哪个类型的函数
    * @param eventData事件对象
    * @param point this指向
    * 
    *  */
    trigger(type, eventData = {}, point=this) {
        if (this.handlers[type]) {
            this.handlers[type].forEach(f => {
                f.call(point, eventData);
                if (f.once) {
                    this.off(type, f)
                }
            });
        }
    }
    /* *
    * once 函数执行一次
    * @param type 事件处理
    * @param handle 事件处理函数
    *  */
    once(type, handler) {
        this.on(type, handler, true);
    }
}

二、使用Event.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        #box {
            position: absolute;
            top: 0;
            left: 0;
            width: 100px;
            height: 100px;
            background: red;
        }
    </style>
    <script src="./event.js"></script>
</head>
<body>
    <div id="box"></div>
    
    
    <script>
        /* 
        * 1.记录摁下时鼠标的位置和元素位置
        * 鼠标位置-摁下时的鼠标位置 = 鼠标移动的位置
        * 元素位置=鼠标移动距离+摁下时元素位置
        **/
        class Drag extends Event{
            // 构造函数
            constructor(el) {
                super(); // 继承
                this.el = el;
                this.startOffset = null; // 鼠标摁下时元素的位置
                this.startPoint = null; // 鼠标的坐标
                let move = (e)=>{
                    this.move(e)
                }
                let end = (e)=>{
                    document.removeEventListener('mousemove', move);
                    document.removeEventListener('mouseup', end);
                    this.end(e)
                }
                el.addEventListener('mousedown', (e)=> {
                    this.start(e);

                    document.addEventListener('mousemove', move);
                    document.addEventListener('mouseup', end);
                })
                
                
            }
            start(e) {
                let {el} = this;
                console.log(this)
                console.log(el)
                this.startOffset = {
                    x: el.offsetLeft,
                    y: el.offsetTop
                }
                this.startPoint = {
                    x: e.clientX,
                    y: e.clientY
                }
                this.trigger('dragstart', e, this.el)
            }
            end(e) {
                this.trigger('dragend',e, this.el)
            }
            move(e) {
                let {el, startOffset, startPoint} = this;
                let nowPoint = {
                    x: e.clientX,
                    y: e.clientY
                }
                let dis = {
                    x: nowPoint.x - startPoint.x,
                    y: nowPoint.y - startPoint.y
                }
                el.style.left = dis.x + startOffset.x + 'px';
                el.style.top = dis.y + startOffset.y + 'px';
                this.trigger('dragmove', e, el)
            }
        }
        
        (function() {
            let box = document.querySelector('#box');
            let dragBox = new Drag(box);

            dragBox.on('dragstart', function(e) {
                console.log(e);
                console.log(this);
                this.style.background = 'yellow';
            })
            dragBox.on('dragend', function(e) {
                console.log('b')
                this.style.background = 'blue';
            })
            dragBox.once('dragmove', function(e) {
                console.log('c')
                // this.style.background = 'blue';
            })
            console.log(dragBox)
        })()
    </script>
</body>
</html>

32:es6中的新增数据类型symsol(符号)的使用方法

es6在string number boolean null undefined object之外又新增了一种Symbol类型。

Symbol意思是符号,有一个特性—每次创建一个Symbol值都是不一样的。

symbol是程序创建并且可以用作属性键的值,并且它能避免命名冲突的风险。

注意: //symbol对象永远不相等,解决属性名相同的问题

//        var a=new Symbol(); //注意不是用new创建
        var a=Symbol();
        var b=Symbol();
        console.log(a===b); //false

用处:赋值对象的属性被修改。

具体:把Symbol作为key,下游的人就没法覆盖key了。

//file1.js
let name=Symbol();
{
    person={};
    person[name]='File1';
    console.log("person[name]",person[name]);
}
//file2.js
{
    let name=Symbol();
    person[name]='File2';
    console.log("person[name]",person[name]);//局部的修改
}
console.log("person[name]",person[name]);
console.log("person:",person);

在这里插入图片描述

本文实例讲述了ES6中的Symbol类型。分享给大家供大家参考,具体如下:

Symbol是在ES6中新加入的类型。

正如我们所知,JavaScript中有以下几种类型:

Undefined ,Null ,Boolean ,Number ,String, Object。

但是上述类型在处理某些情况的时候是远远不够的。下面我们来举一个例子:

假设我们要移动div,也需要在某些情况下判断该div是否处于移动状态,所以我们会想到给div这类的对象设置一个属性。

if (element.isMoving) {
 smoothAnimations(element);
}
element.isMoving = true;

但是这样会存在一些问题,比如:

我们可能和第三方的库冲突

我们可能和未来的标准冲突等。

于是可以采用第二种方法,也就是采用加密函数,制定一个值:

var isMoving = SecureRandom.generateName();
...
if (element[isMoving]) {
 smoothAnimations(element);
}
element[isMoving] = true;

这样确实解决了冲突问题,但却带来了调试问题,我们每次查看该对象属性时都会看见一大堆垃圾命名。

于是为了解决冲突问题,ES6提出了Symbol这样的新类型。

Symbol是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用。我们看demo:

var sym1 = Symbol();
var sym2 = Symbol("foo");
var sym3 = Symbol("foo");

Symbol(“foo”) 不会强制字符串 “foo” 进入一个Symbol,它每次都创建一个新的Symbol:

Symbol("foo") === Symbol("foo"); // false

所以可以利用这个特性来创建私有属性:

(function() {
 // 创建symbol
 var key = Symbol("key");
 function MyClass(privateData) {
  this[key] = privateData;
 }
 MyClass.prototype = {
  doStuff: function() {
   ... this[key] ...
  }
 };
})();
var c = new MyClass("hello")
c["key"] === undefined//无法访问该属性,因为是私有的

也可以利用Symbol来解除上面所说的冲突问题:

// create a unique symbol
var isMoving = Symbol("isMoving");
...
if (element[isMoving]) {
 smoothAnimations(element);
}
element[isMoving] = true;

当然,也可以通过另外的调用方法来生成可以与外界共享的Symbol类型,就是Symbol.for方法。

Symbol.for(key) 方法会根据给定的键 key,来从 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入 symbol 注册表中。

Symbol.for("foo"); // 创建一个 symbol 并放入 symbol 注册表中,键为 "foo"
Symbol.for("foo"); // 从 symbol 注册表中读取键为"foo"的 symbol
Symbol.for("bar") === Symbol.for("bar"); // true,证明了上面说的
Symbol("bar") === Symbol("bar"); // false,Symbol() 函数每次都会返回新的一个 symbol
var sym = Symbol.for("mario");
sym.toString();
// "Symbol(mario)",mario 既是该 symbol 在 symbol 注册表中的键名,又是该 symbol 自身的描述字符串

所以为了防止冲突,我们最好给symbol带上前缀:

Symbol.for("mdn.foo");
Symbol.for("mdn.bar");

32:移动端点透事件

什么是点透?
在移动端,当用户通过绑定touchstart事件监听函数让浮层关闭时,关闭后浮层后面对应位置页面其他元素也会触发click事件,比如浮层的关闭按钮下是一个链接,当用户点击浮层关闭按钮浮层消失后大约300ms页面同时发生跳转。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>tap击穿</title>

  <style>
    body {
      margin: 0;
      padding: 30px;
      font-size: 60px;
    }
    .mask {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      background: rgba(0,0,0,0.8);
      color: #fff;
      padding: 30px;

    }
    .close {
      background: red;
      padding: 10px;
    }
  </style>
</head>
<body>
  <!--<a href="https://jirengu.com">百度</a>-->
  <span class="bg">百度</span>
  <p class="log">0</p>
  <div class="mask">
    <span class="close">X</span>
  </div>

  <script>
    const $ = s => document.querySelector(s)
    const log = str => $('.log').innerText = str

    let i = 1

    $('.close').ontouchstart = (e) => {
      $('.mask').style.display = 'none'
      log('touched ' + (++i))
    }

    $('.bg').onclick = () => {
      log('点击了背景')
    }
  </script>

</body>
</html>

以上代码,当我们点击关闭按钮时,会打印出"点击了背景",如果去掉a的注释,会直接跳到百度页面。这就是点透。

原因
点透产生的原因就跟300ms延时(https://blog.csdn.net/fengfangyuan/article/details/92751878,300ms延时,查看这篇文章)有关。当我们点击关闭按钮,touchstart立即触发,弹窗关闭,手指离开屏幕, 触发 touchend,等待300ms左右后,浏览器在用户手指离开处触发click事件,而此时弹窗以及消失, click事件自然弹窗底部的a标签。过程如下:

1:手指触摸屏幕到屏幕,触发 touchstart
2:手指在屏幕短暂停留(如果是移动,触发 touchmove)
3:手指离开屏幕, 触发 touchend:
4:等待约300ms,看用户在此时间内是否再次触摸屏幕。如果没有
5:300ms后 在用户手指离开的位置触发 click事件

解决方式

1:设置 。表面上看起来解决了300ms延时,就能解决穿透。实际上这种方法不可行。因为click事件是在touchend事件之后,更晚于touchstart(间隔了60~100ms),如果在touchstart 里绑定关闭了浮层,约100ms后 click事件触发的时候仍然会引发问题。

2:关闭浮层用click,不用touchstart, 可行。因为浮层关闭监听的是click,当浮层消失后不会再在原来的位置再冒出个click。
3:浮层关闭的时间延长。可行。当用户触发touchstart关闭浮层时,浮层可以使用渐变消失(可设置浮层关闭动画在300ms以上),此刻click事件在原来的位置触发时点击的还是浮层,而不是浮层后面的链接
4:在ontouchstart 里阻止默认事件。可行。 这样可以阻止click的触发

32:defer和async

就是在页面脚本引用的时候设置defer或者async,为什么会有这两个属相来辅助脚本加载那,因为浏览器在遇到script标签的时候,文档的解析会停止,不再构建document,有时打开一个网页上会出现空白一段时间,浏览器显示是刷新请求状态(也就是一直转圈),这就会给用户很不好的体验,defer和async的合理使用就可以避免这个情况,而且通常script的位置建议写在页面底部(移动端应用的比较多,这两个都是html5中的新属性)。

所以相对于默认的script引用,这里配合defer和async就有两种新的用法,它们之间什么区别那?

1.默认引用
  script type=“text/javascript” src=“x.min.js”>

当浏览器遇到 script 标签时,文档的解析将停止,并立即下载并执行脚本,脚本执行完毕后将继续解析文档。

2.async模式
  script type=“text/javascript” src=“x.min.js” async=“async”>

当浏览器遇到 script 标签时,文档的解析不会停止,其他线程将下载脚本,脚本下载完成后开始执行脚本,脚本执行的过程中文档将停止解析,直到脚本执行完毕。

3.defer模式
  script type=“text/javascript” src=“x.min.js” defer=“defer”>

当浏览器遇到 script 标签时,文档的解析不会停止,其他线程将下载脚本,待到文档解析完成,脚本才会执行。

所以async和defer的最主要的区别就是async是异步下载并立即执行,然后文档继续解析,defer是异步加载后解析文档,然后再执行脚本,这样说起来是不是理解了一点了;

它们的核心功能就是异步,那么两种属性怎么去区分什么情况下用哪个那,主要的参考是如果脚本不依赖于任何脚本,并不被任何脚本依赖,那么则使用 defer,如果脚本是模块化的,不依赖于任何脚本,那么则使用 async;主要功能点说完了,小伙伴们有没有分清楚他们的区别了那。

33:web worker

以前我们总说,JS是单线程没有多线程,当JS在页面中运行长耗时同步任务的时候就会导致页面假死影响用户体验,从而需要设置把任务放在任务队列中;执行任务队列中的任务也并非多线程进行的,然而现在HTML5提供了我们前端开发这样的能力 - Web Workers API,我们一起来看一看 Web Worker 是什么,怎么去使用它,在实际生产中如何去用它来进行产出。

                       1. 概述

Web Workers 使得一个Web应用程序可以在与主执行线程分离的后台线程中运行一个脚本操作。这样做的好处是可以在一个单独的线程中执行费时的处理任务,从而允许主(通常是UI)线程运行而不被阻塞。

它的作用就是给JS创造多线程运行环境,允许主线程创建worker线程,分配任务给后者,主线程运行的同时worker线程也在运行,相互不干扰,在worker线程运行结束后把结果返回给主线程。这样做的好处是主线程可以把计算密集型或高延迟的任务交给worker线程执行,这样主线程就会变得轻松,不会被阻塞或拖慢。这并不意味着JS语言本身支持了多线程能力,而是浏览器作为宿主环境提供了JS一个多线程运行的环境。

不过因为worker一旦新建,就会一直运行,不会被主线程的活动打断,这样有利于随时响应主线程的通性,但是也会造成资源的浪费,所以不应过度使用,用完注意关闭。或者说:如果worker无实例引用,该worker空闲后立即会被关闭;如果worker实列引用不为0,该worker空闲也不会被关闭。

看一看它的兼容性
在这里插入图片描述

2. 使用

2.1 限制
worker线程的使用有一些注意点

1:同源限制
worker线程执行的脚本文件必须和主线程的脚本文件同源,这是当然的了,总不能允许worker线程到别人电脑上到处读文件吧
2:文件限制
为了安全,worker线程无法读取本地文件,它所加载的脚本必须来自网络,且需要与主线程的脚本同源
3:DOM操作限制
worker线程在与主线程的window不同的另一个全局上下文中运行,其中无法读取主线程所在网页的DOM对象,也不能获取 document、window等对象,但是可以获取navigator、location(只读)、XMLHttpRequest、setTimeout族等浏览器API。
4:通信限制
worker线程与主线程不在同一个上下文,不能直接通信,需要通过postMessage方法来通信。

5:脚本限制
worker线程不能执行alert、confirm,但可以使用 XMLHttpRequest 对象发出ajax请求。

2.2 例子
在主线程中生成 Worker 线程很容易:

var myWorker = new Worker(jsUrl, options)

Worker()构造函数,第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载 JS 脚本,否则报错。第二个参数是配置对象,该对象可选。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程。

// 主线程


var myWorker = new Worker('worker.js', { name : 'myWorker' });


// Worker 线程
self.name         // myWorker

关于api什么的,直接上例子大概就能明白了,首先是worker线程的js文件:

// workerThread1.js


let i = 1


function simpleCount() {
  i++
  self.postMessage(i)
  setTimeout(simpleCount, 1000)
}


simpleCount()


self.onmessage = ev => {
  postMessage(ev.data + ' 呵呵~')
}

在HTML文件中的body中:

<!--主线程,HTML文件的body标签中-->


<div>
  Worker 输出内容:<span id='app'></span>
  <input type='text' title='' id='msg'>
  <button onclick='sendMessage()'>发送</button>
  <button onclick='stopWorker()'>stop!</button>
</div>


<script type='text/javascript'>
  if (typeof(Worker) === 'undefined')    // 使用Worker前检查一下浏览器是否支持
    document.writeln(' Sorry! No Web Worker support.. ')
  else {
    window.w = new Worker('workerThread1.js')
    window.w.onmessage = ev => {
      document.getElementById('app').innerHTML = ev.data
    }

    window.w.onerror = err => {
      w.terminate()
      console.log(error.filename, error.lineno, error.message)       // 发生错误的文件名、行号、错误内容
    }

    function sendMessage() {
      const msg = document.getElementById('msg')
      window.w.postMessage(msg.value)
    }

    function stopWorker() {
      window.w.terminate()
    }
  }
</script>

可以自己运行一下看看效果,上面用到了一些常用的api

主线程中的api,worker表示是 Worker 的实例:

worker.postMessage: 主线程往worker线程发消息,消息可以是任意类型数据,包括二进制数据

worker.terminate: 主线程关闭worker线程

worker.onmessage: 指定worker线程发消息时的回调,也可以通过worker.addEventListener(‘message’,cb)的方式

worker.onerror: 指定worker线程发生错误时的回调,也可以 worker.addEventListener(‘error’,cb)

Worker线程中全局对象为 self,代表子线程自身,这时 this指向self,其上有一些api:

self.postMessage: worker线程往主线程发消息,消息可以是任意类型数据,包括二进制数据

self.close: worker线程关闭自己

self.onmessage: 指定主线程发worker线程消息时的回调,也可以self.addEventListener(‘message’,cb)

self.onerror: 指定worker线程发生错误时的回调,也可以 self.addEventListener(‘error’,cb)

注意,w.postMessage(aMessage, transferList) 方法接受两个参数,aMessage 是可以传递任何类型数据的,包括对象,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。一个可选的 Transferable对象的数组,用于传递所有权。如果一个对象的所有权被转移,在发送它的上下文中将变为不可用(中止),并且只有在它被发送到的worker中可用。可转移对象是如ArrayBuffer,MessagePort或ImageBitmap的实例对象,transferList数组中不可传入null。

更详细的API参见 MDN - WorkerGlobalScope。

worker线程中加载脚本的api:

importScripts('script1.js')    // 加载单个脚本
importScripts('script1.js', 'script2.js')    // 加载多个脚本
                      3. 实战场景

个人觉得,Web Worker我们可以当做计算器来用,需要用的时候掏出来摁一摁,不用的时候一定要收起来~

1:加密数据
有些加解密的算法比较复杂,或者在加解密很多数据的时候,这会非常耗费计算资源,导致UI线程无响应,因此这是使用Web Worker的好时机,使用Worker线程可以让用户更加无缝的操作UI。
2:预取数据
有时候为了提升数据加载速度,可以提前使用Worker线程获取数据,因为Worker线程是可以是用 XMLHttpRequest 的。

3:预渲染
在某些渲染场景下,比如渲染复杂的canvas的时候需要计算的效果比如反射、折射、光影、材料等,这些计算的逻辑可以使用Worker线程来执行,也可以使用多个Worker线程,这里有个射线追踪的示例。
4:复杂数据处理场景
某些检索、排序、过滤、分析会非常耗费时间,这时可以使用Web Worker来进行,不占用主线程。
5:预加载图片
有时候一个页面有很多图片,或者有几个很大的图片的时候,如果业务限制不考虑懒加载,也可以使用Web Worker来加载图片,可以参考一下这篇文章的探索,这里简单提要一下。

// 主线程
let w = new Worker("js/workers.js");
w.onmessage = function (event) {
 var img = document.createElement("img");
 img.src = window.URL.createObjectURL(event.data);
 document.querySelector('#result').appendChild(img)
}

// worker线程
let arr = [...好多图片路径];
for (let i = 0, len = arr.length; i < len; i++) {
   let req = new XMLHttpRequest();
   req.open('GET', arr[i], true);
   req.responseType = "blob";
   req.setRequestHeader("client_type", "DESKTOP_WEB");
   req.onreadystatechange = () => {
     if (req.readyState == 4) {
     postMessage(req.response);
   }
 }
 req.send(null);
}

在实战的时候注意

虽然使用worker线程不会占用主线程,但是启动worker会比较耗费资源

主线程中使用XMLHttpRequest在请求过程中浏览器另开了一个异步http请求线程,但是交互过程中还是要消耗主线程资源

34:JavaScript 中的 fetch

fetch 使用

  1. 浏览器支持情况

fetch是相对较新的技术,当然就会存在浏览器兼容性的问题,当前各个浏览器低版本的情况下都是不被支持的,因此为了在所有主流浏览器中使用fetch 需要考虑 fetch 的 polyfill 了

 require('es6-promise').polyfill();
 require('isomorphic-fetch');

引入这两个文件,就可以支持主流浏览器了

2. API

   fetch(url,{ // url: 请求地址
        method: "GET", // 请求的方法POST/GET等
        headers : { // 请求头(可以是Headers对象,也可是JSON对象)
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        }, 
        body: , // 请求发送的数据 blob、BufferSource、FormData、URLSearchParams(get或head方法中不能包含body)
        cache : 'default', // 是否缓存这个请求
        credentials : 'same-origin', //要不要携带 cookie 默认不携带 omit、same-origin 或者 include
        mode : "", 
        /*  
            mode,给请求定义一个模式确保请求有效
            same-origin:只在请求同域中资源时成功,其他请求将被拒绝(同源策略)
            cors : 允许请求同域及返回CORS响应头的域中的资源,通常用作跨域请求来从第三方提供的API获取数据
            cors-with-forced-preflight:在发出实际请求前执行preflight检查
            no-cors : 目前不起作用(默认)

        */
    }).then(resp => {
        /*
            Response 实现了 Body, 可以使用 Body 的 属性和方法:

            resp.type // 包含Response的类型 (例如, basic, cors).

            resp.url // 包含Response的URL.

            resp.status // 状态码

            resp.ok // 表示 Response 的成功还是失败

            resp.headers // 包含此Response所关联的 Headers 对象 可以使用

            resp.clone() // 创建一个Response对象的克隆

            resp.arrayBuffer() // 返回一个被解析为 ArrayBuffer 格式的promise对象

            resp.blob() // 返回一个被解析为 Blob 格式的promise对象

            resp.formData() // 返回一个被解析为 FormData 格式的promise对象

            resp.json() // 返回一个被解析为 Json 格式的promise对象

            resp.text() // 返回一个被解析为 Text 格式的promise对象
        */ 
        if(resp.status === 200) return resp.json(); 
        // 注: 这里的 resp.json() 返回值不是 js对象,通过 then 后才会得到 js 对象
        throw New Error ('false of json');
    }).then(json => {
        console.log(json);
    }).catch(error => {
        consolr.log(error);
    })

3. 常用情况

  1. 请求 json
    fetch('http://xxx/xxx.json').then(res => {
        return res.json();
    }).then(res => {
        console.log(res);
    })
  1. 请求文本
    fetch('/xxx/page').then(res => {
        return res.text();
    }).then(res => {
        console.log(res);
    })
  1. 发送普通 json 数据
    fetch('/xxx', {
        method: 'post',
        body: JSON.stringify({
            username: '',
            password: ''
        })
    });

  1. 发送form 表单数据
    var form = document.querySelector('form');
    fetch('/xxx', {
        method: 'post',
        body: new FormData(form)
    });
  1. 获取图片

URL.createObjectURL()

    fetch('/xxx').then(res => {
        return res.blob();
    }).then(res => {
        document.querySelector('img').src = URL.createObjectURL(imageBlob);
    })
  1. 上传
    var file = document.querySelector('.file')
    var data = new FormData()
    data.append('file', file.files[0])
    fetch('/xxx', {
      method: 'POST',
      body: data
    })

  1. 封装
    require('es6-promise').polyfill();
    require('isomorphic-fetch');

    export default function request(method, url, body) {
        method = method.toUpperCase();
        if (method === 'GET') {
            body = undefined;
        } else {
            body = body && JSON.stringify(body);
        }

        return fetch(url, {
            method,
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            },
            body
        }).then((res) => {
            if (res.status >= 200 && res.status < 300) {
                return res;
            } else {
                return Promise.reject('请求失败!');
            }
        })
    }

    export const get = path => request('GET', path);
    export const post = (path, body) => request('POST', path, body);
    export const put = (path, body) => request('PUT', path, body);
    export const del = (path, body) => request('DELETE', path, body);

注意:ajax和fetch的区别是啥

Ajax
利用的是XMLHttpRequest对象来请求数据的。

fetch
window的一个方法 主要特点是
1、第一个参数是URL
2、第二个参数可选参数 可以控制不同的init对象
3、使用了js 中的promise对象

fetch(url).then(function (response) {
    return response.json()   //执行成功第一步
}).then(function (returnedValue) {
    //执行成功的第二步
}).catch(function (err) {
    //中途的任何地方出错  都会在这里被捕获到
})

注意:
fetch 的第二参数中
1、默认的请求为get请求 可以使用method:post 来进行配置
2、第一步中的 response有许多方法 json() text() formData()
3、Fetch跨域的时候默认不会带cookie 需要手动的指定 credentials:‘include’

使用fetch之后得到的是一个promise对象 在这个promise对象里边再定义执行成功的时候是什么。下面是正确的fetch的使用方法

var promise=fetch('http://localhost:3000/news', {
        method: 'get'
    }).then(function(response) {
             return  response.json()
    }).catch(function(err) {
        // Error :(
    });
    promise.then(function (data) {
          console.log(data)
    }).catch(function (error) {
        console.log(error)
    })

fetch和ajax 的主要区别
1、fetch()返回的promise将不会拒绝http的错误状态,即使响应是一个HTTP 404或者500
2、在默认情况下 fetch不会接受或者发送cookies

使用fetch开发项目的时候的问题
1、所有的IE浏览器都不会支持 fetch()方法
2、服务器端返回 状态码 400 500的时候 不会reject

36:js的继承

既然要实现继承,那么首先我们得有一个父类,代码如下:

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

1、原型链继承
核心: 将父类的实例作为子类的原型

//当超类中包含引用类型属性值时,其中一个子类的多个实例中,只要其中一个实例引用属性只发生修改一个修改,其他实例的引用类型属性值也会立即发生改变
//原因是超类的属性变成子类的原型属性,也就是说当一个实例化的属性数值发生改变的时候另一个实例的数值也是会发生改变的
function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true

特点:

非常纯粹的继承关系,实例是子类的实例,也是父类的实例
父类新增原型方法/原型属性,子类都能访问到
简单,易于实现

缺点:

要想为子类新增属性和方法,可以在Cat构造函数中,为Cat实例增加实例属性。如果要新增原型属性和方法,则必须放在new Animal()这样的语句之后执行。
来自原型对象的所有属性被所有实例共享。(来自原型对象的引用属性是所有实例共享的
创建子类实例时,无法向父类构造函数传参

推荐指数:★★(3、4两大致命缺陷)

2、构造继承
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特点:

解决了1中,子类实例共享父类引用属性的问题
创建子类实例时,可以向父类传递参数
可以实现多继承(call多个父类对象)

缺点:

实例并不是父类的实例,只是子类的实例
只能继承父类的实例属性和方法,不能继承原型属性/方法
无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
推荐指数:★★(缺点3)

3、组合继承
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
  Animal.call(this);//第一次调用
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();//第二次调用

Cat.prototype.constructor = Cat;  //弥补重写原型失去的默认constructor属性

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

特点:

弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
既是子类的实例,也是父类的实例
不存在引用属性共享问题
可传参
函数可复用
缺点:

调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
推荐指数:★★★★(仅仅多消耗了一点内存)

4、寄生组合继承
核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

Cat.prototype.constructor = Cat; // 需要修复下构造函数

特点:

堪称完美
缺点:

实现较为复杂
推荐指数:★★★★(实现复杂,扣掉一颗星)

组合继承和寄生组合继承的区别

组合继承:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

38:js的原型链和原型

构造函数
构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象。每个构造函数都有prototype(原型)属性
在这里插入图片描述

原型模式
每个函数都有prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含特定类型的所有实例共享的属性和方法,即这个原型对象是用来给实例共享属性和方法的。
而每个实例内部都有一个指向原型对象的指针。
在这里插入图片描述

原型链
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含指向原型对象内部的指针。我们让原型对象的实例(1)等于另一个原型对象(2),
此时原型对象(2)将包含一个指向原型对象(1)的指针,
再让原型对象(2)的实例等于原型对象(3),如此层层递进就构成了实例和原型的链条,这就是原型链的概念
在这里插入图片描述
先看看 JS 内置构造器

var obj = {name: 'jack'}
var arr = [1,2,3]
var reg = /hello/g
var date = new Date
var err = new Error('exception')
 
console.log(obj.__proto__ === Object.prototype) // true
console.log(arr.__proto__ === Array.prototype)  // true
console.log(reg.__proto__ === RegExp.prototype) // true
console.log(date.__proto__ === Date.prototype)  // true
console.log(err.__proto__ === Error.prototype)  // true

再看看自定义的构造器,这里定义了一个 Person:

function Person(name) {
  this.name = name;
}
var p = new Person('jack')
console.log(p.__proto__ === Person.prototype) // true

p 是 Person 的实例对象,p 的内部原型总是指向其构造器 Person 的原型对象 prototype。

每个对象都有一个 constructor 属性,可以获取它的构造器,因此以下打印结果也是恒等的:

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('jack')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true

可以看到p.__proto__与Person.prototype,p.constructor.prototype都是恒等的,即都指向同一个对象。

如果换一种方式设置原型,结果就有些不同了:

function Person(name) {
    this.name = name
}
// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('jack')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // false

这里直接重写了 Person.prototype(注意:上一个示例是修改原型)。输出结果可以看出p.__proto__仍然指向的是Person.prototype,而不是p.constructor.prototype。
这也很好理解,给Person.prototype赋值的是一个对象直接量{getName: function(){}},使用对象直接量方式定义的对象其构造器(constructor)指向的是根构造器Object,Object.prototype是一个空对象{},{}自然与{getName: function(){}}不等

原型和原型链是JS实现继承的一种模型。
原型链的形成是真正是靠__proto__ 而非prototype

要深入理解这句话,我们再举个例子,看看前面你真的理解了吗?

 var animal = function(){};
 var dog = function(){};

 animal.price = 2000;
 dog.prototype = animal;
 var tidy = new dog();
 console.log(dog.price) //undefined
 console.log(tidy.price) // 2000

这里解释一下:

var dog = function(){};
 dog.prototype.price = 2000;
 var tidy = new dog();
 console.log(tidy.price); // 2000
 console.log(dog.price); //undefined
var dog = function(){};
 var tidy = new dog();
 tidy.price = 2000;
 console.log(dog.price); //undefined

这个明白吧?想一想我们上面说过这句话:

实例(tidy)和 原型对象(dog.prototype)存在一个连接。不过,要明确的真正重要的一点就是,这个连接存在于实例(tidy)与构造函数的原型对象(dog.prototype)之间,而不是存在于实例(tidy)与构造函数(dog)之间。

35:webpack中的常考的面试题

1、webpack打包原理
把所有依赖打包成一个 bundle.js 文件,通过代码分割成单元片段并按需加载

2、webpack的优势
(1) webpack 是以 commonJS 的形式来书写脚本滴,但对 AMD/CMD 的支持也很全面,方便旧项目进行代码迁移。
(2)能被模块化的不仅仅是 JS 了。
(3) 开发便捷,能替代部分 grunt/gulp的工作,比如打包、压缩混淆、图片转base64等。
(4)扩展性强,插件机制完善

3、webpack 和 gulp 的区别

webpack是一个模块打包器,基于入口的,强调的是一个前端模块化方案,更侧重模块打包,我们可以把开发中的所有资源都看成是模块,通过loader和plugin对资源进行处理。

grunt和gulp是基于任务和流

gulp是一个前端自动化构建工具,强调的是前端开发的工作流程,可以通过配置一系列的task,第一task处理的事情(如代码压缩,合并,编译以及浏览器实时更新等)。然后定义这些执行顺序,来让glup执行这些task,从而构建项目的整个开发流程。自动化构建工具并不能把所有的模块打包到一起,也不能构建不同模块之间的依赖关系。

4、什么是bundle,什么是chunk,什么是module
bundle:是由webpack打包出来的文件
chunk:是指webpack在进行模块依赖分析的时候,代码分割出来的代码块
module:是开发中的单个模块

5、什么是loader,什么是plugin
loader用于加载某些资源文件。因为webpack本身只能打包common.js规范的js文件,对于其他资源如css,img等,是没有办法加载的,这时就需要对应的loader将资源转化,从而进行加载。

plugin用于扩展webpack的功能。不同于loader,plugin的功能更加丰富,比如压缩打包,优化,不只局限于资源的加载。

1:UglifyJsPlugin: 压缩代码
2:HotModuleReplacementPlugin 自动刷新
3:HtmlWebpackPlugin 依据一个简单的index.html模版,生成一个自动引用你打包后的js文件的新index.html
4:ExtractTextWebpackPlugin 它会将入口中引用css文件,都打包都独立的css文件中,而不是内嵌在js打包文件中:
5:Tree-shaking 指在打包中去除那些引入了,但是在代码中没有被用到的那些死代码
6:在webpack中Tree-shaking是通过uglifySPlugin来Tree-shaking,Css需要使用Purify-CSS

6、有哪些常见的Loader?他们是解决什么问题的?

Loader】:用于对模块源码的转换,loader描述了webpack如何处理非javascript模块,并且在buld中引入这些依赖。loader可以将文件从不同的语言(如TypeScript)转换为JavaScript,或者将内联图像转换为data URL。比如说:CSS-Loader,Style-Loader等。

file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
source-map-loader:加载额外的 Source Map 文件,以方便断点调试
image-loader:加载并且压缩图片文件
babel-loader:把 ES6 转换成 ES5
css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
eslint-loader:通过 ESLint 检查 JavaScript 代码

7、有哪些常见的Plugin?他们是解决什么问题的?

Plugin】:目的在于解决loader无法实现的其他事,从打包优化和压缩,到重新定义环境变量,功能强大到可以用来处理各种各样的任务。webpack提供了很多开箱即用的插件:CommonChunkPlugin主要用于提取第三方库和公共模块,避免首屏加载的bundle文件,或者按需加载的bundle文件体积过大,导致加载时间过长,是一把优化的利器。而在多页面应用中,更是能够为每个页面间的应用程序共享代码创建bundle。

define-plugin:定义环境变量
commons-chunk-plugin:提取公共代码
uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码

8、webpack-dev-server和http服务器如nginx有什么区别?
webpack-dev-server使用内存来存储webpack开发环境下的打包文件,并且可以使用模块热更新,比传统的http服务对开发更加简单高效

9、什么 是模块热更新?
模块热更新是webpack的一个功能,他可以使得代码修改过后不用刷新浏览器就可以更新,是高级版的自动刷新浏览器。

10、如何自动生成webpack配置?
webpack-cli、vue-cli

11、什么是tree-shaking? css可以tree-shaking吗?
指打包中去除那些引入了但在代码中没用到的死代码。在wepack中js treeshaking通过UglifyJsPlugin来进行,css中通过purify-CSS来进行.

12、webpack的入口文件怎么配置,多个入口怎么分割?
webpack.config.js

const path = require('path');
module.exports={
    //入口文件的配置项
    entry:{
        entry:'./src/entry.js',
        //这里我们又引入了一个入口文件
        entry2:'./src/entry2.js'
    },
    //出口文件的配置项
    output:{
        //输出的路径,用了Node语法
        path:path.resolve(__dirname,'dist'),
        //输出的文件名称
        filename:'[name].js'
    },
    //模块:例如解读CSS,图片如何转换,压缩
    module:{},
    //插件,用于生产模版和各项功能
    plugins:[],
    //配置webpack开发服务功能
    devServer:{}
}

entry:配置入口文件的地址,可以是单一入口,也可以是多入口。
output:配置出口文件的地址,在webpack2.X版本后,支持多出口配置。
module:配置模块,主要是解析CSS和图片转换压缩等功能。
plugins:配置插件,根据你的需要配置不同功能的插件。
devServer:配置开发服务功能

13、webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

1:初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
2:开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
3:确定入口:根据配置中的 entry 找出所有的入口文件;
4:编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
5:完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
6:输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
7:输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

36:babel的原理

Babel 工作过程
了解了 AST 是什么样的,就可以开始研究 Babel 的工作过程了。

上面说过,Babel 的功能很纯粹,它只是一个编译器。

大多数编译器的工作过程可以分为三部分:

Parse(解析):将源代码转换成更加抽象的表示方法(例如抽象语法树)
Transform(转换):对(抽象语法树)做一些特殊处理,让它符合编译器的期望
Generate(代码生成):将第二步经过转换过的(抽象语法树)生成新的代码

嗯… 既然 Babel 是一个编译器,当然它的工作过程也是这样的。我们来仔细看看这三步分别做了什么事。当然,还是拿上面的🌰来说明 const add = (a, b) => a + b,看看它是如何经过 Babel 变成:

var add = function add(a, b) {
  return a + b;
};

Parse(解析)

一般来说,Parse 阶段可以细分为两个阶段:词法分析(Lexical Analysis, LA)和语法分析(Syntactic Analysis, SA)。

词法分析
词法分析阶段可以看成是对代码进行“分词”,它接收一段源代码,然后执行一段 tokenize 函数,把代码分割成被称为Tokens 的东西。Tokens 是一个数组,由一些代码的碎片组成,比如数字、标点符号、运算符号等等等等,例如这样:

[
    { "type": "Keyword", "value": "const" },
    { "type": "Identifier", "value": "add" },
    { "type": "Punctuator", "value": "=" },
    { "type": "Punctuator", "value": "(" },
    { "type": "Identifier", "value": "a" },
    { "type": "Punctuator", "value": "," },
    { "type": "Identifier", "value": "b" },
    { "type": "Punctuator", "value": ")" },
    { "type": "Punctuator", "value": "=>" },
    { "type": "Identifier", "value": "a" },
    { "type": "Punctuator", "value": "+" },
    { "type": "Identifier", "value": "b" }
]

语法分析

词法分析之后,代码就已经变成了一个 Tokens 数组了,现在需要通过语法分析把 Tokens 转化为上面提到过的 AST。

Transform(转换)

这一步做的事情也很简单,就是操作 AST。如果忘记了 AST 是什么,可以回到上面再看看。

我们可以看到 AST 中有很多相似的元素,它们都有一个 type 属性,这样的元素被称作节点。一个节点通常含有若干属性,可以用于描述 AST 的部分信息。

比如这是一个最常见的 Identifier 节点:

{
    type: 'Identifier',
    name: 'add'
}

表示这是一个标识符。

所以,操作 AST 也就是操作其中的节点,可以增删改这些节点,从而转换成实际需要的 AST。
Babel 对于 AST 的遍历是深度优先遍历,对于 AST 上的每一个分支 Babel 都会先向下遍历走到尽头,然后再向上遍历退出刚遍历过的节点,然后寻找下一个分支。

Generate(代码生成)

经过上面两个阶段,需要转译的代码已经经过转换,生成新的 AST 了,最后一个阶段理所应当就是根据这个 AST 来输出代码。

Babel 是通过 https://github.com/babel/babel/tree/master/packages/babel-generator 来完成的。当然,也是深度优先遍历。

class Generator extends Printer {
  constructor(ast, opts = {}, code) {
    const format = normalizeOptions(code, opts);
    const map = opts.sourceMaps ? new SourceMap(opts, code) : null;
    super(format, map);
    this.ast = ast;
  }
  ast: Object;
  generate() {
    return super.generate(this.ast);
  }
}

经过这三个阶段,代码就被 Babel 转译成功了。

35:JS常用设计模式

单例模式

在执行当前 Single 只获得唯一一个对象
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。

var Single = (function(){
    var instance;
    function init() {
        // 定义私有方法和属性
        // 操作逻辑
        return {
           // 定义公共方法和属性
        };
    }
    return {
        // 获取实例
        getInstance:function(){
            if(!instance){
                instance = init();
            }
            return instance;
        }
    }
})();

var obj1 = Single.getInstance();
var obj2 = Single.getInstance();
console.log(obj1 === obj2);

工厂模式

工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式。

因为工厂模式就相当于创建实例对象的new,我们经常要根据类Class生成实例对象,如A a=new A() 工厂模式也是用来创建实例对象的,所以以后new时就要多个心眼,是否可以考虑使用工厂模式,虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。

function Animal(opts){
    var obj = new Object();
    obj.color = opts.color;
    obj.name= opts.name;
    obj.getInfo = function(){
        return '名称:'+ onj.name+', 颜色:'+ obj.color;
    }
    return obj;
}
var cat = Animal({name: '波斯猫', color: '白色'});
cat.getInfo();

构造函数模式
  ECMAScript中的构造函数可用来创建特定类型的对象,像Array和Object这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象的属性和方法。使用构造函数的方法,既解决了重复实例化的问题,又解决了对象识别的问题。

function Animal(name, color){
    this.name = name;
    this.color = color;
    this.getName = function(){
        return this.name;
    }
}
// 实例一个对象
var cat = new Animal('猫', '白色');
console.log( cat.getName() );

订阅/发布模式(subscribe & publish)

text属性变化了,set方法触发了,但是文本节点的内容没有变化。 如何才能让同样绑定到text的文本节点也同步变化呢? 这里又有一个知识点: 订阅发布模式。
  订阅发布模式又称为观察者模式,定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有的观察者对象。
发布者发出通知 =>主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作。

// 一个发布者 publisher,功能就是负责发布消息 - publish
        var pub = {
            publish: function () {
                dep.notify();
            }
        }
        // 多个订阅者 subscribers, 在发布者发布消息之后执行函数
        var sub1 = { 
            update: function () {
                console.log(1);
            }
        }
        var sub2 = { 
            update: function () {
                console.log(2);
            }
        }
        var sub3 = { 
            update: function () {
                console.log(3);
            }
        }
        // 一个主题对象
        function Dep() {
            this.subs = [sub1, sub2, sub3];
        }
        Dep.prototype.notify = function () {
            this.subs.forEach(function (sub) {
                sub.update();
            });
        }

        // 发布者发布消息, 主题对象执行notify方法,进而触发订阅者执行Update方法
        var dep = new Dep();
        pub.publish();

代理模式

代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。代理模式使得代理对象控制具体对象的引用。代理几乎可以是任何对象:文件,资源,内存中的对象,或者是一些难以复制的东西。

代理模式在前端有一个很好的使用场景-图片懒加载。在真正的图片还没有下载时,先通过一张loading图来显示,然后等到具体的图片下载完成之后再修改img的src属性。

以下是一个通过代理模式完成图片懒加载的方法。

在这里插入图片描述

35:前端模块化开发以及CommonJS,AMD和CMD的区别

什么是模块化开发?

一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。模块开发需要遵循一定的规范,否则就都乱套了。

在模块化开发思想下,所有单独存在的某一个文件都是一个模块。

CommonJS

CommonJS是服务器端模块的规范,Node.js采用了这个规范。Node.JS首先采用了js模块化的概念。

根据CommonJS规范,一个单独的文件就是一个模块。

输出模块:module.exports

输入模块:module.imports

AMD

AMD是一个在浏览器端模块化开发的规范。模块将被异步加载,模块加载不影响后面语句的运行。所有依赖某些模块的语句均放置在回调函数中。

优点:异步并行加载,在AMD的规范下,同时异步加载是不会产生错误的。

目前,实现AMD的库有RequireJS 、curl 、Dojo 、Nodules 等。

定义模块:

AMD规范只定义了一个函数 define,它是全局变量。函数的描述为:

define(id?, dependencies?, factory);

id:指定义中模块的名字,可选;

**dependencies:**依赖参数,可选的,如果忽略,它默认为[“require”, “exports”, “module”]。

**Factory:**模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。

例(config.js):

require.config({
    baseUrl:'js/',
    paths:{
        'jquery':'jquery-3.1.1',
        'index':'index'
    }
});

(index.js)

function show() {
    console.log("hello");
}

引用模块

引入require.js文件

require.js中文网:http://www.requirejs.cn/docs/download.html

<script src="js/require.js" data-main="js/config"></script>

data-main:引入定义的模块

在引入require.js后使用模块:

<script>
    require(['index'],function () {
        show();
    })
</script>

运行在浏览器上,会打印出hello
在这里插入图片描述
required.js就是为了解决这两个问题而诞生的:

1.实现js文件的异步加载,避免网页失去响应;

2.管理模块之间的依赖性,便于代码的编写和维护。

CMD

CMD(Common Module Definition)通用模块定义。该规范是在国内发展出来的。

在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

define(factory);

factory 为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory 方法在执行时,默认会传入三个参数:require、exports 和 module:

define(function(require, exports, module) {
  // 模块代码
});

导出模块:export

导入模块:require

CMD规范地址:https://github.com/seajs/seajs/issues/242

Common.js,AMD,CMD的对比
在这里插入图片描述
RequireJS和common.js的三大区别:

1.Required.Js运行在浏览器上(客户端),CommonJS设计的时候没有考虑浏览器,所以它不适合浏览器环境,运行在服务器上

2.执行顺序不一样,common自动生成依赖关系,RequireJS手动执行依赖关系

3.Require.js主要提供define和require两个方法来进行模块化编程

commonJs通过module.imports, module.exports来进行模块化编程

AMD和CMD的区别:

1.AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。

CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。

2.对于依赖的模块

AMD:提前执行(异步加载:依赖先执行)+延迟执行

CMD:延迟执行(运行到需加载,根据顺序执行)

3.CMD 推崇依赖就近,AMD 推崇依赖前置。看如下代码:


// CMD
 
define(function(require, exports, module) {
 
var a = require('./a')
 
a.doSomething()
 
// 此处略去 n 行
 
var b = require('./b') // 依赖可以就近书写
 
b.doSomething()
 
// ...
 
})
 
 
 
// AMD 默认推荐的是
 
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
 
a.doSomething()
 
// 此处略去 n 行
 
b.doSomething()
 
...
 
})

  1. AMD:API根据使用范围有区别,但使用同一个api接口

CMD:每个API的职责单一

SeaJS与RequireJS的区别:

SeaJS对模块的态度是懒执行, 而RequireJS对模块的态度是预执行

ES模块与CommonJS模块的差异

对于 CommonJS 和 ES6 中的模块化的两者区别是:

前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持。

前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响。

前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化

详细的:https://www.cnblogs.com/mengfangui/p/9067111.html

vue常考知识点

1:vue的生命周期

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

2:created(创建后) 初始化界面后 : 在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数,也就是不会更新视图,SSR可以放这里。

3:beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。完成模板编译,虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated

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

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

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

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

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

.第一次页面加载会触发哪几个钩子?
答:会触发 下面这几个beforeCreate, created, beforeMount, mounted 。

.DOM 渲染在 哪个周期中就已经完成?
答:DOM 渲染在 mounted 中就已经完成了。

另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated钩子函数。

vue生命周期的应用场景:
beforeCreate 可以在此时加一些loading效果,在created时进行移除

created 需要ajax异步请求数据的方法可以在此时执行,完成数据的初始化

mounted 当需要操作dom的时候执行,可以配合$.nextTick 使用进行单一事件对数据的更新后更新dom

updated 当数据更新需要做统一业务处理的时候使用

2:对MVVM开发模式的理解

MVVM分为Model、View、ViewModel三者。

Model 代表数据模型,数据和业务逻辑都在Model层中定义;
View 代表UI视图,负责数据的展示;
ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;
Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。

这种模式实现了 Model 和 View 的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己操作 dom。

3:vue的双向绑定

1.vue双向绑定原理

vue实现双向数据绑定的原理就是利用了 Object.defineProperty() 这个方法重新定义了**对象获取属性值(get)和设置属性值(set)**的操作来实现的。

vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

当一个Vue实例创建时,vue会遍历data选项的属性,用 Object.defineProperty 将它们转为 getter/setter并且在内部追踪相关依赖,在属性被访问和修改时通知变化。

每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新

当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。

我们先来看Object.defineProperty()这个方法:

var obj  = {};
Object.defineProperty(obj, 'name', {
        get: function() {
            console.log('我被获取了')
            return val;
        },
        set: function (newVal) {
            console.log('我被设置了')
        }
})
obj.name = 'fei';//在给obj设置name属性的时候,触发了set这个方法
var val = obj.name;//在得到obj的name属性,会触发get方法

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,那么在设置或者获取的时候我们就可以在get或者set方法里假如其他的触发函数,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一。

2.实现最简单的双向绑定

我们知道通过Object.defineProperty()可以实现数据劫持,是的属性在赋值的时候触发set方法

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="demo"></div>
    <input type="text" id="inp">
    <script>
        var obj  = {};
        var demo = document.querySelector('#demo')
        var inp = document.querySelector('#inp')
        Object.defineProperty(obj, 'name', {
            get: function() {
                return val;
            },
            set: function (newVal) {//当该属性被赋值的时候触发
                inp.value = newVal;
                demo.innerHTML = newVal;
            }
        })
        inp.addEventListener('input', function(e) {
            // 给obj的name属性赋值,进而触发该属性的set方法
            obj.name = e.target.value;
        });
        obj.name = 'fei';//在给obj设置name属性的时候,触发了set这个方法
    </script>
</body>
</html>

当然要是这么粗暴,肯定不行,性能会出很多的问题。

3.讲解vue如何实现

先看原理图
在这里插入图片描述
3.1 observer用来实现对每个vue中的data中定义的属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新。

3.2 我们介绍为什么要订阅者,在vue中v-model,v-name,{{}}等都可以对数据进行显示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变,于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个Dep中增加一个订阅者,其订阅者只是更新自己的指令对应的数据,也就是v-model='name’和{{name}}有两个对应的订阅者,各自管理自己的地方。每当属性的set方法触发,就循环更新Dep中的订阅者。

Proxy与Object.defineProperty()的对比

Proxy的优点:

  1. 可以直接监听对象而非属性,并返回一个新对象

  2. 可以直接监听数值的变化

  3. 可以劫持整个对象,并返回一个新对象

Proxy的缺点:
Proxy是es6提供的新特性,兼容性不好,所以导致Vue3一致没有正式发布让让广大开发者使用

Object.defineProperty的优点:
E9以下的版本不兼容

Object.defineProperty的缺点:
1. 只能劫持对象的属性,我们需要对每个对象的每个属性进行遍历,无法监控到数组下标的变化,导致直接通
2.

4.vue代码实现

4.1 observer实现,主要是给每个vue的属性用Object.defineProperty(),代码如下:

function defineReactive (obj, key, val) {
    var dep = new Dep();
        Object.defineProperty(obj, key, {
             get: function() {
                    //添加订阅者watcher到主题对象Dep
                    if(Dep.target) {
                        // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
                        dep.addSub(Dep.target);
                    }
                    return val;
             },
             set: function (newVal) {
                    if(newVal === val) return;
                    val = newVal;
                    console.log(val);
                    // 作为发布者发出通知
                    dep.notify();//通知后dep会循环调用各自的update方法更新视图
             }
       })
}
        function observe(obj, vm) {
            Object.keys(obj).forEach(function(key) {
                defineReactive(vm, key, obj[key]);
            })
        }

4.2实现compile

compile的目的就是解析各种指令称真正的html。

function Compile(node, vm) {
    if(node) {
        this.$frag = this.nodeToFragment(node, vm);
        return this.$frag;
    }
}
Compile.prototype = {
    nodeToFragment: function(node, vm) {
        var self = this;
        var frag = document.createDocumentFragment();
        var child;
        while(child = node.firstChild) {
            console.log([child])
            self.compileElement(child, vm);
            frag.append(child); // 将所有子节点添加到fragment中
        }
        return frag;
    },
    compileElement: function(node, vm) {
        var reg = /\{\{(.*)\}\}/;
        //节点类型为元素(input元素这里)
        if(node.nodeType === 1) {
            var attr = node.attributes;
            // 解析属性
            for(var i = 0; i < attr.length; i++ ) {
                if(attr[i].nodeName == 'v-model') {//遍历属性节点找到v-model的属性
                    var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                    node.addEventListener('input', function(e) {
                        // 给相应的data属性赋值,进而触发该属性的set方法
                        vm[name]= e.target.value;
                    });
                    new Watcher(vm, node, name, 'value');//创建新的watcher,会触发函数向对应属性的dep数组中添加订阅者,
                }
            };
        }
        //节点类型为text
        if(node.nodeType === 3) {
            if(reg.test(node.nodeValue)) {
                var name = RegExp.$1; // 获取匹配到的字符串
                name = name.trim();
                new Watcher(vm, node, name, 'nodeValue');
            }
        }
    }
}

4.3 watcher实现

function Watcher(vm, node, name, type) {
    Dep.target = this;
    this.name = name;
    this.node = node;
    this.vm = vm;
    this.type = type;
    this.update();
    Dep.target = null;
}

Watcher.prototype = {
    update: function() {
        this.get();
        this.node[this.type] = this.value; // 订阅者执行相应操作
    },
    // 获取data的属性值
    get: function() {
        console.log(1)
        this.value = this.vm[this.name]; //触发相应属性的get
    }
}

4.4 实现Dep来为每个属性添加订阅者

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
        sub.update();
        })
    }
}

这样一来整个数据的双向绑定就完成了。

5.梳理

首先我们为每个vue属性用**Object.defineProperty()**实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会,接着为input会添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。

那为何Object.defineProperty()不能实现对数组下标监听的变化呢
答案:因为性能问题,在源码中对数据进行了判断,本身是可以进行监听的,但是这个开销有点大,不如数组的操作方法

4:v-if 和 v-show 有什么区别?

v-show 仅仅控制元素的显示方式,将 display 属性在 block 和 none 来回切换;而v-if会控制这个 DOM 节点的存在与否。当我们需要经常切换某个元素的显示/隐藏时,使用v-show会更加节省性能上的开销;当只需要一次显示或隐藏时,使用v-if更加合理。

5:父子组件之间的传值和通信

父组件向子组件传值:

1)子组件在props中创建一个属性,用来接收父组件传过来的值;

2)在父组件中注册子组件;

3)在子组件标签中添加子组件props中创建的属性;

4)把需要传给子组件的值赋给该属性

子组件向父组件传值:

1)子组件中需要以某种方式(如点击事件)的方法来触发一个自定义的事件;

2)将需要传的值作为$emit的第二个参数,该值将作为实参传给响应事件的方法;

3)在父组件中注册子组件并在子组件标签上绑定自定义事件的监听。

父组件调用一个子组件,父组件的属性如何能够传递给子组件使用,子组件里的数据如何能传递给父组件?下面我们通过一个demo来解答这个问题

父组件

<template>
  <div class="parent">
    我是父组件
    <!--父组件监听子组件触发的say方法,调用自己的parentSay方法-->
    <!--通过:msg将父组件的数据传递给子组件-->
    <children :msg="msg" @say="parentSay"></children>
  </div>
</template>

<script>

import Children from './children.vue'

export default {
  data () {
    return {
      msg: 'hello, children'
    }
  },

  methods: {
      // 参数就是子组件传递出来的数据
      parentSay(msg){
          console.log(msg) // hello, parent
      }
  },

  // 引入子组件
  components:{
      children: Children
  }
}
</script>

子组件

<template>
  <div class="hello">
    <div class="children" @click="say">
      我是子组件
      <div>
        父组件对我说:{{msg}}
      </div>
    </div>

  </div>
</template>

<script>

  export default {
      //父组件通过props属性传递进来的数据
      props: {
          msg: {
              type: String,
              default: () => {
                  return ''
              }
          }
      },
      data () {
        return {
            childrenSay: 'hello, parent'
        }
      },

      methods: {
          // 子组件通过emit方法触发父组件中定义好的函数,从而将子组件中的数据传递给父组件
          say(){
              this.$emit('say' , this.childrenSay);
          }
      }
  }
</script>

结果
在这里插入图片描述
总结
vue的父子组件间通信可以总结成一句话:
父组件通过 prop 给子组件下发数据,子组件通过$emit触发事件给父组件发送消息,即 prop 向下传递,事件向上传递。

6:Vue的路由实现:hash模式 和 history模式

hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;
特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xxx.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。

浏览器的历史记录栈
概述

浏览器窗口有一个history对象,用来保存浏览历史。

如果当前窗口先后访问了三个网址,那么history对象就包括三项,history.length属性等于3。

history.length // 3

history对象提供了一系列方法,允许在浏览历史之间移动。

back():移动到上一个访问页面,等同于浏览器的后退键。

forward():移动到下一个访问页面,等同于浏览器的前进键。

go():接受一个整数作为参数,移动到该整数指定的页面,比如go(1)相当于forward(),go(-1)相当于back()。

history.back();

history.forward();

history.go(-2);

如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是默默的失败。

history.go(0)相当于刷新当前页面。

之前是是对浏览器中的历史记录栈的解释

history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”

Vue-router 中hash模式和history模式的关系

在vue的路由配置中有mode选项 最直观的区别就是在url中 hash 带了一个很丑的 # 而history是没有#的

mode:“hash”;
mode:“history”;

hash模式和history模式的不同

对于vue这类渐进式前端开发框架,为了构建 SPA(单页面应用),需要引入前端路由系统,这也就是 Vue-Router 存在的意义。前端路由的核心,就在于 —— 改变视图的同时不会向后端发出请求。

为了达到这一目的,浏览器当前提供了以下两种支持:

hash —— 即地址栏 URL 中的 # 符号(此 hash 不是密码学里的散列运算)。比如这个 URL:http://www.abc.com/#/hello,hash 的值为 #/hello。它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
history —— 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定浏览器支持)这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
因此可以说,hash 模式和 history 模式都属于浏览器自身的特性,Vue-Router 只是利用了这两个特性(通过调用浏览器提供的接口)来实现前端路由.

使用场景

一般场景下,hash 和 history 都可以,除非你更在意颜值,# 符号夹杂在 URL 里看起来确实有些不太美丽。

如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成URL 跳转而无须重新加载页面。

另外,根据 Mozilla Develop Network 的介绍,调用 history.pushState() 相比于直接修改 hash,存在以下优势:

pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
pushState() 可额外设置 title 属性供后续使用。
当然啦,history 也不是样样都好。SPA 虽然在浏览器里游刃有余,但真要通过 URL 向后端发起 HTTP 请求时,两者的差异就来了。尤其在用户手动输入 URL 后回车,或者刷新(重启)浏览器的时候。

个人在接入微信的一个活动开发过程中 开始使用的hash模式,但是后面后端无法获取到我#后面的url参数,于是就把参数写在#前面,但是讨论后还是决定去掉这个巨丑的#

于是乎改用history模式,但是开始跑流程的时候是没问题,但是后来发现跳转后刷新或者回跳,会报一个404的错误,找不到指定的路由,最后后端去指向正确的路由 加了/hd/xxx 去匹配是否有这个/hd/{:path} 才得以解决

这两个api的使用方法

history.pushState方法接受三个参数,依次为:

state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

假定当前网址是example.com/1.html,我们使用pushState方法在浏览记录(history对象)中添加一个新记录。

var stateObj = { foo: ‘bar’ };

history.pushState(stateObj, ‘page 2’, ‘2.html’);

添加上面这个新记录后,浏览器地址栏立刻显示example.com/2.html,但并不会跳转到2.html,甚至也不会检查2.html是否存在,它只是成为浏览历史中的最新记录。假定这时你访问了google.com,然后点击了倒退按钮,页面的url将显示2.html,但是内容还是原来的1.html。你再点击一次倒退按钮,url将显示1.html,内容不变。

总之,pushState方法不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应。

如果pushState的url参数,设置了一个新的锚点值(即hash),并不会触发hashchange事件。如果设置了一个跨域网址,则会报错。

// 报错

history.pushState(null, null, ‘https://twitter.com/hello’);

上面代码中,pushState想要插入一个跨域的网址,导致报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上。

history.replaceState()

history.replaceState方法的参数与pushState方法一模一样,区别是它修改浏览历史中当前纪录。

假定当前网页是example.com/example.html。

history.pushState({page: 1}, ‘title 1’, ‘?page=1’);

history.pushState({page: 2}, ‘title 2’, ‘?page=2’);

history.replaceState({page: 3}, ‘title 3’, ‘?page=3’);

history.back()

// url显示为http://example.com/example.html?page=1

history.back()

// url显示为http://example.com/example.html

history.go(2)

// url显示为http://example.com/example.html?page=3

history.state属性

history.state属性返回当前页面的state对象。

history.pushState({page: 1}, ‘title 1’, ‘?page=1’);

history.state

// { page: 1 }

popstate事件

每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。

需要注意的是,仅仅调用pushState方法或replaceState方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。

使用的时候,可以为popstate事件指定回调函数。这个回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前URL所提供的状态对象(即这两个方法的第一个参数)。

window.onpopstate = function (event) {

console.log('location: ’ + document.location);

console.log('state: ’ + JSON.stringify(event.state));

};

// 或者

window.addEventListener(‘popstate’, function(event) {

console.log('location: ’ + document.location);

console.log('state: ’ + JSON.stringify(event.state));

});

上面代码中的event.state,就是通过pushState和replaceState方法,为当前URL绑定的state对象。

这个state对象也可以直接通过history对象读取。

var currentState = history.state;

注意,页面第一次加载的时候,在load事件发生后,Chrome和Safari浏览器(Webkit核心)会触发popstate事件,而Firefox和IE浏览器不会。

总结

1 hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.abc.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

2 history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.abc.com/book/id。如果后端缺少对 /book/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”

3 结合自身例子,对于一般的 Vue + Vue-Router + Webpack + XXX 形式的 Web 开发场景,用 history 模式即可,只需在后端(Apache 或 Nginx)进行简单的路由配置,同时搭配前端路由的 404 页面支持。

7:Vue中computed和watch的区别

计算属性computed :

  1. 支持缓存,只有依赖数据发生改变,才会重新进行计算
  2. 不支持异步,当computed内有异步操作时无效,无法监听数据的变化
    3.computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data中声明过的数据通过计算得到的
  3. 如果一个属性是由其他属性计算而来的,这个属性依赖其他属性,是一个多对一或者一对一,一般用computed
    5.如果computed属性属性值是函数,那么默认会走get方法;函数的返回值就是属性的属性值;在computed中的,属性都有一个get和一个set方法,当数据变化时,调用set方法。

在computed中定义的每一个计算属性,都会被缓存起来,只有当计算属性里面依赖的一个或多个属性变化了,才会重新计算当前计算属性的值。上面的代码片段中,在reversedMessage中,它依赖了message和number这两个属性,一旦其中一个变化了,reversedMessage会立刻重新计算输出新值。

在这里插入图片描述
侦听属性watch:

  1. 不支持缓存,数据变,直接会触发相应的操作;
    2.watch支持异步;
    3.监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
  2. 当一个属性发生变化时,需要执行对应的操作;一对多;
    在这里插入图片描述
    监听的对象也可以写成字符串的形式
    在这里插入图片描述
    当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。这是和computed最大的区别,请勿滥用。嗯,就酱~

就我自己目前来说,watch一般就用来一个数据来影响多个数据的操作,或者比如说是用来监听input然后进行一些即时搜索操作什么的。

大概总结一下,computed和watch的使用场景并不一样,computed的话是通过几个数据的变化,来影响一个数据,而computed的话,是可以一个数据的变化,去影响多个数据

7:vue中的父子组件中的钩子函数的执行顺序

1:加载渲染过程

父beforeCreate->父created->父beforeMount->子beforeCreate->子 created->子beforeMount->子mounted->父mounted
  
2:子组件更新过程
  父beforeUpdate->子beforeUpdate->子updated->父updated

3:父组件更新过程
  父beforeUpdate->父updated
4:销毁过程
  父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

7:vuex中的五大属性使用方式

Vuex有五个核心概念:

state, getters, mutations, actions, modules。

1. state:vuex的基本数据,用来存储变量

2. geeter:从基本数据(state)派生的数据,相当于state的计算属性

3. mutation:提交更新数据的方法,必须是同步的(如果需要异步使用action)。每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。

回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数,提交载荷作为第二个参数。

4. action:和mutation的功能大致相同,不同之处在于 ==》1. Action 提交的是 mutation,而不是直接变更状态。 2. Action 可以包含任意异步操作。

5. modules:模块化vuex,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。
  
state:
state即Vuex中的基本数据!
state就是用来存放数据,若是对数据进行处理输出,比如数据要过滤,一般我们可以写到computed中。但是如果很多组件都使用这个过滤后的数据,这就是getters存在的意义。我们可以认为,【getters】是store的计算属性。

getters(相当于State的计算属性)
1基础用法:

main.js:

const store = new Vuex.Store({
  state: {
    list: [1, 3, 5, 7, 9, 20, 30]
  },
  getters: {
    filteredList: state => {
      return state.list.filter(item => item > 5)
    }
  }
})

index.vue:

<script>
  export default {
    name: "index.vue",
    computed: {
      list() {
        return this.$store.getters.filteredList;
      }
    }
  }
</script>

2.内部依赖
getter 可以依赖其它已经定义好的 getter。比如我们需要统计过滤后的列表数量,就可以依赖之前定义好的过滤函数。

main.js:

const store = new Vuex.Store({
  state: {
    list: [1, 3, 5, 7, 9, 20, 30]
  },
  getters: {
    filteredList: state => {
      return state.list.filter(item => item > 5)
    },
    listCount: (state, getters) => {
      return getters.filteredList.length;
    }
  }
})

index.vue:

<template>
 
  <div>
    过滤后的列表:{{list}}
    <br>
    列表长度:{{listCount}}
  </div>
</template>
 
<script>
  export default {
    name: "index.vue",
    computed: {
      list() {
        return this.$store.getters.filteredList;
      },
      listCount() {
        return this.$store.getters.listCount;
      }
    }
  }
</script>

mutation(提交更改数据的方法,同步!必须是同步函数) :
使用vuex修改state时,有两种方式:
1)可以直接使用 this. s t o r e . s t a t e . 变 量 = x x x ; 2 ) t h i s . store.state.变量 = xxx; 2)this. store.state.=xxx;2this.store.dispatch(actionType, payload)或者 this.$store.commit(commitType, payload)

main.js:

const store = new Vuex.Store({
  strict: true,            //    strict: true, 若开启严格模式只要不经过 mutation的函数,则会报错
  state: {
    cartNum: 0,          // 购物车数量
  },
  mutations: {
    // 加1
    INCREMENT(state) {
      state.cartNum++;
    },
  }
})

index.vue:

import baseStore from '../../../main.js';
methods: {
      addCarts () {
                baseStore.commit('INCREMENT')
     },     
}

异同点:
1)共同点: 能够修改state里的变量,并且是响应式的(能触发视图更新)
2)不同点:
若将vue创建 store 的时候传入 strict: true, 开启严格模式,那么任何修改state的操作,只要不经过 mutation的函数,

vue就会 throw error : [vuex] Do not mutate vuex store state outside mutation handlers。

action(像一个装饰器,包裹mutations,使之可以异步。) :
action的功能和mutation是类似的,都是去变更store里的state,不过action和mutation有两点不同:

1)action主要处理的是异步的操作,mutation必须同步执行,而action就不受这样的限制,也就是说action中我们既可以处理同步,也可以处理异步的操作

2)action改变状态,最后是通过提交mutation

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      setInterval(function(){
        context.commit('increment')
      }, 1000)
    }
  }
})

注意:Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

action函数接受一个与store实例具有相同方法和属性的context对象。可以使用参数解构简化代码:

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

分发actions

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

异步操作:

actions: {
  checkout ({ commit, state }, products) {
    // 把当前购物车的物品备份起来
    const savedCartItems = [...state.cart.added]
    // 发出结账请求,然后乐观地清空购物车
    commit(types.CHECKOUT_REQUEST)
    // 购物 API 接受一个成功回调和一个失败回调
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失败操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

modules ( 模块化Vuex):

背景:在Vue中State使用是单一状态树结构,应该的所有的状态都放在state里面,如果项目比较复杂,那state是一个很大的对象,store对象也将对变得非常大,难于管理。

module:可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

6.辅助函数
mapState

普通写法:


// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
 
export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,
 
    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',
 
    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

或者

mapState([
 // 映射 this.count 为 store.state.count
'count'
])

对象展开运算符

computed: {
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
  })
}

mapGetters

import { mapGetters } from 'vuex'
 
export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

mapActions

import { mapActions } from 'vuex'
 
export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
 
      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

使用下面这两种方法存储数据:

dispatch:异步操作,写法: this.$store.dispatch(‘mutations方法名’,值)

commit:同步操作,写法:this.$store.commit(‘mutations方法名’,值)

7:vue路由的钩子函数

首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。

beforeEach主要有3个参数to,from,next:

to:route即将进入的目标路由对象,

from:route当前导航正要离开的路由

next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转。

8:对keep-alive 的了解?

keep-alive是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
在vue 2.1.0 版本之后,keep-alive新加入了两个属性: include(包含的组件缓存) 与 exclude(排除的组件不缓存,优先级大于include) 。
使用方法

<keep-alive include='include_components' exclude='exclude_components'>
  <component>
    <!-- 该组件是否缓存取决于include和exclude属性 -->
  </component>
</keep-alive>

参数解释
include - 字符串或正则表达式,只有名称匹配的组件会被缓存
exclude - 字符串或正则表达式,任何名称匹配的组件都不会被缓存
include 和 exclude 的属性允许组件有条件地缓存。二者都可以用“,”分隔字符串、正则表达式、数组。当使用正则或者是数组时,要记得使用v-bind 。

使用场景:页面多开,在页面上有类似浏览器界面的tab效果并可以关闭,keep-alive嵌套在想保留的标签之外的作用是保留这个标签之内用户的操作,在用户切换到其他路由再想返回的时候还是保留之前的操作

使用示例

<!-- 逗号分隔字符串,只有组件a与b被缓存。 -->
<keep-alive include="a,b">
  <component></component>
</keep-alive>
 
<!-- 正则表达式 (需要使用 v-bind,符合匹配规则的都会被缓存) -->
<keep-alive :include="/a|b/">
  <component></component>
</keep-alive>
 
<!-- Array (需要使用 v-bind,被包含的都会被缓存) -->
<keep-alive :include="['a', 'b']">
  <component></component>
</keep-alive>

9:vue等单页面应用及其优缺点

优点:Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。

缺点:不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。

10:vue中 key 值的作用?

当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key的作用让每个item有一个唯一的识别身份,可以下标值index或者id, 主要是为了vue精准的追踪到每一个元素,高效的更新虚拟DOM。

key值:用于 管理可复用的元素。因为Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做使 Vue 变得非常快,但是这样也不总是符合实际需求。
2.2.0+ 的版本里,当在组件中使用 v-for 时,key 是必须的。

12:vue的兄弟组件通信(eventBus)

首先在工程中要使用eventBus可以这么干:
先在main.js中指定eventBus:
Vue.prototype.$EventBus = new Vue()
相当于挂载一个Vue的实例。
下面开始尝试使用:
先编写comA用于触发一个事件在父组件中响应减操作并接受B组件的传过来的数据最后console.log出来:

<template>
  <button @click="increment()">+ 加并接受另一个组件的值!</button>
</template>

<script>
export default {
  name: 'IncrementCount',
  data() {
    return {
      num: 1,
      deg: 1
    };
  },
  methods: {
    increment() {
      this.$EventBus.$emit("incremented",{
        num: this.num,
        deg: this.deg
      });
    }
  },
  mounted() {
    // 这是响应change事件
    this.$EventBus.$on("change", test => {
      console.log(test)
    })
  }
}
</script>

然后我们编写comB,用于触发一个事件在父组件中响应加操作并传递值到comA:

<template>
  <button @click="decrease()"> -减 </button>
</template>

<script>
export default {
  name: 'DecreaseCount',
  data() {
    return {
      num: 1,
      deg: 1,
      show_data: 1000
    }
  },
  methods: {
    decrease() {
      this.$EventBus.$emit("decreased", {
        num: this.num,
        deg: this.deg,
        show_data: this.show_data  // show_data就是要传递到组件A的值
      })
    }
  }
}
</script>

最后我们编写一个父组件,将A、B组件组合在一起:

<template>
    <div id="root">
      <IncrementCount />
      <DecreaseCount />
      <div>{{ degValue }}</div>
      <div>{{ fontCount }}</div>
      <div>{{ backCount }}</div>
    </div>
</template>

<script>
import IncrementCount from '../components/add_event_bus';
import DecreaseCount from '../components/decrease_event_bus';

export default {
  name: 'App',
  components: {
    IncrementCount,
    DecreaseCount
  },
  data() {
    return {
      degValue: 0,
      fontCount: 0,
      backCount: 0
    };
  },
  mounted() {
    // 接收A组件的incremented事件
    this.$EventBus.$on("incremented", ({num, deg}) => {
      this.fontCount += num
      this.$nextTick(() => {
        this.backCount += num
        this.degValue += deg
      })
    })
    // 接收B组件的decreased事件
    this.$EventBus.$on("decreased", ({num, deg}) => {
      this.fontCount -= num
      this.$nextTick(() => {
        this.backCount -= num
        this.degValue -= deg
        // 这里触发change事件
        this.$EventBus.$emit("change", 1000)
      })
    })
  }
}
</script>

12:为什么vue中data必须是一个函数

类别引用数据类型

Object是引用数据类型,如果不用function返回,每个组件的data都是内存的同一个地址,一个数据改变了其他也改变了;

JavaScript只有函数构成作用域(注意理解作用域,只有函数{}构成作用域,对象的{}以及if(){}都不构成作用域),data是一个函数时,每个组件实例都有自己的作用域,每个实例相互独立,不会相互影响。

例如:


const MyComponent = function() {};
MyComponent.prototype.data = {
    a: 1,
    b: 2
}
const component1 = new MyComponent();
const component2 = new MyComponent();
 
component1.data.a === component2.data.a; // true
component1.data.b = 5;
component2.data.b //5

如果两个实例同事引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着该;

两个实例应该有自己各自的域才对,需要通过下面的方法进行处理


const MyComponent = function() {
    this.data = this.data();
};
 
MyComponent.prototype.data = function() {
    return {
        a:1,
        b:2
    }
};

这样么一个实例的data属性都是独立的,不会相互影响了。

所以,你现在知道为什么vue组件的data必须是函数了吧。这都是因为js本身的特性带来的,跟vue本身设计无关。其实vue不应该把这个方法取名为data(),应该叫setData或其他更容易理解的方法名。

// 为何在大型项目中data需要使用return返回数据呢
// 答:不使用return包裹的数据会在项目的全局可见,会造成变量污染
// 使用return包裹后数据中变量只在当前组件中生效,不会影响其他组件

13:vue中的虚拟Dom

前言

Vue.js 2.0引入Virtual DOM,比Vue.js 1.0的初始渲染速度提升了2-4倍,并大大降低了内存消耗。那么,什么是Virtual DOM?为什么需要Virtual DOM?它是通过什么方式去提升页面渲染效率的呢?这是本文所要探讨的问题。
模板转换成视图的过程

在正式介绍 Virtual Dom之前,我们有必要先了解下模板转换成视图的过程整个过程(如下图):

Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树

在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。

简单点讲,在Vue的底层实现上,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应到DOM操作上。
在这里插入图片描述
我们先对上图几个概念加以解释:

1:渲染函数:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。
2:VNode 虚拟节点:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。
3:patch(也叫做patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。

Virtual DOM 是什么?

Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。

简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。

对于虚拟DOM,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了模板 → 渲染函数 → 虚拟DOM树 → 真实DOM的一个过程
在这里插入图片描述
Virtual DOM 作用是什么?

虚拟DOM的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。

为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。

其实虚拟DOM在Vue.js主要做了两件事:

提供与真实DOM节点所对应的虚拟节点vnode
将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图

为何需要Virtual DOM?

具备跨平台的优势

由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

操作 DOM 慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。

因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

提升渲染性能

Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要。我们通过patch 的核心----diff 算法,找出本次DOM需要更新的节点来更新,其他的不更新。比如修改某个model 100次,从1加到100,那么有了Virtual DOM的缓存之后,只会把最后一次修改patch到view上。那diff 算法的实现过程是怎样的?

虚拟dom的解释

1.操作dom元素需要把html结构销毁之后,然后再进行重新生成,十分消耗性能

2.虚拟dom,通过diff算法,减少性能的消耗
vue通过建立一个虚拟DOM树对真实DOM发生的变化保持追踪。

一棵真实DOM树的渲染需要先解析CSS样式和DOM树,然后将其整合成一棵渲染树,再通过布局算法去计算每个节点在浏览器中的位置,最终输出到显示器上,
而虚拟DOM则可以理解为保存了一棵DOM树被渲染之前所包含的所有信息,而这些信息可以通过对象的形式一直保存在内存中,并通过JavaScript的操作进行维护。

用传统jquery操作dom的思想,可以先删除,然后再插入新的标签

虚拟dom会如何处理上述问题呢?

第一步:通过树的形式保存旧的dom信息,这些信息可能在页面第一次加载的时候被渲染到浏览器中,但仍是通过虚拟dom的方式创建的

第二步:检测到数据更新,需要更新dom,先在JavaScript中将需要修改的节点全部修改完成

第三步:将最终生成的虚拟DOM更新到视图中去。

用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。

diff 算法
在这里插入图片描述

Vue的diff算法是基于snabbdom改造过来的,仅在同级的vnode间做diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从O(n3)变成O(n)。

diff 算法包括几个步骤:

1:用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
2:当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
3:把所记录的差异应用到所构建的真正的DOM树上,视图就更新了
在这里插入图片描述
diff 算法的实现过程

diff 算法本身非常复杂,实现难度很大。本文去繁就简,粗略介绍以下两个核心函数实现流程:

1:patch(container,vnode) :初次渲染的时候,将VDOM渲染成真正的DOM然后插入到容器里面。
2:patch(vnode,newVnode):再次渲染的时候,将新的vnode和旧的vnode相对比,然后之间差异应用到所构建的真正的DOM树上。

1. patch(container,vnode)

通过这个函数可以让VNode渲染成真正的DOM,我们通过以下模拟代码,可以了解大致过程:

function createElement(vnode) {    
var tag = vnode.tag  
var attrs = vnode.attrs || {}    
var children = vnode.children || []    
if (!tag) {       
 return null  
  }    
// 创建真实的 DOM 元素    
var elem = document.createElement(tag)   
 // 属性    
var attrName    
for (attrName in attrs) {    
    if (attrs.hasOwnProperty(attrName)) { 
           // 给 elem 添加属性
           elem.setAttribute(attrName, attrs[attrName])
        }
    }
    // 子元素
    children.forEach(function (childVnode) {
        // 给 elem 添加子元素,如果还有子节点,则递归的生成子节点。
        elem.appendChild(createElement(childVnode))  // 递归
    })    // 返回真实的 DOM 元素   
 return elem
}

2. patch(vnode,newVnode)

这里我们只考虑vnode与newVnode如何对比的情况:

function updateChildren(vnode, newVnode) {
    var children = vnode.children || []
    var newChildren = newVnode.children || []
  // 遍历现有的children
    children.forEach(function (childVnode, index) {
        var newChildVnode = newChildren[index]
  // 两者tag一样
        if (childVnode.tag === newChildVnode.tag) {
            // 深层次对比,递归
            updateChildren(childVnode, newChildVnode)
        } else { 
  // 两者tag不一样
           replaceNode(childVnode, newChildVnode) 
       }
    }
)}

14:Vue响应式原理Observer、Dep、Watcher理解

Object.defineProperty
相信很多同学或多或少都了解Vue的响应式原理是通过Object.defineProperty实现的。被Object.defineProperty绑定过的对象,会变成「响应式」化。也就是改变这个对象的时候会触发get和set事件。进而触发一些视图更新。举个栗子🌰

function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      console.log('我被读了,我要不要做点什么好?');
      return val;
    },
    set: newVal => {
      if (val === newVal) {
        return;
      }
      val = newVal;
      console.log("数据被改变了,我要把新的值渲染到页面上去!");
    }
  })
}
 
let data = {
  text: 'hello world',
};
 
// 对data上的text属性进行绑定
defineReactive(data, 'text', data.text);
 
console.log(data.text); // 控制台输出 <我被读了,我要不要做点什么好?>
data.text = 'hello Vue'; // 控制台输出 <hello Vue && 数据被改变了,我要把新的值渲染到页面上去!>

Observer 「响应式」
Vue中用Observer类来管理上述响应式化Object.defineProperty的过程。我们可以用如下代码来描述,将this.data也就是我们在Vue代码中定义的data属性全部进行「响应式」绑定。

class Observer {
  constructor() {
    // 响应式绑定数据通过方法
   observe(this.data);
  }
}
 
export function observe (data) {
  const keys = Object.keys(data);
  for (let i = 0; i < keys.length; i++) {
    // 将data中我们定义的每个属性进行响应式绑定
    defineReactive(obj, keys[i]);
  }
}

Dep 「依赖管理」
什么是依赖?

相信没有看过源码或者刚接触Dep这个词的同学都会比较懵。那Dep究竟是用来做什么的呢? 我们通过defineReactive方法将data中的数据进行响应式后,虽然可以监听到数据的变化了,那我们怎么处理通知视图就更新呢?

Dep就是帮我们收集【究竟要通知到哪里的】。比如下面的代码案例,我们发现,虽然data中有text和message属性,但是只有message被渲染到页面上,至于text无论怎么变化都影响不到视图的展示,因此我们仅仅对message进行收集即可,可以避免一些无用的工作。

那这个时候message的Dep就收集到了一个依赖,这个依赖就是用来管理data中message变化的。

<div>
  <p>{{message}}</p>
</div>

data: {
  text: 'hello world',
  message: 'hello vue',
}

当使用watch属性时,也就是开发者自定义的监听某个data中属性的变化。比如监听message的变化,message变化时我们就要通知到watch这个钩子,让它去执行回调函数。

这个时候message的Dep就收集到了两个依赖,第二个依赖就是用来管理watch中message变化的。

watch: {
  message: function (val, oldVal) {
    console.log('new: %s, old: %s', val, oldVal)
  },
} 

当开发者自定义computed计算属性时,如下messageT属性,是依赖message的变化的。因此message变化时我们也要通知到computed,让它去执行回调函数。 这个时候message的Dep就收集到了三个依赖,这个依赖就是用来管理computed中message变化的。

computed: {
  messageT() {
    return this.message + '!';
  }
}

一个属性可能有多个依赖,每个响应式数据都有一个Dep来管理它的依赖。

如何收集依赖
我们如何知道data中的某个属性被使用了,答案就是Object.defineProperty,因为读取某个属性就会触发get方法。可以将代码进行如下改造:

function defineReactive (obj, key, val) {
  let Dep; // 依赖
 
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      console.log('我被读了,我要不要做点什么好?');
      // 被读取了,将这个依赖收集起来
      Dep.depend(); // 本次新增
      return val;
    },
    set: newVal => {
      if (val === newVal) {
        return;
      }
      val = newVal;
      // 被改变了,通知依赖去更新
      Dep.notify(); // 本次新增
      console.log("数据被改变了,我要把新的值渲染到页面上去!");
    }
  })
}

什么是依赖
那所谓的依赖究竟是什么呢?上面的图中已经暴露了答案,就是Watcher。

Watcher 「中介」
Watcher就是类似中介的角色,比如message就有三个中介,当message变化,就通知这三个中介,他们就去执行各自需要做的变化。

Watcher能够控制自己属于哪个,是data中的属性的还是watch,或者是computed,Watcher自己有统一的更新入口,只要你通知它,就会执行对应的更新方法。

因此我们可以推测出,Watcher必须要有的2个方法。一个就是通知变化,另一个就是被收集起来到Dep中去。

class Watcher {
  addDep() {
    // 我这个Watcher要被塞到Dep里去了~~
  },
  update() {
    // Dep通知我更新呢~~
  }, 
}

总结
回顾一下,Vue响应式原理的核心就是Observer、Dep、Watcher。

Observer中进行响应式的绑定,在数据被读的时候,触发get方法,执行Dep来收集依赖,也就是收集Watcher。

在数据被改的时候,触发set方法,通过对应的所有依赖(Watcher),去执行更新。比如watch和computed就执行开发者自定义的回调方法。

本篇文章属于入门篇,能够先简单的理解Observer、Dep、Watcher三者的作用和关系。后面会逐渐详细和深入,循序渐进的理解和学习。

计算机网络部分

1:http的状态码

一些常见的状态码为:
200 - 服务器成功返回网页
404 - 请求的网页不存在
503 - 服务不可用
详细分解:

1xx(临时响应)
表示临时响应并需要请求者继续执行操作的状态代码。

100 (继续) 请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。
101 (切换协议) 请求者已要求服务器切换协议,服务器已确认并准备切换。

2xx (成功)
表示成功处理了请求的状态代码。

200 (成功) 服务器已成功处理了请求。通常,这表示服务器提供了请求的网页。
201 (已创建) 请求成功并且服务器创建了新的资源。
202 (已接受) 服务器已接受请求,但尚未处理。
203 (非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源。
204 (无内容) 服务器成功处理了请求,但没有返回任何内容。
205 (重置内容) 服务器成功处理了请求,但没有返回任何内容。
206 (部分内容) 服务器成功处理了部分 GET 请求。

3xx (重定向)
表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。

300 (多种选择) 针对请求,服务器可执行多种操作。服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。
301 (永久移动) 请求的网页已永久移动到新位置。服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
303 (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。
304 (未修改) 自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容。
305 (使用代理) 请求者只能使用代理访问请求的网页。如果服务器返回此响应,还表示请求者应使用代理。
307 (临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。

4xx(请求错误)
这些状态代码表示请求可能出错,妨碍了服务器的处理。

400 (错误请求) 服务器不理解请求的语法。
401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
403 (禁止) 服务器拒绝请求。
404 (未找到) 服务器找不到请求的网页。
405 (方法禁用) 禁用请求中指定的方法。
406 (不接受) 无法使用请求的内容特性响应请求的网页。
407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。
408 (请求超时) 服务器等候请求时发生超时。
409 (冲突) 服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息。
410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应。
411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。
412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。
413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。
414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。
415 (不支持的媒体类型) 请求的格式不受请求页面的支持。
416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。
417 (未满足期望值) 服务器未满足"期望"请求标头字段的要求

5xx(服务器错误)
这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。

500 (服务器内部错误) 服务器遇到错误,无法完成请求。
501 (尚未实施) 服务器不具备完成请求的功能。例如,服务器无法识别请求方法时可能会返回此代码。
502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。
503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。通常,这只是暂时状态。
504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。
505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本。

HttpWatch状态码Result is
200 - 服务器成功返回网页,客户端请求已成功。
302 - 对象临时移动。服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
304 - 属于重定向。自上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容。
401 - 未授权。请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
404 - 未找到。服务器找不到请求的网页。
2xx - 成功。表示服务器成功地接受了客户端请求。
3xx - 重定向。表示要完成请求,需要进一步操作。客户端浏览器必须采取更多操作来实现请求。例如,浏览器可能不得不请求服务器上的不同的页面,或通过代理服务器重复该请求。
4xx - 请求错误。这些状态代码表示请求可能出错,妨碍了服务器的处理。
5xx - 服务器错误。表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。

2:输入一个url页面经历了哪些过程

步骤
→ 1- 输入网址
→ 2- 缓存解析
→ 3- 域名解析
→ 4- tcp连接,三次握手
→ 6- 页面渲染

一:输入网址

那肯定是输入你要访问的网站网址了,俗称url;

二:缓存解析

浏览器获取了这个url,当然就去解析了,它先去缓存当中看看有没有,从 浏览器缓存-系统缓存-路由器缓存 当中查看,如果有从缓存当中显示页面,然后没有那就进行步骤三;
缓存就是把你之前访问的web资源,比如一些js,css,图片什么的保存在你本机的内存或者磁盘当中。

将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取;
将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取;
资源的来源是缓存当中,从缓存当中获取了这些就可以直接显示在页面中,不需要发送http请求;

三: 域名解析

那么在发送http请求前,浏览器做了什么?

在发送http之前,需要进行DNS解析即域名解析。
DNS解析:域名到IP地址的转换过程。域名的解析工作由DNS服务器完成。解析后可以获取域名相应的IP地址

DNS的解析步骤
第一步:客户机提出域名解析请求,并将该请求发送给本地的域名服务器.
  第二步:当本地的域名服务器收到请求后,就先查询本地的缓存,如果有该纪录项,则本地的域名服务器就直接把查询的结果返回.
  第三步:如果本地的缓存中没有该纪录,则本地域名服务器就直接把请求发给根域名服务器,然后根域名服务器再返回给本地域名服务器一个所查询域(根的子域)的主域名服务器的地址.
  第四步:本地服务器再向上一步返回的域名服务器发送请求,然后接受请求的服务器查询自己的缓存,如果没有该纪录,则返回相关的下级的域名服务器的地址.
  第五步:重复第四步,直到找到正确的纪录.
  第六步:本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时还将结果返回给客户机.

让我们举一个例子来详细说明解析域名的过程.假设我们的客户机如果想要访问站点:www.linejet.com此客户本地的域名服务器是dns.company.com , 一个根域名服务器是NS.INTER.NET , 所要访问的网站的域名服务器是dns.linejet.com,域名解析的过程如下所示.
  (1)客户机发出请求解析域名www.linejet.com的报文
  (2)本地的域名服务器收到请求后, 查询本地缓存, 假设没有该纪录, 则本地域名服务器dns.company.com则向根域名服务器NS.INTER.NET发出请求解析域名www.linejet.com
  (3)根域名服务器NS.INTER.NET收到请求后查询本地记录得到如下结果:linejet.com NS dns.linejet.com (表示linejet.com域中的域名服务器为:dns.linejet.com ), 同时给出dns.linejet.com的地址,并将结果返回给域名服务器dns.company.com
  (4)域名服务器dns.company.com 收到回应后,再发出请求解析域名www.linejet.com的报文.
  (5)域名服务器 dns.linejet.com收到请求后,开始查询本地的记录,找到如下一条记录:www.linejet.com A 211.120.3.12 (表示linejet.com域中域名服务器dns.linejet.com的IP地址为:211.120.3.12),并将结果返回给客户本地域名服务器dns.company.com
  (6)客户本地域名服务器将返回的结果保存到本地缓存,同时将结果返回给客户机.
  这样就完成了一次域名解析过程.

四:tcp连接,三次握手

在域名解析之后,浏览器向服务器发起了http请求,tcp连接,三次握手建立tcp连接。TCP协议是面向连接的,所以在传输数据前必须建立连接

(1)客户端向服务器发送连接请求报文;
(2)服务器端接受客户端发送的连接请求后后回复ACK报文,并为这次连接分配资源。
(3)客户端接收到ACK报文后也向服务器端发生ACK报文,并分配资源。

这样TCP连接就建立了。
在此之后,浏览器开始向服务器发送http请求,请求数据包。请求信息包含一个头部和一个请求体。

五:服务器收到请求

服务器收到浏览器发送的请求信息,返回一个响应头和一个响应体。

六:页面渲染

浏览器收到服务器发送的响应头和响应体,进行客户端渲染,生成Dom树、解析css样式、js交互。

大致可以分为如下7步:

输入网址;
发送到DNS服务器,并获取域名对应的web服务器对应的ip地址;
与web服务器建立TCP连接;
浏览器向web服务器发送http请求;
web服务器响应请求,并返回指定url的数据(或错误信息,或重定向的新的url地址);
浏览器下载web服务器返回的数据及解析html源文件;
生成DOM树,解析css和js,渲染页面,直至显示完成;

还是用 www.taobao.com 举例子。
当在地址栏输入后,浏览器会分析这个url,并设置好请求报文发出。请求报文中包括请求行(包括请求的方法,路径和协议版本)、请求头(包含了请求的一些附加的信息,一般是以键值的形式成对存在)、空行(协议中规定请求头和请求主体间必须用一个空行隔开)、请求主体(对于post请求,所需要的参数都不会放在url中,这时候就需要一个载体了,这个载体就是请求主体)。服务端收到这个请求后,会根据url匹配到的路径做相应的处理,最后返回浏览器需要的页面资源。处理后,浏览器会收到一个响应报文,而所需要的资源就就在报文主体上。与请求报文相同,响应报文也有与之对应的起始行(响应报文的起始行同样包含了协议版本,与请求的起始行不同的是其包含的还有状态码和状态码的原因短语)、响应头(对应请求报文中的请求头,格式一致,但是各自有不同的首部)、空行、报文主体(请求所需要的资源),不同的地方在于包含的东西不一样。

3:浏览器的渲染机制

一 : 为什么要了解浏览器渲染页面的机制,主要还是性能的优化。
了解浏览器如何进行加载,我们可以在引用外部样式文件,外部JS时,将它们放到合适的位置,是浏览器以最快的速度,将文件加载完毕。

了解浏览器如何进行解析,我们可以在构建DOM结构,组织CSS选择器的时候,选择最优的写法,提高浏览器的解析速率。

了解浏览器如何进行渲染,明白渲染的过程,我们在设置元素属性,编写JS文件时,可以减少“重绘”,“重新布局”的消耗。

二 : 要了解清楚渲染机制,要先弄明白几个基本概念:
DOM:Document Object Model,浏览器将HTML解析成树形的数据结构,简称DOM。
CSSOM:CSS Object Model,浏览器将CSS解析成树形的数据结构,简称CSSOM。
Render Tree: DOM和CSSOM合并后生成Render Tree,如下图:
在这里插入图片描述
Layout: 计算出Render Tree每个节点的具体位置。
Painting:通过显卡,将Layout后的节点内容分别呈现到屏幕上。
三 : 需要注意的是:
1:当我们浏览器获得HTML文件后,会自上而下的加载,并在加载过程中进行解析和渲染。

2:加载说的就是获取资源文件的过程,如果在加载过程中遇到外部CSS文件和图片,浏览器会另外发送一个请求,去获取CSS文件和相应的图片,这个请求是异步的,并不会影响HTML文件的加载。

3:但是如果遇到Javascript文件,HTML文件会挂起渲染的进程,等待JavaScript文件加载完毕后,再继续进行渲染。
为什么HTML需要等待JavaScript呢?因为JavaScript可能会修改DOM,导致后续HTML资源白白加载,所以HTML必须等待JavaScript文件加载完毕后,再继续渲染,这也就是为什么JavaScript文件在写在底部body标签前的原因。

四 : 浏览器渲染的整个流程
在这里插入图片描述
浏览器整个流程如上图所示:

1:当用户输入一个URL时,浏览器就会向服务器发出一个请求,请求URL对应的资源
2:接受到服务器的响应内容后,浏览器的HTML解析器,会将HTML文件解析成一棵DOM树,DOM树的构建是一个深度遍历的过程,当前节点的所有子节点都构建完成以后,才会去构建当前节点的下一个兄弟节点。
3:将CSS解析成CSSOM树(CSS Rule Tree)
4:根据DOM树和CSSOM树,来构建Render Tree(渲染树),注意渲染树,并不等于DOM树,因为一些像head或display:none的东西,就没有必要放在渲染树中了。
5:有了Render Tree,浏览器已经能知道网页中有哪些节点,各个节点的CSS定义,以及它们的从属关系,下一步操作就是Layout,顾名思义,就是计算出每个节点在屏幕中的位置。
6:Layout后,浏览器已经知道哪些节点要显示,每个节点的CSS属性是什么,每个节点在屏幕中的位置是哪里,就进入了最后一步painting,按照算出来的规则,通过显卡,把内容画到屏幕上。

这里还要说两个概念,一个是Reflow,另一个是Repaint。这两个不是一回事。
Repaint ——改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。

Reflow ——元件的几何尺寸变了,我们需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化。

reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显 示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲 染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。

注:display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发现位置变化。
五 : DOM和CSSOM的具体构建流程
DOM 和 CSSOM 都是以" Bytes → characters → tokens → nodes → object model. " 这样的方式生成最终的数据。如下图所示:
在这里插入图片描述
具体到DOM树的构建,如下图:
在这里插入图片描述
1、 当服务器返回一个HTML文件给浏览器的时候,浏览器接受到的是一些字节数据。
2、 然后浏览器根据HTTP响应中的编码方式(通过是UTF8),解析字节数据,得到一些字符。如果这个时候编码方式跟文件的字节编码不一致,便会出现乱码。所以我们通过使用<meta http-equiv="content-type"content=“text/html;charset=utf-8”>来告诉浏览器我们页面使用的是什么编码。

3、 这个时候,浏览器再根据DTD中的对元素(标签)的定义,对这些接受到的字符进行语义化(token)。我们经常在html文件的第一行,定义,这个DTD就是告诉浏览器,那些字符是有意义的,那些字符是无意义的。DTD常见的有严格、过渡、框架和HTML5三种。不同的DTD中,有不同的元素定义。

4、 接着,浏览器再使用这些语义块(token)创建对象,形成一个个节点了。

5、 然后HTML解析器就会从HTML文件的头部到尾部,一个个地遍历这些节点。当这些节点是普通节点的话,HTML解析器就会将这些节点加入到DOM树中。当这些节点是JS代码的话,HTML解析器就会将控制权交给JS解析器。如果这些节点是CSS代码的话,HTML解析器就会将控制权交给CSS解析器。不过,当外联的JS代码和CSS代码还没从服务器传到浏览器的时候,这个时候如果DOM树上有可视元素的话,浏览器通常会选择在这个时候,将一些内容提前渲染到屏幕上来。

6、 当HTML解析器读到最后一个节点的时候,整个DOM树也构建完成了,这个时候就会触发domContentloaded事件。而很多JS库(像JQ)通常会在这个时候有所反应的。

至此,DOM树就全部构建完成了。

3:tcp中三次握手和4次挥手

三次握手

(1)客户端向服务器发送连接请求报文;
(2)服务器端接受客户端发送的连接请求后后回复ACK报文,并为这次连接分配资源。
(3)客户端接收到ACK报文后也向服务器端发生ACK报文,并分配资源。

四次挥手

SYN:同步标志
该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列编号
ACK:确认标志
同时提示远端系统已经成功接收所有数据。
FIN:结束标志
带有该标志置位的数据包用来结束一个TCP回话,但对应端口仍处于开放状态,准备接收后续数据

第一次挥手:

Client (可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向 Server发送一个FIN报文段;此时,Client 进入FIN_WAIT_1状态;这表示 Client 没有数据要发送给 Server了;

客户端发送第一次挥手后,就不能在向 服务端发送数据了。

第二次挥手:

Server 收到了 Client 发送的FIN报文段,向 Client 回一个ACK报文段,Acknowledgment Number 为 Sequence Number 加 1;Client 进入 FIN_WAIT_2 状态;Server 告诉 Client ,我“同意”你的关闭请求;

Server 第一次响应后,还可以继续向 Client 发送数据,这里只是告诉 Client ,我收到你发送的关闭请求。

第三次挥手

Server 向 Client 发送 FIN 报文段,请求关闭连接,同时 Server 进入 CLOSE_WAIT 状态;

当 Server 的数据响应完成后,再告诉 Client,我这边也可以关闭请求了, 这时
Server 就不能再向 Client 发送数据了

第四次挥手

Client 收到 Server 发送的 FIN 报文段,向 Server 发送 ACK 报文段,然后 Client 进入
TIME_WAIT 状态;Server 收到 Client 的 ACK 报文段以后,就关闭连接;此时,Client
等待2MSL后依然没有收到回复,则证明 Server 端已正常关闭,那好,Client 也可以关闭连接了。

1.为什么需要三次握手,两次不可以吗?或者四次、五次可以吗?
我们来分析一种特殊情况,假设客户端请求建立连接,发给服务器SYN包等待服务器确认,服务器收到确认后,如果是两次握手,假设服务器给客户端在第二次握手时发送数据,数据从服务器发出,服务器认为连接已经建立,但在发送数据的过程中数据丢失,客户端认为连接没有建立,会进行重传。假设每次发送的数据一直在丢失,客户端一直SYN,服务器就会产生多个无效连接,占用资源,这个时候服务器可能会挂掉。这个现象就是我们听过的“SYN的洪水攻击”。

总结:第三次握手是为了防止:如果客户端迟迟没有收到服务器返回确认报文,这时会放弃连接,重新启动一条连接请求,但问题是:服务器不知道客户端没有收到,所以他会收到两个连接,浪费连接开销。如果每次都是这样,就会浪费多个连接开销。

防止已失效的连接请求又传送到服务器端,因而产生错误
1:为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤

2:如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认

4:http和https区别

Http 协议运行在 TCP 之上,明文传输,客户端与服务器端都无法验证对方的身份;Https 是身披 SSL(Secure Socket Layer)外壳的 Http,运行于 SSL 上,SSL 运行于 TCP 之上,是添加了加密和认证机制的 HTTP。二者之间存在如下不同:

1:端口不同:Http 与 Https使用不同的连接方式,用的端口也不一样,前者是 80,后者是 443;
2:资源消耗:和 HTTP 通信相比,Https 通信会由于加减密处理消耗更多的 CPU 和内存资源;
3:开销:Https 通信需要证书,而证书一般需要向认证机构购买;

4:Https 的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制。

5:udp和tcp的区别是啥

1、TCP与UDP区别总结:
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接

2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付

Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。

3、UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。

4.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信

5、TCP对系统资源要求较多,UDP对系统资源要求较少。

2、为什么UDP有时比TCP更有优势?

UDP以其简单、传输快的优势,在越来越多场景下取代了TCP,如实时游戏。

(1)网速的提升给UDP的稳定性提供可靠网络保障,丢包率很低,如果使用应用层重传,能够确保传输的可靠性。

(2)TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程,由于TCP内置的系统协议栈中,极难对其进行改进。

采用TCP,一旦发生丢包,TCP会将后续的包缓存起来,等前面的包重传并接收到后再继续发送,延时会越来越大,基于UDP对实时性要求较为严格的情况下,采用自定义重传机制,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成影响。

6:进程还有线程的区别

1:进程是资源分配最小单位,线程是程序执行的最小单位;
2:进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据;

3:CPU切换一个线程比切换进程花费小;

4:创建一个线程比进程开销小;

5:线程占用的资源要⽐进程少很多。
6:线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)

7:多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间);
8:进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;

一个线程崩溃,一定会造成整个进程崩溃吗? 不会的,同时本身不会直接造成其他线程崩溃,但是可能会出现,比如说堆溢出,一个线程崩溃之后,将回收堆资源,但是可能对象呗其他线程引用,回收失败,最后其他的线程也会出现堆溢出,最终导致所有的线程崩溃,进程崩溃。

进程之间通信的方式:
进程间通信主要包括管道, 系统IPC(包括消息队列,信号,共享存储), 套接字(SOCKET).
管道包括三种:
1)普通管道PIPE, 通常有两种限制,一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用.
2)流管道s_pipe: 去除了第一种限制,为半双工,可以双向传输.
3)命名管道:name_pipe, 去除了第二种限制,可以在许多并不相关的进程之间进行通讯.

什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

如何避免线程死锁?

1:破坏互斥条件

这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

2:破坏请求与保持条件

一次性申请所有的资源。

3:破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

4:破坏循环等待条件

靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

6:数组和链表的区别

不同:链表是链式的存储结构;数组是顺序的存储结构。

链表通过指针来连接元素与元素,数组则是把所有元素按次序依次存储。

链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难;

数组寻找某个元素较为简单,但插入与删除比较复杂,由于最大长度需要再编程一开始时指定,故当达到最大长度时,扩充长度不如链表方便。

相同:两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构。

7:http中的get和post区别

表单提交中get和post方式的区别有5点

1.get是从服务器上获取数据,post是向服务器传送数据。

2.get是把参数数据队列加到提交表单的ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到。post是通过HTTPpost机制,将表单内各个字段与其内容放置在HTML HEADER内一起传送到ACTION属性所指的URL地址。用户看不到这个过程。

3.对于get方式,服务器端用Request.QueryString获取变量的值,对于post方式,服务器端用Request.Form获取提交的数据。

4.get传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。但理论上,IIS4中最大量为80KB,IIS5中为100KB。(这里有看到其他文章介绍get和post的传送数据大小跟各个浏览器、操作系统以及服务器的限制有关)

5.get安全性非常低,post安全性较高。

详细来看

首先最直观的是语义上的区别。
而后又有这样一些具体的差别:

从缓存的角度,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
从编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。
从参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。
从幂等性的角度,GET是幂等的,而POST不是。(幂等表示执行相同的操作,结果也是相同的)
从TCP的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)

6:HTTP版本区别

HTTP/1.0与HTTP/1.1

主要体现在长连接与部分发送上面

1:在 HTTP/1.0 时代,每一个请求都会重新建立一个 TCP 连接,一旦响应返回,就关闭连接,这种就是短连接,HTTP/1.1版本就支持Keep-Alive 模式,实现长连接了。

2:HTTP 1.1支持只发送header信息(不带任何body信息),如果服务器认为客户端有权限请求服务器,则返回100,否则返回401。客户端如果接受到100,才开始把请求body发送到服务器。这样当服务器返回401的时候,客户端就可以不用发送请求body了,节约了带宽。

HTTP/1.1 与HTTP/2.0

主要体现在多路复用上面。

1:HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。当然HTTP1.1也可以多建立几个TCP连接,来支持处理更多并发的请求,但是创建TCP连接本身也是有开销的。

2:支持header数据压缩

3:支持服务器推送

三、服务器推送的概念
服务器推送(server push)指的是,还没有收到浏览器的请求,服务器就把各种资源推送给浏览器。

比如,浏览器只请求了index.html,但是服务器把index.html、style.css、example.png全部发送给浏览器。这样的话,只需要一轮 HTTP 通信,浏览器就得到了全部资源,提高了性能。

常见的http响应头和请求头

1:http请求头:
在这里插入图片描述
2:http响应头

在这里插入图片描述

8:https对称加密和非对称加密

简介:

对称加密:
发送方和接收方需要持有同一把密钥,发送消息和接收消息均使用该密钥。相对于非对称加密,对称加密具有更高的加解密速度,但双方都需要事先知道密钥,密钥在传输过程中可能会被窃取,因此安全性没有非对称加密高。
非对称加密:
接收方在发送消息前需要事先生成公钥和私钥,然后将公钥发送给发送方。发送放收到公钥后,将待发送数据用公钥加密,发送给接收方。接收到收到数据后,用私钥解密。
在这个过程中,公钥负责加密,私钥负责解密,数据在传输过程中即使被截获,攻击者由于没有私钥,因此也无法破解。
非对称加密算法的加解密速度低于对称加密算法,但是安全性更高。

ssl握手的过程

SSL握手

证书主要作用是在SSL握手中,我们来看一下SSL的握手过程

  1. 客户端提交https请求

  2. 服务器响应客户,并把证书公钥发给客户端

  3. 客户端验证证书公钥的有效性

  4. 有效后,会生成一个会话密钥

  5. 用证书公钥加密这个会话密钥后,发送给服务器

  6. 服务器收到公钥加密的会话密钥后,用私钥解密,回去会话密钥

  7. 客户端与服务器双方利用这个会话密钥加密要传输的数据进行通信

数字签名

数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。它是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术来实现的,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。数字签名是非对称密钥加密技术与数字摘要技术的应用

SSL证书作用

https 起到了以下几个作用

  1. 帮助客户端对服务器身份进行验证

  2. 让需要传输的数据加密化

  3. 验证传输的数据是否完整

网站如何通过加密和用户安全通信

SSL (Secure Sockets Layer) 是用来保障你的浏览器和网站服务器之间安全通信,免受网络“中间人”窃取信息。

SSL原理很简单。当你的浏览器向服务器请求一个安全的网页(通常是 https://)
在这里插入图片描述
服务器就把它的证书和公匙发回来
在这里插入图片描述
浏览器检查证书是不是由可以信赖的机构颁发的,确认证书有效和此证书是此网站的。
在这里插入图片描述
使用公钥加密了一个随机对称密钥,包括加密的URL一起发送到服务器
在这里插入图片描述
服务器用自己的私匙解密了你发送的钥匙。然后用这把对称加密的钥匙给你请求的URL链接解密。
在这里插入图片描述
服务器用你发的对称钥匙给你请求的网页加密。你也有相同的钥匙就可以解密发回来的网页了
在这里插入图片描述
在网站通过SSL来与用户建立安全的通信中,对称加密算法和非对称加密算法起到了很大作用。

http请求的几种类型

http请求中的8种请求方法

1、opions

返回服务器针对特定资源所支持的HTML请求方法 或web服务器发送*测试服务器功能(允许客户端查看服务器性能)

2、Get

向特定资源发出请求(请求指定页面信息,并返回实体主体)

3、Post

向指定资源提交数据进行处理请求(提交表单、上传文件),又可能导致新的资源的建立或原有资源的修改

4、Put

向指定资源位置上上传其最新内容(从客户端向服务器传送的数据取代指定文档的内容)

5、Head

HEAD就像GET,只不过服务端接受到HEAD请求后只返回响应头,而不会发送响应内容。当我们只需要查看某个页面的状态的时候,使用HEAD是非常高效的,因为在传输的过程中省去了页面内容。

6、Delete

请求服务器删除request-URL所标示的资源*(请求服务器删除页面)

7、Trace

回显服务器收到的请求,主要用于测试和诊断

8、Connect

HTTP/1.1协议中能够将连接改为管道方式的代理服务器

TCP超时重传、滑动窗口、拥塞控制

TCP超时重传
原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的ACK报文,那么就重新发送数据,直到发送成功为止。

TCP滑动窗口
作用:(1)提供TCP的可靠性;(2)提供TCP的流控特性

TCP的滑动窗口的可靠性也是建立在“确认重传”基础上的。
发送窗口只有收到对端对于本段发送窗口内字节的ACK确认,才会移动发送窗口的左边界。
接收端可以根据自己的状况通告窗口大小,从而控制发送端的接收,进行流量控制。

TCP拥塞控制

拥塞控制是一个全局性的过程; 流量控制是点对点通信量的控制

拥塞窗口(cwnd,congestion window),其大小取决于网络的拥塞程度,并且动态地在变化。

慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。

拥塞控制就是防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。
TCP进行拥塞控制的算法有4种,即慢开始,拥塞避免,快重传和快恢复

TCP流量控制
所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。

慢开始

当主机开始发送数据时,由于并不清楚网络的负荷情况,所以如果立即把大量数据字节注入到网络中,那么就有可能引起网络发送拥塞。
所以较好的办法就是由小到大逐渐增大发送窗口。在每收到一个新的报文段的确认后,可以把拥塞窗口增加最多一个SMSS(最大报文段)的数值。

拥塞避免
让拥塞窗口缓慢增大,即每经过一个往返时间就把发送方的拥塞窗口加1,而不是像慢开始那样加倍增长。这表明在拥塞阶段,拥塞窗口按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。

快重传:
快重传算法可以让发送方尽早知道发生了个别报文段的丢失。快重传算法首先要求接收方不要等待自己发送的数据时才进行捎带确认,而是立即进行确认,即使收到失序的报文段也要立即发出对已收到的报文段的重复确认。

osi七层模型

在这里插入图片描述
在这里插入图片描述
<1> 应用层: OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。

<2> 表示层: 表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。

<3> 会话层:会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。

<4> 传输层:传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。

<5> 网络层: 本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。

<6> 数据链路层 :将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。数据链路层又分为2个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。MAC子层处理CSMA/CD算法、数据出错校验、成帧等;LLC子层定义了一些字段使上次协议能共享数据链路层。 在实际使用中,LLC子层并非必需的。

<7> 物理层 :实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。

前端性能方向的优化

1:页面白屏现象出现的原因

为什么打开一个H5页面会有一长段时间白屏?因为它做了很多事情,大概是:

初始化webview->请求页面->下载数据->解析HTML->请求js/css资源->dom渲染->解析JS执行->JS请求数
据->解析渲染->下载渲染图片

一些简单的页面可能没有JS请求数据这一步,但大部分功能模块应该是有的,根据当前用户信息,JS向后台请求相关数据再渲染,是常规开发方式。

一般页面在dom渲染后能显示雏形,在这之前用户看到的都是白屏,等到下载渲染图片后整个页面才完整显示,首屏秒开优化就是要减少这个过程的耗时。

可能是因为:
1、css文件加载需要一些时间,在加载的过程中页面是空白的。 解决:可以考虑将css代码前置和内联。

2、首屏无实际的数据内容,等待异步加载数据再渲染页面导致白屏。 解决:在首屏直接同步渲染html,后续的滚屏等再采用异步请求数据和渲染html。

3、首屏内联js的执行会阻塞页面的渲染。 解决:尽量不在首屏html代码中放置内联脚本。

前端优化

上述打开一个页面的过程有很多优化点,包括前端和客户端,常见的前端和后端的性能优化在桌面时代已经有最佳实践,主要的是:

降低请求量:合并资源,减少http请求数,minify/zip压缩,懒加载,webP
加快请求速度:预解析DNS,减少域名数,并行加载,CDN分发
缓存:HTTP协议缓存请求,离线缓存manifest,离线数据缓存localStorage
渲染:JS/CSS优化,加载顺序,服务端渲染
其中对首屏启动速度影响最大的就是网络请求,所以优化的重点就是缓存,这里着重说一下前端对请求的缓存策略。这里细分一下,分成HTML的缓存,JS/CSS/image资源的缓存,以及json数据的缓存。

HTML和JS/CSS/image资源都属于静态文件,HTTP本身提供了缓存协议,浏览器实现了这些协议,可以做到静态文件的缓存,总的来说就是两种缓存:

**询问是否有更新:**根据if-Modified-Since/ETag等协议向后端请求询问是否有更新,没有更新返回304,浏览器使用本地缓存
**直接使用本地缓存:**协议里的Cache-Control/Expires字段去确定多长时间内可以不去发送请求询问更新,直接使用本地缓存

前端能做的最大限度的缓存策略是:HTML文件每次都向服务器询问是否有更新,JS/CSS/image资源文件则不请求更新,直接使用本地缓存。那JS/CSS资源文件如何更新?常见的做法是在构建页面的过程中给每个资源文件一个版本号或hash值,若资源文件有更新,版本号和hash值变化,这个资源请求的URL就变化了,同时对应的 HTML 页面更新,变成请求新的资源URL,资源也就更新了。

json 数据的缓存可以用 localStorage 缓存请求下来的数据,可以在首次显示时先用本地数据,再请求更新,这都由前端 JS 控制。

这些缓存策略可以实现 JS/CSS 等资源文件以及用户数据的缓存的全缓存,可以做到每次都直接使用本地缓存数据,不用等待网络请求。但 HTML 文件的缓存做不到,对于 HTML 文件,如果把 Expires / max-age 时间设长了,长时间只使用本地缓存,那更新就不及时,如果设短了,每次打开页面都要发网络请求询问是否有更新,再确定是否使用本地资源,一般前端在这里的策略是每次都请求,这在弱网情况下用户感受到的白屏时间仍然会很长。所以 HTML 文件的“缓存”和跟“更新”间存在矛盾。

DNS解析

当我们在浏览器中输入如www.taobao.com的时候,DNS解析充当了一个翻译的角色,把网址「翻译」成了IP地址。DNS解析的过程就是域名到IP地址的转换的过程。域名解析也叫域名指向、服务器设置、域名配置以及反向IP登记等等。说得简单点就是将好记的域名解析成IP,服务由DNS服务器完成,把域名解析到一个IP地址,然后在此IP地址的主机上将一个子目录与域名绑定。

针对DNS Lookup环节,我们可以针对性的进行DNS解析优化。

1:DNS缓存优化
2:DNS预加载策略
3:稳定可靠的DNS服务器

DNS Prefetch 应该尽量的放在网页的前面,推荐放在 后面。具体使用方法如下:

<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.zhix.net">
<link rel="dns-prefetch" href="//api.share.zhix.net">
<link rel="dns-prefetch" href="//bdimg.share.zhix.net">

需要注意的是,虽然使用 DNS Prefetch 能够加快页面的解析速度,但是也不能滥用,因为有开发者指出 禁用DNS 预读取能节省每月100亿的DNS查询 。

如果需要禁止隐式的 DNS Prefetch,可以使用以下的标签:

<meta http-equiv="x-dns-prefetch-control" content="off">

预解析的实现:

  1. 用meta信息来告知浏览器, 当前页面要做DNS预解析:
  2. 在页面header中使用link标签来强制对DNS预解析:

注:dns-prefetch需慎用,多页面重复DNS预解析会增加重复DNS查询次数。

PS:DNS预解析主要是用于网站前端页面优化,在SEO中的作用湛蓝还未作验证,但作为增强用户体验的一部分rel="dns-prefetch"或许值得大家慢慢发现。

说到DNS解析,我们能做的就是将解析过程前置,就是俗称的预解析,dns解析后会缓存,待真正请求资源时,就不会再花费DNS解析的时间。

注:浏览器会将我们页面中使用的域名自动进行dns解析,所以我们只需配置页面中没有出现的域名即可,但是要注意的是,要合理配置dns的解析,否则会浪费一些不必要的资源
在这里插入图片描述
CDN优化

CDN的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。
最简单的CDN网络由一个DNS 服务器和几台缓存服务器就可以组成,当用户输入URL按下回车,经过本地DNS系统解析,DNS系统会最终将域名的解析权交给CNAME指向的CDN专用DNS服务器,然后将得到全局负载均衡设备的IP地址,用户向全局负载均衡设备发送内容访问请求,全局负载均衡设备将实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度

客户端浏览器先检查本地缓存是否过期,如果过期,则向CDN边缘节点发起请求,CDN边缘节点会检测用户请求数据的缓存是否过期,如果没有过期,则直接响应用户请求,此时一个完成http请求结束;如果数据已经过期,那么CDN还需要向源站发出回源请求(back to the source request),来拉取最新的数据。

CDN 的分流作用:
1、减少了用户的访问延时,
2、也减少了源站的负载。

但其缺点也很明显:当网站更新时,如果CDN节点上数据没有及时更新,即便用户再浏览器使用 Ctrl +F5/command+shift+r 的方式使浏览器端的缓存失效,也会因为 CDN 边缘节点没有同步最新数据而导致用户访问异常。
CDN 缓存策略
CDN 边缘节点缓存策略因服务商不同而不同,但一般都会遵循http标准协议,通过 http 响应头中的 Cache-control: max-age 的字段来设置 CDN 边缘节点数据缓存时间。

CDN 缓存时间会对“回源率”产生直接的影响。

若CDN缓存时间较短,CDN 边缘节点上的数据会经常失效,导致频繁回源,增加了源站的负载,同时也增大的访问延时;

若CDN缓存时间太长,会带来数据更新时间慢的问题。开发者需要增对特定的业务,来做特定的数据缓存时间管理。

CDN 缓存刷新
CDN边缘节点对开发者是透明的,相比于浏览器Ctrl+F5的强制刷新来使浏览器本地缓存失效,开发者可以通过CDN服务商提供的“刷新缓存”接口来达到清理CDN边缘节点缓存的目的。这样开发者在更新数据后,可以使用“刷新缓存”功能来强制CDN节点上的数据缓存过期,保证客户端在访问时,拉取到最新的数据。

CDN 是如何分散源服务器的压力的?
CDN 的核心点有两个: 一个是缓存,一个是回源。

通过缓存和回源策略,达到分散源服务器的压力。首先将从根服务器请求来的资源按要求缓存。然后当有用户访问某个资源的时候,如果被解析到的那个 CDN 节点没有缓存响应的内容,或者是缓存已经到期,就会回源站去获取。

白屏的原因分析

客户端方向
1.JavaScript异常

在头部加载js会阻塞页面渲染。
资源的加载顺序决定页面逻辑是否能正常执行。

2.客户端请求异常

无效请求
错误路径

服务端方向
1.反向代理服务器异常
访问请求打到代理服务器上,代理服务器异常,无法正常解析路径等操作,导致资源加载异常。

2.服务器异常
服务器宕机,通常会报502错误。

网络方向
1.DNS解析异常
DNS不能将域名转换为IP地址。此时任何请求都无用。

2.链接超时
请求资源太大,服务器当中设置的链接时长,在网速较慢的情况下,无法下载完请求的资源,导致白页。

3.CDN服务器异常
CDN节点故障,CND服务器异常

网页性能优化点详情列举

列举点大概如下

网络方面
web应用,总是会有一部分的时间浪费在网络连接和资源下载方面。往往建立一次网络连接是需要时间成本的。而且浏览器同一时间所发送的网络请求数是有限的。所以,这个层面的优化可以从「减少请求数目」开始:

减少http请求:在YUI35规则中也有提到,主要是优化js、css和图片资源三个方面,因为html是没有办法避免的。因此,我们可以做一下的几项操作:

合并js文件
合并css文件
雪碧图的使用(css sprite)
使用base64表示简单的图片

通过使用data: URL模式可以在Web页面包含图片但无需任何额外的HTTP请求。data: URL中的URL是经过base64编码的。格式如下

<img src="data:image/gif;base64....." alt="home">

由于使用内联图片(图片base64)是内联在HTML中的,因此在跨越页面时不会被缓存。一般情况下,不要将网站的Logo做图片base64的处理,因为编码过的Logo会导致页面变大。可将图片作为背景,放在CSS样式表中,此时CSS可被浏览器缓存

.home {
 background-image: url(data:image/gif;base64.....)
}

上述四个方法,前面两者我们可以使用webpack之类的打包工具进行打包;雪碧图的话,也有专门的制作工具;图片的编码是使用base64的,所以,对于一些简单的图片,例如空白图等,可以使用base64直接写入html中。

回到之前网络层面的问题,除了减少请求数量来加快网络加载速度,往往整个资源的体积也是,平时我们会关注的方面。

减小资源体积:可以通过以下几个方面进行实施:

gzip压缩
js混淆
css压缩
图片压缩

gzip压缩主要是针对html文件来说的,它可以将html中重复的部分进行一个打包,多次复用的过程。js的混淆可以有简单的压缩(将空白字符删除)、丑化(丑化的方法,就是将一些变量缩小)、或者可以使用php对js进行混淆加密。css压缩,就是进行简单的压缩。图片的压缩,主要也是减小体积,在不影响观感的前提下,尽量压缩图片,使用png等图片格式,减少矢量图、高清图等的使用。这样子的做法不仅可以加快网页显示,也能减少流量的损耗。

除了以上两部分的操作之外,在网络层面我们还需要做好缓存工作。真正的性能优化来说,缓存是效率最高的一种,往往缩短的加载时间也是最大的。

缓存:可以通过以下几个方面来描述:

DNS缓存
CDN部署与缓存
http缓存

由于浏览器会在DNS解析步骤中消耗一定的时间,所以,对于一些高访问量网站来说,做好DNS的缓存工作,就会一定程度上提升网站效率。CDN缓存,CDN作为静态资源文件的分发网络,本身就已经提升了,网站静态资源的获取速度,加快网站的加载速度,同时也给静态资源做好缓存工作,有效的利用已缓存的静态资源,加快获取速度。http缓存,也是给资源设定缓存时间,防止在有效的缓存时间内对资源进行重复的下载,从而提升整体网页的加载速度。

其实,网络层面的优化还有很多,特别是针对于移动端页面来说。众所周知,移动端对于网络的敏感度更加的高,除了目前的4G和WIFI之外,其他的移动端网络相当于弱网环境,在这种环境下,资源的缓存利用是相当重要的。而且,减少http的请求次数,也是至关重要的,移动端弱网环境下,对于http请求的时间也会增加。所以,我们可以看一下我们在移动端网络方面可以做的优化:

移动端优化:使用以下几种方式来加快移动端网络方面的优化:

使用长cache,减少重定向
首屏优化,保证首屏加载数据小于14kb
不滥用web字体

「使用长cache」,可以使得移动端的部分资源设定长期缓存,这样可以保证资源不用向服务器发送请求,来比较资源是否更新,从而避免304的情况。304重定向,在PC端或许并不会影响网页的加载速度,但是,在移动端网络不稳定的前提下,多一次请求,就多了一部分加载时间。「首屏优化」,对于移动端来说是至关重要的。2s时间是用户的最佳体验,一旦超出这个时间,将会导致用户的流失。所以,针对移动端的网络情况,不可能在这么短时间内加载完成所有的网页资源,所以我们必须保证首屏中的内容被优先显示出来,而且基于TCP的慢启动和拥塞控制,第一个14kb的数据是非常重要的,所以需要保证首部加载数据能够小于14kb。「不滥用web字体」,web字体的好处就是,可以代替某些图片资源,但是,在移动端过多的web字体的使用,会导致页面资源加载的繁重,所以,慎用web字体

渲染和DOM操作方面
首先,简单的聊一下优化渲染的重要性。在网页初步加载时,获取到HTML文件之后,最初的工作是构建DOM和构建CSSOM两个树,之后将他们合并形成渲染树,最后对其进行打印。我们可以通过图片来看一下,简单的过程:

DOM渲染

这里整个过程拉出来写,具体可以再写一篇文章,恕我偷下懒,推荐一篇比较好的文章给大家吧。浏览器渲染过程与性能优化
继续我们的话题,我们可以如何去缩短这个过程呢?可以从以下几个操作进行优化。

优化网页渲染

css的文件放在头部,js文件放在尾部或者异步
尽量避免內联样式

css文件放在「头部加载」,可以保证解析DOM的同时,解析css文件。因为,CSS(外链或内联)会阻塞整个DOM的渲染,然而DOM解析会正常进行,所以将css文件放在头部进行解析,可以加快网页的构建速度。假设将其放在尾部,那时DOM树几乎构建,这时就得等到CSSOM树构建完成,才能够继续下面的步骤。「js放在尾部」:js文件不同,将js文件放在尾部或者异步加载的原因是JS(外链或内联)会阻塞后续DOM的解析,后续DOM的渲染也将被阻塞,而且一旦js中遇到DOM元素的操作,很可能会影响。这方面可以推荐一篇文章——异步脚本载入提高页面性能。「避免使用内联样式」,可以有效的减少html的体积,一般考虑内联样式的时候,往往是样式本身体积比较小,往往加载网络资源的时间会大于它的时候。

除了页面渲染层面的优化,当然最重要的就是DOM操作方面的优化,这部分的优化应该是最多的,而且也是平时开发可以注意的地方。如果开发前期明白这些原理,同时付诸实践的话,就可以在后期的性能完善上面少下很多功夫。那么,接下来我们可以来看一下具体的操作:

DOM操作优化

避免在document上直接进行频繁的DOM操作
使用classname代替大量的内联样式修改
对于复杂的UI元素,设置position为absolute或fixed
尽量使用css动画
使用requestAnimationFrame代替setInterval操作
适当使用canvas
尽量减少css表达式的使用
使用事件代理

前面三个操作,其实都是希望『减少回流和重绘』。其实,进行一次DOM操作的代价是非常之大的,以前可以通过网页操作是否卡顿来进行判断,但是,现代浏览器的进步已经大大减少了这方面的影响。但是,我们还是需要清楚,如何去减少回流和重绘的问题。因为这里不想细说这方面的知识,想要了解的话,可以看这篇文章——回流与重绘:CSS性能让JavaScript变慢?。这可是张鑫旭大大的一篇文章呦(.)。「尽量使用css动画」,是因为本身css动画比较简单,而且相较于js的复杂动画,浏览器本身对其进行了优化,使用上面不会出现卡顿等问题。「使用requestAnimationFrame代替setInterval操作」,相信大家都有所耳闻,setInterval定时器会有一定的延时,对于变动性高的动画来说,会出现卡顿现象。而requestAnimationFrame正好解决的整个问题。「适当使用canvas」,不得不说canvas是前端的一个进步,出现了它之后,前端界面的复杂性也随之提升了。一些难以完成的动画,都可以使用canvas进行辅助完成。但是,canvas使用频繁的话,会加重浏览器渲染的压力,同时导致性能的下降。所以,适当时候使用canvas是一个不错的建议。「尽量减少css表达式的使用」,这个在YUI规则中也被提到过,往往css的表达式在设计之初都是美好的,但在使用过程中,由于其频繁触发的特性,会拖累网页的性能,出现卡顿。因此在使用过程中尽量减少css表达式的使用,可以改换成js进行操作。「使用事件代理」:往往对于具备冒泡性质的事件来说,使用事件代理不失为一种好的方法。举个例子:一段列表都需要设定点击事件,这时如果你给列表中的每一项设定监听,往往会导致整体的性能下降,但是如果你给整个列表设置一个事件,然后通过点击定位目标来触发相应的操作,往往性能就会得到改善。

DOM操作的优化,还有很多,当然也包括移动端的。这个会在之后移动端优化部分被提及,此处先卖个关子。上面我们概述了开始渲染的时候和DOM操作的时候的一些注意事项。接下来要讲的是一些小细节的注意,这些细节可能对于页面影响不大,但是一旦堆积多了,性能也会有所影响。

操作细节注意:

避免图片或者frame使用空src
在css属性为0时,去掉单位
禁止图像缩放
正确的css前缀的使用
移除空的css规则
对于css中可继承的属性,如font-size,尽量使用继承,少一点设置
缩短css选择器,多使用伪元素等帮助定位

上述的一些操作细节,是平时在开发中被要求的,更可以理解为开发规范。(基本操作,坐下_)

列举完基本操作之后,我们再来聊一下移动端在DOM操作方面的一些优化。

移动端优化

长列表滚动优化
函数防抖和函数节流
使用touchstart、touchend代替click
HTML的viewport设置
开启GPU渲染加速

首先,长列表滚动问题,是移动端需要面对的,IOS尽量使用局部滚动,android尽量使用全局滚动。同时,需要给body添加上-webkit-overflow-scrolling: touch来优化移动段的滚动。如果有兴趣的同学,可以去了解一下ios和android滚动操作上的区别以及优化。「防抖和节流」,设计到滚动等会被频繁触发的DOM事件,需要做好防抖和节流的工作。它们都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。

介绍:函数防抖,当调用动作过n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间;函数节流,预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。

「touchstart、touchend代替click」,也是移动端比较常用的操作。click在移动端会有300ms延时,这应该是一个常识呗。(不知道的小伙伴该收藏一下呦)。这种方法会影响用户的体验。所以做优化时,最简单的方法就是使用touchstart或者touchend代替click。因为它们事件执行顺序是touchstart->touchmove->touchend->click。或者,使用fastclick或者zepto的tap事件代替click事件。「HTML的viewport设置」,可以防止页面的缩放,来优化性能。「开启GPU渲染加速」,小伙伴们一定听过CPU吧,但是这里的GPU不能和CPU混为一谈呦。GPU的全名是Graphics Processing Unit,是一种硬件加速方式。一般的css渲染,浏览器的渲染引擎都不会使用到它。但是,在3D渲染时,计算量较大,繁重,浏览器会开启显卡的硬件加速来帮助完成这些操作。所以,我们这里可以使用css中的translateZ设定,来欺骗浏览器,让其帮忙开启GPU加速,加快渲染进程。

DOM部分的优化,更多的是习惯。需要自己强制要求自己在开发过程中去注意这些规范。所以,这部分的内容可以多关注一下,才能够慢慢了解。同时,本人对于上述几点的描述是概括性的。并没有对其进行详细的展开。因此,也要求你去细细的查阅Google呦。

数据方面
数据,也可以说是前端优化方面比较重要的一块内容。页面与用户的交互响应,往往伴随着数据交互,处理,以及ajax的异步请求等内容。所以,我们也可以来聊聊这一块的知识。首先是对于图片数据的处理:

图片加载处理

图片预加载
图片懒加载
首屏加载时进度条的显示

「图片预加载」,预加载的寓意就是提前加载内容。而图片的预加载往往会被用在图片资源比较大,即时加载时会导致很长的等待过程时,才会被使用的。常见场景:图片漫画展示时。往往会预加载一张到两张的图片。「图片懒加载」,懒加载或许你是第一次听说,但是,这种方式在开发中会被经常使用。首先,我们需要明白一个道理:往往只有看到的资源是必须的,其他资源是可以随着用户的滚动,随即显示的。所以,特别是对于图片资源特别多的网站来说,做好图片的懒加载是可以大大提升网页的载入速度的。

常见的图片懒加载的方式就是:在最初给图片的src设置一个比较简单的图片,然后将图片的真实地址设置给自定义的属性,做一个占位,然后给图片设置监听事件,一旦图片到达视口范围,从图片的自定义属性中获取出真是地址,然后赋值给src,让其进行加载。

「首屏进度条的显示」:往往对于首屏优化后的数据量并不满意的话,同时也不能进一步缩短首屏包的长度了,就可以使用进度条的方式,来提醒用户进行等待。

讲完了图片这一块数据资源的处理,往往我们需要去优化一下异步请求这一部分的内容。因为,异步的数据获取也是前端不可分割的。这一部分我们也可以做一定的处理:

异步请求的优化

使用正常的json数据格式进行交互
部分常用数据的缓存
数据埋点和统计

「JSON交互」,JSON的数据格式轻巧,结构简单,往往可以大大优化前后端的数据通信。「常用数据的缓存」,可以将一些用户的基本信息等常用的信息做一个缓存,这样可以保证ajax请求的减少。同时,HTML5新增的storage的内容,也不用怕cookie暴露,引起的信息泄漏问题。「数据埋点和统计」,对于资深的程序员来说,比较了解。而且目前的大部分公司也会做这方面的处理。有心的小伙伴可以自行查阅。

最后,还有就是大量数据的运算。对于javascript语言来说,本身的单线程就限制了它并不能计算大量的数据,往往会造成页面的卡顿。而可能业务中有些复杂的UI需要去运行大量的运算,所以,webWorker的使用是至关重要的。或许,前端标准普及的落后,会导致大家对于这些新生事物的短暂缺失吧。

拓展:前端埋点和性能监控

1:前端监控

数据监控(用户行为)
pv,uv
记录操作系统
用户在每一个页面的停留时间(离开页面,进入页面)
用户进入的入口
用户在相应页面的触发行为,点击按钮
性能监控 (js中的performance)
用户的首屏加载
http请求响应时间
页面渲染时间
页面交互动画完成时间

使用 performance.timing 这个api就可以获取到绝大部分性能相关的数据
在这里插入图片描述
有前一个网页的情况下

  • navigationStart:在同一个浏览器上下文中,前一个网页(与当前页面不一定同域)unload 的时间戳,如果无前一个网页
    unload ,则与 fetchStart 值相等
  • unloadEventStart:前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload
    或者前一个网页与当前页面不同域,则值为 0
  • redirectStart:第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0
  • redirectEnd:最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内的重定向才算,否则值为 0

开始加载当前页面

  • fetchStart:浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前

网络传输阶段 DNS TCP

  • domainLookupStart:DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与
    fetchStart 值相等
  • domainLookupEnd:DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart
    值相等
  • connectStart:HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart值相等,如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间
  • **secureConnectionStart:**HTTPS 连接开始的时间,如果不是安全连接,则值为 0
  • connectEnd:HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等,如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间

读取文档阶段

  • requestStart:HTTP请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存,连接错误重连时,这里显示的也是新建立连接的时间
  • responseStart:HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存
  • responseEnd:HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存

解析文档阶段

  • domLoading:开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出
    readystatechange 相关事件
  • domInteractive:完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出
    readystatechange 相关事件
  • domContentLoadedEventStart:DOM
    解析完成后,网页内资源加载开始的时间,代表DOMContentLoaded事件触发的时间节点
  • domContentLoadedEventEnd:DOM 解析完成后,网页内资源加载完成的时间(如 JS
    脚本加载执行完毕),文档的DOMContentLoaded 事件的结束时间,也就是jQuery中的domready时间;
  • domComplete:DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件
  • loadEventStart:load 事件发送给文档,也即 load 回调函数开始执行的时间,如果没有绑定 load 事件,值为 0
  • loadEventEnd:load 事件的回调函数执行完毕的时间,如果没有绑定 load 事件,值为 0

各个阶段时间段查询

DNS查询耗时 = domainLookupEnd - domainLookupStart

TCP链接耗时 = connectEnd - connectStart

request请求耗时 = responseEnd - responseStart

解析dom树耗时 = domComplete - domInteractive

白屏时间 = domloadng - fetchStart

domready时间 = domContentLoadedEventEnd - fetchStart

onload时间 = loadEventEnd - fetchStart

2:前端埋点

埋点方案的确定
业界的埋点方案主要分为以下三类:

代码埋点:在需要埋点的节点调用接口,携带数据上传。如百度统计等;
可视化埋点:使用可视化工具进行配置化的埋点,即所谓的「无痕埋点」,前端在页面加载时,可以读取配置数据,自动调用接口进行埋点。如开源的Mixpanel;
无埋点:无埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,优点是前端只要一次加载埋点脚本。缺点是流量和采集的数据过于庞大,服务器性能压力山大

代码埋点
代码埋点分为 命令式埋点 与 声明式埋点

命令式埋点
命令式埋点,顾名思义,开发者需要手动在需要埋点的节点处进行埋点。如点击按钮或链接后的回调函数、页面ready时进行请求的发送。大家肯定都很熟悉这样的代码:

// 页面加载时发送埋点请求
$(document).ready(function(){
   // ... 这里存在一些业务逻辑
   sendRequest(params);
});
// 按钮点击时发送埋点请求
$('button').click(function(){
   // ... 这里存在一些业务逻辑
   sendRequest(params);
});

可以很容易发现,这样的做法很有可能会将埋点代码侵入业务代码,这使整体业务代码变得繁琐,容易出错,且后续代码会愈加膨胀,难以维护。所以,我们需要让埋点的代码与具体的业务逻辑解耦,即 声明式埋点 ,从而提高埋点的效率和代码的可维护性。

声明式埋点
例:统计某 dom 点击事件
首先封装一个 vue 指令(directive);

// 注册一个全局自定义指令 `v-tracking`
Vue.directive("tracking", {
  // 只调用一次,指令第一次绑定到元素时调用。
  bind: (el, binding) => {
    // 给元素绑定事件
    el.addEventListener(
      "click", _ => {
        // 默认参数设置
        let def = {
          url:'/url',
        }
        let data =  Object.assign(def,binding.value);
        //binding.value为传入的对象字面量,将其转为字符串再通过RSA加密来压缩埋点内容
        console.log(RSA.encrypt(JSON.stringify(data)));
        // 发送埋点数据
      },
      false
    );
  }
});

html

  <button v-tracking="{ tag: '1', remake:'1' }">按钮1</button>
  <button v-tracking="{ tag: '2', remake:'2' }">按钮2</button>

目前埋点代码与业务代码混合再一起的,声明式埋点可以再一定程度上解耦业务代码
一些复杂场景依然需要命令式埋点

理论上,声明式埋点只需要关注两个问题

1:需要埋点的DOM节点;
2:所需携带的数据
因此,可以很快想出一个声明式埋点的方法:

// key表示埋点的唯一标识;act表示埋点方式

<button data-stat="{key:'111', act: 'click'}">埋点</button>

那么可以去遍历DOM树,找到 [data-stat] 的节点,给这个button绑上click事件,把这些参数在回调函数中通过请求发出去。

在DOM节点(html)上声明埋点,与业务逻辑(通常在Javascript文件中)就解耦了。调用也很方便。

36:小程序 与 App 与 H5 之间的区别

小程序的实现原理

根据微信官方的说明,微信小程序的运行环境有 3 个平台,iOS 的 WebKit(苹果开源的浏览器内核),Android 的 X5 (QQ 浏览器内核),开发时用的 nw.js(C++ 实现的 web 转桌面应用)。
在这里插入图片描述

小程序运行时会创建两个线程:View Thread 和 AppService Thread,相互隔离,通过桥接协议 WeixinJsBridage 进行通信(包括 setData 调用、canvas 指令和各种 DOM 事件)。

下述表格展示了两个线程的区别:
在这里插入图片描述

两个线程是通过系统层的 JSBridage 来通信的,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。

小程序的框架包含两部分,分别是渲染层和AppService逻辑层,渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本,进行逻辑处理、数据请求及接口调用等,一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端进行中转,逻辑层把数据变化通知到渲染层,触发渲染层页面更新,渲染层把触发的事件通知到逻辑层进行业务处理。

下面附上一张小程序框架图:
  在这里插入图片描述
解析(从下往上看):

1、最底层是微信,当我们发版时小程序开发工具会把我们的代码和框架一起进行打包,当我们在微信里打开小程序时其实微信会把打包好的代码下载到微信app里,这样我们就可以像在开发工具里一样在微信里运行我们的小程序了。

2、native层就是小程序的框架,这个框架里封装了ui层组件和逻辑层组件,这些组件可以通过微信app提供的接口调用手机硬件信息。

3、最上层的两个框,是我们真正需要进行操作的视图层和逻辑层,视图层和逻辑层的交互是通过数据经由native层进行交互的。视图层和逻辑层都可以调用native框架里封装好的组件和方法。

四、小程序的生命周期

关于小程序的生命周期,可以分为两个部分来理解:应用生命周期和页面生命周期。

应用的生命周期:

1、用户首次打开小程序,触发 onLaunch(全局只触发一次)。

2、小程序初始化完成后,触发onShow方法,监听小程序显示。

3、小程序从前台进入后台,触发 onHide方法。

4、小程序从后台进入前台显示,触发 onShow方法。

5、小程序后台运行一定时间,或系统资源占用过高,会被销毁。

页面生命周期:

1、小程序注册完成后,加载页面,触发onLoad方法。

2、页面载入后触发onShow方法,显示页面。

3、首次显示页面,会触发onReady方法,渲染页面元素和样式,一个页面只会调用一次。

4、当小程序后台运行或跳转到其他页面时,触发onHide方法。

5、当小程序有后台进入到前台运行或重新进入页面时,触发onShow方法。

6、当使用重定向方法wx.redirectTo()或关闭当前页返回上一页wx.navigateBack(),触发onUnload。

同时,应用生命周期会影响到页面生命周期

小程序与 App 的区别

运行环境

原生 App 直接运行在操作系统的单独进程中(在 Android 中还可以开启多进程),而小程序只能运行在微信的进程中。

开发成本

原生 App 的开发涉及到 Android/iOS 多个平台、开发工具、开发语言、不同设备的适配等问题;而小程序只需要开发一个就可以在 Android/iOS 等不同平台不同设备上运行。

原生 App 需要在商店上架(Android 需要上架各种商店);小程序只能在微信平台发布。

系统权限

原生 App 调用的是系统资源,也就是说系统提供给开发的的 API 都可以使用;而小程序是基于微信的,小程序所有的功能都受限于微信,也就是说微信给开发者提供 API 才可以使用,不能绕过微信直接使用系统提供的 API。

原生 App 可以给用户推送消息;小程序不允许主动给用户发送消息,只能回复模版消息 。

原生 App 有独立的数据库,可以做离线存储;小程序只能存储到 LocalStorage,无法做离线存储。

原生 App 需要下载,安装包比较大;小程序无需下载,可以通过小程序码等方式通过微信直接打开。

运行流畅度

原生 App 运行在操作系统中,所有的原生组件可以直接调用 GPU 进行渲染;而小程序运行在微信的进程中,只能通过 WebView 进行渲染。

小程序与 H5 的区别

运行环境

网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。网页开发者可以使用到各种浏览器暴露出来的 DOM API,进行 DOM 选中和操作。而如上文所述,小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API。这一区别导致了前端开发非常熟悉的一些库,例如 jQuery、 Zepto 等,在小程序中是无法运行的。同时 JSCore 的环境同 NodeJS 环境也是不尽相同,所以一些 NPM 的包在小程序中也是无法运行的。

网页开发者需要面对的环境是各式各样的浏览器,PC 端需要面对 IE、Chrome、QQ浏览器等,在移动端需要面对Safari、Chrome以及 iOS、Android 系统中的各式 WebView 。而小程序开发过程中仅需要面对的是两大操作系统 iOS 和 Android 的微信客户端,以及用于辅助开发的小程序开发者工具,小程序中三大运行环境也是有所区别的

简单来说,小程序是一种应用,运行的环境是微信(App);H5 是一种技术,依附的外壳是是浏览器

H5 的运行环境是浏览器,包括 WebView,而微信小程序的运行环境并非完整的浏览器,因为小程序的开发过程中只用到一部分H5 技术。

小程序的运行环境是微信开发团队基于浏览器内核完全重构的一个内置解析器,针对性做了优化,配合自己定义的开发语言标准,提升了小程序的性能。

小程序中无法使用浏览器中常用的 window 对象和 document 对象,H5 可以随意使用。

开发成本
H5 的开发,涉及开发工具(vscode、Atom等)、前端框架(Angular、react等)、模块管理工具(Webpack 、Browserify 等)、任务管理工具(Grunt、Gulp等),还有 UI 库选择、接口调用工具(ajax、Fetch Api等)、浏览器兼容性等等。

尽管这些工具可定制化非常高,大部分开发者也有自己的配置模板,但对于项目中各种外部库的版本迭代、版本升级,这些成本加在一起那就是个不小数目了。

而开发一个微信小程序,由于微信团队提供了开发者工具,并且规范了开发标准,则简单得多。前端常见的 HTML、CSS 变成了微信自定义的 WXML、WXSS,官方文档中都有明确的使用介绍,开发者按照说明专注写程序就可以了。

需要调用后端接口时,调用发起请求API;需要上传下载时,调用上传下载API;需要数据缓存时,调用本地存储API;引入地图、使用罗盘、调用支付、调用扫码等等功能都可以直接使用;UI 库方面,框架带有自家 weui 库加成。

并且在使用这些 API 时,不用考虑浏览器兼容性,不用担心出现 BUG,显而易见微信小程序的开发成本相对低很多。

系统权限
微信小程序相对于 H5 能获得更多的系统权限,比如:网络通信状态、数据缓存能力等,这些系统级权限都可以和微信小程序无缝衔接。

而这一点恰巧是 H5 被诟病的地方,这也是 H5 的大多应用场景被定位在业务逻辑简单、功能单一的原因。

运行流畅度
这条无论对于用户还是开发者来说,都是最直观的感受。长久以来,当HTML5应用面对复杂的业务逻辑或者丰富的页面交互时,它的体验总是不尽人意,需要不断的对项目优化来提升用户体验。但是由于微信小程序运行环境独立,尽管同样用 HTML +CSS + JS 去开发,但配合微信的解析器最终渲染出来的是原生组件的效果,自然体验上将会更进一步。

前端中的算法面试总结

链接地址:前端算法总结详情

1:排序中稳定性和复杂度分析表

稳定性与复杂度

稳定性:指排序后,相同元素保持出现的先后顺序。

时间复杂度是O(N2),额外空间负责度O(1):

l 冒泡排序:当遇到相同数时,该数不交换,将后面的数往下沉。可以稳定;
l 插入排序:当遇到相同数时,该数不交换;可以稳定;
l 选择排序:做不到稳定性。因为你要从后面的所有数中找到最小的,然后将前面的某一个a与该最值交换,如果有多个a存在,那么,a的先后顺序将无法保证。故做不到。

复杂度是O(N*logN):

l 归并排序:merge时,当相同时先拷贝左边(小区域)的数;可以稳定; 额外空间复杂度为O(N);
l 快排:做不到稳定性; 额外空间复杂度为O(logN);
l 堆排:做不到稳定性。在建大根堆的时候,就都已经不能保证稳定性了。 额外空间复杂度为O(1);

总结表:
在这里插入图片描述
三、相关概念

1、时间复杂度

时间复杂度可以认为是对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。

常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n),线性对数阶O(nlog2n),平方阶O(n2)

时间复杂度O(1):算法中语句执行次数为一个常数,则时间复杂度为O(1)

2、空间复杂度

空间复杂度是指算法在计算机内执行时所需存储空间的度量,它也是问题规模n的函数

空间复杂度O(1):当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1)

空间复杂度O(log2N):当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(log2n)

空间复杂度O(n):当一个算法的空间复杂度与n成线性比例关系时,可表示为O(n).

4:js中的栈和队列

:先进后出(向一个栈插入新元素,称作入栈;它是把元素放到栈顶,成为新的栈顶元素;删除时先删除栈顶元素)

用push入栈,pop出栈

队列:先进先出(线性表,只允许在表前端进行删除,在表后端进行插入;插入操作是队尾,删除操作是队头)

用unshift添加到队列,用pop从队列中移除

5: 数据结构中的堆还有hash表

一、什么是Hash表
要想知道什么是哈希表,那得先了解哈希函数
哈希函数

对比之前博客讨论的二叉排序树 二叉平衡树 红黑树 B B+树,它们的查找都是先从根节点进行查找,从节点取出数据或索引与查找值进行比较。那么,有没有一种函数H,根据这个函数和查找关键字key,可以直接确定查找值所在位置,而不需要一个个比较。这样就**“预先知道”**key所在的位置,直接找到数据,提升效率。


地址index=H(key)
说白了,hash函数就是根据key计算出应该存储地址的位置,而哈希表是基于哈希函数建立的一种查找表

一、什么是堆

堆(英语:heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。
堆的性质:
1.堆中某个节点的值总是不大于或不小于其父节点的值。
2.堆总是一棵完全二叉树。

什么是完全二叉树呢?

若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中

在最左边,这就是完全二叉树。我们知道二叉树可以用数组模拟,堆自然也可以。

参与评论 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页

打赏作者

qq_43239820

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值