前端高频面试题

262 阅读57分钟

数据结构

数据结构

树、图、链表、栈、队列、数组、列表。

数组和链表的区别

物理存储结构不同,数组是顺序存储结构,链表是链式存储结构,逻辑结构相同,都是由零个或多个数据元素构成的线性表。 内存分配方式不同,数组的存储空间一般采用静态分配,不宜动态扩展,申请的内存区域一般在栈区,链表的存储空间一般采用动态分配,长度可以动态扩展,申请的内存区域一般在堆区。 元素的存取方式不同,数组可以直接通过元素下标进行直接存取,时间复杂度为O(1),链表需要遍历,时间复杂度为O(n)

设计模式及原则

设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。

七大原则

总原则——开闭原则:一个软件实体,如类、模块和函数应该 对扩展开放,对修改关闭

单一职责原则:一个类应该只有一个发生变化的原因。

里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象。

依赖倒置原则:上层模块不应该依赖底层模块,它们都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。

接口隔离原则:客户端不应该依赖它不需要的接口;类间的依赖关系应该建立在最小的接口上。

迪米特法则(最少知道原则):一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。

合成复用原则:尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的。

设计模式的三大类

创建型模式(Creational Pattern): 对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。

(5种)工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式

结构型模式(Structural Pattern):关注于对象的组成以及对象之间的依赖关系,描述如何将类或者对象结合在一起形成更大的结构,就像搭积木,可以通过简单积木的组合形成复杂的、功能更为强大的结构。

(7种)适配器模式、装饰者模式、代理模式、外观模式、桥接模式、组合模式、享元模式

行为型模式(Behavioral Pattern):关注于对象的行为问题,是对在不同的对象之间划分责任和算法的抽象化;不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。

(11种)策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式 blog.csdn.net/weixin_5256…

前端的订阅发布模式:blog.csdn.net/m0_47553675…

eventBus: www.cnblogs.com/yalong/p/14…

git: worktile.com/kb/ask/2027…

常见数据结构排序算法

image.png

HTML

HTML5的离线存储

工作原理

在HTML文件的头部使用manifest属性引用清单文件,在清单文件中列出要缓存的文件。当Web应用程序首次加载时,浏览器会下载这些文件并将它们存储到本地缓存中。当Web应用程序第二次请求时,浏览器会检查manifest文件中的缓存清单,检查缓存清单中列出的文件是否已经被更改,如果没有更改,浏览器会读取已缓存的文件,否则,浏览器会下载最新的文件并更新缓存文件。

将数据和资源缓存到本地,当用户再次访问时,不需要从服务器端获取数据,而是直接从本地缓存中读取。这种方式可以提高Web应用程序的性能和可靠性,并减少用户的等待时间

使用方法

1: 创建并配置缓存清单 缓存清单文件是一个文本文件,它包含一个或多个CACHE、NETWORK和FALLBACK部分。文件名必须指定其相对或绝对路径。

   CACHE MANIFEST

   CACHE:
   index.html
   css/style.css
   js/main.js

   NETWORK:
   *

   FALLBACK:
   /test.html /fallback.html
  • CACHE中列出了需要缓存的文件;
  • NETWORK中表示所有其他文件要求在线;
  • FALLBACK中指定了当某个文件无法下载时应该使用哪个备用文件。

2: 将缓存清单与HTML文件相关联

   <html manifest="/demo.appcache">

3: 使用JavaScript调用应用程序缓存对象 使用 window.applicationCache对象来访问应用程序缓存

   var appCache = window.applicationCache;
   appCache.update();
   appCache.addEventListener('updateready', function(e){
     if (appCache.status == appCache.UPDATEREADY) {
       appCache.swapCache(); // We have a new updated offline cache, let's use it
     }
   }, false);

CSS

CSS选择器及其优先级

选择器格式优先级权重
id选择器#id100
类选择器.classname10
属性选择器a[ref="eee"]10
伪类选择器li:last-child10
标签选择器div1
伪元素选择器li::after1
相邻兄弟选择器h1 + p0
子选择器ul > li0
后代选择器li a0
通配符选择器*0

注意事项

  • !important声明的样式的优先级最高;
  • 如果优先级相同,则最后出现的样式生效;
  • 继承得到的样式的优先级最低
  • 样式表的来源不同时,优先级顺序为:内联样式(标签内部style) > 内部样式(head标签中的style) > 外部样式(link标签引入外部文件) > 浏览器用户自定义样式 > 浏览器默认样式

隐藏元素的方法有哪些

  • display: none; 渲染树不会包含该渲染对象,因此该元素不会在页面中占据位置,也不会响应绑定的监听事件。
  • visibility: hidden; 元素在页面中仍占据空间,但是不会相应绑定的监听事件。
  • position: absolute; 通过使用绝对定位将元素移除可视区域内,以此来实现元素的隐藏;
  • opacity: 0; 将元素的透明度设置为0。元素在页面中占据位置,并且能够响应绑定的监听事件;
  • z-index: 负值; 使其他元素遮住该元素,以此来实现隐藏;
  • clip/clip-path: 使用元素裁剪,元素仍在页面中占据位置,但是不会响应绑定的监听事件;
  • transform: scale(0, 0); 将元素缩放为0,元素仍在页面中占据位置,但是不会响应绑定的监听事件。

link和@import的区别

两者都是外部引用CSS的方式,它们的区别如下:

  • link是XHTML标签,除了加载CSS外,还可以定义RSS等其他事务;@import属于CSS范畴,只能加载CSS。
  • link引用CSS时,在页面载入时同时加载;@import需要页面网页完全载入以后加载。
  • link是XHTML标签,无兼容问题;@import是在CSS2.1提出的,低版本的浏览器不支持。
  • link支持使用Javascript控制DOM去改变样式;而@import不支持。

伪元素和伪类的区别和作用

  • 伪元素:在内容元素的前后插入额外的元素或样式,但是这些元素实际上并不在文档中生成。它们只在外部显示可见,但不会在文档的源代码中找到他们。
  • 伪类:将特殊的效果添加到特定选择器上。它是已有元素上添加类别的,不会产生新的元素。

总结:伪类是通过在元素选择器上加⼊伪类改变元素状态,⽽伪元素通过对元素的操作进⾏对元素的改变。

盒模型

  • 标准盒模型的width和height属性的范围只包含了content;
  • IE盒模型的width和height属性的范围包含了border、padding和content。
box-sizing: content-box; // 标准盒模型 默认值
box-sizing: border-box;

单行、多行文本溢出隐藏

  • 单行文本溢出
overflow: hidden;            // 溢出隐藏
text-overflow: ellipsis;      // 溢出用省略号显示
white-space: nowrap;         // 规定段落中的文本不进行换行
  • 多行文本溢出
overflow: hidden;            // 溢出隐藏
text-overflow: ellipsis;     // 溢出用省略号显示
display:-webkit-box;         // 作为弹性伸缩盒子模型显示。
-webkit-box-orient:vertical; // 设置伸缩盒子的子元素排列方式:从上到下垂直排列
-webkit-line-clamp:3;        // 显示的行数

px、em、rem 的区别及使用场景

区别:

  • px是固定的像素,但设置了就无法因为适应页面大小而改变。
  • em 和 rem 相对于 px 更具有灵活性,它们是相对长度单位,其长度不是固定的,更适用于响应式布局。
  • em 是相对于其父元素来设置字体大小,这样就会存在一个问题,进行任何元素设置,都有可能需要知道它父元素的大小。而 rem 是相对于根元素,这样就意味着,只需要在根元素确定一个参考值。

使用场景:

  • 对于只需要适配少部分移动设备,且分辨率对页面影响不大的,使用px即可。
  • 对于需要适配各种移动设备,使用 rem,例如需要适配iPhone和iPad等分辨率差别较大的设备。

两栏布局

  • 浮动布局,左侧固定宽高,向左浮动

    • 右侧设置margin-left,宽度auto;
    • 右侧设置overflow: hidden; 触发BFC,BFC的区域不会与浮动元素发生重叠。
  • flex布局,左边元素设置固定宽高,右边设置flex: 1;

  • 绝对定位,父级元素设置为相对定位:

    • 左边元素设置为absolute定位,宽度200px,右边margin-left: 100px;
    • 左边宽度为100px,右边设置为absolute,left: 200px,其余方向为0。

三栏布局

  • 绝对定位,左右绝对定位,中间设置margin值
  • flex布局,左右固定大小,中间flex: 1;
  • 浮动,左右固定大小,对应方向浮动,中间设置Margin值,注意中间一栏必须放到最后。
  • 圣杯布局: 利用浮动和负边距来实现。父级元素设置左右的 padding,三列均设置向左浮动,中间一列放在最前面,宽度设置为父级元素的宽度,因此后面两列都被挤到了下一行,通过设置 margin 负值将其移动到上一行,再利用相对定位,定位到两边。(margin-left: -100%; 意思就是向左移动整个屏幕的距离)
  • 双飞翼布局: 双飞翼布局相对于圣杯布局来说,左右位置的保留是通过中间列的 margin 值来实现的,而不是通过父元素的 padding 来实现的。本质上来说,也是通过浮动和外边距负值来实现的。

水平垂直居中

  • 利用绝对定位,先将元素的左上角通过top:50%和left:50%定位到页面的中心,然后再通过translate来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。
  • 利用绝对定位,设置四个方向的值都为0,并将margin设置为auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该方法适用于盒子有宽高的情况。
  • 利用绝对定位,先将元素的左上角通过top:50%和left:50%定位到页面的中心,然后再通过margin负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况.
  • 使用flex布局,通过align-items:center和justify-content:center设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。该方法要考虑兼容的问题,该方法在移动端用的较多。

Flex布局

容器

  • flex-direction属性决定主轴的方向(即项目的排列方向)。
  • flex-wrap属性定义,如果一条轴线排不下,如何换行。
  • flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。
  • justify-content属性定义了项目在主轴上的对齐方式。
  • align-items属性定义项目在交叉轴上如何对齐。
  • align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

项目

  • order属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
  • flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
  • flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
  • flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
  • flex属性是flex-grow,flex-shrink和flex-basis的简写,默认值为0 1 auto。
  • align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。

清除浮动

在网页设计中,当一个元素设置了浮动属性后,其周围的其他元素可能会受到影响而产生错位或者无法正常显示。为了解决这个问题,可以通过清除浮动的方式来恢复布局的正常显示。

  1. 最后一个浮动元素之后添加一个空的div标签:

    <div style="clear:both;"></div>
    
  2. 使用伪元素:

    .clearfix::after {
       content: "";
       display: table;
       clear: both;
       }
    
  3. 使用父元素的overflow属性:

    .parent-element {
       overflow: auto;
       }
    

实现一个三角形

border属性是由三角形组成的

div {
   width: 0;
   height: 0;
   border: 100px solid;
   border-color: orange blue red green;
}

三角形

div {
   width: 0;
   height: 0;
   border-top: 50px solid red;
   border-right: 50px solid transparent;
   border-left: 50px solid transparent;
}

JavaScript

JavaScript的数据类型

八种数据类型:Undefined、Null、Boolean、String、Number、Symbol、Object、BigInt。

栈:原始数据类型(Undefined、Null、Boolean、String、Number)

堆:引用数据类型(对象、数组和函数)

两种类型的区别在于存储位置的不同:

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;

  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:

  • 在数据结构中,栈中数据的存取方式为先进后出。
  • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。

在操作系统中,内存被分为栈区和堆区:

  • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

数据类型检测方式

  1. typeof

    console.log(typeof 2);               // number
    console.log(typeof true);            // boolean
    console.log(typeof 'str');           // string   
    console.log(typeof undefined);       // undefined
    console.log(typeof function(){});    // function
    console.log(typeof {});              // object
    console.log(typeof []);              // object 
    console.log(typeof null);            // object
    

    数组、对象、null都会被判断为object,其他判断都正确。

  2. instanceof 只能正确判断引用数据类型,可以用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性

    console.log(2 instanceof Number);                    // false
    console.log(true instanceof Boolean);                // false 
    console.log('str' instanceof String);                // false 
    
    console.log([] instanceof Array);                    // true
    console.log(function(){} instanceof Function);       // true
    console.log({} instanceof Object);                   // true
    
  3. constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
  1. Object.prototype.toString.call()
var a = Object.prototype.toString;
 
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

Object.is()与比较操作符“===”、“==”的区别

  • ==:如果两边的类型不一致,则会进行强制类型转换后再进行比较
  • ===:如果两边的类型不一致,不会进行强制类型转换,直接返回false
  • Object.is():一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的

ES6

let、const、var的区别

var不存在块级作用域,存在变量提升,生命的变量为全局对象,并且可以重复声明,不存在暂时性死区。 const必须设置初始值,不允许改变指针指向。

箭头函数与普通函数的区别

  1. 箭头函数更加简洁。 如果函数体的返回值只有一句,可以省略大括号;如果函数体不需要返回值且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数:
    let fn = () => void doesNotReturn();
    
  2. 箭头函数没有自己的this,它只会在自己作用域的上一层继承this。
  3. 箭头函数继承来的this指向永远不会变。
  4. call()、apply()、bind()等方法不能改变箭头函数中this的指向。
  5. 箭头函数不能作为构造函数使用。
  6. 箭头函数没有自己的arguments。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
  7. 箭头函数没有prototype。
  8. 箭头函数不能做Generator函数,不能使用yeild关键字。

对象与数组的解构

  1. 数组的解构 在解构数组时,以元素的位置为匹配条件来提取想要的数据:

    const [a, b, c] = [1, 2, 3];
    const [a, , c] = [1, 2, 3];
    
  2. 对象的解构 在解构对象时,是以属性的名称为匹配条件来提取想要的数据:

    const stu = {
      name: 'Bob',
      age: 24
    };
    const { name, age } = stu;
    // 可以调换位置
    const { age, name } = stu;
    

提取高度嵌套的对象里的指定属性

const school = {
   classes: {
      stu: {
         name: 'Bob',
         age: 24,
      }
   }
};
const { classes } = school;
const { stu } = classes;
const { name } = stu;

const { classes: { stu: { name } }} = school;
   
console.log(name)  // 'Bob';

可以在解构出来的变量名右侧,通过冒号+{目标属性名}这种形式,进一步解构它,一直解构到拿到目标数据为止。

扩展运算符的作用

  1. 对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。

    let bar = { a: 1, b: 2 };
    let baz = { ...bar }; // { a: 1, b: 2 }
    let baz = Object.assign({}, bar); // { a: 1, b: 2 }
    
  2. 数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

    console.log(...[1, 2, 3])
    // 1 2 3
    console.log(...[1, [2, 3, 4], 5])
    // 1 [2, 3, 4] 5
    
    // 合并数组
    const arr1 = ['two', 'three'];
    const arr2 = ['one', ...arr1, 'four', 'five'];// ["one", "two", "three", "four", "five"]
    
    const [...rest, last] = [1, 2, 3, 4, 5];  // 报错
    const [first, ...rest, last] = [1, 2, 3, 4, 5];  // 报错
    
    // 将字符串转为真正的数组
    [...'hello']    // [ "h", "e", "l", "l", "o" ]
    
    const numbers = [9, 4, 7, 1];
    Math.min(...numbers); // 1
    Math.max(...numbers); // 9
    
  3. 把一个分离的参数序列整合成一个数组 :

    function mutiple(...args) {
      let result = 1;
      for (var val of args) {
        result *= val;
      }
      return result;
    }
    mutiple(1, 2, 3, 4) // 24
    
    

继承

原型链继承

构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。

继承的本质就是复制,即重写原型对象,代之以一个新类型的实例

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
}

function SubType() {
    this.subproperty = false;
}

// 这里是关键,创建SuperType的实例,并将该实例赋值给SubType.prototype
SubType.prototype = new SuperType(); 

SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

var instance = new SubType();
console.log(instance.getSuperValue()); // true

缺点:多个实例对引用类型的操作会被篡改。

function SuperType(){
  this.colors = ["red", "blue", "green"];
}
function SubType(){}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black"

构造函数继承

使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)

function  SuperType(){
    this.color=["red","green","blue"];
}
function  SubType(){
    //继承自SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,green,blue,black"

var instance2 = new SubType();
alert(instance2.color);//"red,green,blue"

核心代码是SuperType.call(this),创建子类实例时调用SuperType构造函数,于是SubType的每个实例都会将SuperType中的属性复制一份。

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

组合继承

用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

function SubType(name, age){
  // 继承属性
  // 第二次调用SuperType()
  SuperType.call(this, name);
  this.age = age;
}

// 继承方法
// 构建原型链
// 第一次调用SuperType()
SubType.prototype = new SuperType(); 
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

缺点:

  • 第一次调用SuperType():给SubType.prototype写入两个属性name,color。
  • 第二次调用SuperType():给instance1写入两个属性name,color。

实例对象instance1上的两个属性就屏蔽了其原型对象SubType.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

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

object()对传入其中的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"

缺点:

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

另外,ES5中存在Object.create()的方法,能够代替上面的object方法。

寄生式继承

核心:在原型式继承的基础上,增强对象,返回构造函数。

function createAnother(original){
  var clone = object(original); // 通过调用 object() 函数创建一个新对象
  clone.sayHi = function(){  // 以某种方式来增强对象
    alert("hi");
  };
  return clone; // 返回这个对象
}

函数的主要作用是为构造函数新增属性和方法,以增强函数

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

缺点(同原型式继承):

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

寄生组合式继承

结合构造函数传递参数和寄生模式实现继承

function inheritPrototype(subType, superType){
  var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType;                    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  subType.prototype = prototype;                      // 指定对象,将新创建的对象赋值给子类的原型
}

// 父类初始化实例属性和原型属性
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
  SuperType.call(this, name);
  this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function(){
  alert(this.age);
}

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance2.colors.push("3"); // ["red", "blue", "green", "3"]

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()

这是最成熟的方法,也是现在库实现的方法

混入方式继承多个对象

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do something
};

Object.assign会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。

ES6类继承extends

extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显式指定构造方法,则会添加默认的 constructor方法,使用例子如下。

class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    
    // Getter
    get area() {
        return this.calcArea()
    }
    
    // Method
    calcArea() {
        return this.height * this.width;
    }
}

const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200

-----------------------------------------------------------------
// 继承
class Square extends Rectangle {

  constructor(length) {
    super(length, length);
    
    // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    this.name = 'Square';
  }

  get area() {
    return this.height * this.width;
  }
}

const square = new Square(10);
console.log(square.area);
// 输出 100

extends继承的核心代码如下,其实现和上述的寄生组合式继承方式一样

function _inherits(subType, superType) {
  
    // 创建对象,创建父类原型的一个副本
    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
    // 指定对象,将新创建的对象赋值给子类的原型
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: {
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}

总结

1、函数声明和类声明的区别

函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个ReferenceError。

let p = new Rectangle(); 
// ReferenceError

class Rectangle {}

2、ES5继承和ES6继承的区别

  • ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this)).
  • ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。

this/call/apply/bind

对this对象的理解

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

  • 函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
  • 方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
  • 构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  • apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

call()、apply()、bind()区别

function getDate(month, day) {
 console.log(this.year + '-' + month + '-' + day);
}

// 传参不一样
let obj = {year: 2022};
getDate.call(obj, 3, 8);     //2022-3-8
getDate.apply(obj, [6, 8]);  //2022-6-8
getDate.bind(obj)(3, 8);     //2022-3-8

const people = { name: 'cc' };
function myAge(age) {
 console.log(`${ this.name } is ${ age }`);
}
myAge.call(people, 21); // cc is 21
myAge.bind(people, 22);
myAge.bind(people)(22); // cc is 22

对于一些需要写循环以遍历数组各项的需求,我们可以用apply完成以避免循环。

let num = [1,3,5,7,8];
console.log(Math.max.apply(null, num)); // 8

ajax、axios、fetch的区别

  1. Ajax(异步JavaScript和XML)是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax可以使网页实现异步更新。

    缺点:

    • 本身是基于MVC编程,不符合MVVM的浪潮
    • 基于原生XHR开发,XHR本身的架构不清晰
    • 不符合关注分离原则
    • 配置和调用方式非常混乱,而且基于事件的异步模型不友好
  2. fetch是基于promise设计的。Fetch的代码结构比起Ajax简单多。fetch不是Ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。 优点:

    • 语法简洁,更加语义化
    • 基于标准Promise实现,支持async/await
    • 更加底层,提供的API丰富(request, response)
    • 脱离了XHR,是ES规范里新的实现方式

    缺点:

    • 只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
    • fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
    • fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
    • fetch没有办法原生监测请求的进度,而XHR可以
  3. Axios是一种基于Promise封装的HTTP客户端,特点:

    • 浏览器端发起XMLHttpRequest请求
    • node端发起http请求
    • 支持Promise API
    • 监听请求和返回
    • 对请求和返回进行转化
    • 取消请求
    • 自动转换json数据
    • 客户端支持抵御XSRF攻击

Promise

Promise的实例有三个状态:Pedding、Resolved、Rejected。当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、失败了就变成了Rejected。

Promise的特点:

  • 对象的状态不受外界影响。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“承诺”;
  • 一旦状态改变就不会再变,任何时候都可以得到这个结果。与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。

Promise的缺点:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

Promise的基本用法

一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolvepromise.reject这两个方法。

const promise = new Promise(function(resolve, reject) { 
    // ... some code 
    if (/* 异步操作成功 */){ 
        resolve(value); 
    } else { 
        reject(error); 
    } 
});

Promise.resolve(11).then(function(value){ 
    console.log(value); 
    // 打印出11 
});
then()

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。 then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

catch()

该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。

all()

all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected

race()

race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected

当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
finally()

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

Promise.then的第二个参数和catch的区别

  1. reject是用来抛出异常的,catch是用来处理异常的;
  2. reject是Promise的方法,而then和catch是Promise实例的方法。

catch

  • then方法返回的是一个全新的Promise,后面的catch方法其实是给前一个的then方法的Promise指定失败回调,并不是直接给第一个Promise指定失败回调,只不过这是Promise链条,前面的异常会一直往后传递,所以catch能够捕获第一个Promise的异常。

then方法的第二个参数

  • 只能捕获第一个Promise的异常,它只是给第一个Promise注册的失败回调

推荐使用catch方法来进行异常的捕获

对async/await的理解

async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。 async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:

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

await 表达式的运算结果取决于它等的是什么。

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

async/await对比Promise的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担

  • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅

  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余

  • 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

浏览器原理

浏览器安全

XSS攻击

XSS攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如cookie等。 XSS的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。 攻击者可以通过这种攻击方式进行以下操作:

  • 获取页面的数据,如DOM、cookie、localStorage;
  • DOS攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器;
  • 破坏页面结构;
  • 流量劫持(将链接指向某网站);

XSS可分为存储型、反射型和DOM型:

  • 存储型指的是恶意脚本会存储在目标服务器上,当浏览器请求数据时,脚本从服务器传回并执行。
  • 反射型指的是攻击者诱导用户访问一个带有恶意代码的URL后,服务器端接收数据后处理,然后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有XSS代码的数据后当作脚本执行,最终完成XSS攻击。
  • DOM型指的是通过修改页面的DOM节点形成的XSS。

如何防御XSS攻击

  • 对用户输入进行过滤和校验,对特殊字符进行转义或剔除。例如,禁止在输入框中输入HTML标签(使用编程语言提供的内置函数或正则表达式等方式)等内容。
  • 采用CSP(内容安全策略)设置限制页面中脚本的来源,只允许指定的信任来源执行脚本。
    • 它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。
    • 通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式。
  • 在页面中添加验证码,并对必要的操作进行二次确认。这可以有效防止机器人或恶意用户利用漏洞进行攻击。
  • 定期进行安全扫描和修复漏洞,以确保网站的安全性。

CSRF攻击

CSRF攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登陆状态,那么攻击者就可以利用这个登陆状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。CSRF攻击的本质是利用cookie会在同源请求中携带数据发送给服务器的特点来实现用户的冒充。

常见的CSRF攻击有三种:

  • GET类型的CSRF攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。
  • POST类型的CSRF攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。
  • 链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。

如何防御CSRF攻击

  • 进行同源检测,服务器根据http请求头中origin或者refer信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当 origin 或者 referer 信息都不存在的时候,直接阻止请求。这种方式的缺点是有些情况下 referer 可以被伪造,同时还会把搜索引擎的链接也给屏蔽了。所以一般网站会允许搜索引擎的页面请求,但是相应的页面请求这种请求方式也可能被攻击者给利用。(Referer 字段会告诉服务器该网页是从哪个页面链接过来的)
  • 使用CSRF token进行验证,服务器向用户返回一个随机数Token,当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,然后服务器对这个 token 进行验证。这种方法解决了使用 cookie 单一验证方式时,可能会被冒用的问题,
    • 需要给网站中的所有请求都添加上这个 token,操作比较繁琐。
    • 一般不会只有一台网站服务器,如果请求经过负载平衡转移到了其他的服务器,但是这个服务器的 session 中没有保留这个 token 的话,就没有办法验证了。这种情况可以通过改变 token 的构建方式来解决。
  • 对cookie进行双重验证,服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较,来进行验证。
    • 利用了攻击者只能利用 cookie,但是不能访问获取 cookie 的特点。这种方法比 CSRF Token 的方法更加方便,并且不涉及到分布式访问的问题。
    • 缺点是如果网站存在 XSS 漏洞的,那么这种方式会失效。同时这种方式不能做到子域名的隔离。
  • 设置cookie属性的时候设置Samesite,限制cookie不能作为被第三方使用。Samesite 一共有两种模式,一种是严格模式,在严格模式下 cookie 在任何情况下都不可能作为第三方 Cookie 使用,在宽松模式下,cookie 可以被请求是 GET 请求,且会发生页面跳转的请求所使用。

中间人攻击

中间⼈ (Man-in-the-middle attack, MITM) 是指攻击者与通讯的两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对⽅直接对话, 但事实上整个会话都被攻击者完全控制。在中间⼈攻击中,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。

攻击过程如下:

  • 客户端发送请求到服务端,请求被中间⼈截获
  • 服务器向客户端发送公钥
  • 中间⼈截获公钥,保留在⾃⼰⼿上。然后⾃⼰⽣成⼀个伪造的公钥,发给客户端
  • 客户端收到伪造的公钥后,⽣成加密hash值发给服务器
  • 中间⼈获得加密hash值,⽤⾃⼰的私钥解密获得真秘钥,同时⽣成假的加密hash值,发给服务器
  • 服务器⽤私钥解密获得假密钥,然后加密数据传输给客户端

跨域

同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议端口号域名必须一致。

跨域:在用Ajax、Fetch进行请求的时候存在访问问题,在script标签中引用别的源不会出现问题。

  1. CORS 后端设置响应头 Access-Control-Allow-Origin: * 优点:操作简单,支持所有http请求类型(get、post、put等) 缺点:IE8以下的浏览器不支持。
  2. jsonp 利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。 缺点:具有局限性,仅支持get方法;不安全,可能会遭受XSS攻击。
  3. 代理转发nginx 同源策略是浏览器需要遵循的标准,如果是服务器向服务器请求就无需遵循同源策略。 把实际请求转到了本地运行的服务,在转发到后端地址,从而跳过了浏览器及其同源策略,也就不会跨域。
  4. postMessage HTML5中新增的跨文档消息传递机制,是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题 a) 页面和其打开的新窗口的数据传递 b) 多窗口之间的消息传递 c) 页面与嵌套的iframe消息传递 d) 上面三个场景的跨域数据传递

计算机网络

HTTP1.0和HTTP1.1之间有哪些区别

  • 连接方面,http1.0默认使用非持久连接,而http1.1默认使用持久连接。http1.1通过使用持久连接来使多个http请求复用同一个TCP连接,以此来避免使用非持久连接时每次需要建立连接的时延。
  • 资源请求方面,http1.0中存在浪费带宽的现象,并且不支持断点续传功能,http1.1在请求头引入了range头域,允许只请求资源的某个部分,即返回码是206。

登陆实现

Cookie+Session

  1. 用户访问 a.com/pageA,并输入密码登录。
  2. 服务器验证密码无误后,会创建 SessionId,并将它保存起来。
  3. 服务器端响应这个 HTTP 请求,并通过 Set-Cookie 头信息,将 SessionId 写入 Cookie 中。

服务器端的 SessionId 可能存放在很多地方,例如:内存、文件、数据库等。

第一次登录完成之后,后续的访问就可以直接使用 Cookie 进行身份验证了:

  • 用户访问 a.com/pageB 页面时,会自动带上第一次登录时写入的 Cookie

  • 服务器端比对 Cookie 中的 SessionId 和保存在服务器端的 SessionId 是否一致。

  • 如果一致,则身份验证成功,访问页面;如果无效,则需要用户重新登录。

存在的问题:

  • 由于服务器端需要对接大量的客户端,也就需要存放大量的 SessionId,这样会导致服务器压力过大。

  • 如果服务器端是一个集群,为了同步登录态,需要将 SessionId 同步到每一台机器上,无形中增加了服务器端维护成本。

  • 由于 SessionId 存放在 Cookie 中,所以无法避免 CSRF 攻击。

Token登录

Token 是通过服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个 Token 并返回给客户端,客户端后续访问时,只需带上这个 Token 即可完成身份认证。

Token生成方式:

最常见的 Token 生成方式是使用 JWTJson Web Token),它是一种简洁的、自包含的方法,用于通信双方之间以 JSON 对象的形式安全的传递信息。

使用 Token 后,服务器端并不会存储 Token,那怎么判断客户端发过来的 Token 是合法有效的呢?

答案其实就在 Token 字符串中,其实 Token 并不是一串杂乱无章的字符串,而是通过多种算法拼接组合而成的字符串。

JWT 算法主要分为 3 个部分:header(头信息),playload(消息体),signature(签名)。

  • header 部分指定了该 JWT 使用的签名算法;
  • playload 部分表明了 JWT 的意图;
  • signature 部分为 JWT 的签名,主要为了让 JWT 不能被随意篡改。

优缺点:

  • 服务器端不需要存放 Token,所以不会对服务器端造成压力,即使是服务器集群,也不需要增加维护成本。

  • Token 可以存放在前端任何地方,可以不用保存在 Cookie 中,提升了页面的安全性。

  • Token 下发之后,只要在生效时间之内,就一直有效,但是如果服务器端想收回此 Token 的权限,并不容易。

SSO单点登录

单点登录是指在同一帐号平台下的多个应用系统中,用户只需登录一次,即可访问所有相互信任的应用系统。本质就是在多个应用系统中共享登录状态。

首次访问:

  • 用户访问网站 a.com 下的 pageA 页面。

  • 由于没有登录,则会重定向到认证中心,并带上回调地址 www.sso.com?return_uri=a.com/pageA,以便登录后直接进入对应页面。

  • 用户在认证中心输入账号密码,提交登录。

  • 认证中心验证账号密码有效,然后重定向 a.com?ticket=123 带上授权码 ticket,并将认证中心 sso.com 的登录态写入 Cookie

  • a.com 服务器中,拿着 ticket 向认证中心确认,授权码 ticket 真实有效。

  • 验证成功后,服务器将登录信息写入 Cookie(此时客户端有 2 个 Cookie 分别存有 a.comsso.com 的登录态)。

实现方式

一般情况下,用户的登录状态是记录在 Session 中的,要实现共享登录状态,就要先共享 Session,但是由于不同的应用系统有着不同的域名,尽管 Session 共享了,但是由于 SessionId 是往往保存在浏览器 Cookie 中的,因此存在作用域的限制,无法跨域名传递,也就是说当用户在 a.com 中登录后,Session Id 仅在浏览器访问 a.com 时才会自动在请求头中携带,而当浏览器访问 b.com 时,Session Id 是不会被带过去的。实现单点登录的关键在于,如何让 Session Id(或 Token)在多个域中共享。

1. 父域Cookie

如果将 Cookiedomain 属性设置为当前域的父域,那么就认为它是父域 CookieCookie 有一个特点,即父域中的 Cookie 被子域所共享,也就是说,子域会自动继承父域中的 Cookie

利用 Cookie 的这个特点,可以将 Session Id(或 Token)保存到父域中就可以了。我们只需要将 Cookiedomain 属性设置为父域的域名(主域名),同时将 Cookiepath 属性设置为根路径,这样所有的子域应用就都可以访问到这个 Cookie 了。不过这要求应用系统的域名需建立在一个共同的主域名之下,如 tieba.baidu.com 和 map.baidu.com,它们都建立在 baidu.com 这个主域名之下,那么它们就可以通过这种方式来实现单点登录。

总结:此种实现方式比较简单,但不支持跨主域名。

2. 认证中心

用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 Token 写入 Cookie。(注意这个 Cookie 是认证中心的,应用系统是访问不到的)

应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心进行登录。由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了。如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统。

应用系统拿到 Token 之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(这个 Cookie 是当前应用系统的,其他应用系统是访问不到的)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了。

总结:此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法。

3. LocalStorage跨域

单点登录的关键在于,如何让 Session Id(或 Token)在多个域中共享。但是 Cookie 是不支持跨主域名的,而且浏览器对 Cookie 的跨域限制越来越严格。

在前后端分离的情况下,完全可以不使用 Cookie,我们可以选择将 Session Id (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将 LocalStorage 的数据传递给服务端。这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session Id (或 Token )放在响应体中传递给前端。

在这样的场景下,单点登录完全可以在前端实现。前端拿到 Session Id (或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中。

总结:此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域。

SSO单点登出

目前我们已经完成了单点登录,在同一套认证中心的管理下,多个产品可以共享登录态。现在我们需要考虑退出了,即:在一个产品中退出了登录,怎么让其他的产品也都退出登录?

原理其实不难,可以在每一个产品在向认证中心验证 ticket(token) 时,其实可以顺带将自己的退出登录 api 发送到认证中心。

当某个产品 c.com 退出登录时:

  1. 清空 c.com 中的登录态 Cookie
  2. 请求认证中心 sso.com 中的退出 api
  3. 认证中心遍历下发过 ticket(token) 的所有产品,并调用对应的退出 api,完成退出。

OAuth第三方登录

这里以微信开放平台的接入流程为例:

  • 首先,a.com 的运营者需要在微信开放平台注册账号,并向微信申请使用微信登录功能。

  • 申请成功后,得到申请的 appidappsecret

  • 用户在 a.com 上选择使用微信登录。

  • 这时会跳转微信的 OAuth 授权登录,并带上 a.com 的回调地址。

  • 用户输入微信账号和密码,登录成功后,需要选择具体的授权范围,如:授权用户的头像、昵称等。

  • 授权之后,微信会根据拉起 a.com?code=123 ,这时带上了一个临时票据 code

  • 获取 code 之后, a.com 会拿着 codeappidappsecret,向微信服务器申请 token,验证成功后,微信会下发一个 token

  • 有了 token 之后, a.com 就可以凭借 token 拿到对应的微信用户头像,用户昵称等信息了。

  • a.com 提示用户登录成功,并将登录状态写入 Cookie,以作为后续访问的凭证。

Vue

Vue基础

Vue基本原理

当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。

双向数据绑定的原理

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:

  1. 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
  2. compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
  3. Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
  4. MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

MVC、MVVM、MVP

  1. MVC MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。
  • Model:存储页面的业务数据,以及对相应数据的操作;
  • View:页面的显示逻辑;
    • Controller:负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
  1. MVVM MVVM 分为 Model、View、ViewModel:

    • Model:代表数据模型,数据和业务逻辑都在Model层中定义;
    • View:UI视图,负责数据的展示;
    • ViewModel:负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

    Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。

    这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。

  2. MVP MVC:Model和View之间使用观察者模式, View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。 MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。 MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新; MVP 模式中,View 层的接口暴露给了 Presenter,因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。

Vue2和Vue3的响应式实现原理

Vue2

Vue2 是通过 Object.defineProperty 将对象的属性转换成 getter/setter 的形式来进行监听它们的变化,当读取属性值的时候会触发 getter 进行依赖收集,当设置对象属性值的时候会触发 setter 进行向相关依赖发送通知,从而进行相关操作。

由于 Object.defineProperty 只对属性 key 进行监听,无法对引用对象进行监听,所以在 Vue2 中创建一个了 Observer 类对整个对象的依赖进行管理,当对响应式对象进行新增或者删除则由响应式对象中的 dep 通知相关依赖进行更新操作。

Object.defineProperty 也可以实现对数组的监听的,但因为性能的原因(首先这种直接通过下标获取数组元素的场景就比较少,其次即便通过了 Object.defineProperty 对数组进行监听,但也监听不了 push、pop、shift 等对数组进行操作的方法,所以还是需要通过对数组原型上的那 7 个方法进行重写监听。) Vue2 放弃了这种方案,改由重写数组原型对象上的 7 个能操作数组内容的变更的方法,从而实现对数组的响应式监听。

原理就是使用拦截器覆盖 Array.prototype,之后再去使用 Array 原型上的方法的时候,其实使用的是拦截器提供的方法,在拦截器里面才真正使用原生 Array 原型上的方法去操作数组。

Vue2 的响应式存在很多的问题,例如:

  • 初始化时需要遍历对象所有 key,如果对象层次较深,性能不好
  • 通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多
  • 无法监听到数组元素的变化,只能通过劫持重写了几个数组方法
  • 动态新增,删除对象属性无法拦截,只能用特定 set/delete API 代替
  • 不支持 Map、Set 等数据结构

Vue3

Vue3 则是通过 Proxy 对数据实现 getter/setter 代理,从而实现响应式数据,然后在副作用函数中读取响应式数据的时候,就会触发 Proxy 的 getter,在 getter 里面把对当前的副作用函数保存起来,将来对应响应式数据发生更改的话,则把之前保存起来的副作用函数取出来执行。

Vue3 对数组实现代理时,用于代理普通对象的大部分代码可以继续使用,但由于对数组的操作与对普通对象的操作存在很多的不同,那么也需要对这些不同的操作实现正确的响应式联系或触发响应。这就需要对数组原型上的一些方法进行重写。

比如通过索引为数组设置新的元素,可能会隐式地修改数组的 length 属性的值。同时如果修改数组的 length 属性的值,也可能会间接影响数组中的已有元素。另外用户通过 includes、indexOf 以及 lastIndexOf 等对数组元素进行查找时,可能是使用代理对象进行查找,也有可能使用原始值进行查找,所以我们就需要重写这些数组的查找方法,从而实现用户的需求。原理很简单,当用户使用这些方法查找元素时,先去响应式对象中查找,如果没找到,则再去原始值中查找。

另外如果使用 push、pop、shift、unshift、splice 这些方法操作响应式数组对象时会间接读取和设置数组的 length 属性,所以我们也需要对这些数组的原型方法进行重新,让当使用这些方法间接读取 length 属性时禁止进行依赖追踪,这样就可以断开 length 属性与副作用函数之间的响应式联系了。

Vue3.0

Vue3.0有什么更新

  1. 监测机制的改变
  • 3.0 将带来基于代理 Proxy的 observer 实现,提供全语言覆盖的反应性跟踪。
  • 消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
  1. 只能检测属性,不能监测对象
  • 检测属性的添加和删除;
  • 检测数组索引和长度的变更;
  • 支持 Map、Set、WeakMap 和 WeakSet。
  1. 模板
  • 作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。

  • 同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。

  1. 对象式的组件声明方式
  • vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。

  • 3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易

  1. 其他方面的更改
  • 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。

  • 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。

  • 基于 tree shaking 优化,提供了更多的内置功能。

Virtual DOM

Virtual DOM

从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。

Virual DOM的解析过程

虚拟DOM的解析过程:

  • 首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。

  • 当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。

  • 最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。

性能优化

性能指标

未命名文件.png

CDN

CDN的核心点:缓存和回源。

缓存:将从根服务器请求来的资源按要求缓存;

回源:当有用户访问某个资源的时候,如果被解析到的那个CDN节点没有缓存响应的内容,或者是缓存已经到期,就会回源站去获取。没有人访问,CDN节点不会主动去源站请求资源。

CDN是静态资源提速的重要手段。

CDN的优势

  1. CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;
  2. 大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源站的负载;
  3. 降低“广播风暴”的影响,提高网络访问的稳定性,节省骨干网带宽,减少带宽需求量。

懒加载

懒加载指的是在长网页中延迟图片的加载,是一种较好的网页性能优化方式。在滚动屏幕之前,可视区域之外的图片不会进行加载,滚动的时候才会加载,使网页的加载速度更快,减少了服务器的负载。 特点:减少无用资源的加载、提升用户体验、防止加载过多的图片而影响其他资源文件的加载。

回流与重绘

节流与防抖

浏览器原理

浏览器安全

XSS攻击

XSS攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行