前端面试宝典

还在不断更新中,欢迎大家指正

一、浏览器中输入url会发生什么?

1.DNS域名解析;

 1 参考详细内容:https://blog.csdn.net/ZWE7616175/article/details/80473982
 2 
 3 * 在浏览器DNS搜索
 4 
 5 * 在操作系统DNS缓存中搜索
 6 
 7 * 读取系统hosts文件,查找其中是否有对应的IP
 8 
 9 * 向本地配置的首选DNS服务器发起域名解析请求
10 
11 DNS三个组成部分:
12 
13 * 域名空间
14 
15 * 域名服务器
16 
17 * 域名解析程序
View Code

2.建立TCP连接;

参考详细内容:https://blog.csdn.net/zwe7616175/article/details/80432486

确认好了IP和端口号,则可以向该IP地址对应的服务器的该端口号发起TCP连接请求。

TCP三次握手大致总结为以下几个步骤:
发送端首先发送一个带SYN的数据包给接收方;接收方收到后,回传一个带SYN/ACK的数据包表示传达确认信息。最后发送方再回传一个带ACK的数据包,代表握手结束。

3.发送HTTP请求;

参考详细内容:https://blog.csdn.net/zwe7616175/article/details/80304093

三次握手成功后,开始通讯,根据HTTP协议的要求,组织一个请求的数据包,里面含有请求的资源路径、身份信息等。

请求报文是由请求方法、请求URL、协议版本、可选的请求首部字段和内容实体构成的。

4.服务器处理请求;

发送请求后,服务器响应请求,将数据返回给浏览器,数据可以是根据HTML协议组织的网页,里面包含页面的布局、文字等,也可以是图片或脚本程序。

响应报文基本上是由版本协议、状态码、用以解释状态码的原因短语、可选的响应首部字段以及实体主题构成。

5.返回响应结果;

我们常见的情况,如果资源路径指定的资源不存在,服务器就会返回404错误。还有一些其他的响应码,如下表。

HTTP响应码  类别             原因短语
1XX                  Informational(信息状态码)     接受的请求正在处理
2XX      Success(成功状态码)        请求正常处理完毕
3XX         Redirection(重定向状态码)     需要进行附加操作以完成请求
4XX        Client Error(客户端错误状态码)    服务器无法处理请求
5XX        Server Error(服务器错误状态码)   服务器处理请求出错

6.关闭TCP连接;

四次挥手

参考详细内容:四次挥手过程

7.浏览器解析HTML;

浏览器按顺序解析html文件,构建DOM树,在解析到外部的css和js文件时,向服务器发起请求下载资源。
若是下载css文件,则解析器会在下载的同时继续解析后面的html来构建DOM树,则在下载js文件和执行它时,解析器会停止对html的解析。便出现了js阻塞问题。

预加载器

当浏览器被脚本文件阻塞时,预加载器(轻量级解析器) 会继续解析后面的html,寻找需要下载的资源。若发现有需要下载的资源,预加载器开始接收这些资源。预加载器只能检索HTML标签中的URL,无法检测到使用脚本添加的URL,这些资源要等脚本代码执行才会获取。预解析并不改变DOM树,将这个工作留给主解析过程。

浏览器解析css,形成CSSOM树,当DOM树构建完成后,浏览器引擎通过DOM树和CSSOM树构造树渲染树。渲染树中包含可视节点的样式信息。

8.浏览器布局渲染;

布局

通过计算得到每个渲染对象在可视区域中的具体位置(大小和位置),是一个递归的过程。

绘制

将计算好的每个像素点信息绘制在屏幕上。

利用DOM和CSSOM构建一个渲染树,布局渲染树,绘制渲染树。

DOM树是由HTML文件中的标签排列组成,渲染树是在DOM树中加入CSS或HTML中的style样式而形成的。渲染树只包含需要显示在页面中的DOM元素,像元素或display属性值为none的元素都不在渲染树中。

在浏览器还没接收到完整的HTML文件时,它就开始渲染页面了,在遇到外部链入的脚本标签或样式标签或图片时,会再次发送http请求重复上述的步骤。在收到CSS文件后对已经渲染的页面重新渲染,加入他们应有样式,图片文件加载完立刻显示在相应位置。在这一过程中可能会出发页面重绘或重排。

 二、js基本数据类型

在 ECMAScript 规范中,共定义了 7 种数据类型,分为 基本类型 和 引用类型 两大类,如下所示:
基本类型:String、Number、Boolean、Symbol、Undefined、Null
引用类型:Object

基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。
引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆(heap)中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 Object 外,还包括 Function 、Array、RegExp、Date 等等。

三、判断js数据类型

方法一:typeof

 typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。
typeof ''; // string 有效
typeof 1; // number 有效
typeof Symbol(); // symbol 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Function(); // function 有效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效

有些时候,typeof 操作符会返回一些令人迷惑但技术上却正确的值:
对于基本类型,除 null 以外,均可以返回正确的结果。
对于引用类型,除 function 以外,一律返回 object 类型。
对于 null ,返回 object 类型。
对于 function 返回 function 类型。
其中,null 有属于自己的数据类型 Null , 引用类型中的 数组、日期、正则 也都有属于自己的具体类型,而 typeof 对于这些类型的处理,只返回了处于其原型链最顶端的 Object 类型,没有错,但不是我们想要的结果。

方法二:instanceof
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型,我们用一段伪代码来模拟其内部执行过程:
instanceof (A,B) = {
var L = A.__proto__;
var R = B.prototype;
if(L === R) {
// A的内部属性 __proto__ 指向 B 的原型对象
return true;
}
return false;
}
从上述过程可以看出,当 A 的 __proto__ 指向 B 的 prototype 时,就认为 A 就是 B 的实例,我们再来看几个例子:
[] instanceof Array; // true
{} instanceof Object;// true
new Date() instanceof Date;// true

function Person(){};
new Person() instanceof Person;

[] instanceof Object; // true
new Date() instanceof Object;// true
new Person instanceof Object;// true

虽然 instanceof 能够判断出 [ ] 是Array的实例,但它认为 [ ] 也是Object的实例,为什么呢?
从 instanceof 能够判断出 [ ].__proto__ 指向 Array.prototype,而 Array.prototype.__proto__ 又指向了Object.prototype,最终 Object.prototype.__proto__ 指向了null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链.

console.log(Object instanceof Object);//true
console.log(Function instanceof Function);//true
console.log(Number instanceof Number);//false
console.log(String instanceof String);//false

console.log(Function instanceof Object);//true

console.log(Foo instanceof Function);//true
console.log(Foo instanceof Foo);//false


从原型链可以看出,[] 的 __proto__ 直接指向Array.prototype,间接指向 Object.prototype,所以按照 instanceof 的判断规则,[] 就是Object的实例。依次类推,类似的 new Date()、new Person() 也会形成一条对应的原型链 。因此,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。
ES5 提供了 Array.isArray() 方法 ,该方法用以确认某个对象本身是否为 Array 类型.
if (Array.isArray(value)){
//对数组执行某些操作
}
Array.isArray() 本质上检测的是对象的 [[Class]] 值,[[Class]] 是对象的一个内部属性,里面包含了对象的类型信息,其格式为 [object Xxx] ,Xxx 就是对应的具体类型 。对于数组而言,[[Class]] 的值就是 [object Array] 。

方法三:constructor
当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性,并让其指向 F 的引用。
当执行 var f = new F() 时,F 被当成了构造函数,f 是F的实例对象,此时 F 原型上的 constructor 传递到了 f 上,因此 f.constructor == F
可以看出,F 利用原型对象上的 constructor 引用了自身,当 F 作为构造函数来创建对象时,原型上的 constructor 就被遗传到了新创建的对象上, 从原型链角度讲,构造函数 F 就是新对象的类型。这样做的意义是,让新对象在诞生以后,就具有可追溯的数据类型。
同样,JavaScript 中的内置对象在内部构建时也是这样做的.
细节问题:
1. null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。
2. 函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失,constructor 会默认为 Object
为什么变成了 Object?
因为 prototype 被重新赋值的是一个 { }, { } 是 new Object() 的字面量,因此 new Object() 会将 Object 原型上的 constructor 传递给 { },也就是 Object 本身。
因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。

方法四:toString
toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用

function DataType(tgt, type) {
  const dataType = Object.prototype.toString.call(tgt).replace(/[object /g, "").replace(/]/g, "").toLowerCase();
  return type ? dataType === type : dataType;
}
console.log(DataType("yajun")); // "string"
DataType(19941112); // "number"
DataType(true); // "boolean"
DataType([], "array"); // true
DataType({}, "array"); // false
View Code

四、手动实现new

用new Object() 的方式新建了一个对象 obj
取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
返回 obj
function Person(name,age){
  this.name = name;
  this.age = age;
  this.sex = 'male';
}
Person.prototype.isHandsome = true;
Person.prototype.sayName = function(){
  console.log(`Hello , my name is ${this.name}`);
}
function objectFactory() {
  let obj = new Object(),//从Object.prototype上克隆一个对象
  Constructor = [].shift.call(arguments);//取得外部传入的构造器
  console.log({Constructor})
  const F=function(){};
  F.prototype= Constructor.prototype;
  obj=new F();//指向正确的原型
  Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性
  return obj;//返回 obj
};
function createNew(Con, ...args) {
  let obj = {};
  obj.__proto__ = Con.prototype;//关联原型
  let result = Con.apply(obj, args);//this指向
  return result instanceof Object ? result : obj;

}
let handsomeBoy = objectFactory(Person,'Nealyang',25);
console.log(handsomeBoy.name) // Nealyang
console.log(handsomeBoy.sex) // male
console.log(handsomeBoy.isHandsome) // true
handsomeBoy.sayName(); // Hello , my name is Nealyang
View Code

 五、前端缓存

  缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。

浏览器第一次向服务器发起该请求后拿到请求结果,会根据 响应报文 中 HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中。

  

  

 5.1HTTP缓存

  HTTP缓存是在HTTP请求传输时用到的缓存,主要在服务器代码上设置。

  Http缓存可以分为两大类,强制缓存(也称强缓存)和协商缓存。两类缓存规则不同,强制缓存在缓存数据未失效的情况下,不需要再和服务器发生交互;而协商缓存,顾名思义,需要进行比较判断是否可以使用缓存。

  两类缓存规则可以同时存在,强制缓存优先级高于协商缓存,也就是说,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行协商缓存规则。

  5.1.1强缓存

  强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。

  强制缓存的情况主要有三种,如下:

  ①不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致),如下图:

  

  ②存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存,如下图:

  

   ③存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果,如下图:

  

   强制缓存的缓存规则:

  当浏览器向服务器发起请求时,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是 Expires 和 Cache-Control,其中 Cache-Control 优先级比 Expires 高。

  Expires:

  Expires是HTTP/1.0控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,如果客户端的时间小于Expires的值时,直接使用缓存结果。到了HTTP/1.1,Expire已经被Cache-Control替代,原因在于Expires控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存则会直接失效,这样的话强制缓存的存在则毫无意义。

  Cache-Control:

  在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,主要取值为:

  • public:所有内容都将被缓存(客户端和代理服务器都可缓存)
  • private:所有内容只有客户端可以缓存,Cache-Control的默认取值
  • no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
  • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
  • max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效

  接下来,我们直接看一个例子,如下:

  

  由上面的例子我们可以知道:

          ①HTTP响应报文中expires的时间值,是一个绝对值

          ②HTTP响应报文中Cache-Control为max-age=600,是相对值

  由于Cache-Control的优先级比expires高,那么直接根据Cache-Control的值进行缓存,意思就是说在600秒内再次发起该请求,则会直接使用缓存结果,强制缓存生效。

  注:在无法确定客户端的时间是否与服务端的时间同步的情况下,Cache-Control相比于expires是更好的选择,所以同时存在时,只有Cache-Control生效。

  了解强制缓存的过程后,我们进行下一步思考:浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效?

  

  这里我们以博客的请求为例,状态码为灰色的请求则代表使用了强制缓存,请求对应的Size值则代表该缓存存放的位置,分别为 from memory cache 和 from disk cache

  么from memory cache 和 from disk cache又分别代表的是什么呢?什么时候会使用from disk cache,什么时候会使用from memory cache呢?

    from memory cache代表使用内存中的缓存,

    from disk cache则代表使用的是硬盘中的缓存,

  浏览器读取缓存的顺序为memory –> disk –> 服务器请求。

   那么接下来我们一起详细分析一下缓存读取问题,这里仍让以我的博客为例进行分析:

   首次访问页面  

  

  关闭博客的标签页,重新打开页面

  

  刷新页面

  

  内存缓存(from memory cache):内存缓存具有两个特点,分别是速度快和时间限制。

  硬盘缓存(from disk cache):硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行I/O操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。

  在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。

  5.1.2协商缓存

     协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

    1、协商缓存生效,返回304,如下

    

    2、协商缓存失效,返回200和请求结果结果,如下

     

    同样,协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。

    Last-Modified是服务器响应请求时,返回该资源文件在服务器最后被修改的时间,如下。

        

    If-Modified-Since则是客户端再次发起该请求时,携带上次请求返回的Last-Modified值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有If-Modified-Since字段,则会根据If-Modified-Since的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于If-Modified-Since的字段值,则重新返回资源,状态码为200;否则则返回304,代表资源无更新,可继续使用缓存文件,如下。
    

    Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),如下。

    

    If-None-Match是客户端再次发起该请求时,携带上次请求返回的唯一标识Etag值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有If-None-Match,则会根据If-None-Match的字段值与该资源在服务器的Etag值做对比,一致则返回304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为200,如下。

   

    注:Etag / If-None-Match优先级高于Last-Modified / If-Modified-Since,同时存在则只有Etag / If-None-Match生效。对于协商缓存,使用 Ctrl+F5强制刷新可以使得缓存无效。但是对于强缓存,在未过期时,必须更新资源路径才能发起新的请求(更改了路径相当于是另一个资源了,这也是前端工程化中常用到的技巧)。

     强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存,主要过程如下:

      

5.2浏览器缓存

  浏览器缓存则主要由前端开发在前端js上进行设置。

  5.2.1 本地存储小容量

    Cookie主要用于用户信息的存储,Cookie的内容可以自动在请求的时候被传递给服务器。

    LocalStorage的数据将一直保存在浏览器内,直到用户清除浏览器缓存数据为止。

    SessionStorage的其他属性同LocalStorage,只不过它的生命周期同标签页的生命周期,当标签页被关闭时,SessionStorage也会被清除。

  5.2 本地存储大容量

    WebSql和IndexDB主要用在前端有大容量存储需求的页面上,例如,在线编辑浏览器或者网页邮箱。

    

 六、回流和重绘

  回流(reflow):对于DOM结构中的各个元素都有自己的盒子模型,这些都需要浏览器根据各种样式(浏览器的、开发人员定义的等)来计算并根据计算结果将元素放到它该出现的位置,这个过程称之为reflow ; 回流(reflow)就是元素的位置发生了改变(不管是添加、删除元素,还是元素尺寸改变),会触发回流。

  触发回流时机:  

    【布局与几何位置发送变化】

      添加或删除可见的DOM元素
      元素的位置发生变化
      元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
      内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
      页面一开始渲染的时候(这肯定避免不了)
      浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

      重绘(repaint):当各种盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来后,浏览器于是便把这些元素都按照各自的特性绘制了一遍,于是页面的内容出现了,这个过程称之为repaint。重绘(repaint)简单的说,元素位置不会发生改变,视觉效果会有所改变。

   触发重绘时机:

    回流一定会发送重绘,重绘不一定发送回流
    对于不修改几何属性,只是颜色的样式发送变化发送重绘

七、简单介绍前端MVC/MVVM/MVP模式特点及区别

https://blog.csdn.net/Lniuniu/article/details/103541369

http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html

 1、概述

    MVC,MVP,MVVM是三种常见的前端架构模式(Architectural Pattern),它通过分离关注点来改进代码组织方式。不同于设计模式(Design Pattern),只是为了解决一类问题而总结出的抽象方法,一种架构模式往往能使用多种设计模式。

   MVC模式是MVP,MVVM模式的基础,这两种模式更像是MVC模式的优化改良版,他们三个的MV即Model,view相同,不同的是MV之间的纽带部分。本文主要介绍MVC与MVVM的应用与区别,因为MVP好像不是很常用。

  https://www.cnblogs.com/xhyccc/p/13391487.html

2、MVC

  Model: 模型层(用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法,数据模型或业务模型,就是我们要显示给用户查看的内容)
  View: 视图层(渲染页面,用户直接看到的界面)
  Controller: 控制器(M和V之间的连接器,用于控制应用程序的流程,及页面的业务逻辑)

  MVC特点:

    MVC模式的特点在于实现关注点分离,即应用程序中的数据模型与业务和展示逻辑解耦。在客户端web开发中,就是将模型(M-数据、操作数据)、视图(V-显示数据的HTML元素)之间实现代码分离,松散耦合,使之成为一个更容易开发、维护和测试的客户端应用程序。
    1、View 传送指令到 Controller ;
    2、Controller 完成业务逻辑后,要求 Model 改变状态 ;
    3、Model 将新的数据发送到 View,用户得到反馈。

  MVC优点:

    1、耦合性低,视图层和业务层分离,这样就允许更改视图层代码而不用重新编译模型和制器代码。
    2、重用性高
    3、生命周期成本低
    4、MVC使开发和维护用户接口的技术含量降低
    5、可维护性高,分离视图层和业务逻辑层也使得WEB应用更易于维护和修改
    6、部署快

  MVC缺点:

    1、不适合小型,中等规模的应用程序,花费大量时间将MVC应用到规模并不是很大的应用程序通常会得不偿失。
    2、视图与控制器间过于紧密连接,视图与控制器是相互分离,但却是联系紧密的部件,视图没有控制器的存在,其应用是很有限的,反之亦然,这样就妨碍了他们的独立重用。
    3、视图对模型数据的低效率访问,依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能。

  

   MVC允许在不改变视图的情况下改变视图对用户输入的响应方式,用户对View的操作交给了Controller处理,在Controller中响应View的事件调用Model的接口对数据进行操作,一旦Model发生变化便通知相关视图进行更新。

  如果前端没有框架,只使用原生的html+js,MVC模式可以这样理解。将html看成view;js看成controller,负责处理用户与应用的交互,响应对view的操作(对事件的监听),调用Model对数据进行操作,完成model与view的同步(根据model的改变,通过选择器对view进行操作);将js的ajax当做Model,也就是数据层,通过ajax从服务器获取数据。

  我们先实现一个非常常见的例子:加按钮与减按钮。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <div id="app">
            <!--<span>0</span>
            <button id="add">+</button>
            <button id="minus">-</button>-->
        </div>
        
        <script type="text/javascript">
            // 加按钮与减按钮
            
            // 普通版本 JS 计数器
//            const numberWrapper = document.querySelector('#app span');
//            const addBtn = document.querySelector('#add');
//            const minusBtn = document.querySelector('#minus');
//            addBtn.addEventListener('click', () => {
//              const newNumber = parseInt(numberWrapper.innerText) + 1;
//              numberWrapper.innerText = newNumber.toString();
//            });
//            minusBtn.addEventListener('click', () => {
//              const newNumber = parseInt(numberWrapper.innerText) - 1;
//              numberWrapper.innerText = newNumber.toString();
//            });

            // MVC 版本 JS 计数器
//            const app = document.querySelector('#app');
//            const html = `
//              <span>0</span>
//              <button id="add">+</button>
//              <button id="minus">-</button>
//            `;
//            const counter = document.createElement('div');
//            counter.innerHTML = html;
//            app.appendChild(counter);
            
//            const app = document.querySelector('#app');
//            const model = {
//                  data: {
//                    number: 100
//                  }
//            };
//            const view = {
//                  html: `
//                    <span>{{number}}</span>
//                    <button id="add">+</button>
//                    <button id="minus">-</button>
//                  `,
//                  render() {
//                    const counter = document.createElement('div');
////                    counter.innerHTML = view.html;
//                    counter.innerHTML = view.html.replace('{{number}}', model.data.number);
//                    app.appendChild(counter);
//                }
//            };
//            view.render();
//            const controller = {
//                  ui: {},
//                  init() {
//                    this.ui = {
//                        numberWrapper: document.querySelector('#app span'),
//                        addBtn: document.querySelector('#add'),
//                        minusBtn: document.querySelector('#minus')
//                    };
//                    controller.bindEvents();
//                },
//                  bindEvents() {
////                      controller.ui.addBtn.addEventListener('click', () => {
////                        const newNumber = parseInt(controller.ui.numberWrapper.innerText) + 1;
////                        controller.ui.numberWrapper.innerText = newNumber.toString();
////                    });
////                      controller.ui.minusBtn.addEventListener('click', () => {
////                        const newNumber = parseInt(controller.ui.numberWrapper.innerText) - 1;
////                        controller.ui.numberWrapper.innerText = newNumber.toString();
////                      });
//                    controller.ui.addBtn.addEventListener('click', () => {
//                          model.data.number += 1;
//                    });
//                    controller.ui.minusBtn.addEventListener('click', () => {
//                          model.data.number -= 1;
//                    });
//                  }
//            };
//            但是我们这样操作虽然说修改了数据,可是并没有重新渲染到页面上呀。所以每次提交之后需要重新 render。
//            此时问题出现了:点击 + 或者 - 后,数字只会变化一次,第二次点击便毫无用处!
//            这是为什么呢?
//            很简单,因为我们重新 render,导致俩绑定了事件的 button 全都不是曾经的那个他了。
//            所以我们使用事件代理来解决这个问题——将事件绑定在外层的 div 上,然后判断点击对象的 id 即可。
            
            const model = {
                  data: {
                    number: parseInt(window.localStorage.getItem('number')) || 0
                  },
                  save() {
                    window.localStorage.setItem('number', model.data.number.toString());
                  }
            };
            const view = {
                el: null,
                html: `
                  <span>{{n}}</span>
                  <button id="add">+</button>
                  <button id="minus">-</button>
                `,
                render(container) {
                    if (!view.el) {
                        // 创建 div,并将 div 赋值给 el
                          const counter = document.createElement('div');
                          view.el = counter;
                          counter.innerHTML = view.html.replace('{{n}}', model.data.number.toString());
                          container.appendChild(counter);
                    } else {
                        // 将 el 的 innerHTML 更换为新的内容
                          view.el.innerHTML = view.html.replace('{{n}}', model.data.number.toString());
                    }
                }
            }
            const controller = {
                init(container) {
                    controller.ui = {
                          container
                    };
                    view.render(container);
                    controller.bindEvents();
                },
                  ui: {},
                  bindEvents() {
                    controller.ui.container.addEventListener('click', e => {
                          switch (e.target.id) {
                            case 'add':
                                  model.data.number += 1;
                                  break;
                            case 'minus':
                                  model.data.number -= 1;
                                  break;
                            default:
                                  return;
                          }
                          model.save();
                          view.render();
                    });
                  }
            };
            const app = document.querySelector('#app');
            controller.init(app);
        </script>
    </body>
</html>
View Code

 3、MVP

  MVP(Model-View-Presenter)是MVC的改良模式,由IBM的子公司Taligent提出。和MVC的相同之处在于:Controller/Presenter负责业务逻辑,Model管理数据,View负责显示只不过是将 Controller 改名为 Presenter,同时改变了通信方向。

  MVP特点

    1、M、V、P之间双向通信。
    2、View 与 Model 不通信,都通过 Presenter 传递。Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。
    3、View 非常薄,不部署任何业务逻辑,称为”被动视图”(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
    4、Presenter与具体的View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变,这样就可以重用。不仅如此,还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试–从而不需要使用自动化的测试工具。

  与MVP的区别

    1、在MVP中,View并不直接使用Model,它们之间的通信是通过Presenter(MVC中的Controller)来进行的,所有的交互都发生在Presenter内部。

    2、在MVC中,View会直接从Model中读取数据而不是通过 Controller。  

  MVP优点

    1、模型与视图完全分离,我们可以修改视图而不影响模型;
    2、可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部;
    3、我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁;
    4、如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)。

   MVP缺点

    视图和Presenter的交互会过于频繁,使得他们的联系过于紧密。也就是说,一旦视图变更了,presenter也要变更。

  可应用与Android开发。

4、MVVM

  Model:实体模型,代表基本业务逻辑
  View:对应于Activity和xml,负责View的绘制以及与用户交互
  ViewModel:将view和model联系在一起,起到桥梁的作用,负责完成View于Model间的交互,负责业务逻辑

  Vue采用的是MVVM设计模式

  

  1、双向绑定的原理是什么?
    (当视图改变的时候更新模型层,当模型层改变的时候更新视图层)
    vue中采用了数据劫持&订阅发布模式:
    vue在创建vm的时候,会将数据配置在实例当中,然后会使用Object.defineProperty对这些数据进行处理,为这些数据添加getter与setter方法。当获取数据的时候,会触发对应的getter方法,当设置数据的时候,会触发对应的setter方法,从而进一步触发vm上的watcher方法,然后数据了,vm进一步去更新视图。

  2、 数据劫持:

    vue.js 则是采用数据劫持结合发布者-订阅者模式,通过Object.defineProperty()来劫持各个属性的setter,getter。在数据变动时发布消息给订阅者,触发响应的监听回调。

  MVVM的优点

    1、MVVM模式和MVC模式类似,主要目的是分离视图(View)和模型(Model)。
    2、低耦合,视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的”View”上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
    3、可重用性,可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
    4、独立开发,开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xml代码。
    5、可测试,界面向来是比较难于测试的,而现在测试可以针对ViewModel来写。

八、JavaScript中的Event Loop(事件循环)机制

   相关文档:https://zhuanlan.zhihu.com/p/145383822

  1. JavaScript是单线程,非阻塞的    

     JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

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

    非阻塞:通过 event loop 实现。

  2. 浏览器的事件循环(Event Loop)

  1. 先执行同步阻塞任务,同步任务会等待上一个执行完毕以后执行下一个,当同步任务执行完毕,再执行异步任务,遇到异步任务会将异步任务的回调函数注册在异步任务队列里。注意,如果主线程上没有同步任务会直接调用异步任务的微任务。
  2. 执行宏任务,遇到微任务将都添加到微任务队列里。
  3. 开始执行微任务队列,当宏任务执行完后执行微任务队列,直到微任务队列全部执行完,微任务队列为空。
  4. 执行宏任务,如果在执行宏任务期间有微任务,将微任务添加到微任务队列里,执行完宏任务之后执行微任务,直到微任务队列全部执行完。
  5. 继续执行宏任务队列。

    重复2, 3, 4,5……直到宏微任务为空。

    

    

    执行栈和事件队列

      执行栈: 同步代码的执行,按照顺序添加到执行栈中

      事件队列: 异步代码的执行,遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。当异步事件返回结果,将它放到事件队列中,被放入事件队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码。

    宏任务和微任务

      宏任务(macrotask )和微任务(microtask )

      macrotask 和 microtask 表示异步任务的两种分类。

      宿主环境提供的叫宏任务,由语言标准提供的叫微任务;

      为什么要引入微任务,只有一种类型的任务不行么?

        页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。     

      不同的异步任务被分为:宏任务和微任务
        宏任务:

          script(整体代码)

          setTimeout()

          setInterval()

          postMessage

          I/O

          UI交互事件

        微任务:

          new Promise().then(回调)

          MutationObserver(html5 新特性)

        setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。

        事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。

     运行机制

        异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去。

        在当前执行栈为空时,主线程会查看微任务队列是否有事件存在。

          存在,依次执行队列中的事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的事件,把当前的回调加到当前指向栈。

          如果不存在,那么再去宏任务队列中取出一个事件并把对应的回调加入当前执行栈;

        当前执行栈执行完毕后时会立刻处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

        简单总结一下执行的顺序:
          执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环

九、ES6、ES7、ES8、ES9、ES10

  一、ES6

    (1)let/const

      ES6最基本的功能:let和const。let与var类似,但使用let声明的变量的作用域是在声明它们的块中。

      例如,在条件块中使用let将在块内作用域变量,在块外不可用。     

if (true) {    
  let foo = "word";
}
console.log(foo); // error

      const是另一个用于声明变量的ES6关键字。不同之处在于const创建的变量在声明之后不能更改,这个特点可以有效的避免BUG的出现,因此在编写代码过程中,建议尽量写纯函数(纯函数,就是给定函数固定的输入,输出的结果就是固定的,不会受函数外的变量等的影响)。

      例如:

const a = 2021;
a = 2020 // error

    (2)for...of

      不要与for..in语法混淆;他们是完全不同的东西。for..in将获得数组/对象中的属性,而for..of将获得实际想要迭代的数据。

    (3)Iterable

    (4)Generator:生成器

    (5)Default Parameter:默认参数

      过去实现默认参数:

function addOne(num) {    
  if (num === undefined) {

    num = 0;

  }

  return num + 1;
}
addOne();

      现在实现默认参数:

function addOne(num = 0) {    
  return num + 1;
}
addOne();

    (6)Destructuring Syntax:解构语法

      解构赋值语法是一种 Javascript 表达式。通过解构赋值, 可以将属性/值从对象/数组中取出,赋值给其他变量。

      如果要将对象传递给函数,则可以轻松选择对象的属性,然后使用ES6分解语法将它们放在单独的变量中:

function foo({ a, b, c = 4}) {    
  console.log(a, b, c); // 1, 2, 4
}
foo({ a: 1, b: 2 });

// 觉得参数的名称太长,咱再来个重命名  解构时重命名简化命名
function getFullName ({firstName: first, lastName: last}) {
}

      解构技巧同样也适用数组。

        解构参数:

function foo([a, b]) {    
  console.log(a, b); // 1, 2
}
foo([1, 2, 3]);

        解构赋值:

function foo(arr) {    
  const [a, b] = arr;

  console.log(a, b); // 1, 2  
}

    (7)Rest/Spread:剩余/展开参数

      在解构数组时,可以使用 ... 语法来获取数组中的所有其他项。 

      c现在是一个包含自己的数组,包含了其余的元素:3,4,5。这里的操作就是Rest操作。

function foo([a, b, ...c]) {    
  console.log(c); // [3, 4, 5]
}
foo([1, 2, 3, 4, 5]);

      这个语法同样适用于赋值:

function foo(arr) {    
  const [a, b, ...c] = arr;

  console.log(c); // [3, 4, 5]
}
foo([1, 2, 3, 4, 5]);

      rest操作符也可以单独使用,无需解构:

function foo(...nums) {    
  console.log(nums); // [1, 2, 3, 4, 5]
}
foo(1, 2, 3, 4, 5);

      在这里,我们将数字作为独立参数传递,而不是作为单个数组传递。但是在函数内部,使用rest运算符将数字作为单个数组收集。当遍历这些参数时,这很有用。

      rest语法 ... 与另一个ES6特性操作符扩展完全相同。

const a = [1, 2];
const b = [3, 4];
const c = [...a, ...b];
console.log(c); // [1, 2, 3, 4]

      spread操作符用于将所有项展开,并将它们放入不同的数组中。

    (7)Arrow Function:箭头函数

      箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

      使用箭头语法来创建更简洁的函数:

const addOne = (num) => {    
  return num + 1;
};
// 此函数将自动返回表达式num +1的求值作为返回值,不需要显式的使用return关键字。
const addOne = (num) => num + 1;
const addOne = num => num + 1;

    (8)Object literal extensions:对象字面量的扩展

      如果在一个对象中放入两个项目,它们的属性键与变量相同,可以用传统的Javascript做这样的事情: 

const a = 1;
const b = 2;
const obj = {
  a: a,

  b: b,
};

      但是在ES6中,语法可以更简单:

const a = 1;
const b = 2;
const obj = {
  a,
  b,
  getA() {
    return this.a;

  },

  getB() {

    return this.b;

  },
};

    (9)Class:类

class Person {
    constructor(name, hobby) {
        this.name = name;
        this.hobby = hobby;
    }
    introduce() {
        console.log(`大家好,我的名字叫:${this.name},我喜欢${this.hobby}。`);
    }
}
const devpoint = new Person("DevPoint", "coding");
devpoint.introduce();
class ProfessionalPerson extends Person {    
  constructor(name, hobby, profession) {

    super(name, hobby);
    // 执行 Person 的构造函数

    this.profession = profession;

  }

introduce() {
    super.introduce();
    // 调用 Person 类的方法

    console.log(`我的职业是 ${this.profession}。`);

  }
}

    (10)ES6新增了两种数据结构:Map和Set

    (11)Promise

    (12)Object.assign()

      Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。提供了一种简单的方法来浅克隆现有对象。

const obj1 = { a: 1 }
const obj2 = Object.assign({}, obj1)

    (13)String.prototype.repeat()

      构造并返回一个新字符串,该字符串包含被连接在一起的指定数量的字符串的副本。

const str = "DevPoint ".repeat(3);
console.log(str); // DevPoint DevPoint DevPoint

    (14)String.prototype.startsWith()

      用来判断当前字符串是否以另外一个给定的子字符串开头(区分大小写),并根据判断结果返回 true 或 false。

const str = "DevPoint".startsWith("D");
const str2 = "DevPoint".startsWith("d");
console.log(str); // true
console.log(str2); // false

    (15)String.prototype.endsWith()

      用来判断当前字符串是否是以另外一个给定的子字符串“结尾”的,根据判断结果返回 true 或 false。

const str = "DevPoint".endsWith("t"); 
console.log(str); // true

    (16)String.prototype.includes()

      用于判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false。

const str = "DevPoint".includes("P");
console.log(str); // true

    (17)Array.prototype.find()

      返回数组中满足提供的过滤函数的第一个元素的值,否则返回 undefined。

const arrNumbers = [5, 12, 8, 130, 44];
const foundNumbers = arrNumbers.find((number) => number > 10);
console.log(foundNumbers);   // 12是数组第一个大于10的数

    (18)Function.name

      这不是方法而是属性,返回函数实例的名称,每个函数都有一个name属性,该属性提供字符串形式的函数名称

setTimeout.name; // "setTimeout"
const weather = () => {
    console.log("今天天气真好!");
};
console.log(weather.name); // weather

    (19)proxy

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

const target = {
  name: 'wjg',
  age: 18
}
const proxy = new Proxy(target, {
  get(target,prop) {
    console.log('Get Value: ' + target[prop])
    return target[prop]
    // return target[prop] 等价于: return Reflect.get(target,prop)
  ,
  set(target, prop, value) {
    console.log('Set prop: ' + prop + ' to value:' + value)
    target[prop] = value
    // target[prop] = value 等价于: Reflect.set(target, prop, value)
  }
})
console.log(proxy.name)
proxy.age = 19
console.log(proxy.age)
console.log(target.age)

十、JS的执行上下文

  相关文档:https://zhuanlan.zhihu.com/p/72959191

  执行上下文:指当前执行环境中的变量、函数声明,参数(arguments),作用域链,this等信息。分为全局执行上下文、函数执行上下文,其区别在于全局执行上下文只有一个,函数执行上下文在每次调用函数时候会创建一个新的函数执行上下文。

  全局执行上下文

    全局执行上下文只有一个,在客户端中一般由浏览器创建,也就是我们熟知的window对象,我们能通过this直接访问到它。全局对象window上预定义了大量的方法和属性,我们在全局环境的任意处都能直接访问这些属性方法,同时window对象还是var声明的全局变量的载体。我们通过var创建的全局对象,都可以通过window直接访问。

  函数执行上下文

    函数执行上下文可存在无数个,每当一个函数被调用时都会创建一个函数上下文;需要注意的是,同一个函数被多次调用,都会创建一个新的上下文。说到这你是否会想,上下文种类不同,而且创建的数量还这么多,它们之间的关系是怎么样的,又是谁来管理这些上下文呢,这就不得不说说执行上下文栈了。

  执行上下文栈

    执行上下文栈(下文简称执行栈)也叫调用栈,执行栈用于存储代码执行期间创建的所有上下文,具有LIFO(Last In First Out后进先出,也就是先进后出)的特性。

    JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内;由于执行栈LIFO的特性,所以可以理解为,JS代码执行完毕前在执行栈底部永远有个全局执行上下文。

  执行上下文创建阶段

    执行上下文创建分为创建阶段与执行阶段两个阶段,较为难理解应该是创建阶段,我们先说创建阶段。

    JS执行上下文的创建阶段主要负责三件事:确定this---创建词法环境组件(LexicalEnvironment)---创建变量环境组件(VariableEnvironment)

    1、确定this

      官方的称呼为This Binding,在全局执行上下文中,this总是指向全局对象,例如浏览器环境下this指向window对象。

      而在函数执行上下文中,this的值取决于函数的调用方式,如果被一个对象调用,那么this指向这个对象。否则this一般指向全局对象window或者undefined(严格模式)。

    2、词法环境组件  

      词法环境是一个包含标识符变量映射的结构,这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用。

      词法环境由环境记录与对外部环境引入记录两个部分组成。

      其中环境记录用于存储当前环境中的变量和函数声明的实际位置;外部环境引入记录很好理解,它用于保存自身环境可以访问的其它外部环境,那么说到这个,是不是有点作用域链的意思?

      我们在前文提到了全局执行上下文与函数执行上下文,所以这也导致了词法环境分为全局词法环境与函数词法环境两种。

      全局词法环境组件:

        对外部环境的引入记录为null,因为它本身就是最外层环境,除此之外它还记录了当前环境下的所有属性、方法位置。

      函数词法环境组件:

        包含了用户在函数中定义的所有属性方法外,还包含了一个arguments对象。函数词法环境的外部环境引入可以是全局环境,也可以是其它函数环境,这个根据实际代码而来。

    3、变量环境组件

      变量环境可以说也是词法环境,它具备词法环境所有属性,一样有环境记录与外部环境引入。在ES6中唯一的区别在于词法环境用于存储函数声明与let const声明的变量,而变量环境仅仅存储var声明的变量。

十一、基于 Token 的身份验证流程

  1、前端使用用户名跟密码请求首次登录。

  2、后服务端收到请求,去验证用户名与密码是否正确。

  3、验证成功后,服务端会根据用户id、用户名、定义好的秘钥、过期时间生成一个 Token,再把这个 Token 发送给前端。

  4、前端收到 返回的Token ,把它存储起来,比如放在 Cookie 里或者 Local Storage 里。

  5、前端每次路由跳转,判断 localStroage 有无 token ,没有则跳转到登录页。没有则请求获取用户信息,改变登录状态

  6、前端每次向服务端请求资源的时候需要在请求头里携带服务端签发的Token

  7、服务端收到请求,然后去验证前端请求里面带着的 Token。没有或者 token 过期,返回401。如果验证成功,就向前端返回请求的数据。

  8、前端得到 401 状态码,重定向到登录页面。

十二、require和import的区别

十三、比较两个对象是否相等

function deepEqual(object1, object2) {
  const keys1 = Object.keys(object1);
  const keys2 = Object.keys(object2);

  if (keys1.length !== keys2.length) {
    return false;
  }

  for (let index = 0; index < keys1.length; index++) {
    const val1 = object1[keys1[index]];
    const val2 = object2[keys2[index]];
    const areObjects = isObject(val1) && isObject(val2);
    if (areObjects && !deepEqual(val1, val2) || 
        !areObjects && val1 !== val2) {
      return false;
    }
  }

  return true;
}

function isObject(object) {
  return object != null && typeof object === 'object';
}

 十四、去除对象中的 value 为 null,空  ,undefined 的 key 

function filterParams(obj) {
  const keys = Object.keys(obj)
  keys.forEach(key => {
    const value = obj[key]
    if (isObject(value)) filterParams(value)
    if (isEmpty(value)) delete obj[key]
  })
  return obj
}
function isEmpty(input) {
  return [, undefined, null].includes(input)
}
function isObject(input) {
  return input !== null && (!Array.isArray(input)) && typeof input ===  object 
}

十五、axios

axios为什么能直接调用发送请求?

因为axios本身是一个函数,触发原型上的request方法

为什么能够axios.get()?

因为axios内部源码axios.Axios = Axios,Axios原型上有get等方法。

十六、h5性能优化

一、性能指标及数据采集

1. 页面加载时间 —— 页面以多快的速度加载和渲染元素到页面上,具体如下:

  • First contentful paint (FCP):测量页面开始加载到某一块内容显示在页面上的时间。

  • Largest contentful paint (LCP):测量页面开始加载到最大文本块内容或图片显示在页面中的时间。

  • DomContentLoaded Event:DOM 解析完成时间。

  • OnLoad Event:页面资源加载完成时间。

 十七、闭包

闭包就是能够读取其他函数内部变量的函数。

var name = "The Window";
var object = {
  name : "My Object",
   getNameFunc : function(){
     return function(){
       return this.name;
     };
   }
}; alert(object.getNameFunc()());// The Window

注意点:

  1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

  2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

使用场景

1、函数防抖

/**
 * @function debounce 函数防抖
 * @param {Function} fn 需要防抖的函数
 * @param {Number} interval 间隔时间
 * @return {Function} 经过防抖处理的函数
 * */
function debounce(fn, interval) {
    let timer = null; // 定时器
    return function() {
        // 清除上一次的定时器
        clearTimeout(timer);
        // 拿到当前的函数作用域
        let _this = this;
        // 拿到当前函数的参数数组
        let args = Array.prototype.slice.call(arguments, 0);
        // 开启倒计时定时器
        timer = setTimeout(function() {
            // 通过apply传递当前函数this,以及参数
            fn.apply(_this, args);
            // 默认300ms执行
        }, interval || 300)
    }
}
概念:

  就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
  通俗一点:在一段固定的时间内,只能触发一次函数,在多次触发事件时,只执行最后一次。

使用时机:

  搜索功能,在用户输入结束以后才开始发送搜索请求,可以使用函数防抖来实现;

2、函数节流

/**
 * @function throttle 函数节流
 * @param {Function} fn 需要节流的函数
 * @param {Number} interval 间隔时间
 * @return {Function} 经过节流处理的函数
 * */
function throttle(fn, interval) {
    let timer = null; // 定时器
    let firstTime = true; // 判断是否是第一次执行
    // 利用闭包
    return function() {
        // 拿到函数的参数数组
        let args = Array.prototype.slice.call(arguments, 0);
        // 拿到当前的函数作用域
        let _this = this;
        // 如果是第一次执行的话,需要立即执行该函数
        if(firstTime) {
            // 通过apply,绑定当前函数的作用域以及传递参数
            fn.apply(_this, args);
            // 修改标识为null,释放内存
            firstTime = null;
        }
        // 如果当前有正在等待执行的函数则直接返回
        if(timer) return;
        // 开启一个倒计时定时器
        timer = setTimeout(function() {
            // 通过apply,绑定当前函数的作用域以及传递参数
            fn.apply(_this, args);
            // 清除之前的定时器
            timer = null;
            // 默认300ms执行一次
        }, interval || 300)
    }
}
概念

  就是限制一个函数在一定时间内只能执行一次。

使用时机
  • 改变浏览器窗口尺寸,可以使用函数节流,避免函数不断执行;
  • 滚动条scroll事件,通过函数节流,避免函数不断执行。
函数节流与函数防抖的区别:

我们以一个案例来讲一下它们之间的区别:
设定一个间隔时间为一秒,在一分钟内,不断的移动鼠标,让它触发一个函数,打印一些内容。

  函数防抖:会打印1次,在鼠标停止移动的一秒后打印。

  函数节流:会打印60次,因为在一分钟内有60秒,每秒会触发一次。

  • 总结:节流是为了限制函数的执行次数,而防抖是为了限制函数的执行时机。

十八、垃圾回收

浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大并且GC时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。 

只有函数内的变量才可能被回收

不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。

function fn1() {
    var obj = {name: 'hanzichi', age: 10};
}
function fn2() {
    var obj = {name:'hanzichi', age: 10};
    return obj;
}

var a = fn1();
var b = fn2();

我们来看代码是如何执行的。首先声明了两个函数,分别叫做 fn1 和 fn2,当 fn1 被调用时,进入 fn1 的环境,会开辟一块内存存放对象{name: 'hanzichi', age: 10},而当调用结束后,出了fn1的环境,那么该块内存会被 JS 引擎中的垃圾回收器自动释放;在 fn2 被调用的过程中,返回的对象被全局变量 b 所指向,所以该块内存并不会被释放。

垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。通常情况下有两种实现方式:标记清除引用计数。引用计数不太常用,标记清除较为常用。

js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。

function test(){
  var a = 10 ;       // 被标记 ,进入环境 
  var b = 20 ;       // 被标记 ,进入环境
}
test();            // 执行完毕 之后 a、b又被标离开环境,被回收。

垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

十九、内存泄露

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

1. 循环引用

一个很简单的例子:一个DOM对象被一个Javascript对象引用,与此同时又引用同一个或其它的Javascript对象,这个DOM对象可能会引发内存泄露。这个DOM对象的引用将不会在脚本停止的时候被垃圾回收器回收。要想破坏循环引用,引用DOM元素的对象或DOM对象的引用需要被赋值为null。

2. 闭包

在闭包中引入闭包外部的变量时,当闭包结束时此对象无法被垃圾回收(GC)。

var a = function() {
  var largeStr = new Array(1000000).join('x');
  return function() {
    return largeStr;
  }
}();

3. DOM泄露

当原有的COM被移除时,子结点引用没有被移除则无法回收。

var select = document.querySelector;
var treeRef = select('#tree');
 
//在COM树中leafRef是treeFre的一个子结点
var leafRef = select('#leaf'); 
var body = select('body');
 
body.removeChild(treeRef);
 
//#tree不能被回收入,因为treeRef还在
//解决方法:
treeRef = null;
 
//tree还不能被回收,因为叶子结果leafRef还在
leafRef = null;
 
//现在#tree可以被释放了。

4. Timers计(定)时器泄露

定时器也是常见产生内存泄露的地方

for (var i = 0; i < 90000; i++) {
  var buggyObject = {
    callAgain: function() {
      var ref = this;
      var val = setTimeout(function() {
        ref.callAgain();
      }, 90000);
    }
  }
  buggyObject.callAgain();
  //虽然你想回收但是timer还在
  buggyObject = null;
}

 二十、css设置多行隐藏

overflow:hidden;
text-overflow:ellipsis; display:-webkit-box;
/* autoprefixer: off */ -webkit-box-orient:vertical; -webkit-line-clamp:2;

 二十一、模块化编程

什么是模块?

  1.将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起

  2.块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

https://www.cnblogs.com/huiguo/p/7967168.html

CommonJS

概述

CommonJS规范诞生比较早,Mozilla工程师Kevin Dangoo在2009年发起,它出现的目的是希望JS可以运行到更多地方,主要是服务端,前期的nodejs采用了这种规范。概述:node应用又模块组成,采用commonjs模块规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

模块定义

CommonJS规范规定,一个文件就是一个模块,用module变量代表当前模块。 Node在其内部提供一个Module的构建函数。所有模块都是Module的实例。实例代码如下:

function Module(id, parent) {

  this.id = id;

  this.exports = {};

  this.parent = parent;

  this.filename = null;

  this.loaded = false;

  this.children = [];

}

module.exports = Module;

var module = new Module(filename, parent);

每个模块内部,都有一个module对象,代表当前模块。它的属性如下:

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 初始值为一个空对象{},表示模块对外输出的接口。

2.1 module.exports属性

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。

例如,我们在moduleA.js文件中定义funA方法,并用module.exports变量把该方法暴露出,实例代码如下:

复制代码
//moduleA.js

module.exports.funcA= function(){

  console.log('This is moduleA!');

} 
复制代码

然后,在moduleB模块中加载引入moduleA模块,便可以使用funA方法了,示例代码如下:

//moduleB.js

var a = require('./moduleA');

a.funcA();//打印'This is moduleA!'

2.2 exports变量

为了方便,Node为每个模块提供一个exports变量,指向module.exports。在模块内部大概是这样的:

var exports = module.exports={};

在对外输出模块接口时,可以向exports对象添加方法。

复制代码
//a.js

var funA=function(){

console.log('This is module a!');

};

exports.funA=funA;//等同于module.exports.funA=funA;
复制代码

exports 赋值其实是给 module.exports 这个空对象添加myName属性而已,为什么是给exports添加属性,而不直接exports= funA呢?

因为, exports 是指向的 module.exports 的引用。如果直接是给exports赋值而不是添加属性的话,exports 就不再指向module.exports 了。当exports 被改变的时候,module.exports将不会被改变,而模块导出的时候,真正导出的执行是module.exports,而不是exports。例如,将a.js改为:

复制代码
//a.js

var funA=function(){

console.log('This is module a!');

};

exports =funA;
复制代码

这样是无效的。因为,前面是通过给 exports 添加属性,而现在对 exports 指向的内存做了修改,exports 和 module.exports 不再指向同一块内存,即 module.exports 指向的那块内存并没有做任何改变,仍然为一个空对象 {},所以funA方法输出无效。

如果觉得module.exports和exports难以分清的话,个人建议可以全部使用module.exports来应对所有的情况,并尽量减少犯错的机会。

模块引用

require函数的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。当我们用require()获取module时,Node会根据module.id找到对应的module,并返回module. exports,这样就实现了模块的输出。

require函数使用一个参数,参数值可以带有完整路径的模块的文件名,也可以为模块名。

假如,有三个文件:一个是a.js(存放路径:home/a.js),一个是b.js(存放路径:home/user/b.js), 一个是c.js(存放路径:home/user/c.js)。我们在a.js文件中引用三个模块,实例代码如下:

var httpModule=require('HTTP');//用 “模块名”加载服务模块http

var b=require('./user/b');//用“相对路径”加载文件b.js

var b=require('../ home/user/c');//用“绝对路径”加载文件c.js

模块标识

模块标识就是传递给require方法的参数,必须符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径,默认文件名后缀.js。在Node实现中,正是基于这样一个标识符进行模块查找的,如果没有发现指定模块会报错。

根据参数的不同格式,require命令去不同路径寻找模块文件。加载规则如下:

(1)如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js。

(2)如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js。

(3)如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。

举例来说,脚本/home/user/projects/foo.js执行了require('bar.js')命令,Node会依次搜索以下文件。

复制代码
/usr/local/lib/node/bar.js

/home/user/projects/node_modules/bar.js

/home/user/node_modules/bar.js

/home/node_modules/bar.js

/node_modules/bar.js
复制代码

这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。

(4)如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径。

(5)如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。

(6)如果想得到require命令加载的确切文件名,使用require.resolve()方法。

CommonJS是同步的,意味着你想调用模块里的方法,必须先用require加载模块。这对服务器端的Nodejs来说不是问题,因为模块的JS文件都在本地硬盘上,CPU的读取时间非常快,同步不是问题。但如果是浏览器环境,要从服务器加载模块。模块的加载将取决于网速,如果采用同步,网络情绪不稳定时,页面可能卡住,这就必须采用异步模式。所以,就有了 AMD解决方案。

用法实例

module.exports或exports负责对外暴漏数据,require来引入

<!--a.js-->
module.exports = {
    name: '四大名将'
}
<!--也可以用exports导出-->
<!--exports let name = '四大名将'-->

<!--b.js-->
const res = require('./a.js')
console.log(res.name) // 四大名将

 module.exports 和 exports的区别:

  乍一看,还以为CommonJS提供了两种方法来导出数据,其实不然,require并不认识exports,之所以它也好使,那是因为模块内部这些代码的作用。

var module = {
    exports: {
        <!--导出的内容-->
    }
}
var exports = module.exports
return module.exports

 require方能看到的只有module.exports这个对象,它是看不到exports这个对象的,而我们在编写模块时用到的exports实际上是对module.exports的引用。

 AMD

1、概述

AMD(Asynchronous Module Definition),也就是异步模块定义。AMD规范,制定了定义模块的规则,使得模块之间的依赖可以被异步加载。这和浏览器的异步加载模块的环境刚好适应(浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题)。

所谓异步,就是所有的模块将被异步加载,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这就是浏览器端模块加载器核心所在。

AMD规范由CommonJs规范演进而来,大部分思想跟CommonJS类似,属于Modules/Async流派。但AMD规范是专注于浏览器端的,根据浏览器特点做了自己的一些定义实现。下面我们来了解一下AMD规范。

 

Talk is cheap,show me the code
原文地址:https://www.cnblogs.com/qc-one/p/14575467.html