前端面试题总结(持续更新)

345 阅读1小时+

一、html+css

1 img标签的title和alt属性有什么区别

  • alt:图片加载失败时,显示在网页上的替代文字
  • title:鼠标(手机端该属性无意义)放在图片上时显示的文字
  • alt是必需属性(但属性值可为空),title非必需

2 图片懒加载

  1. 懒加载原理

    一张图片就是一个<img>标签,浏览器是否发起请求图片是根据<img>的src属性,所以实现懒加载的关键就是,在图片没有进入可视区域时,先不给<img>的src赋值,这样浏览器就不会发送请求了,等到图片进入可视区域再给src赋值。

  2. 懒加载思路及实现

    实现懒加载有四个步骤,如下: 1.加载loading图片

    在这里插入图片描述

    2.判断哪些图片要加载【重点】

    在这里插入图片描述3.隐形加载图片 4.替换真图片

    // onload是等所有的资源文件加载完毕以后再绑定事件
    window.onload = function(){
    	// 获取图片列表,即img标签列表
    	var imgs = document.querySelectorAll('img');
    
    	// 获取到浏览器顶部的距离
    	function getTop(e){
    		return e.offsetTop;
    	}
    
    	// 懒加载实现
    	function lazyload(imgs){
    		// 可视区域高度
    		var h = window.innerHeight;
    		//滚动区域高度
    		var s = document.documentElement.scrollTop || document.body.scrollTop;
    		for(var i=0;i<imgs.length;i++){
    			//图片距离顶部的距离大于可视区域和滚动区域之和时懒加载
    			if ((h+s)>getTop(imgs[i])) {
    				// 真实情况是页面开始有2秒空白,所以使用setTimeout定时2s
    				(function(i){
    					setTimeout(function(){
    						// 不加立即执行函数i会等于9
    						// 隐形加载图片或其他资源,
    						//创建一个临时图片,这个图片在内存中不会到页面上去。实现隐形加载
    						var temp = new Image();
    						temp.src = imgs[i].getAttribute('data-src');//只会请求一次
    						// onload判断图片加载完毕,真是图片加载完毕,再赋值给dom节点
    						temp.onload = function(){
    							// 获取自定义属性data-src,用真图片替换假图片
    							imgs[i].src = imgs[i].getAttribute('data-src')
    						}
    					},2000)
    				})(i)
    			}
    		}
    	}
    	lazyload(imgs);
    
    	// 滚屏函数
    	window.onscroll =function(){
    		lazyload(imgs);
    	}
    }
    
    

3 回流和重绘

  1. 浏览器渲染过程:

    img

    1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树
    2. 将DOM树和CSSOM树结合(Attachment),生成渲染树(Render Tree)
    3. Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
    4. Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
    5. Display:将像素发送给GPU,展示在页面上。
  2. 回流(Layout) 当render树的一部分或者全部因为大小边距等问题发生改变而需要重建的过程,叫做回流

    重绘(Painting) 当诸如颜色背景等不会引起页面布局变化,而只需要重新渲染的过程叫做重绘

    回流一定引起重绘,重绘不一定由回流引起

  3. 回流触发条件:

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

    现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以下属性或者使用以下方法:

    • offsetTop、offsetLeft、offsetWidth、offsetHeight
    • scrollTop、scrollLeft、scrollWidth、scrollHeight
    • clientTop、clientLeft、clientWidth、clientHeight
    • getComputedStyle()
    • getBoundingClientRect

    以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来。

  5. 减少回流和重绘

    1. 最小化重绘和重排 合并多次对DOM和样式的修改,然后一次处理掉

      const el = document.getElementById('test');
      el.style.padding = '5px';
      el.style.borderLeft = '1px';
      el.style.borderRight = '2px';
      //修改后
      const el = document.getElementById('test'); 
      el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;'; 
      
    2. 批量修改DOM

      1. 使元素脱离文档流
      2. 对其进行多次修改
      3. 将元素带回到文档中
    3. 避免触发同步布局事件 即尽量少使用会强制队列刷新的属性

      function initP() {
          for (let i = 0; i < paragraphs.length; i++) {
              paragraphs[i].style.width = box.offsetWidth + 'px';
          }
      }
      //优化后
      const width = box.offsetWidth;
      function initP() {
          for (let i = 0; i < paragraphs.length; i++) {
              paragraphs[i].style.width = width + 'px';
          }
      }
      

4 H5新增标签

常见的有:article、aside、audio、video、footer、header、nav、section

5 SVG和CANVAS的区别?

SVG 表示以XML格式定义图像的可伸缩矢量图形。

CANVAS 通过 JavaScript 来绘制 2D 图形。

6 svg如何调整颜色

标签fill属性设置即可

7 background属性

  • background-image:设置元素的背景图像。
  • background-origin:规定背景图片的定位区域。
  • background-size :规定背景图片的尺寸。
  • background-repeat:设置是否及如何重复背景图像。

8 word-wrap属性

word-wrap 属性允许长单词或 URL 地址换行到下一行。

注:所有主流浏览器都支持 word-wrap 属性。

基础语法:

word-wrap: normal|break-word;

9 text-shadow属性

text-shadow 属性:向文本设置阴影。

text-shadow基础语法:

text-shadow: 5px 5px 5px #FF0000;

参数分别表示:水平阴影,垂直阴影,模糊距离,阴影颜色;

10 border-image属性

border-image:将图片规定为包围 div 元素的边框

border-image基础语法:

border-image: url(border.png) 30 30 round

11 父容器高度塌陷的解决方案?

  1. 父元素开启BFC,设置overflow:hidden

  2. clearfix

    .clearfix::before,
    .clearfix::after{
        /*解决外边距重叠*/
        content:'';
        display: table;
        /*解决浮动问题*/
        clear: both;
    }
    
  3. 添加一个空div

    <style>
        .cf {
            clear: both;
        }
    </style>
    <div>
        .......
        .......
        <div class="cf"></div>
    </div>
    

12 less和sass的区别?

不管是Sass,还是Less,都可以视为一种基于CSS之上的高级语言,其目的是使得CSS开发更灵活和更强大,Sass的功能比Less强大,基本可以说是一种真正的编程语言了,Less则相对清晰明了,易于上手,对编译环境要求比较宽松。考虑到编译Sass要安装Ruby,而Ruby官网在国内访问不了,个人在实际开发中更倾向于选择Less。

13 图片在安卓上,有些设备模糊问题

这个问题是devicePixelRatio的不同导致的,因为手机分辨率太小,如果按照分辨率来显示网页,字会非常小,所以苹果系统当初酒把iphone4的960x640像素的分辨率在网页里改为480x320像素,这devicePixelRatio=2。而Android的devicePixelRatio比较乱,值有1.5、2和3.为了在手机里更为清晰地显示图片,必须使用2倍宽高的背景图来代替img标签(一般情况下都是用2倍)。

例如一个div的宽高是100PX x 100PX,背景图必须是200PX x 200PX,然后设置background-size:contain样式,显示出来的图片酒比较清晰了。

14 移动端底部input被弹出的键盘遮挡

Element.srollIntoView():方法让当前的元素滚动到浏览器窗口的可视区域内。

// 只要在input的点击事件或者获取焦点的事件中,加入这个api就好了
document.querySelector('#inputed').srollIntoView();

15 click的300ms延迟问题和点击穿透问题

  1. 300ms延迟导致用户体验不好。为了解决这个问题,一般在移动端用touchstart、touchend、touchmove、tap(模拟的事件)事件来取代click事件。

  2. 方案二:FastClick

    FastClick是FTLabs专门为解决移动端浏览器300ms点击延迟问题所开发的一个轻量级的库。FastClick的实现原理是在检测到touchend事件的时候,会通过DOM自定义事件立即触发膜牛一个click事件,并把浏览器在300ms之后的click事件阻止掉。

16 Doctype作用?标准模式与兼容模式各有什么区别? 你知道多少种Doctype文档类型

  1. Doctype作用

告知浏览器的解析器用什么文档标准解析这个文档。

  1. 标准模式与兼容模式各有什么区别?

    标准模式的排版 和JS运作模式都是以该浏览器支持的最高标准运行。

    在兼容模式中,页面以宽松的向后兼容的方式显示,模拟老式浏览器的行为以防止站点无法工作。

    简单的说,就是尽可能的显示能显示的东西给用户看。

  2. 你知道多少种Doctype文档类型

    HTML 4.01中有3种DTD(文档类型定义)声明可以选择:过渡的(Transitional)、严格的(Strict)和框架的(Frameset)

17 BFC

  1. BFC全称:块级格式化上下文(block formatting context),简单来说它就是一种属性,这种属性会影响元素与元素之间的位置、间距

  2. 形成BFC的条件

    1、float:给元素添加浮动(属性值为left、right,但none除外)

      2、position:给元素添加定位(属性值为absolute或fixed)

      3、display:给元素添加display属性(属性值为 inline-block、table-cell或table-caption)

      4、overflow:给元素添加overflow 属性(属性值为hidden、auto或scroll,但visible除外)

  3. BFC形成后出现的常见问题

    1. margin重叠问题
    2. 浮动相关问题

18 css计算属性

  1. calccss3中新增的计算属性,让很多属性增加了一个表达式的说法;

    标准写法:

     .class{
           /*
    		 area: expression;
           */
    	   width:calc();
    	   padding:calc();
    	   margin-top:calc();
    	   ...
       }
    
  2. calc可以做用于任何具有大小的东东,比如border、margin、pading、font-sizewidth等属性设置动态值

    支持的运算单位: rem , em , percentage , px

    计算优先级别和数学一致

  3. calc 内部的表达式,在使用运算符号时,两遍必须加上空格(虽然乘除可以无视,但还是建议带上)

    	width:calc(10 * 10px);
    	width:calc(50% - 50px);
    	width:calc(50% + 5em);
    	width:calc(10% / 1rem);
    

19 transition transfrom和translate

  1. transition属性可以被指定为一个或多个 CSS 属性的过渡效果,多个属性之间用逗号进行分隔。

    /* Apply to 1 property */
    /* property name | duration */
    transition: margin-right 4s;
    
    /* property name | duration | delay */
    transition: margin-right 4s 1s;
    
    /* property name | duration | timing function */
    transition: margin-right 4s ease-in-out;
    
    /* property name | duration | timing function | delay */
    transition: margin-right 4s ease-in-out 1s;
    
    /* Apply to 2 properties */
    transition: margin-right 4s, color 1s;
    
    /* Apply to all changed properties */
    transition: all 0.5s ease-out;
    
  2. CSS 属性 translate 允许你单独声明平移变换,并独立于 transform 属性

    /* Keyword values */
    translate: none;
    
    /* Single values */
    translate: 100px;
    translate: 50%;
    
    /* Two values */
    translate: 100px 200px;
    translate: 50% 105px;
    
    /* Three values */
    translate: 50% 105px 5rem;
    
    
    1. 单个长度/百分比值

      一个长度值或百分比,表示二维平移,与声明了 X 轴和 Y 轴的平移一样(此时省略的第二个值默认为0)。等同于在 translate() 函数(2D 平移)中指定单个值。

    2. 两个长度/百分比值

      两个长度值或百分比表示在二维上分别按照指定X轴和Y轴的值进行的平移。等同于在 translate() 函数(2D 平移)中函数指定两个值。

    3. 三个长度/百分比值

      三个长度值或百分比,表示分别指定 X 轴、Y 轴、Z 轴的值进行三维平移。等同于translate3d() 函数(3D 平移)。

    4. none 表示不应用平移效果。

  3. CSS**transform属性允许你旋转**,缩放倾斜平移给定元素。这是通过修改CSS视觉格式化模型的坐标空间来实现的。

19 css画一个平行四边形

//HTML代码
<div class="parallelogram "></div>

//CSS代码
.parallelogram {
           width: 150px;
           height: 100px;
           background-color:green;
           transform:skew(30deg);
        }

20 box-sizing

  1. content-box 默认值。如果你设置一个元素的宽为 100px,那么这个元素的内容区会有 100px 宽,并且任何边框和内边距的宽度都会被增加到最后绘制出来的元素宽度中。
  2. border-box告诉浏览器:你想要设置的边框和内边距的值是包含在 width 内的。也就是说,如果你将一个元素的 width 设为 100px,那么这 100px 会包含它的 border 和 padding,内容区的实际宽度是 width 减 去(border + padding) 的值。大多数情况下,这使得我们更容易地设定一个元素的宽高。 **注:**border-box 不包含 margin。

21 居中的方法实现

1水平居中
1.1 行内元素
.parent {
    text-align: center;
}
复制代码
1.2 块级元素

1.2.1 块级元素一般居中方法

.son {
    margin: 0 auto;
}
复制代码

1.2.2 子元素含 float

.parent{
    width:fit-content;
    margin:0 auto;
}

.son {
    float: left;
}

1.2.3 Flex 弹性盒子

1 flex 2012版

.parent {
    display: flex;
    justify-content: center;
}

2 flex 2009版

.parent {
    display: box;
    box-orient: horizontal;
    box-pack: center;
}

1.2.4 绝对定位

1 transform

.son {
    position: absolute;
    left: 50%;
    transform: translate(-50%, 0);
}

2 left: 50%

.son {
    position: absolute;
    width: 宽度;
    left: 50%;
    margin-left: -0.5*宽度
}

3 left/right: 0

.son {
    position: absolute;
    width: 宽度;
    left: 0;
    right: 0;
    margin: 0 auto;
}
2 垂直居中
2.1 行内元素
.parent {
    height: 高度;
}

.son {
    line-height: 高度;
}

注:① 子元素 line-height 值为父元素 height 值。② 单行文本。

2.2 块级元素
2.2.1 行内块级元素
.parent::after, .son{
    display:inline-block;
    vertical-align:middle;
}
.parent::after{
    content:'';
    height:100%;
}

适应 IE7。

2.2.2 table
.parent {
  display: table;
}
.son {
  display: table-cell;
  vertical-align: middle;
}

优点

  • 元素高度可以动态改变, 不需再CSS中定义, 如果父元素没有足够空间时, 该元素内容也不会被截断。

缺点

  • IE6~7, 甚至IE8 beta中无效。
2.2.3 Flex 弹性盒子

1 flex 2012版

.parent {
    display: flex;
    align-items: center;
}
复制代码

优点

  • 内容块的宽高任意, 优雅的溢出。
  • 可用于更复杂高级的布局技术中。

缺点

  • IE8/IE9不支持。
  • 需要浏览器厂商前缀。
  • 渲染上可能会有一些问题。

2 flex 2009版

.parent {
    display: box;
    box-orien: vertical;
    box-pack: center;
}

优点

  • 实现简单, 扩展性强。

缺点

  • 兼容性差, 不支持IE。
2.2.4 绝对定位

1 transform

.son {
    position: absolute;
    top: 50%;
    transform: translate( 0, -50%);
}
复制代码

优点

  • 代码少。

缺点

  • IE8不支持, 属性需要追加浏览器厂商前缀, 可能干扰其他 transform 效果, 某些情形下会出现文本或元素边界渲染模糊的现象。

2 top: 50%

.son {
    position: absolute;
    top: 50%;
    height: 高度;
    margin-top: -0.5高度;
}

优点

  • 适用于所有浏览器。

缺点

  • 父元素空间不够时, 子元素可能不可见(当浏览器窗口缩小时,滚动条不出现时).如果子元素设置了overflow:auto, 则高度不够时, 会出现滚动条。

3 top/bottom: 0;

.son {
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
}

优点

  • 简单。

缺点

  • 没有足够空间时, 子元素会被截断, 但不会有滚动条。

二、javascript基础知识

1 函数实现一秒钟输出一个数

for(let i=0;i<=10;i++){   //用var打印的都是11
 setTimeout(()=>{
    console.log(i);
 },1000*i)
}

2 创建10个标签,点击的时候弹出来对应的序号?

var a
for(let i=0;i<10;i++){
 a=document.createElement('a')
 a.innerHTML=i+'<br>'
 a.addEventListener('click',function(e){
     console.log(this)  //this为当前点击的<a>
     e.preventDefault()  //如果调用这个方法,默认事件行为将不再触发。
     //例如,在执行这个方法后,如果点击一个链接(a标签),浏览器不会跳转到新的 URL 去了。我们可以用 event.isDefaultPrevented() 来确定这个方法是否(在那个事件对象上)被调用过了。
     alert(i)
 })
 const d=document.querySelector('div')
 d.appendChild(a)  //append向一个已存在的元素追加该元素。
}

3 继承的方式有那几种?

  1. 原型链继承 父类的实例作为子类的原型
  2. 借用构造函数继承(伪造对象、经典继承) 复制父类的实例属性给子类
  3. 实例继承(原型式继承)
  4. 组合式继承 调用父类构造函数,继承父类的属性,通过将父类实例作为子类原型,实现函数复用
  5. 寄生组合继承 通过寄生的方式来修复组合式继承的不足,完美的实现继承
  6. es6 extends继承

4 ES5继承和ES6继承的区别

  1. es5继承首先是在子类中创建自己的this指向,最后将方法添加到this中

    Child.prototype=new Parent() || Parent.apply(this) || Parent.call(this)

  2. es6继承是使用关键字先创建父类的实例对象this,最后在子类class中修改this

5 原型链的理解

原型链,简单理解就是原型组成的链,对象的proto它的是原型,而原型也是一个对象,也有proto属性,原型的proto又是原型的原型,就这样可以一直通过proto想上找,这就是原型链,当向上找找到Object的原型的时候,这条原型链就算到头了。

6 判断数据类型的方式

  1. typeof 数组和null返回object

    typeof "John"                // 返回 string
    typeof 3.14                  // 返回 number
    typeof false                 // 返回 boolean
    typeof [1,2,3,4]             // 返回 object
    typeof {name:'John', age:34} // 返回 object
    typeof undefined             // undefined
    typeof null                  // object
    let a = Symbol()
    typeof a = symbol  //symbol
    
  2. instanceof

  3. constructor

  4. Object.prototype.toString.call()

7 数组去重的方式

  1. 使用set
  2. 使用 filter()
  3. 使用 reduce()
const array = [' ', 1,  2, ' ',' ', 3];

// 1: "Set"
[...new Set(array)];

// 2: "Filter"
array.filter((item, index) => array.indexOf(item) === index);

// 3: "Reduce"
array.reduce((unique, item) =>unique.includes(item) ? unique : [...unique, item], []);


// RESULT:
// [' ', 1, 2, 3];

8 同时发送多个异步请求

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// expected output: Array [3, 42, "foo"]

9 跨域解决方法:

  1. jsonp方式
  2. 代理服务器的方式
  3. 服务端允许跨域访问(CORS)
  4. 取消浏览器的跨域限制()

10 登录鉴权怎么处理?

  1. HTTP Auth Authentication
  2. Cookie + Session
  3. JWT
  4. OAuth

11 两个项目如何共享cookie?

cookie除了name,value,expire等信息以外,还有domain和path属性,domain就是当前域,默认为请求的地址,如网址为www.jb51.net/test/test.aspx,那么domain默认为www.jb51.net,path默认就是当前项目的根目录,path和domain只要有一个不同,那就表示跨域,无法实现共享,而不同项目之间domain可能相同,而path一定是不同的。 因此在设定cookie的时候可以domain和path也一起设置,为了实现共享,两个项目应该设置成一样的。修改上面设置cookie的方法,具体设置什么要视情况而定

function setCookie(c_name, value, expiredays){
     var exdate=new Date();
    exdate.setDate(exdate.getDate() + expiredays);
    document.cookie=c_name+ "=" + escape(value) 
                      + ((expiredays==null) ? "" : ";expires="+exdate.toGMTString())
                      +";path=/"
                      +";domain=localhost";
}

12 cookie和session的区别

  1. cookie数据存放在客户的浏览器上,session数据放在服务器上。
  2. cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
  3. session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
  4. 单个cookie保存的数据不能超过4k,很多浏览器都限制一个站点最多保存20个cookie。

13 闭包

  • 闭包:一个函数和它的周围状态的引用捆绑在一起的组合

    是指有权访问另一个函数作用域中的变量的函数

  • 闭包的功能

    • 在函数外,访问函数内的值
    • 保持引用,不被垃圾回收
    • 正常情况:
    function fn(){
        let n = 123
    }
    console.log(n) //报错
    
    • 闭包后
    function fn(){
        let n = 123      
        return function(){
            console.log(n)
        }	
    }
    fn() //123 return的匿名函数与变量n捆绑在一起  //这个被return返回的匿名函数就是闭包
    

14 定时器的执行顺序或机制?

**因为js是单线程的,浏览器遇到setTimeout或者setInterval会先执行完当前的代码块,在此之前会把定时器推入浏览器的待执行事件队列里面,等到浏览器执行完当前代码之后会看一下事件队列里面有没有任务,有的话才执行定时器的代码。**所以即使把定时器的时间设置为0还是会先执行当前的一些代码。

function test(){
    var aa = 0;
    var testSet = setInterval(function(){
        aa++;
        console.log(123);
        if(aa<10){
            clearInterval(testSet);
        }
    },20);
  var testSet1 = setTimeout(function(){
    console.log(321)
  },1000);
  for(var i=0;i<10;i++){
    console.log('test');
  }
}
test()

//输出
test //10次
undefined
123
321

15 fetch发送2次请求的原因

fetch发送post请求的时候,总是发送2次,第一次状态码是204,第二次才成功?

原因很简单,因为你用fetch的post请求的时候,导致fetch 第一次发送了一个Options请求,询问服务器是否支持修改的请求头,如果服务器支持,则在第二次中发送真正的请求。

16 什么是Bom?有哪些常用的Bom属性?

Bom是浏览器对象

location对象

  • location.href-- 返回或设置当前文档的URL
  • location.search -- 返回URL中的查询字符串部分。例如 www.dreamdu.com/dreamd... 返回包括(?)后面的内容?id=5&name=dreamdu
  • location.hash -- 返回URL#后面的内容,如果没有#,返回空 location.host -- 返回URL中的域名部分,例如www.dreamdu.com
  • location.hostname -- 返回URL中的主域名部分,例如dreamdu.com
  • location.pathname -- 返回URL的域名后的部分。例如 www.dreamdu.com/xhtml/ 返回/xhtml/
  • location.port -- 返回URL中的端口部分。例如 www.dreamdu.com:8080/xhtml/ 返回8080
  • location.protocol -- 返回URL中的协议部分。例如 www.dreamdu.com:8080/xhtml/ 返回(//)前面的内容http:
  • location.assign -- 设置当前文档的URL
  • location.replace() -- 设置当前文档的URL,并且在history对象的地址列表中移除这个URL location.replace(url);
  • location.reload() -- 重载当前页面

history对象

  • history.go() -- 前进或后退指定的页面数
  • history.go(num); history.back() -- 后退一页
  • history.forward() -- 前进一页

Navigator对象

  • navigator.userAgent -- 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
  • navigator.cookieEnabled -- 返回浏览器是否支持(启用)cookie

17 indexOf 和 includes区别

  1. 看函数的返回值:indexOf返回的是数值型的而includes返回的是布尔型

  2. 都可以支持第二参数,而且的第二个参数都支持负数形式

  3. 数组中的indexOf不能判断数组中是否有NaN而includes可以

    var ary = [NaN];
    console.log(ary.indexOf(NaN))//-1
    console.log(ary.includes(NaN))//true
    
  4. 判断稀疏数组不同

    var ary = [,,];
    console.log(ary.indexOf(undefined))//-1
    console.log(ary.includes(undefined))//true
    

18 字符串的indexOf和数组中的indexOf的比较

  1. 这两个方法都可以接收两个参数

  2. 这两个方法在没有查找的指定的字符都返回-1

  3. 字符串中的indexOf中的第二个参数不支持负数而数组的indexOf支持(倒数第一个开始索引)

    let str = "abcd";
    let ary = ["a","b","c","d"];
    console.log(str.indexOf("a", -1)); //0
    console.log(ary.indexOf("a", -1)); //-1
    
  4. 字符串的indexOf在传入参数不是字符串的情况下默认会转换为字符串而数组的indexOf不会进行数据类的转换

    let str = "1";
    let ary = ["1"];
    console.log(str.indexOf(1)); //0
    console.log(ary.indexOf(1)); //-1
    

19 字符串的slice和数组中的slice的比较

  1. 在不传入任何参数的情况下都是把原来的值拷贝一份
  2. 字符串的slice的第二个参数是不支持负数的而数组的可以
  3. 都可以接收两个参数

20 indexOf()和search()?

相同点:查找不到匹配的字符串时都返回-1 ,查找到返回的都是对应字符串的起始位置

不同点:

  1. search()的参数是查找的字符串或者正则表达式。,而indexOf()的参数只是普通字符串。indexOf()是比search()更加底层的方法
  2. 如果只是对一个具体字符串来查找,那么使用indexOf()的系统资源消耗更小,效率更高;如果是查找具有某些特征的字符串(比如查找以a开头,后面是数字的字符串),那么indexOf()就无能为力,必须要使用正则表达式和search()方法了。

21 Js变量的数据类型

一个变量可以存放两种类型的值,基本类型的值(primitive values)和引用类型的值(reference values)。

  1. 基本数据类型 :Undefined、Null、Boolean、Number、String、Symbol !
  2. 引用类型 :除过上面的 6 种基本数据类型外,剩下的就是引用类型了,统称为 Object 类型。细分的话,有:Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型 等。

22 基本数据类型的变量是存放在栈内存(Stack)里的过程

image-20220302101926989

23 引用类型数据存储过程

image-20220302101957072

24 与var关键字不同,使用let在全局作用域中声明的变量不会作为window对象的属性

25 this指向问题

一般情况下this最终指向的是那个调用它的对象

  1. 全局作用域或者普通函数this指向全局对象window (定时器里的this也指向window)

    console.log(this) //window
    
    function fn(){
        console.log(this) //window
    }
    
    setTimeout(function(){
        console.log(this) //window
    },1000)//window.setTimeout
    
  2. 方法调用中谁调用this指向谁

    var obj = {
        sayHi :function(){
            console.log(this) 
        }
    }
    obj.sayHi() //obj
    
    var btn = document.querySelector('button')
    btn.onclick = function(){
        console.log(this) //btn
    }
    
  3. 构造函数中this指向构造函数的实例

    function fun(){
        console.log(this)
    }
    var fun1 = new fun();//指向fun1
    

26 for of 和 for in的区别

遍历Object

for-of

var obj = {
  a: 1,
  b: [],
  c: function () {}
};
for (var key of obj) {
   console.log(key);
}
// 出错:
// Uncaught TypeError: obj is not iterable 不可迭代对象

for-in

var obj = {
  a: 1,
  b: [],
  c: function () {}
};
for (var key in obj) {
   console.log(key);
}
// 结果是:
// a
// b
// c
遍历数组

for-in

var arr = [3, 5, 7];
for (var i in arr) {
   console.log(i);
}
// 结果是:
// 0
// 1
// 2

for-of

var arr = [3, 5, 7];
for (var i of arr) {
   console.log(i);
}
// 结果是:
// 3
// 5
// 7
总结
  1. for-of 无法遍历 不可迭代对象

    可迭代对象包括: Array,Map,Set,String,TypedArray,arguments等等

  2. for-of 遍历的是值,for-in遍历的是key

27 实现属性私有化

1. 约定俗成

JS界以一种不成文的规定,在变量前加上下划线"_"前缀,约定这是一个私有属性;但实际上,它仍然是一个穿上皇帝新衣般的公共属性。

img

2. 闭包

constructor作用域内定义局部变量,内部载通过闭包的方式对外暴露该变量。

img

这种方式,虽然实现了私有属性外部不可访问,但在类内部,该属性同样没法在不同的方法内共享,仍然不是严格意义上的“私有属性”。

3. Symbols & Getters

利用Symbol变量可以作为对象key的特点,我们可以模拟实现更真实的私有属性。

img

可是,也不是毫无破绽:

img

借助getOwnPropertySymbols方法可以取出对象的Symbol键值。

4. WeakMap & Getters

WeakMap的实现与Symbol如出一辙。

img

28 强引用和弱引用

1 强引用

在JS中,如果我们将一个引用通过变量或常量保存时,那么这个变量或常量就是强引用。在内部,有一条线将这个变量和引用地址连接在一起了,那么这个引用就不会被当作“垃圾”回收掉。而如果我们为该引用又分配给了一个新的变量,那么在内存当中,又会为这个新变量和该引用创建一根新的“线”将他们连接在一起。

const user = {name:"alex"}
const user2 = user
复制代码

在上面的代码中,我们将一个引用:{name:"alex"}赋值给了user。那么在实际的内存当中,就会有一根线连接user和该引用:

o16ymn.png

然后我们又创建了一个变量user2,并将user赋值给该变量,这里赋值的值其实就是这个引用,所以相当于是这样的关系:

o1cRHI.png

接下来我们再来看当我们使用ES6新引入的类型WeakSetWeakMap在存储引用值时,是一种什么样的情况。

2 弱引用

ES6新引入有多种类型,其中就有包括WeakSet和WeakMap两种类型,这两种类型均只能接收引用值存储,并且这些引用值都是弱引用。接下来我们着重从WeakSet中介绍这种弱引用特性,那么我们来看以下代码情况:

// 还是刚才那种定义
const user = {name:"alex"}
const user2 = user

// 我们使用WeakSet来创建一个WeakSet实例
const ws = new WeakSet()
// 我们为由WeakSet构造出的实例ws添加一个数据:user2
ws.add(user2)
复制代码

到目前为止,我们多做了两件事情,第一是我们创建了一个WeakSet实例,第二是我们在该实例中添加了一个user2,user2对应的引用值是{name:"alex"}。那么我们用画图来表示则是以下这种情况:

o1xhaq.png

你会看到,ws实例中的值{name:"alex"}引用指向于{name:"alex"}(在实际内存中他指向的是该数据的栈的指针引用,该栈指向对应堆中的对应的那个地址的值)。并且需要特别注意的是,这条弱引用的“线”是透明的,这是什么意思?他和强引用的区别在哪里?一句话概述:强引用被{name:"alex"}这个引用认可为一个“连接”,而弱引用不被认可。即该引用并不知道它被ws实例所引用。造成的后果就是该引用并不知道自己被ws实例所引用,这说明垃圾回收也不知道该引用被ws实例所引用。那么如果该引用的所有强引用连接都被断开了(变量被赋值为null或其他情况),那么该引用会被当作垃圾销毁,即使ws实例还在引用着该引用。

// 还是刚才那种定义
const user = {name:"alex"}
const user2 = user

// 我们使用WeakSet来创建一个WeakSet实例
const ws = new WeakSet()
// 我们为由WeakSet构造出的实例ws添加一个数据:user2
ws.add(user2)
user = null
user2 = null
console.log(ws) // {}

继续解析我们上述代码,我们在为ws实例添加了一个新的值,它指向引用{name:"alex"},这时我们将user和user2都赋值为null,相当于把他们的强引用连接断开。这时会发生什么?

o3kpKf.png

这时,因为所有的强引用都断开了,那么垃圾回收认为该引用{name:"alex"}不需要了,就会将他销毁。那么对应的ws实例所用到的该引用也都不复存在了,即使ws实例还在使用着该引用。这就是弱引用的特性。

29 WeakSet应用场景

一个很典型的应用场景: 储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。 友好一点来解释的话不如上代码

carbon (3).png 假设我们需要给记录页面上的禁用标签,那么一个Set对象存放就可以了,这样写功能上没有问题,但如果写成这样,当点击事件发生后,button 的dom被移除,那么整份js中 disabledElements 这个对象因为是强引用,其中的值依然存在于内存中的,那么内存泄漏就造成了,于是我们可以换成 WeakSet 来存放

carbon (4).png

效果是一样的,这里当 button 被移除,disabledElements 中的内容会因为是弱引用而直接变成空,也就是disabledElements被垃圾回收掉了其中的内存,避免了一个小小的内存泄漏的产生

30 内存泄漏

  1. 不正常的闭包

    function fn2(){
      let test = new Array(1000).fill('isboyjc')
      return function(){
        console.log(test)
        return test
      }
    }
    let fn2Child = fn2()
    fn2Child()
    

    显然它也是闭包,并且因为 return 的函数中存在函数 fn2 中的 test 变量引用,所以 test 并不会被回收,也就造成了内存泄漏。

    那么怎样解决呢?

    function fn2(){
      let test = new Array(1000).fill('isboyjc')
      return function(){
        console.log(test)
        return test
      }
    }
    let fn2Child = fn2()
    fn2Child()
    fn2Child = null 
    

    在函数调用后,把外部的引用关系置空

  2. 隐式全局变量

    function fn(){
      // 没有声明从而制造了隐式全局变量test1
      test1 = new Array(1000).fill('isboyjc1')
      
      // 函数内部this指向window,制造了隐式全局变量test2
      this.test2 = new Array(1000).fill('isboyjc2')
    }
    fn()
    

    当我们在使用全局变量存储数据时,要确保使用后将其置空或者重新分配,当然也很简单,在使用完将其置为 null 即可,特别是在使用全局变量做持续存储大量数据的缓存时,我们一定要记得设置存储上限并及时清理,不然的话数据量越来越大,内存压力也会随之增高。

    var test = new Array(10000)
    
    // do something
    
    test = null
    
  3. 游离DOM引用

    <div id="root">
      <ul id="ul">
        <li></li>
        <li></li>
        <li id="li3"></li>
        <li></li>
      </ul>
    </div>
    <script>
      let root = document.querySelector('#root')
      let ul = document.querySelector('#ul')
      let li3 = document.querySelector('#li3')
      
      // 由于ul变量存在,整个ul及其子元素都不能GC
      root.removeChild(ul)
      
      // 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
      ul = null
      
      // 已无变量引用,此时可以GC
      li3 = null
    </script>
    

    当我们使用变量缓存 DOM 节点引用后删除了节点,如果不将缓存引用的变量置空,依然进行不了 GC,也就会出现内存泄漏。

    假如我们将父节点置空,但是被删除的父节点其子节点引用也缓存在变量里,那么就会导致整个父 DOM 节点树下整个游离节点树均无法清理,还是会出现内存泄漏,解决办法就是将引用子节点的变量也置空,如下图:

    img

  4. 遗忘的定时器

    // 获取数据
    let someResource = getData()
    setInterval(() => {
      const node = document.getElementById('Node')
    	if(node) {
        node.innerHTML = JSON.stringify(someResource))
    	}
    }, 1000)
    //其代码中每隔一秒就将得到的数据放入到 Node 节点中去,但是在 setInterval 没有结束前,回调函数里的变量以及回调函数本身都无法被回收。
    

    所以,当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout来清除,另外,浏览器中的 requestAnimationFrame 也存在这个问题,我们需要在不需要的时候用 cancelAnimationFrame API 来取消使用。

  5. 遗忘的事件监听器

    <template>
      <div></div>
    </template>
    
    <script>
    export default {
      created() {
        window.addEventListener("resize", this.doSomething)
      },
      beforeDestroy(){
        window.removeEventListener("resize", this.doSomething)
      },
      methods: {
        doSomething() {
          // do something
        }
      }
    }
    </script>
    
  6. 遗忘的监听者模式

    <template>
      <div></div>
    </template>
    
    <script>
    export default {
      created() {
        eventBus.on("test", this.doSomething)
      },
      beforeDestroy(){
        eventBus.off("test", this.doSomething)
      },
      methods: {
        doSomething() {
          // do something
        }
      }
    }
    </script>
    
  7. 遗忘的Map、Set对象

    1. 当使用 MapSet 存储对象时,同 Object 一致都是强引用,如果不将其主动清除引用,其同样会造成内存不自动进行回收。

    2. 如果使用 Map ,对于键为对象的情况,可以采用 WeakMapWeakMap 对象同样用来保存键值对,对于键是弱引用(注:WeakMap 只对于键是弱引用),且必须为一个对象,而值可以是任意的对象或者原始值,由于是对于对象的弱引用,不会干扰 Js 的垃圾回收。

    3. 如果需要使用 Set 引用对象,可以采用 WeakSetWeakSet 对象允许存储对象弱引用的唯一值,WeakSet 对象中的值同样不会重复,且只能保存对象的弱引用,同样由于是对于对象的弱引用,不会干扰 Js 的垃圾回收。

    4. 谈弱引用,我们先来说强引用,之前我们说 JS 的垃圾回收机制是如果我们持有对一个对象的引用,那么这个对象就不会被垃圾回收,这里的引用,指的就是 强引用 ,而弱引用就是一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,因此可能在任何时刻被回收。

      // obj是一个强引用,对象存于内存,可用
      let obj = {id: 1}
      
      // 重写obj引用
      obj = null 
      // 对象从内存移除,回收 {id: 1} 对象
      

      上面是一个简单的通过重写引用来清除对象引用,使其可回收。

      let obj = {id: 1}
      let user = {info: obj}
      let set = new Set([obj])
      let map = new Map([[obj, 'hahaha']])
      
      // 重写obj
      obj = null 
      
      console.log(user.info) // {id: 1}
      console.log(set)
      console.log(map)
      

      此例我们重写 obj 以后,{id: 1} 依然会存在于内存中,因为 user 对象以及后面的 set/map 都强引用了它,Set/Map、对象、数组对象等都是强引用,所以我们仍然可以获取到 {id: 1} ,我们想要清除那就只能重写所有引用将其置空了。

    5. 接下来我们看 WeakMap 以及 WeakSet

      let obj = {id: 1}
      let weakSet = new WeakSet([obj])
      let weakMap = new WeakMap([[obj, 'hahaha']])
      
      // 重写obj引用
      obj = null
      
      // {id: 1} 将在下一次 GC 中从内存中删除
      

      如上所示,使用了 WeakMap 以及 WeakSet 即为弱引用,将 obj 引用置为 null 后,对象 {id: 1} 将在下一次 GC 中被清理出内存。

  8. 未清理的Console输出

    写代码的过程中,肯定避免不了一些输出,在一些小团队中可能项目上线也不清理这些 console,殊不知这些 console 也是隐患,同时也是容易被忽略的,我们之所以在控制台能看到数据输出,是因为浏览器保存了我们输出对象的信息数据引用,也正是因此未清理的 console 如果输出了对象也会造成内存泄漏。

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <title>test</title>
    </head>
    
    <body>
      <button id="click">click</button>
    
      <script>
        !function () {
          function Test() {
            this.init()
          }
          Test.prototype.init = function () {
            this.a = new Array(10000).fill('isboyjc')
            console.log(this)
          }
    
          document.querySelector('#click').onclick = function () {
            new Test();
          }
        }()
      </script>
    </body>
    
    </html>
    

31 JS中的new Option() options

  1. Option() 用于创建HTMLOptionElement的构造函数。

    var optionElementReference = new Option(text, value, defaultSelected, selected);
    

    文字text可选

    表示元素内容的DOMString,即显示的文本。如果没有指定,则使用默认值""(空字符串)。

    值value可选

    表示HTMLOptionElement的值的DOMString,即value等价的<option>的属性。如果未指定,则将文本的值用作值,例如,将表单提交给服务器时,相关联的<select>元素的值。

    defaultSelected 可选

    设置selected属性值的Boolean ,也就是说这个<option>是默认值当第一次加载页面时,在<select>元素中选择。如果没有指定,false则使用默认值。请注意,true 如果选项尚未被选中,则该值不会将选项设置为选中状态。

    selected可选

    A Boolean 设置选项的选择状态; 默认是false(未选中)。如果省略,即使defaultSelected参数是true,该选项没有被选中。

  2. 动态创建select

    function createSelect(){ 
    var mySelect = document.createElement("select"); 
    mySelect.id = "mySelect"; 
    document.body.appendChild(mySelect); 
    } 
    
  3. 添加选项option

    1. obj.add(new Option("文本","值"))
    2. obj.options.add(new Option("text","value"))
    function addOption(){ 
    //根据id查找对象, 
    var obj=document.getElementById('mySelect'); 
    //添加一个选项 
    obj.add(new Option("文本","值")); //这个只能在IE中有效 
    obj.options.add(new Option("text","value")); //这个兼容IE与firefox 
    } 
    
  4. 删除所有选项option

    obj.options.length=0

    function removeAll(){ 
    var obj=document.getElementById('mySelect'); 
       obj.options.length=0; 
    } 
    
  5. 删除一个选项option

    obj.options.remove(index)

    function removeOne(){ 
    var obj=document.getElementById('mySelect'); 
    //index,要删除选项的序号,这里取当前选中选项的序号 
    var index=obj.selectedIndex; 
      obj.options.remove(index); 
    } 
    
  6. 获得选项option的值 var val = obj.options[index].value

    var obj=document.getElementById('mySelect'); 
    var index=obj.selectedIndex; //序号,取当前选中选项的序号 
    var val = obj.options[index].value; 
    
  7. 获得选项option的文本 obj.options[index].text

    var obj=document.getElementById('mySelect'); 
    var index=obj.selectedIndex; //序号,取当前选中选项的序号 
    var val = obj.options[index].text;
    
  8. 修改选项option

    obj.options[index].text]=new Option(“新文本”,“新值”)

    var obj=document.getElementById('mySelect'); 
    var index=obj.selectedIndex; //序号,取当前选中选项的序号 
    var val = obj.options[index]=new Option("新文本","新值"); 
    

总结:

options: option 集合可返回包含元素中所有的一个数组。 数组中的每个元素对应一个 标签 - 由 0 起始。

  1. 如果把 options.length 属性设置为 0,Select 对象中所有选项都会被清除。
  2. 如果 options.length 属性的值比当前值小,出现在数组尾部的元素就会被丢弃。
  3. 如果把 options[] 数组中的一个元素设置为 null,那么选项就会从 Select 对象中删除。
  4. 可以通过构造函数 Option() 来创建一个新的 option 对象(需要设置 options.length 属性)。

32

三、javascript原生函数

1 手写 instanceof

instanceof作用:判断一个实例是否是其父类或者祖先类型的实例。instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,查找失败,返回 false

function myInstanceof (target,origin){
      while(target){
          if( target._proto_ === origin.prototype){
               return ture
          }		
         target = target._proto_ 
      }
    return false
}

let a = [1,2,3]
console.log(myInstanceof(a,Array)) //true
console.log(myInstanceof(a,Object))  //true

2 原生map方法

思路: 1.在原型上添加一个方法 2.传一个函数和this 3.call 方法传的参数和封装好的map方法的参数是一样的。

Array.prototype.MyMap = function(fn,context) {
	let arr = this;
    //arr指向调用这个方法的数组
	let temp = [];
	for(let i=0;i<arr.length;i++){
		let result = fn.call(context,arr[i],i,arr);
		temp.push(result);
	}
	return temp;
}
let arr1 =[1,2,3,4]
arr1.MyMap(function(item){
      return item*item
})//[1,4,9,16]

reduce实现数组的map方法

Array.prototype.myMap = function(fn,thisValue){
     var res = [];
     thisValue = thisValue||[];
     this.reduce(function(pre,cur,index,arr){
         return res.push(fn.call(thisValue,cur,index,arr));
     },[]);
     return res;
}

var arr = [2,3,1,5];
arr.myMap(function(item,index,arr){
 console.log(item,index,arr);
})

3 手写数组的reduce方法

reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的又一个数组逐项处理方法

参数:

  • callback(一个在数组中每一项上调用的函数,接受四个函数:)

    • previousValue(上一次调用回调函数时的返回值,或者初始值)

    • currentValue(当前正在处理的数组元素)

    • currentIndex(当前正在处理的数组元素下标)

    • array(调用reduce()方法的数组)

  • initialValue(可选的初始值。作为第一次调用回调函数时传给previousValue的值)

Array.prototype.myreduce= function (arr, callback, initialValue){
     var num = initValue == undefined? num = arr[0]: initValue;
     var i = initValue == undefined? 1: 0
     for (i; i< arr.length; i++){
        num = callback(num,arr[i],i)
     }
     return num
 }
 
 function fn(result, currentValue, index){
     return result + currentValue
 }
 
 var arr = [2,3,4,5]
 var b = arr.myreduce(fn,10)
 var c = arr.myreduce(fn)
 console.log(b)   // 24

4 contact递归实现数组扁平化

var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
 function flatten(arr) {
     var res = [];
     for (let i = 0, length = arr.length; i < length; i++) {
     if (Array.isArray(arr[i])) {
           res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
      //res.push(...flatten(arr[i])); //扩展运算符 
     } else {
         res.push(arr[i]);
       }
     }
     return res;
 }
 flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

5 函数柯里化

柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?

有两种思路:

  1. 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数
  2. 在调用柯里化工具函数时,手动指定所需的参数个数

将这两点结合一下,实现一个简单 curry 函数:

/**
 * 将函数柯里化
 * @param fn    待柯里化的原函数
 * @param len   所需的参数个数,默认为原函数的形参个数
 */
function curry(fn,len = fn.length) {
    return _curry.call(this,fn,len)
}

/**
 * 中转函数
 * @param fn    待柯里化的原函数
 * @param len   所需的参数个数
 * @param args  已接收的参数列表
 */
function _curry(fn,len,...args) {
    return function (...params) {
         let _args = [...args,...params];
         if(_args.length >= len){
             return fn.apply(this,_args);
         }else{
          return _curry.call(this,fn,len,..._args)
         }
    }
}

//验证
let _fn = curry(function(a,b,c,d,e){
 console.log(a,b,c,d,e)
});

_fn(1,2,3,4,5);     // print: 1,2,3,4,5
_fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
_fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

6 深拷贝和浅拷贝

浅拷贝:只拷贝一层,更深层的对象级别的只拷贝引用 深拷贝:拷贝多层,每一级别的数据都会拷贝。这样更改拷贝值就不影响另外的对象 ES6浅拷贝方法:Object.assign(target,...sources)

let obj={
 id:1,
 name:'Tom',
 msg:{
 age:18
 }
}
let o={}
//实现深拷贝  递归    可以用于生命游戏那个题对二维数组的拷贝,
//但比较麻烦,因为已知元素都是值,直接复制就行,无需判断
function deepCopy(newObj,oldObj){
     for(var k in oldObj){
         let item=oldObj[k]
         //判断是数组?对象?简单类型?
         if(item instanceof Array){
             newObj[k]=[]
             deepCopy(newObj[k],item)
         }else if(item instanceof Object){
             newObj[k]={}
             deepCopy(newObj[k],item)
         }else{  //简单数据类型,直接赋值
             newObj[k]=item
         }
     }
}

//或者  JSON.parse(JSON.stringify())
function deepCopy(o) {
    return JSON.parse(JSON.stringify(o))
}
//这种拷贝方法不可以拷贝一些特殊的属性(例如正则表达式,undefined,function)
var c = {
    age: 1,
    name: undefined,
    sex: null,
    tel: /^1[34578]\d{9}$/,
    say: () => {
        console.log('hahha')
    }
}
console.log(deepCopy(c))// { age: 1, sex: null, tel: {} }

7 手写callapplybind

  1. 手写call

    //传递参数从一个数组变成逐个传参了,不用...扩展运算符的也可以用arguments代替
    Function.prototype.myCall = function (context, ...args) {
        //这里默认不传就是给window,也可以用es6给参数设置默认参数
        context = context || window
         //如果 args=undefined 那么args重新赋值成为空数组 解构时不会报错
        args = args ? args : []
        //给context新增一个独一无二的属性以免覆盖原有属性
        const key = Symbol()
        context[key] = this
        //通过隐式绑定的方式调用函数
        const result = context[key](...args)
        //删除添加的属性
        delete context[key]
        //返回函数调用的返回值
        return result
    }
    
    //用法:f.call(obj,arg1)
    function f(a,b){
     console.log(a+b)
     console.log(this.name)
    }
    let obj={
     name:1
    }
    f.myCall(obj,1,2) //否则this指向window  输出 3 1 
    
    obj.greet.call({name: 'Spike'}) //打出来的是 Spike
    
  2. 手写apply

    //context是this要指向的对象  不给则指向window
    Function.prototype.myApply = function (context, args) {
        //这里默认不传就是给window,也可以用es6给参数设置默认参数
        context = context || window
        //如果 args=undefined 那么args重新赋值成为空数组 
        args = args ? args : []
        //给context新增一个独一无二的属性以免覆盖原有属性
        const key = Symbol()
        //this是调用的这个函数 fn 
        //给context添加一个方法 相当于context.key=function(){}
        context[key] = this
        //通过隐式绑定的方式调用函数 通过context[key]调用 相当于context.key(..args) this指向context
        const result = context[key](...args)
        //删除添加的属性
        delete context[key]
        //返回函数调用的返回值
        return result
    }
    function f(a,b){
       console.log(a,b)
       console.log(this.name)
    }
    let obj={
     name:'张三'
    }
    f.myApply(obj,[1,2])  //arguments[1]
    
  3. 手写bind

    Function.prototype.bind = function(context, ...outerArgs) {
     var fn = this;
     return function(...innerArgs) {   //返回了一个函数,...rest为实际调用时传入的参数
     return fn.apply(context,[...outerArgs, ...innerArgs]);  //返回改变了this的函数,
     //参数合并
      }
    }
    

8 手动实现new

new的过程文字描述:

  1. 创建一个空对象 obj;
  2. 将空对象的隐式原型(proto)指向构造函数的prototype。
  3. 使用 call 改变 this 的指向
  4. 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。
function Person(name,age){
   this.name=name
   this.age=age
}
Person.prototype.sayHi=function(){
 console.log('Hi!我是'+this.name)
}
let p1=new Person('张三',18)

////手动实现new
function create(){
    let obj={}
   //获取构造函数
    console.log('arguments:')
    console.log(arguments)
    let fn=[].shift.call(arguments)  //将arguments对象提出来转化为数组,arguments并不是数组而是对象    !!!这种方法删除了arguments数组的第一个元素,!!这里的空数组里面填不填元素都没关系,不影响arguments的结果      或者let arg = [].slice.call(arguments,1)
    console.log(fn)
    obj.__proto__=fn.prototype
    let res=fn.apply(obj,arguments)    //改变this指向,为实例添加方法和属性
    //确保返回的是一个对象(万一fn不是构造函数)
    return typeof res==='object'?res:obj
}

let p2=create(Person,'李四',19)
p2.sayHi()

9 手写Promise方法

// Promise/A+ 规范规定的三种状态
const STATUS = {
 PENDING: 'pending',  //初始化
 FULFILLED: 'fulfilled',  //履行  成功
 REJECTED: 'rejected'//未履行  失败
}

class MyPromise {
 // 构造函数接收一个执行回调
 constructor(executor) {
     this._status = STATUS.PENDING // Promise初始状态
     this._value = undefined // then回调的值
     this._resolveQueue = [] // resolve时触发的成功队列
     this._rejectQueue = [] // reject时触发的失败队列
    
 // 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
 const resolve = value => {
     const run = () => {
         // Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
         if (this._st	atus === STATUS.PENDING) {
             this._status = STATUS.FULFILLED // 更改状态
             this._value = value // 储存当前值,用于then回调
            
             // 执行resolve回调
             while (this._resolveQueue.length) {
                 const callback = this._resolveQueue.shift()
                 callback(value)
             }
         }
     }
     //把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
     setTimeout(run)
 }

 // 同 resolve
 const reject = value => {
     const run = () => {
         if (this._status === STATUS.PENDING) {
         this._status = STATUS.REJECTED
         this._value = value
        
         while (this._rejectQueue.length) {
             const callback = this._rejectQueue.shift()
             callback(value)
         }
     }
 }
     setTimeout(run)
 }

     // new Promise()时立即执行executor,并传入resolve和reject
     executor(resolve, reject)
 }

 // then方法,接收一个成功的回调和一个失败的回调
 function then(onFulfilled, onRejected) {
  // 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
  typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
  typeof onRejected !== 'function' ? onRejected = error => error : null

  // then 返回一个新的promise
  return new MyPromise((resolve, reject) => {
    const resolveFn = value => {
      try {
        const x = onFulfilled(value)
        // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }
  }
}

  const rejectFn = error => {
      try {
        const x = onRejected(error)
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }

    switch (this._status) {
      case STATUS.PENDING:
        this._resolveQueue.push(resolveFn)
        this._rejectQueue.push(rejectFn)
        break;
      case STATUS.FULFILLED:
        resolveFn(this._value)
        break;
      case STATUS.REJECTED:
        rejectFn(this._value)
        break;
    }
 })
 }
 catch (rejectFn) {
  return this.then(undefined, rejectFn)
}
// promise.finally方法
finally(callback) {
  return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
    MyPromise.resolve(callback()).then(() => error)
  })
}

 // 静态resolve方法
 static resolve(value) {
      return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
  }

 // 静态reject方法
 static reject(error) {
      return new MyPromise((resolve, reject) => reject(error))
    }

 // 静态all方法
 static all(promiseArr) {
      let count = 0
      let result = []
      return new MyPromise((resolve, reject) =>       {
        if (!promiseArr.length) {
          return resolve(result)
        }
        promiseArr.forEach((p, i) => {
          MyPromise.resolve(p).then(value => {
            count++
            result[i] = value
            if (count === promiseArr.length) {
              resolve(result)
            }
          }, error => {
            reject(error)
          })
        })
      })
    }

 // 静态race方法
 static race(promiseArr) {
      return new MyPromise((resolve, reject) => {
        promiseArr.forEach(p => {
          MyPromise.resolve(p).then(value => {
            resolve(value)
          }, error => {
            reject(error)
          })
        })
      })
    }
}

10 手写原生AJAX

  1. 创建 XMLHttpRequest 实例
  2. 发出 HTTP 请求
  3. 服务器返回 XML 格式的字符串
  4. JS 解析 XML,并更新局部页面不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。
myButton.addEventListener('click', function () {
  ajax()
})

function ajax() {
  let xhr = new XMLHttpRequest() //实例化,以调用方法
  xhr.open('get', 'https://www.google.com')  //参数2,url。参数三:异步
  xhr.onreadystatechange = () => {  //每当 readyState 属性改变时,就会调用该函数。
    if (xhr.readyState === 4) {  //XMLHttpRequest 代理当前所处状态。
      if (xhr.status >= 200 && xhr.status < 300) {  //200-300请求成功
        let string = request.responseText
        //JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
        let object = JSON.parse(string)
      }
    }
  }
  request.send() //用于实际发出 HTTP 请求。不带参数为GET请求
}

Promise实现AJAX

function ajax(url) {
  const p = new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest()
    xhr.open('get', url)
    xhr.onreadystatechange = () => {
      if (xhr.readyState == 4) {
        if (xhr.status >= 200 && xhr.status <= 300) {
          resolve(JSON.parse(xhr.responseText)) //JSOn.parse  把JSON数据转为对象格式
        } else {
          reject('请求出错')
        }
      }
    }
    xhr.send()  //发送hppt请求
  })
  return p
}
let url = '/data.json'
ajax(url).then(res => console.log(res))
  .catch(reason => console.log(reason))

11 手写节流和防抖函数

防抖:

  1. 登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
  2. 调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
  3. 文本编辑器实时保存,当无任何更改操作一秒后进行保存
function debounce(fn, delay) {
     if(typeof fn!=='function') {
        throw new TypeError('fn不是函数')
     }
     let timer; // 维护一个 timer
     return function () {
         var _this = this; // 取debounce执行作用域的this(原函数挂载到的对象)
         var args = arguments;
         if (timer) {
            clearTimeout(timer);
         }
         timer = setTimeout(function () {
            fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
         }, delay);
     };
}

input1.addEventListener('keyup', debounce(() => {
 console.log(input1.value)
}), 600)

节流:

  1. scroll 事件,每隔一秒计算一次位置信息等
  2. 浏览器播放事件,每隔一秒计算一次进度信息等
  3. input 框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求 (也可做防抖)
function throttle(fn, delay) {
  let timer;
  return function () {
    var _this = this;
    var args = arguments;
    if (timer) {
      return;
    }
    timer = setTimeout(function () {
      fn.apply(_this, args); // 这里args接收的是外边返回的函数的参数,不能用arguments
      // fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
      timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
    }, delay)
  }
}

div1.addEventListener('drag', throttle((e) => {
  console.log(e.offsetX, e.offsetY)
}, 100))
  • 防抖:防止抖动,单位时间内事件触发会被重置,避免事件被误伤触发多次。代码实现重在清零 clearTimeout
  • 节流:控制流量,单位时间内事件只能触发一次,如果服务器端的限流即 Rate Limit。代码实现重在开锁关锁 timer=timeout; timer=null

12 手写Promise加载图片

function getData(url) {
  return new Promise((resolve, reject) => {
    $.ajax({
      url,
      success(data) {
        resolve(data)
      },
      error(err) {
        reject(err)
      }
    })
  })
}
const url1 = './data1.json'
const url2 = './data2.json'
const url3 = './data3.json'
getData(url1).then(data1 => {
  console.log(data1)
  return getData(url2)
}).then(data2 => {
  console.log(data2)
  return getData(url3)
}).then(data3 =>
  console.log(data3)
).catch(err =>
  console.error(err)
)

四、Vue.js

1 双向绑定的原理?

vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过 object.defineproperty劫持各个属性的settergetter,在数据 变动时发布消息给订阅者,触发相应的的监听回调。

2 vue的生命周期有哪些?

  1. img

    Vue3把beforeDestroydestroyed替换为beforeUnmountedUnmounted

  2. Vue子组件和父组件执行顺序

    加载渲染过程:beforeCreate(父) —> created(父)—>beforeMount(父)—>beforeCreate(子)—>created(子)—>beforeMount(子)—>mounted(子)—>mounted(父)

    更新过程:beforeUpdate(父) —> beforeUpdate(子) —> update(子) —> update(父)

    销毁过程:beforeDestory(父) —> beforeDestory(子) —> destoryed(子) —> destoryed(父)

  3. created 和 mounted 的区别

    • created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
    • mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
  4. vue请求异步数据在哪个周期函数?

    created、beforeMount、mounted中进行调用。因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

    推荐在 created 钩子函数中,优点:

    • 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
    • SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。

3 第一次加载页面会触发哪些钩子函数?

beforeCreate, created, beforeMount, mounted、

4 说说兄弟组件的生命周期

父组件在执行到beforeMount就开始初始化兄弟组件A,兄弟组件A同样执行到beforeMount就初始化兄弟组件B,当兄弟组件B执行beforeMount完的时候,兄弟组件A才开始挂载。在父兄子组件挂载前,各组件的实例已经初始化完成。

5 keep alive生命周期及应用场景

  1. activated: 页面第一次进入的时候,钩子触发的顺序是created->mounted->activated
  2. deactivated: 页面退出的时候会触发deactivated,当再次前进或者后退的时候只触发activated
  3. 应用场景:

6 computed和watch的区别?

  1. computed

    1. 支持缓存,只有依赖数据发生改变,才会重新进行计算

    2. 不支持异步,当computed内有异步操作时无效,无法监听数据的变化

    3. computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data中声明过或者父组件传递的props中的数据通过计算得到的值

    4. 如果一个属性是由其他属性计算而来的,这个属性依赖其他属性,是一个多对一或者一对一,一般用computed5.如果computed属性属性值是函数,那么默认会走get方法;函数的返回值就是属性的属性值;在computed中的,属性都有一个get和一个set方法,当数据变化时,调用set方法。

      computed:{
      				//计算属性需要配置为一个对象
      				fullName:{
      					//get有什么作用?当有人读取fullName时,get就会被调用,且返回值就作为fullName的值
      					//get什么时候调用?1.初次读取fullName时。2.所依赖的数据(此处是this.firstName和this.lastName)发生变化时。
      					get(){
      						console.log('get被调用了')
      						// console.log(this) //此处的this是vm
      						return this.firstName + '-' + this.lastName
      					},
      					//set什么时候调用? 当fullName被修改时。
      					set(value){
      						console.log('set',value)
      						//将value值分成两个数组 分隔符为-
      						const arr = value.split('-')
      						this.firstName = arr[0]
      						this.lastName = arr[1]
      					}
      				}
      
  2. watch

    1. 不支持缓存,数据变,直接会触发相应的操作;

    2. watch支持异步;

    3. 监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;

    4. 当一个属性发生变化时,需要执行对应的操作;一对多;

    5. 监听数据必须是data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数。immediate:组件加载立即触发回调函数执行;deep: 深度监听,为了发现对象内部值的变化,复杂类型的数据时使用,例如数组中的对象内容的改变,注意监听数组的变动不需要这么做。注意:deep无法监听到数组的变动和对象的新增,参考vue数组变异,只有以响应式的方式触发才会被监听到。

      watch:{
      				firstName(val){  //监视属性简写  val=newValue
      					//设置延时1s后再修改
      					setTimeout(()=>{
      						console.log(this)
      						this.fullName = val + '-' + this.lastName
      					},1000);
      				},
      				lastName(val){  //监视属性简写  val=newValue
      					this.fullName = this.firstName + '-' + val
      				}
      			}
      

7 diff算法的作用?

Diff算法的作用是用来计算出 Virtual DOM 中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面。

8 首屏加载空白的解决方案?

单页面应用的 html 是靠 js 生成,因为首屏需要加载很大的js文件(app.js vendor.js),所以当网速差的时候会产生一定程度的白屏。

解决办法:

  1. 优化 webpack 减少模块打包体积,code-split 按需加载
  2. 服务端渲染,在服务端事先拼装好首页所需的 html
  3. 首页加 loading 或 骨架屏 (仅仅是优化体验)
  4. 服务端开启gzip压缩
  5. 打包文件分包,提取公共文件包

9 谈谈对keep-alive的理解

在平常开发中,有部分组件没有必要多次初始化,这时,我们需要将组件进行持久化,使组的状态维持不变,在下一次展示时,也不会进行重新初始化组件。

也就是说,kee-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染 。也就是所谓的组件缓存。

10 vue性能优化方案?

  1. 均衡页面加载文件的大小和数量
    1. 项目中小图片图片转base64,通过工具如webpack进行图片压缩,文件进行压缩混淆等
    2. vue-router 懒加载,异步路由
    3. 第三方依赖按需加载,比如使用element-ui框架,但是里面的组件只用到了其中一部分,可以单独建一个引入element组件的文件,在里面引入我们项目中需要的组件,然后vue.use它
    4. 通过webpack进行处理,有一个externals属性,可以在里面设置不需要打包的文件,比如可以设置将vue、vue-router、element-ui等等设置进去,打包的时候就不会打包他们,然后将vue、vue-router、element-ui等资源在html中引入
    5. 可以借助开启gzip压缩文件,减小文件大小;
    6. 生产环境build时不生成map文件
  2. 减少等待通过xhr获取数据的时间
    1. 在redis中添加缓存
    2. 在并发允许且数据量较多的情况下,分页可以交给后端做,利用数据库进行排序后取出需要的分页内容,这样虽然增加了xhr请求,但是单次请求耗费时间会大大降低;后端分页每次取数据不一定是仅取当前分页的数据,可以一次性取当前页以及当前页的前后各两页的数据,这样用户进行前后页的切换时,不需要重新继续发起xhr请求。
    3. 一些内容固定的数据(但又需要进行管理),可以将这些数据的获取合并为一个请求,每次刷新只需要取一次
    4. 提前发起xhr请求:可以在dom渲染完成之前就发起xhr请求,而不是等待dom渲染完成之后才进行。created时,或者beforeRouteEnter时就调用。
  3. 通过交互,在视觉效果上提升
    1. 可以通过一些加载loading动画,以及资源加载完成前,可以通过占位符占位的方式,避免渲染时出现空白页,视觉上提升加载速度
    2. 优先加载当前用户可视区域的内容,其他内容待用户切换tab或者滚动鼠标或者可视区域加载完成后再继续加载
    3. 图片预渲染,可以在当前页上根据页面上的跳转链接(或者tab页可能的切换),预渲染一些图片

11 vue和jquery的对比

  1. jQuery是使用选择器()选取DOM对象,对其进行赋值、取值、事件绑定等操作,其实和原生的HTML的区别只在于可以更方便的选取和操作DOM对象,而数据和界面是在一起的。比如需要获取label标签的内容:)选取DOM对象,对其进行赋值、取值、事件绑定等操作,其实和原生的HTML的区别只在于可以更方便的选取和操作DOM对象,而数据和界面是在一起的。比如需要获取label标签的内容:("lable").val();,它还是依赖DOM元素的值。

  2. Vue则是通过Vue对象将数据和View完全分离开来了。对数据进行操作不再需要引用相应的DOM对象,可以说数据和View是分离的,他们通过Vue对象这个vm实现相互的绑定。

  3. vue适用的场景:复杂数据操作的后台页面,表单填写页面

    jquery适用的场景:比如说一些html5的动画页面,一些需要js来操作页面样式的页面

    然而二者也是可以结合起来一起使用的,vue侧重数据绑定,jquery侧重样式操作,动画效果等,则会更加高效率的完成业务需求。

12 mvvm和mvc区别

MVVM即Model-View-ViewModel的简写。即模型-视图-视图模型。模型(Model)指的是后端传递的数据。视图(View)指的是所看到的页面。视图模型(ViewModel)是mvvm模式的核心,它是连接view和model的桥梁。它有两个方向:一是将模型(Model)转化成视图(View),即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将视图(View)转化成模型(Model),即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。这两个方向都实现的,我们称之为数据的双向绑定。

MVC是Model-View- Controller的简写。即模型-视图-控制器。M和V指的意思和MVVM中的M和V意思一样。C即Controller指的是页面业务逻辑。使用MVC的目的就是将M和V的代码分离。MVC是单向通信。也就是View跟Model,必须通过Controller来承上启下。MVC和MVVM的区别并不是VM完全取代了C,只是在MVC的基础上增加了一层VM,只不过是弱化了C的概念,ViewModel存在目的在于抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。也就是说MVVM实现的是业务逻辑组件的重用,使开发更高效,结构更清晰,增加代码的复用性。

13 局部混入,多个vue文件操作一个变量,会乱吗

不会,一个vue文件操作后,另外一个vue文件操作时,变量的值还是原来的

14 nextTick

  1. 语法:this.$nextTick(回调函数)
  2. 作用:在下一次 DOM 更新结束后执行其指定的回调。
  3. 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。比如需要在输入框渲染后再获取焦点

15 vue3.0为什么要用Proxy API替代defineProperty API?

响应式优化。

1.**defineProperty API的局限性最大原因是它只针对单例属性做监听。**vue2.x中的响应式实现是基于defineProperty中的descriptor,对data中的属性遍历+递归,为每个属性设置了getter、setter。这也就是为什么vue只能对data中预定义的属性做出响应的原因,在vue中使用下标的方式直接修改属性的值或者添加一个预先不存在的对象属性是无法做到settter监听的,这是defineProperty的局限性。

2.**Proxy API的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性,将会带来很大的性能提升和更优的代码。**Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

3.**响应式是惰性的。**在vue2.x中,对于一个深层属性嵌套的对象,要劫持它内部深层次的变化,就需要递归便利这个对象,执行Object.defineProperty把每一层对象数据都变成响应式的,这无疑会有很大的性能消耗。在vue3.0中,使用Proxy API并不能监听到对象内部深层次的属性变化,因此它的处理方式是在getter中去递归响应式,这样的好处是真正访问到的内部属性才会变成响应式,简单可以说是按需实现响应式,减少性能消耗。

16 说说你对proxy的理解

vue的数据劫持有两个缺点:

  1. 无法监听到索引修改数组的指的变化
  2. 无法监听object的值的变化

所以vue2.x中才会有$set属性的存在

proxy是set中推出的新api,可以弥补以上两个缺点,所以vue3.x版本用proxy替换object.defineproperty。

17 你了解的组件通信方式

  • props 父组件=>子组件
  • props绑定函数 子组件=>父组件
  • 自定义事件 子组件=>父组件
  • vuex 任意组件
  • 全局事件总线 任意组件
  • 消息订阅发布模式 任意组件
  • atrrsatrrs listeners 跨层通信

18 写 React/Vue 项目时为什么要在列表组件中写 key,其作用是什么?

vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中(建议先了解一下diff算法过程)。 在交叉对比中,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点(这里对应的是一个key => index 的map映射)。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。

19 Vue中key绑定index和id的区别

image-20220301221407761

image-20220301221423267

20 MVVM和MVC

五、Node.js

1 npm 实现原理

输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):

  1. 执行工程自身 preinstall 当前 npm 工程如果定义了 preinstall 钩子此时会被执行。
  2. 确定首层依赖模块 首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。 工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。
  3. 获取模块 获取模块是一个递归的过程,分为以下几步:
  • 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
  • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
  • 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。

2 JSON Web Token认证流程

fa66c316dba9681474969de34c9327f2.png

什么时候应该使用JSON Web Token?

以下是JSON Web Token 有用的一些情况:

  • 授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
  • 信息交换:JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确定发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。

3 jwt 如何防止token被拦截,jwt认证中如何防止他人冒充token?

  1. token设置较短的有效期
  2. 在用户退出,或是修改密码的时候,使此用户的所有 JWT 立即失效。
  3. 采用https或者代码层面也可以做安全检测,比如ip地址发生变化,MAC地址发生变化等等,可以要求重新登录

六、Http

1 常见状态代码、状态描述、说明:

  • 200 OK //客户端请求成功

  • 400 Bad Request //客户端请求有语法错误,不能被服务器所理解

  • 401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用

  • 403 Forbidden //服务器收到请求,但是拒绝提供服务

  • 404 Not Found //请求资源不存在,eg:输入了错误的URL

  • 500 Internal Server Error //服务器发生不可预期的错误

  • 503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常

  • 以 3xx 为开头的都表示需要进行附加操作以完成请求

    状态码含义:

    • 301 永久性重定向,该状态码表示请求的资源已经重新分配 URI,以后应该使用资源现有的 URI

    • 302 临时性重定向,该状态码表示请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问。

    • 303 该状态码表示由于请求对应的资源存在着另一个 URI,应使用 GET 方法定向获取请求的资源。

    • 304 该状态码表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但未满足条件的情况。

    • 307 临时重定向。该状态码与 302 Found 有着相同的含义。

2 三次握手的过程?

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。

  1. 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认; SYN:同步序列编号(Synchronize Sequence Numbers)
  2. 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  3. 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

img

  1. 客户端发送SYN包
  2. 服务端接受申请,并向客户端发送SYN+ACK包 ACK包是为了让客户端确认是由它发起的建立连接申请
  3. 客户端接收 SYN+ACK包 并向服务器再次发送ACK包确认

3 get请求传参长度的误区、get和post请求在缓存方面的区别

实际上HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对get请求参数的限制是来源与浏览器或web服务器,浏览器或web服务器限制了url的长度。为了明确这个概念,我们必须再次强调下面几点:

  • HTTP 协议 未规定 GET 和POST的长度限制
  • GET的最大长度显示是因为 浏览器和 web服务器限制了 URI的长度
  • 不同的浏览器和WEB服务器,限制的最大长度不一样
  • 要支持IE,则最大长度为2083byte,若只支持Chrome,则最大长度 8182byte

补充补充一个get和post在缓存方面的区别:

  • get请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存
  • post不同,post做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。因此get请求适合于请求缓存

4 http和https的区别?

  1. http: 直接通过明文在浏览器和服务器之间传递信息。

  2. https: 采用 对称加密非对称加密 结合的方式来保护浏览器和服务端之间的通信安全。

    对称加密算法加密数据+非对称加密算法交换密钥+数字证书验证身份=安全

    https其实是有两部分组成:HTTP + SSL / TLS,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。

  3. http传输的数据都是未加密的,也就是明文的,网景公司设置了SSL协议来对http协议传输的数据进行加密处理,简单来说https协议是由http和ssl协议构建的可进行加密传输和身份认证的网络协议,比http协议的安全性更高。 主要的区别如下:

    • Https协议需要ca证书,费用较高。

    • http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

    • 使用不同的链接方式,端口也不同,一般而言,http协议的端口为80,https的端口为443

    • http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

5 TCP四种拥塞控制算法

1.慢开始 2.拥塞控制 3.快重传 4.快恢复

6 Cookie中的httponly的属性和作用

如果cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性,即便是这样,也不要将重要信息存入cookie。XSS全称Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏洞,XSS属于被动式且用于客户端的攻击方式,所以容易被忽略其危害性。其原理是攻击者向有XSS漏洞的网站中输入(传入)恶意的HTML代码,当其它用户浏览该网站时,这段HTML代码会自动执行,从而达到攻击的目的。如,盗取用户Cookie、破坏页面结构、重定向到其它网站等。

HttpOnly的设置样例

response.setHeader(``"Set-Cookie"``, "cookiename=httponlyTest;Path=/;Domain=domainvalue;Max-Age=seconds;HTTPOnly");



//设置cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly")


//设置多个cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
response.addHeader("Set-Cookie", "timeout=30; Path=/test; HttpOnly");

 

//设置https的cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; Secure; HttpOnly");

设置完毕后通过js脚本是读不到该cookie的,但使用如下方式可以读取。

Cookie cookies[ ]=request.getCookies();

7 Https过程

img

加密过程:

img

8 TCP、UDP

TCP与UDP区别 TCP UDP详解 image-20220303144750316

  1. 基于连接与无连接;
  2. 对系统资源的要求(TCP较多,UDP少);
  3. UDP程序结构较简单;
  4. 流模式与数据报模式 ;
  5. TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。

9 http1.x和http2.x的区别

  1. http1的解析是基于文本协议的各式解析,而http2.0的协议解析是二进制格式,更加的强大
  2. 多路复用(Mutiplexing) : 一个连接上可以有多个request,且可以随机的混在一起,每个不同的request都有对应的id,服务端可以通过request_id来辨别,大大加快了传输速率
  3. header压缩: http1.x中的header需要携带大量信息.而且每次都要重复发送.http2.0使用encode来减少传输的header大小.而且客户端和服务端可以各自缓存(cache)一份header filed表,避免了header的重复传输,还可以减少传输的大小.
  4. 服务端推送(server push): 可以通过解析html中的依赖,只能的返回所需的其他文件(css或者js等),而不用再发起一次请求.

多路复用的示意图:

img

10 tcp建立连接为什么需要三次握手

三次握手的目的是“为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误”,这种情况是:一端(client)A发出去的第一个连接请求报文并没有丢失,而是因为某些未知的原因在某个网络节点上发生滞留,导致延迟到连接释放以后的某个时间才到达另一端(server)B。本来这是一个早已失效的报文段,但是B收到此失效的报文之后,会误认为是A再次发出的一个新的连接请求,于是B端就向A又发出确认报文,表示同意建立连接。如果不采用“三次握手”,那么只要B端发出确认报文就会认为新的连接已经建立了,但是A端并没有发出建立连接的请求,因此不会去向B端发送数据,B端没有收到数据就会一直等待,这样B端就会白白浪费掉很多资源。如果采用“三次握手”的话就不会出现这种情况,B端收到一个过时失效的报文段之后,向A端发出确认,此时A并没有要求建立连接,所以就不会向B端发送确认,这个时候B端也能够知道连接没有建立。

总结: 为了确认双方的发送接收能力没问题

11 tcp关闭连接为什么需要四次挥手

本质的原因是tcp是全双公的,要实现可靠的连接关闭,A发出结束报文FIN,收到B确认后A知道自己没有数据需要发送了,B知道A不再发送数据了,自己也不会接收数据了,但是此时A还是可以接收数据,B也可以发送数据;当B发出FIN报文的时候此时两边才会真正的断开连接,读写分开。

  1. 客户端发出结束报文
  2. 服务端向客户端发出确认 此时客户端只可以接收数据 服务器端只可以发送数据
  3. 服务端发出FIN报文 正式断开连接
  4. 客户端发出确认ACK包

注意: 接收到FIN报文的一方只能回复一个ACK, 它是无法马上返回对方一个FIN报文段的,因为结束数据传输的“指令”是上层应用层给出的,我只是一个“搬运工”,我无法了解“上层的意志”

12 强缓存和弱缓存的区别

  1. 强缓存不发请求到服务器,所以有时候资源更新了浏览器还不知道,但是协商缓存会发请求到服务器,所以资源是否更新,服务器肯定知道。

  2. 大部分web服务器都默认开启协商缓存。

13 从输入URL到页面渲染全过程

  1. DNS域名解析
  2. 构建浏览器和服务器的TCP链接(3次握手)
  3. 浏览器发送HTTP请求
  4. 服务器处理HTTP请求,并返回请求的资源(HTML,CSS,JS)
  5. 浏览器解析并渲染界面
  6. 断开TCP链接(4次挥手)

七、算法

1 js实现一个堆以及堆排序

/* 排序思路:(降序)
 * 将堆根保存于尾部,并对剩余序列调用调整函数,调整完成后,再将最大跟保存于尾部-1(-1,-2,...,-i),
 * 再对剩余序列进行调整,反复进行该过程,直至排序完成。
 */

/* 将最大的元素调整到堆顶*/
function AdjustHeap(arr, pos, len){
    var swap = arr[pos];      //保存当前节点
    var child = pos * 2 + 1;  //定位到当前节点的左边的子节点
    while(child < len){       //递归遍历所有的子节点
        //判断当前节点是否有右节点,若右节点较大,就采用右节点和当前节点进行比较
        if(child + 1 < len && arr[child] < arr[child + 1]){
            child += 1;
        }
        //比较当前节点和最大的子节点,小于就交换,交换后将当前节点定位到子节点上
        if(arr[pos] < arr[child]){
            arr[pos] = arr[child];
            pos = child;
            child = pos * 2 + 1;
        }
        else{
            break;
        }
        arr[pos] = swap;
    }
}

/* 构建堆:
 * 满足:树中任一非叶子结点的关键字均不大于(或不小于)其左右孩子结点的关键字。
 * 实现:从最后一个拥有子节点的节点开始,将该节点和其他节点进行比较,将最大的数交换给该节点,
 *      交换后再依次向前节点进行相同的交换处理,直到构建出大顶堆。
 */
function BuildHeap(arr){
  for(var i=arr.length/2; i>=0; i--){  //构建打顶堆
      AdjustHeap(arr, i, arr.length);
  }
}

/*堆排序算法*/
function HeapSort(arr){
    BuildHeap(arr); //构建堆
    for(var i=arr.length-1; i>0; i--){   //从数组的尾部进行调整
        var swap = arr[i];  //堆顶永远是最大的元素,将堆顶和尾部元素交换,最大元素就保存在尾部,并且不参与后面的调整
        arr[i] = arr[0];
        arr[0] = swap;
        AdjustHeap(arr, 0, i); //将最大的元素进行调整,将最大的元素调整到堆顶
    }
}

var arr = [46,12,33,72,68,19,80,33];
console.log('before: ' + arr);
HeapSort(arr);
console.log(' after: ' + arr);

2 js快速排序

//冒泡排序
var arr=[2,4,3,1,5,6]
for(var i=0;i<=arr.length-1;i++){
    for(var j=0;j<=arr.length-1;j++){
        if(arr[j]<arr[j+1]){
            var temp = arr[j]
            arr[j] = arr[j+1]
            arr[j+1] = temp
        }
    }
}

//sort排序
var arr=[2,4,3,1,5,6]
arr.sort(function(a,b){
    //return a-b 升学的顺序排列
    //return b-a 降序的顺序排列
})

//快速排序
Array.prototype.quickSort = function() {
    const rec = (arr) => {
        // 递归都是要有尽头的,不然会无限进行下去
        // 直到Maximum call stack size exceeded
        // 而且注意,这里要有小于1,不然也会报错
        if(arr.length <= 1) return arr;
        let left = [];
        let right = [];
        const base = arr[0];
        // 因为基准线是arr[0],所以从下标是1也就是第二个开始
        for(let i = 1; i < arr.length; i += 1) {
            if(arr[i] < base) {
                left.push(arr[i])
            } else {
                right.push(arr[i])
            }
        }
        // 解构一下
        // 递归左边数组和右边数组
        // 左边加上右边加上基准才是完整数组哈
        return [...rec(left), base, ...rec(right)];
    }
    const res = rec(this);
    // 遍历res,赋值到this也就是当前数组本身
    res.forEach((item, key) => {
        this[key] = item;
    })
}

const arr = [1, 5, 9, 3, 18, 6, 2, 7]
arr.quickSort()
console.log(arr);

3 排序算法,事件复杂度,归并,快排,堆排的应用场景

4 复原ip地址算法

由于我们需要找出所有可能复原出的 IP 地址,因此可以考虑

使用回溯的方法,对所有可能的字符串分隔方式进行搜索,并筛选出满足要求的作为答案。

设题目中给出的字符串为 s。我们用递归函数 dfs(segId,segStart)dfs(segId,segStart)dfs(segId,segStart) 表示我们正在从 s[segStart]s[segStart]s[segStart] 的位置开始,搜索 IP 地址中的第 segIdsegIdsegId 段,其中 segId∈0,1,2,3segId∈{0,1,2,3}segId∈0,1,2,3。由于 IP 地址的每一段必须是 [0,255] 中的整数,因此我们从 segStartsegStartsegStart 开始,从小到大依次枚举当前这一段 IP 地址的结束位置 segEndsegEndsegEnd。如果满足要求,就递归地进行下一段搜索,调用递归函数 dfs(segId+1,segEnd+1)dfs(segId+1,segEnd+1)dfs(segId+1,segEnd+1)。

特别地,由于 IP 地址的每一段不能有前导零,因此如果 s[segStart] 等于字符 0,那么 IP 地址的第 segId 段只能为 0,需要作为特殊情况进行考虑。

在搜索的过程中,如果我们已经得到了全部的 4 段 IP 地址(即 segId=4),并且遍历完了整个字符串(即 segStart=∣s∣,其中 ∣s∣ 表示字符串 s 的长度),那么就复原出了一种满足题目要求的 IP 地址,我们将其加入答案。在其它的时刻,如果提前遍历完了整个字符串,那么我们需要结束搜索,回溯到上一步。

var restoreIpAddresses = function(s) {
    const SEG_COUNT = 4;
    const segments = new Array(SEG_COUNT);
    const ans = [];

    const dfs = (s, segId, segStart) => {
        // 如果找到了 4 段 IP 地址并且遍历完了字符串,那么就是一种答案
        if (segId === SEG_COUNT) {
            if (segStart === s.length) {
                ans.push(segments.join('.'));
            }
            return;
        }

        // 如果还没有找到 4 段 IP 地址就已经遍历完了字符串,那么提前回溯
        if (segStart === s.length) {
            return;
        }

        // 由于不能有前导零,如果当前数字为 0,那么这一段 IP 地址只能为 0
        if (s.charAt(segStart) === '0') {
            segments[segId] = 0;
            dfs(s, segId + 1, segStart + 1);
        }

        // 一般情况,枚举每一种可能性并递归
        let addr = 0;
        for (let segEnd = segStart; segEnd < s.length; ++segEnd) {
            addr = addr * 10 + (s.charAt(segEnd) - '0');
            if (addr > 0 && addr <= 0xFF) {
                segments[segId] = addr;
                dfs(s, segId + 1, segEnd + 1);
            } else {
                break;
            }
        }
    }

    dfs(s, 0, 0);
    return ans;
};

八、网络安全

1 XSS 跨站脚本攻击

跨站脚本攻击(XSS)通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。

常用的XSS攻击手段和目的有:

  1. 盗用cookie,获取敏感信息。
  2. 利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。
  3. 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
  4. 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
  5. 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果。

2 CSRF 跨站请求伪造

跨站请求伪造(CSRF)是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。

攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

防御CSRF攻击:

  1. 目前防御 CSRF 攻击主要有三种策略:验证 HTTP Referer 字段;在请求地址中添加 token 并验证;在 HTTP 头中自定义属性并验证。
  2. 跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

九、WebSocket

  1. Http协议无法实现服务器主动向客户端发起消息

十、Webpack

十一、CDN

一、CDN简介

主要思路: 尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。

实现方法: 通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接和负载状况以及到用户的距离响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,加快访问速度。

目的: 使用户可就近取得所需内容,解决Internet网络拥挤的状况,提高用户访问网站的响应速度

优势:

  1. CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;
  2. 大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源站的负载。

二、CDN基本工作流程

首先是不使用CDN的情况下的流程:

  1. 用户在自己的浏览器中输入要访问的网站域名。
  2. 浏览器向 本地DNS服务器 请求对该域名的解析。
  3. 本地DNS服务器中如果缓存有这个域名的解析结果,则直接响应用户的解析请求。
  4. 本地DNS服务器中如果没有关于这个域名的解析结果的缓存,则以递归方式向整个DNS系统请求解析,获得应答后将结果反馈给浏览器。
  5. 浏览器得到域名解析结果,就是该域名相应的服务设备的 IP地址 。
  6. 浏览器向服务器请求内容。
  7. 服务器将用户请求内容传送给浏览器。

使用了CDN时,DNS 服务器根据用户 IP 地址,将域名解析成相应节点的缓存服务器IP地址,实现用户就近访问。使用 CDN 服务的网站,只需将其域名解析权交给 CDN 的全局负载均衡(GSLB)设备,将需要分发的内容注入 CDN,就可以实现内容加速了。

  1. 当用户点击网站页面上的内容URL,经过本地DNS系统解析,DNS 系统会最终将域名的解析权交给 CNAME 指向的 CDN 专用 DNS 服务器。
  2. CDN 的 DNS 服务器将 CDN 的全局负载均衡设备 IP 地址返回用户。
  3. 用户向 CDN 的全局负载均衡设备发起内容 URL 访问请求。
  4. CDN 全局负载均衡设备根据用户 IP 地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求。
  5. 基于以下这些条件的综合分析之后,区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址:
    • 根据用户 IP 地址,判断哪一台服务器距用户最近;
    • 根据用户所请求的 URL 中携带的内容名称,判断哪一台服务器上有用户所需内容;
    • 查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。
  6. 全局负载均衡设备把服务器的 IP 地址返回给用户。
  7. 用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容,而区域均衡设备依然将它分配给了用户,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。

三、CDN的作用

CDN最常用的功能当然是加速,但还有一些其他功能。

1. 加速访问

CDN可以使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

还提供服务器端加速,解决由于用户访问量大造成的服务器过载问题;

2. 实现跨运营商、跨地域的全网覆盖

互联不互通、区域ISP地域局限、出口带宽受限制等种种因素都造成了网站的区域性无法访问。

CDN加速可以覆盖全球的线路,通过和运营商合作,部署IDC资源,在全国骨干节点商,合理部署CDN边缘分发存储节点,充分利用带宽资源,平衡源站流量。

3. 保障你的网站安全

CDN的负载均衡和分布式存储技术,可以加强网站的可靠性,相当无无形中给你的网站添加了一把保护伞,应对绝大部分的互联网攻击事件。防攻击系统也能避免网站遭到恶意攻击。

4. 异地备援

当某个服务器发生意外故障时,系统将会调用其他临近的健康服务器节点进行服务,进而提供接近100%的可靠性,这就让你的网站可以做到永不宕机。

5. 节约成本

能克服网站分布不均的问题,投入使用CDN加速可以实现网站的全国铺设,你根据不用考虑购买服务器与后续的托管运维,服务器之间镜像同步,也不用为了管理维护技术人员而烦恼,并且能降低网站自身建设和维护成本。

6. 让你更专注业务本身

CDN加速厂商一般都会提供一站式服务,业务不仅限于CDN,还有配套的云存储、大数据服务、视频云服务等,而且一般会提供7x24运维监控支持,保证网络随时畅通,你可以放心使用。并且将更多的精力投入到发展自身的核心业务之上。

四、CDN工作原理

CDN的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。

1. 用户访问cdn资源的过程
  1. 用户向浏览器输入www.web.com这个域名,浏览器第一次发现本地没有DNS缓存,则向网站的DNS服务器请求;
  2. 网站的DNS域名解析器设置了CNAME,指向了www.web.51cdn.com,请求指向了CDN网络中的智能DNS负载均衡系统;
  3. 智能DNS负载均衡系统解析域名,把对用户响应速度最快的IP节点(CDN服务器)返回给用户;
  4. 用户向该IP节点(CDN服务器)发出请求;
  5. 由于是第一次访问,CDN服务器会向原web站点请求,并缓存内容;
  6. 请求结果发给用户。
2. cdn主要特点
  1. 本地Cache加速 提高了企业站点(尤其含有大量图片和静态页面站点)的访问速度,并大大提高以上性质站点的稳定性
  2. 镜像服务 消除了不同运营商之间互联的瓶颈造成的影响,实现了跨运营商的网络加速,保证不同网络中的用户都能得到良好的访问质量。
  3. 远程加速 远程访问用户根据DNS负载均衡技术智能自动选择Cache服务器,选择最快的Cache服务器,加快远程访问的速度
  4. 带宽优化 自动生成服务器的远程Mirror(镜像)cache服务器,远程用户访问时从cache服务器上读取数据,减少远程访问的带宽、分担网络流量、减轻原站点WEB服务器负载等功能。
  5. 集群抗攻击 广泛分布的CDN节点加上节点之间的智能冗余机制,可以有效地预防黑客入侵以及降低各种D.D.o.S攻击对网站的影响,同时保证较好的服务质量 。

五、CDN对网络的优化:

  1. 解决服务器端的“第一公里”问题
  2. 缓解甚至消除了不同运营商之间互联的瓶颈造成的影响
  3. 减轻了各省的出口带宽压力
  4. 缓解了骨干网的压力
  5. 优化了网上热点内容的分布

第一公里 是指万维网流量向用户传送的第一个出口,是网站服务器接入互联网的链路所能提供的带宽。 这个带宽决定了一个 网站能为用户提供的访问速度和并发访问量。如果业务繁忙,用户的访问数越多,拥塞越严重,网站会在最需要向用户提供服务时失去用户。

中间一公里 代表互联网中节点与节点之间的传输网络。

最后一公里 万维网流量向用户传送的最后一段接入链路。

六、CDN的应用场景

1. 网站站点/应用加速

站点或者应用中大量静态资源的加速分发,建议将站点内容进行动静分离,动态文件可以结合云服务器ECS,静态资源如各类型图片、html、css、js文件等,建议结合 对象存储OSS 存储海量静态资源,可以有效加速内容加载速度,轻松搞定网站图片、短视频等内容分发

2. 视音频点播/大文件下载分发加速

支持各类文件的下载、分发,支持在线点播加速业务,如mp4、flv视频文件或者平均单个文件大小在20M以上,主要的业务场景是视音频点播、大文件下载(如安装包下载)等,建议搭配对象存储OSS使用,可提升回源速度,节约近2/3回源带宽成本。

3. 视频直播加速(内测中)

视频流媒体直播服务,支持媒资存储、切片转码、访问鉴权、内容分发加速一体化解决方案。结合弹性伸缩服务,及时调整服务器带宽,应对突发访问流量;结合媒体转码服务,享受高速稳定的并行转码,且任务规模无缝扩展。目前CDN直播加速已服务内部用户测试并优化,即将上线

4. 移动应用加速

移动APP更新文件(apk文件)分发,移动APP内图片、页面、短视频、UGC等内容的优化加速分发。提供httpDNS服务,避免DNS劫持并获得实时精确的DNS解析结果,有效缩短用户访问时间,提升用户体验。

七、CDN缓存

缓存是空间换时间的思路,通过使用多余的空间,换来更快的访问速度。

  • 不使用cdn缓存时

所有的用户都直接访问源服务器

img

  • 使用cdn缓存时

客户端浏览器先检查是否有本地缓存是否过期,如果过期,则向CDN边缘节点发起请求,CDN边缘节点会检测用户请求数据的缓存是否过期,如果cdn数据没有过期,则直接响应用户请求,此时一个完成http请求结束;如果cdn数据已经过期,那么CDN还需要向源站发出回源请求,来拉取最新的数据。

img

缓存优点: CDN的分流作用不仅减少了用户的访问延时,也减少的源站的负载。

缺点: 当网站更新时,如果CDN节点上数据没有及时更新,即便用户再浏览器使用Ctrl+F5的方式使浏览器端的缓存失效,也会因为CDN边缘节点没有同步最新数据而导致用户访问异常。

八、解决CDN缓存更新的办法

  1. 资源url参数加时间戳

url的参数加上时间戳,每次更新时时间戳也跟随更新,重新使cdn边缘节点同步源服务器最新数据。

http://www.cdn.com/static/images/test.png # 没加时间戳
http://www.cdn.com/static/images/test.png?_t=202012290910 # 加了时间戳
  1. 调用cdn服务商提供的刷新缓存接口

CDN边缘节点对开发者是透明的,相比于浏览器Ctrl+F5的强制刷新来使浏览器本地缓存失效,开发者可以通过CDN服务商提供的“刷新缓存”接口来达到清理CDN边缘节点缓存的目的。

这样开发者在更新数据后,可以使用“刷新缓存”功能来强制CDN节点上的数据缓存过期,保证客户端在访问时,拉取到最新的数据。

九、cdn的组成

1. 部署架构

CDN 系统设计的首要目标是尽量减少用户的访问响应时间,为达到这一目标,CDN 系统应该尽量将用户所需要的内容存放在距离用户最近的位置。也就是说,负责为用户提供内容服务的 Cache 设备应部署在物理上的网络边缘位置,我们称这一层为CDN边缘层。CDN 系统中负责全局性管理和控制的设备组成中心层,中心层同时保存着最多的内容副本,当边缘层设备未命中时,会向中心层请求,如果在中心层仍未命中,则需要中心层向源站回源。

不同CDN系统设计之间存在差异,中心层可能具备用户服务能力,也可能不直接提供服务,只向下级节点提供内容。如果CDN网络规模较大,边缘层设备直接向中心层请求内容或服务会造成中心层设备压力过大,就要考虑在边缘层和中心层之间部署一个区域层,负责一个区域的管理和控制,也保存部分内容副本供边缘层访问。

如图是一个典型的CDN系统三级部署示意图:

img

2. 设备组成

CDN网络中包含的功能实体主要由以下几个部分组成:

  • 内容缓存设备
  • 内容交换机
  • 内容路由器
  • CDN内容管理系统
1. 内容缓存设备

内容缓存为CDN网络节点,位于用户接入点,是面向最终用户的内容提供设备,可缓存静态Web内容和流媒体内容,实现内容的边缘传播和存储,以便用户的就近访问。

2. 内容交换机

内容交换机处于用户接入集中点,可以均衡单点多个内容缓存设备的负载,并对内容进行缓存负载平衡及访问控制。

3. 内容路由器

内容路由器负责将用户的请求调度到适当的设备上。

内容路由通常通过负载均衡系统来实现,动态均衡各个内容缓存站点的载荷分配,为用户的请求选择最佳的访问站点,同时提高网站的可用性。

内容路由器可根据多种因素制定路由,包括站点与用户的临近度、内容的可用性、网络负载、设备状况等。

负载均衡系统是整个CDN的核心。负载均衡的准确性和效率直接决定了整个CDN的效率和性能。

4. 内容管理系统

内容管理系统负责整个CDN的管理,是可选部件,作用是进行内容管理,如内容的注入和发布、内容的分发、内容的审核、内容的服务等。