# HTML

# 视口

视口 (viewport) 是用来显示网页的那一块区域, 不局限于浏览器可视区域的大小. 移动设备的分辨率一般较小, 为了显示为桌面端设计的页面, 往往浏览器会将自己的默认 viewport 设定的比浏览器可视区域大. 这个也被称为 "layout viewport". 用来代表浏览器可视区域大小的叫 "visual viewport". 不同的手机会有不同的分辨率, 为了让页面尺寸在各种分辨率下显示都差不多. 还有一个 "ideal viewport" 来代表移动设备的理想宽度.

<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>

width 设为 width-device 则把当前的 viewport 宽度设置为 "ideal viewport" 的宽度

  • initial-scale:初始缩放比例,也即是当页面第一次 load 的时候缩放比例。
  • visual viewport 宽度通过 window.innerWidth 获取
  • layout viewport 宽度通过 document.documentElement.clientWidth 获取

# CSS

# border-radius

border-radius 允许你设置元素的外边框圆角, 当使用一个半径时确定一个圆形,当使用两个半径时确定一个椭圆。这个(椭)圆与边框的交集形成圆角效果。

# JS 基础

# 基础概念

# 编译期 & 执行期

JavaScript 的编译过程分为两个阶段:编译期 & 执行期

在 "编译期" 阶段, 由解释器完成, 它主要分为下面几个步骤:

  • 词法分析: 将由代码分解成(对编程语言来说)有意义的代码块,这些代码块被称为 "词法单元". 例如,var a = 2; 这段程序通常会被分解成为下面这些词法单元: var, a, =, 2, ;.
  • 语法分析: 将 "词法单元" 转换为一代表了程序语法结构的树结构, 被称为 "抽象语法树" .
  • 生成可执行代码: 将抽象语法树转换成机器可以执行的代码.

在 "执行期" 阶段, 由 JavaScript 引擎完成, 主要分成以下步骤:

  • 创建执行上下文: 执行上下文用以描述代码执行时所处的环境;
  • 执行代码: 执行上下文创建完之后, 处于内部的代码会被引擎逐句执行;

# 作用域

作用域可以理解为一套规则, 它定义了变量和函数的可被访问范围,控制着变量和函数的可见性与生命周期

作用域可分为静态作用域, 或者动态作用域. JavaScript 采用词法作用域 (lexical scoping), 也就是静态作用域

  • 静态 (词法) 作用域: 静态作用域在代码的 "词法分析" 阶段就确定了. 变量的可访问范围取决于源代码, 与程序的执行流程没关系. 作用域的确定不需要程序运行, 只通过静态分析就可以.
  • 动态作用域: 动态作用域是根据程序的运行动态确定的. 动态作用域并不关心变量和函数是如何声明以及在何处声明的, 它只关心他们是在何处被调用的.

# 执行上下文

执行上下文 (execution context), 是一个抽象概念, 用于描述代码执行时所处的作用域环境. 它定义代码语句对变量或函数的访问权.

在代码的 "执行期", JavaScript 引擎会创建执行上下文. 在 JavaScript 中, 它表现为一个内部对象. 每当 Javascript 代码在运行的时候,它都是在执行上下文中运行.

JavaScript 中有三种执行上下文:

  • 全局执行上下文: 默认的代码运行环境,一旦代码被载入执行,引擎最先创建的就是这个环境. 不写在函数内的代码, 被执行时处于全局执行上下文.
  • 函数执行上下文: 写在函数内的代码运行时, 处于函数执行上下文.
  • eval 执行上下文: 作为 eval 函数参数的代码, 运行时处于 eval 执行上下文. 这里略过不讲.

在函数被调用之前, 函数的执行上下文会被创建. 在创建过程中, 主要做如下三件事:

  • 创建变量对象;
  • 创建作用域链;
  • 确定 this 指向;

# 调用栈 (执行环境栈)

调用栈是 JavaScript 引擎用以追踪函数执行流的一种机制

当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。

它遵循 "先进后出" 的栈结构. 第一个被创建, 并推入栈的一定为 "全局执行上下文". 之后, 当一个函数要被调用之前, JavaScript 引擎会为它创建 "函数执行上下文", 然后压入调用栈中。

当函数执行完毕, 函数的执行环境被从栈中弹出销毁, 把控制权交给之前的执行环境. 即使是同一个函数, 它每次被调用时, 都会创建一个单独的执行上下文.

到最后, 全部代码执行结束, "全局执行上下文" 被弹出栈销毁.

# 变量对象 (Variable Object)

在创建执行上下文的时候, 变量对象会被创建. 执行上下文中的所有变量声明, 函数声明都会被  JS 引擎扫描出来, 然后在变量对象上创建同名属性. 如果是在函数执行上下文中的话, 变量对象里还包括了函数的形参集合.

通过变量对象, 执行上下文就可以知道自己可以获取哪些数据. 这个对象是给 JavaScript 引擎用的, 开发者不可以访问.

函数执行上下文中, 变量对象的创建,依次经历了以下几个过程:

  1. 创建 arguments 对象. 检测函数调用时所处上下文传入的参数, 在该对象下创建属性, 和初始化属性值;
  2. 扫描函数内的所有函数声明. 为每一个函数声明,在变量对象上创建一个同名属性, 属性值为函数在内存中的引用. 如果已有同名属性存在, 则属性值被新引用覆盖;
  3. 扫描函数内的变量声明. 为每一个变量声明, 在变量对象创建一个同名属性, 属性值初始化为 undefined. 如果已有同名属性存在, 为防止同名函数被重写为 undefined. 变量声明会被跳过, 原属性值不会被修改;

属性的赋值操作, 会在函数中的赋值语句被执行时才进行.

# 活动对象 (Activation Object)

活动对象, 和变量对象其实指的都是同一个对象, 但只有在执行栈顶部的执行上下文中的变量对象里的属性才可以被访问, 它也就被称为 "活动对象".

# 作用域链

作用域链, 由当前执行上下文和它所有上层的执行上下文的 "变量对象" 组成. 它保证了当前执行环境对可访问到变量和函数的有序访问。

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链.

在创建函数执行上下文的时候, 作用域链会被建立. 作用域链会保存在函数的内部属性 [[Scope]] 上. (内部属性供 JavaScript 引擎使用, 开发者是访问不到这个属性的)

# this

this 指向是函数执行时所在的环境对象 (上下文). 在函数被调用前, 创建执行上下文的过程中被确定.  也就是说函数的调用方式决定了 this 指向. 在之后在函数执行的过程中, this 的指向已经被确定,就不可更改了.

# 直接调用

直接调用函数时, this 指向全局对象, 在浏览器中为 window 对象, 在 Node 中为 global 对象. 这里需要注意的一点是,直接调用并不是指在全局作用域下进行调用,在任何作用域下,直接通过 函数名(...) 来对函数进行调用的方式,都称为直接调用。例如:

(function() {
  function test() {
    console.log(this); // window 或者 global
  }
  test(); // 非全局作用域下的直接调用
})();

# 方法调用

通过对象来调用其方法函数, 函数的 this 指向它的调用者. 如果函数被一个对象所拥有, 该函数被对象调用时, this 指向该对象. 如果函数独立调用, this 的值为 undefined. 非严格模式下, 当 this 的值为 undefined 时, 它会被自动指向全局对象.

# new 调用

通过 new 操作符调用构造函数时, this 指向这个新创建的实例对象.

 使用 new 调用函数时, 会经历以下四个阶段:

  1. 创建一个新的对象;
  2. 构造函数的 this 指向新对象;
  3. 为这个新对象添加构造函数中的属性和方法;
  4. 返回新对象

原型方法做为一个函数, 它被实例对象调用, 那它的 this 也就指向这个实例对象.

WARNING

在 ES6 中, 虽然 class 定义的类用 typeof 运算符得到的仍然是 "function",但它不能像普通函数一样直接调用;同时,class 中定义的方法函数,也不能当作构造函数用 new 来调用。

# 箭头函数 this

箭头函数没有自己的 this 绑定。箭头函数中使用的 this,其实是直接包含它的那个函数的 this

箭头函数让大家在使用闭包的时候不需要太纠结 this,不需要通过像 _this 这样的局部变量来临时引用 this 给闭包函数使用。

WARNING

注意, 箭头函数不能用 new 调用,不能 bind() 到某个对象

# call, apply, bind

通过 call, apply, bind, 我们可以显式的改变函数的 this 指向.

  • call  将函数的参数一个一个地传入 (打电话一个一个地打);
  • apply 将函数的参数放到  数组中传入;

bind 的作用是将当前函数与指定的对象绑定,并返回一个新函数. 这个新函数无论以什么样的方式调用,其 this 始终指向绑定的对象.

WARNING

bind 返回的函数使用 applycall 没有用.

# 事件循环

JavaScript 是 "单线程" 的, 也就是说同一时间只能处理一个操作. JavaScript 作为浏览器的脚本语言, 主要用途是来处理用户交互, 以及操作 DOM. 这使得多线程的设计会导致很复杂的同步问题. (举例说, 如果 JavaScript 可以同时操纵两个线程. 一个线程添加在某个 DOM 节点上添加内容, 另一个线程在这个 DOM 节点下删除内容. 那么浏览器, 该听谁的呢? 所以 JavaScript 被设计成了单线程的)

JavaScript 的另一个特点就是 "非阻塞 I/O", 也称 "异步式 I/O". 当主线程遇到 I/O 操作时 (磁盘读写, 网络通信),不会以阻塞的方式等待 I/O 操作的完成, 或数据的返回. 而只是将 I/O 操作交给浏览器,然后自己继续执行下一条语句。 当浏览器完成 I/O 操作时,会将用以处理 I/O 操作结果的处理的回调函数推入到一个任务队列, 等待主线程后续进行处理.

前面说过执行上下文创建完, 会被推入调用栈. JavaScript 引擎会逐句执行最顶部的执行上下文中的代码. 在执行过程中, 同步任务逐句被执行. 当遇到了异步任务, JavaScript 引擎会将它们交给浏览器上对应的 Web API 去处理. 浏览器处理完毕之后, 会将用以处理结果的回调函数, 推入到一个 "任务队列" 中.

当调用栈中只剩全局执行上下文的时候, 主线程就会去查询任务队列了. 任务队列中的任务会被逐一取出放入调用栈执行. 当处理任务的时候, 又遇到了新的异步任务, 则会重复之前的操作. 直到任务队列完全清空了, 至此程序执行完毕. 而这个循环过程就被叫做 "事件循环".

# 宏任务 & 微任务

在浏览器中, 异步任务分成 "宏任务" (macro-task) 和 "微任务" (micro-task) 两种. 这两种任务也都各自有一条任务队列.

  • 宏任务: 包括 script(整体代码), setTimeout, setInterval, setImmediate,requestAnimationFrame, I/O, UI rendering.
  • 微任务: 包括 process.nextTick, Promise, Object.observe, MutationObserver

读取整体的 script 代码也算是一个宏任务, 所以第一次事件循环从宏任务 (macro-task) 开始. 之后全局执行上下文被创建, 推入执行栈. 直到最后执行栈只剩全局执行上下文时, 主线程执行所有的 micro-task 队列中的任务. 清空后, 线程从 macro-task 队列首部取一个任务, 然后到最后再清空 micro-task 队列. 直到 macro-task, micro-taks 队列都清空了, 全局执行上下文出栈, 程序结束.

也就是: "总是先拿一个宏任务, 清空微任务队列, 循环直至都清空".

# 原型

prototype4

  • 构造函数的 prototype 属性指向原型对象;
  • 实例的 __proto__ 属性指向原型对象;
  • 原型对象的 constructor 属性指向构造函数;

# 原型链

原型链是实现继承的主要方法. 基本思想是把一个类型的实例, 作为另外一个类型的原型对象. 从而实现后面类型可以访问前一个类型的属性和方法.

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

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

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

//继承了SuperType
SubType.prototype = new SuperType();

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

var instance = new SubType();

alert(instance.getSuperValue()); //true

可以通过两种方式来确定原型和实例之间的关系:

第一种方式是使用 instanceof 操作符. 只要操作符前面实例对象的原型链中, 存在操作符后面  类型的实例对象, 就返回 true:

alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true

第二种方式是使用 isPrototypeOf() 方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此 isPrototypeOf() 方法也会返回 true

alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

# 属性枚举

# for/in 循环

for/in 循环用于遍历对象中所有可枚举的属性. 在循环中, 遍历出的属性名称被赋值给循环变量.

var o = {
  a: 1,
  b: 2,
  c: 3
};

for (prop in o) {
  console.log(prop); // 依次输出 a, b, c
}

WARNING

for in 应用于数组, 循环返回的是数组的下标和属性, 和原型上的方法和属性. for in 应用于对象, 循环返回的是对象的属性名, 和原型中的方法和属性.

for of 可用在可迭代对象上, 循环一个数组时, 返回的是数组项的值.

# Object.keys()

Object.keys() 返回一个数组, 数组由对象的 "可枚举的自有属性的名称" 组成 (不含 Symbol 属性).

操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用 for...in 循环,而用 Object.keys() 代替。

# Object.getOwnPropertyNames()

Object.getOwnPropertyNames() 返回一个数组, 数组由对象的 "自有属性名称" 组成 (不含 Symbol 属性), 而不仅仅是可枚举的.

# Object.getOwnPropertySymbols()

Object.getOwnPropertySymbols() 返回一个数组,包含对象 "自有" 的所有 Symbol 属性的键名。

# Reflect.ownKeys()

Reflect.ownKeys() 返回一个数组,包含对象 "自身" 的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

# 闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数.

var scope = "global scope";
function checkscope() {
  var scope = "local scope";
  function f() {
    return scope;
  }
  return f;
}

var foo = checkscope();
foo();

f 函数执行的时候,checkscope 函数上下文已经被销毁了. 之所以, f 能够读取到 checkscope 作用域下的 scope 值. 是因为 f 执行上下文维护了一个作用域链.

fContext = {
  Scope: [AO, checkscopeContext.AO, globalContext.VO]
};

因为这个作用域链,函数依然可以读取到 checkscopeContext.AO 的值. 即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它. 从而实现了闭包这个概念.

# 私有变量

任何在函数中定义的变量,都可以认为是私有变量, 因为不能在函数的外部访问这些变量. 通过闭包, 可以访问这些私有变量.我们把有权访问私有变量和私有函数的公有方法称为 "特权方法"(privileged method)

function Person(name) {
  this.getName = function() {
    return name;
  };

  this.setName = function(value) {
    name = value;
  };
}

var person = new Person("Nicholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"
var singleton = (function() {
  //私有变量和私有函数
  var privateVariable = 10;

  //特权/公有方法和属性
  return {
    publicProperty: true,

    publicMethod: function() {
      privateVariable++;
    }
  };
})();

# 对象创建

# 把原型对象封装在构造函数中

function Person(name, age, job) {
  //属性
  this.name = name;
  this.age = age;
  this.job = job;

  //方法
  if (typeof this.sayName != "function") {
    Person.prototype.sayName = function() {
      alert(this.name);
    };
  }
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

# 定义 Class

ES6 引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return "(" + this.x + ", " + this.y + ")";
  }
}

WARNING

  • 方法之间 "不需要逗号分隔",加了会报错;
  • 与 ES5 不同的是, 类的内部所有定义的方法,都是不可枚举的. 这一点与 ES5 的行为不一致
  • 类不存在变量提升(hoist)

# 继承

# 原型链继承

让一个类型的原型对象等于另一个类型的实例, 此时这个类型的原型对象将包含一个指向另一个原型的指针. 像这样层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

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

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

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

//继承了SuperType
SubType.prototype = new SuperType();

原型链继承的问题:

  • 在创建子类型的实例时,不能向超类型的构造函数中传递参数;
  • 作为子类型原型的超类型的实例的所有属性, 都会被所有子类型实例共享

# 借用构造函数

借用构造函数, 也叫做伪造对象或经典继承, 思想是在子类型构造函数的内部调用超类型构造函数. 通过使用 apply()call() 方法可以在(将来)新创建的对象上执行构造函数.

function SuperType(name) {
  this.name = name;
}

function SubType() {
  //让超类型的构造函数执行时, this 指向新创建的子类型实例
  SuperType.call(this, "Garrik");
}

var instance1 = new SubType();

借用构造函数的问题:

  •  超类型的方法在每一个实例中都独立存在, 没有实现共享

# 组合继承

组合继承, 也叫做伪经典继承, 思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承.

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

SuperType.prototype.sayName = function() {
  alert(this.name);
};

function SubType(name, age) {
  //继承属性
  // 因为在这里传递参数, SubType 原型中的 SuperType 属性会被覆盖
  SuperType.call(this, name);

  this.age = age;
}

//继承方法
SubType.prototype = new SuperType();

// 因为 SubType 原型是 SuperType 实例, 所以 原型的 construtor 指向的是超类型, 要手动设置.
SubType.prototype.constructor = SubType;

# extends 继承

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的 constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + " " + super.toString(); // 调用父类的toString()
  }
}

let cp = new ColorPoint(25, 8, "green");

cp instanceof ColorPoint; // true
cp instanceof Point; // true

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。

通过 Object.getPrototypeOf 方法可以判断一个类, 是否是另一个类的子类:

Object.getPrototypeOf(ColorPoint) === Point;

# let & const

通过 var 声明的变量存在变量提升的特性.

letconst 声明的变量:

  • 不会被提升
  • 重复声明报错

const 用于声明常量,保证了变量指向的那个内存地址所保存的数据不得改动. 在声明变量的时候, 必须进行初始化赋值.

letconst 声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,会导致报错:

# Symbol

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。

Symbol 值通过 Symbol 函数生成。Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述. 主要是为了在控制台显示,或者转为字符串时,比较容易区分. 相同参数的 Symbol 函数的返回值是不相等的.

# 作为属性名的 Symbol

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性. 能防止某一个键被不小心改写或覆盖.

Symbol 值作为对象属性名时,不能用点运算符, 要使用 []:

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = "Hello!";

// 第二种写法
let a = {
  [mySymbol]: "Hello!"
};

a[mySymbol]; // "Hello!"

# 消除魔术字符串

魔术字符串指的是在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。

// 例子 4-1

// bad
const TYPE_AUDIO = "AUDIO";
const TYPE_VIDEO = "VIDEO";
const TYPE_IMAGE = "IMAGE";

// good
const TYPE_AUDIO = Symbol();
const TYPE_VIDEO = Symbol();
const TYPE_IMAGE = Symbol();

function handleFileResource(resource) {
  switch (resource.type) {
    case TYPE_AUDIO:
      playAudio(resource);
      break;
    case TYPE_VIDEO:
      playVideo(resource);
      break;
    case TYPE_IMAGE:
      previewImage(resource);
      break;
    default:
      throw new Error("Unknown type of resource");
  }
}

# 私有变量

Symbol 也可以用于私有变量的实现。

const Example = (function() {
  var _private = Symbol("private");

  class Example {
    constructor() {
      this[_private] = "private";
    }
    getName() {
      return this[_private];
    }
  }

  return Example;
})();

var ex = new Example();

console.log(ex.getName()); // private
console.log(ex.name); // undefined

# 属性名的遍历

Symbol 作为属性名,该属性不会出现在 for...infor...of 循环中,也不会被 Object.keys()Object.getOwnPropertyNames()JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。

Reflect.ownKeys 方法可以返回所有类型的键名.

let obj = {
  [Symbol("my_key")]: 1,
  enum: 2,
  nonEnum: 3
};

Reflect.ownKeys(obj);
//  ["enum", "nonEnum", Symbol(my_key)]

# Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set 本身是一个构造函数,用来生成 Set 数据结构。Set 函数可以接受一个数组(或者具有 Iterable 接口的其他数据结构)作为参数,用来初始化。

# 数组去重

const set = new Set([1, 2, 3, 4, 4]);
// 转换为数组
[...set];
// [1, 2, 3, 4]

# 遍历操作

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

Set 的遍历顺序就是插入顺序. 可以用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用.

keys 方法、values 方法、entries 方法返回的都是遍历器对象. 由于 Set 结构没有键名,只有键值. 所以 keys 方法和 values 方法的行为完全一致. 同时, Set 结构的实例默认可遍历.

let set = new Set(["red", "green", "blue"]);

for (let item of set) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

# Map

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是 “键” 的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

const m = new Map();
const o = { p: "Hello World" };

m.set(o, "content");
m.get(o); // "content"

m.has(o); // true
m.delete(o); // true
m.has(o); // false

Map 也可以接受一个数组作为参数:

const map = new Map([["name", "张三"], ["title", "Author"]]);

map.size; // 2
map.has("name"); // true
map.get("name"); // "张三"
map.has("title"); // true
map.get("title"); // "Author"

# 遍历操作

Map 的遍历顺序就是插入顺序.

const map = new Map([["F", "no"], ["T", "yes"]]);

for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

# 条件语句的优化

// bad
function test(color) {
  switch (color) {
    case "red":
      return ["apple", "strawberry"];
    case "yellow":
      return ["banana", "pineapple"];
    case "purple":
      return ["grape", "plum"];
    default:
      return [];
  }
}

// better
const fruitColor = new Map()
  .set("red", ["apple", "strawberry"])
  .set("yellow", ["banana", "pineapple"])
  .set("purple", ["grape", "plum"]);

function test(color) {
  return fruitColor.get(color) || [];
}

# Iterator

Iterator 是一种接口,为各种不同的数据结构提供统一的访问机制。

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是 “可遍历的”(iterable)。

一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用 for...of 循环遍历它的成员。也就是说,for...of 循环内部调用的是数据结构的 Symbol.iterator 方法。

Symbol.iterator 属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。

ES6 的有些数据结构原生具备 Iterator 接口,  即不用任何处理,就可以被 for...of 循环遍历。例如:

  • Array
  • Map
  • Set
  • String
  • 函数的 arguments 对象
  • NodeList 对象

对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数, for...of 循环会自动遍历它们。

# 迭代过程

  • 通过 Symbol.iterator 创建一个迭代器,指向当前数据结构的起始位置;
  • 随后通过 next 方法进行向下迭代指向下一个位置, next 方法会返回当前位置的对象,对象包含了 valuedone 两个属性, value 是当前属性的值,done 用于判断是否遍历结束;
  • donetrue 时则遍历结束;
const items = ["zero", "one", "two"];
const it = items[Symbol.iterator]();

it.next();
// {value: "zero", done: false}
it.next();
// {value: "one", done: false}
it.next();
// {value: "two", done: false}
it.next();
// {value: undefined, done: true}

# 异步

# 回调地狱

doA(function() {
  doC();
  doD(function() {
    doF();
  });
  doE();
});
doB();
  1. 回调函数执行顺序, 与写代码的顺序不相符, 不利于维护, 调试
  2. 嵌套的回调函数把  异步代码的执行顺序写死了. 上面代码中, doD 的回调函数必须等 doA 的回调函数得到执行之后才能被执行.
  3. 在第三方提供的  工具中使用回调函数的时候, 实际上我们就把代码一部分的操控权交给了第三方. 这被称为 "控制反转". 这加大了代码不确定性.

# Promise

Promise 维护了一个状态机, 其有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。

ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolvereject。它们是两个函数,由 JavaScript 引擎提供. Promise 构造函数调用后会立即执行。

resolve 函数的作用是,将 Promise 对象的状态从 “未完成” 变为 “成功”. 在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;

reject 函数的作用是,将 Promise 对象的状态从 “未完成” 变为 “失败”, 在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。then 函数是 Promise 状态改变时的回调函数. then 方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 resolved 时调用,第二个回调函数是 Promise 对象的状态变为 rejected 时调用。

const myPromise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

myPromise.then(
    function(value) {
    // success
    },
    function(error) {
    // failure
    }
);

then 方法返回的是一个新的 Promise 实例。因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法. 前面 then 函数中回调函数参数返回的值, 会作为后一个 then 函数的回调函数参数的参数.

catch 函数用于指定发生错误时的回调函数. 如果异步操作抛出错误,状态就会变为 rejected,就会调用 catch 方法指定的回调函数. catch 方法返回的还是一个 Promise 对象,因此后面还可以接着调用 then 方法。

# all, race

Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。接受一个数组作为参数,数组里的每一项必须都是 Promise 实例,如果不是,就会先调用 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。(Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例)。

const p = Promise.all([p1, p2, p3]);

p 的状态由 p1、p2、p3 决定,分成两种情况:

  • 只有 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
  • 第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

Promise.race 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数

# Promise.resolve & Promise.reject

Promise.resolve(value) 的返回值也是一个 Promise 对象, 状态为 resolved.

  • 如果参数是 Promise 实例,那么 Promise.resolve 将不做任何修改、原封不动地返回这个实例;
  • 如果参数是 thenable 对象. thenable 对象指的是具有 then 方法的对象. Promise.resolve 方法会将这个对象转为 Promise 对象,然后就立即执行 thenable 对象的 then 方法;
  • 如果参数是一个原始值,或者是一个不具有 then 方法的对象. 这个参数会作为后续方法的参数;
let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value); // 42
});

const p = Promise.resolve("Hello");

p.then(function(s) {
  console.log(s);
});
// Hello

Promise.reject(reason) 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected. Promise.reject() 方法的参数,会原封不动地作为 reject 的理由,变成后续方法的参数。

# Generator

Generator 函数是 ES6 提供的一种异步编程解决方案. 可以把它理解成,Generator 函数是一个 "状态机",封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象, 可以依次遍历 Generator 函数内部的每一个状态。

声明 Genrator 函数时, function 关键字与函数名之间有一个 "星号" (星号位置无所谓); 函数体内部使用 yield 表达式,定义不同的内部状态.

function* helloWorldGenerator() {
  yield "hello";
  yield "world";
  return "ending";
}

var hw = helloWorldGenerator();

hw.next();
// { value: 'hello', done: false }

hw.next();
// { value: 'world', done: false }

hw.next();
// { value: 'ending', done: true }

hw.next();
// { value: undefined, done: true }

调用遍历器对象的 next 方法,使得指针移向下一个状态。每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止.

next 方法调用后, 返回一个有着 valuedone 两个属性的对象。value 属性表示当前的内部状态的值,是 yield 表达式后面那个表达式的值;done 属性是一个布尔值,表示是否遍历结束。

Generator 函数的暂停执行的效果,意味着可以把异步操作写在 yield 表达式里面,等到调用 next 方法时再往后执行。

# Async/Await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 函数简单说就是 Generator 函数的语法糖。

async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await.

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。await 命令后面一般是一个 Promise 对象. await 命令返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。但只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。

const asyncFun = async function() {
  let a = await new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Hello");
    }, 1000);
  });

  let b = await new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("World");
    }, 1000);
  });

  return a + " " + b;
};

asyncFun().then(result => {
  console.log(result); // Hello World
});

如果 await 后面的异步操作出错,那么等同于 async 函数返回的 Promise 对象被 reject。防止出错的方法,也是将其放在 try...catch 代码块之中。

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log("Final: ", val3);
  } catch (err) {
    console.error(err);
  }
}

# 异步执行顺序判断

async function async1(){
  console.log('async1 start');
  await async2();
  console.log('async1 end')
}
async function async2(){
  console.log('async2')
}
console.log('script start');
setTimeout(function(){
  console.log('setTimeout')
},0);
async1();
new Promise(function(resolve){
  console.log('promise1');
  resolve();
}).then(function(){
  console.log('promise2')
});
console.log('script end')

// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

队列任务优先级:promise 的回调 > setTimeout > setImmediate

这类的执行结果可以用一句话总结,先执行同步代码,遇到异步代码就先加入队列,然后按入队的顺序执行异步代码,最后执行 setTimeout 队列的代码。

# DOM

# 为什么 0.1 + 0.2 != 0.3

JS 采用 IEEE 754 双精度版本(64 位)

# JS 功能函数实现

# call, apply, bind  模拟实现

# call

思路:

  1. 将函数设为对象的属性;
  2. 执行该函数;
  3. 删除该函数;
Function.prototype.call2 = function(context) {
  if (typeof this !== "function") {
    throw new Error("不是函数");
  }

  var context = context || window;
  context.fn = this;

  var args = [];
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push("arguments[" + i + "]");
  }

  var result = eval("context.fn(" + args + ")");

  delete context.fn;
  return result;
};

# apply

Function.prototype.apply = function(context, arr) {
  if (typeof this !== "function") {
    throw new Error("不是函数");
  }

  var context = Object(context) || window;
  context.fn = this;

  var result;
  if (!arr) {
    result = context.fn();
  } else {
    var args = [];
    for (var i = 0, len = arr.length; i < len; i++) {
      args.push("arr[" + i + "]");
    }
    result = eval("context.fn(" + args + ")");
  }

  delete context.fn;
  return result;
};

# bind

Function.prototype.bind2 = function(context) {
  if (typeof this !== "function") {
    throw new Error("不是函数");
  }

  var fn = this;
  var args = Array.prototype.slice.call(arguments, 1);

  var fNOP = function() {};

  var fBound = function() {
    var bindArgs = Array.prototype.slice.call(arguments);
    return fn.apply(
      this instanceof fNOP ? this : context,
      args.concat(bindArgs)
    );
  };

  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();

  return fBound;
};

# 类数组 => 数组

简单说, 类数组对象就是一个拥有 length 属性和若干索引属性的对象.

例如下面这样的:

var arrayLike = {
  0: "name",
  1: "age",
  2: "gender",
  length: 3
};

类数组不具备数组的方法, 如果想使用, 可以用 call 间接调用:

var arrayLike = { 0: "name", 1: "age", 2: "sex", length: 3 };

Array.prototype.join.call(arrayLike, "&"); // name&age&sex

Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"]
// slice可以做到类数组转数组

Array.prototype.map.call(arrayLike, function(item) {
  return item.toUpperCase();
});

# 类数组转数组

var arrayLike = { 0: "name", 1: "age", 2: "sex", length: 3 };
// 1. slice
Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"]
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"]
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"]
// 4. apply
Array.prototype.concat.apply([], arrayLike);
// 5. Object.assign
Object.assign([], arrayLike);

# 防抖 debounce

防抖是将多次执行变为最后一次执行.

思路:

  1. 函数被放到一个延时器里;
  2. debounce 函数返回一个用来被实际调用的函数;
  3. 当返回的函数调用时, 之前延时器被消除, 然后重新设置;
/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为 ture 时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce(func, wait = 50, immediate = true) {
  let timer, context, args;

  // 延迟执行函数
  const later = () =>
    setTimeout(() => {
      // 延迟函数执行完毕,清空缓存的定时器序号
      timer = null;
      // 延迟执行的情况下,函数会在延迟函数中执行
      // 使用到之前缓存的参数和上下文
      if (!immediate) {
        func.apply(context, args);
        context = args = null;
      }
    }, wait);

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
      timer = later();
      // 如果是立即执行,调用函数
      // 否则缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params);
      } else {
        context = this;
        args = params;
      }
      // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
      // 这样做延迟函数会重新计时
    } else {
      clearTimeout(timer);
      timer = later();
    }
  };
}

# 节流 throttle

节流是将多次执行变成每隔一段时间执行.

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器.

# 时间戳

// 第一版
function throttle(func, wait = 1000) {
  var context, args;
  var previous = 0;

  return function() {
    // 使用 + 将时间转换成 Number 类型
    var now = +new Date();
    context = this;
    args = arguments;
    if (now - previous > wait) {
      func.apply(context, args);
      previous = now;
    }
  };
}

# 定时器

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

# Ajax 实现

简单版:

// 实例化
let xhr = new XMLHttpRequest();
// 初始化
xhr.open(method, url, async);
// 发送请求。如果请求是异步的(默认),那么该方法将在请求发送后立即返回
xhr.send(data)
// 设置状态变化回调函数, 处理请求结果
xhr.onreadystatechange = () => {
  if (xhr.readyStatus === 4 && xhr.status === 200) {
    console.log(xhr.responseText)
  }
}

readyStatus 的值:

状态 描述
0 UNSENT XMLHttpRequest 代理已被创建,但尚未调用 open() 方法
1 OPENED open() 方法已经被触发。在这个状态中,可以通过 setRequestHeader() 方法来设置请求的头部, 可以调用 send() 方法来发起请求
2 HEADERS_RECEIVED send() 方法已经被调用,响应头也已经被接收
3 LOADING 响应体部分正在被接收
4 DONE 请求操作已经完成。这意味着数据传输已经彻底完成或失败

基于 Promise 实现:

function ajax (options) {
  // 请求地址
  const url = options.url
  // 请求方法
  const method = options.method.toLocaleLowerCase() || 'get'
  // 默认为异步true
  const async = options.async
  // 请求参数
  const data = options.data
  // 实例化
  const xhr = new XMLHttpRequest()
  // 请求超时
  if (options.timeout && options.timeout > 0) {
    xhr.timeout = options.timeout
  }
  // 返回一个Promise实例
  return new Promise ((resolve, reject) => {
    xhr.ontimeout = () => reject && reject('请求超时')
    // 监听状态变化回调
    xhr.onreadystatechange = () => {
      if (xhr.readyState == 4) {
        // 200-300 之间表示请求成功,304资源未变,取缓存
        if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
          resolve && resolve(xhr.responseText)
        } else {
          reject && reject()
        }
      }
    }
    // 错误回调
    xhr.onerror = err => reject && reject(err)
    let paramArr = []
    let encodeData
    // 处理请求参数
    if (data instanceof Object) {
      for (let key in data) {
        // 参数拼接需要通过 encodeURIComponent 进行编码
        paramArr.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
      }
      encodeData = paramArr.join('&')
    }
    // get请求拼接参数
    if (method === 'get') {
      // 检测url中是否已存在 ? 及其位置
      const index = url.indexOf('?')
      if (index === -1) url += '?'
      else if (index !== url.length -1) url += '&'
      // 拼接url
      url += encodeData
    }

    // 初始化
    xhr.open(method, url, async)
    // 发送请求
    if (method === 'get') xhr.send(null)
    else {
      // post 方式需要设置请求头
      xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8')
      xhr.send(encodeData)
    }
  })
}

# Promise 实现

# JS 设计模式

# 前端算法

# 数组

# 数组去重

  • 利用对象存储不重复的数字
  • 循环数组,只要对象里面不存在该数字,则存储进去
  • 如果已经存在,则在数组中利用索引删除该数字
function removeDuplicates(arr) {
  const obj = {};
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    if (obj.hasOwnProperty(item)) {
      arr.splice(i, 1);
      i--;
    }
    obj[item] = item;
  }

  return arr.length;
}

# 旋转一维数组

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

输入: [1,2,3,4,5,6,7] k = 3
输出: [5,6,7,1,2,3,4]
function arrRotate(arr, num) {
  if (num <= 0) return arr;
  for (let i = 0; i < num; i++) {
    arr.unshift(arr[arr.length - 1]);
    arr.pop();
  }
}

# 两个数组的交集

给定两个数组,编写一个函数来计算它们的交集。

输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2,2]

# 买卖股票的最佳时机

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3

# 循环前后两者比较

思路:

  • 循环数组(不遍历最后一项),让当前项与后一项比较
  • 如果当前项小于后一项,则购入当前项,然后在后一项的时候卖出
  • 如果大于或等于,则什么也不做

缺点: 会产生连续购入和卖出

function maxProfit(arr) {
  let totalProfit = 0;

  for (let i = 0; i < arr.length - 1; i++) {
    const curValue = arr[i];
    const nextValue = arr[i + 1];
    if (nextValue > curValue) {
      totalProfit += nextValue - curValue;
    }
  }

  return totalProfit;
}

# 优化循环比较

  • 增加记录买入、卖出时的索引
  • 增加比较,如果前一个不存在或者大于当前值,并且当前值小于后一个值,则记录当前索引为买入(此时实际买入),后一个索引为卖出索引(只是记录可能卖出,不是实际卖出)
function maxProfit2(arr) {
  const record = [];
  let totalProfit = 0;
  let buyIndex = 0;
  let sellIndex = 0;

  for (let i = 0; i < arr.length - 1; i++) {
    const curValue = arr[i];
    const nextValue = arr[i + 1];
    if (nextValue >= curValue) {
      sellIndex = i + 1;
    } else {
      if (sellIndex !== buyIndex) {
        totalProfit += arr[sellIndex] - arr[buyIndex];
        record.push({
          buy: buyIndex,
          sell: sellIndex
        });
      }
      buyIndex = i + 1;
      sellIndex = i + 1;
    }
  }

  if (sellIndex !== buyIndex) {
    totalProfit += arr[sellIndex] - arr[buyIndex];
    record.push({
      buy: buyIndex,
      sell: sellIndex
    });
  }

  return totalProfit;
}

# 链表

链表存储有序的元素集合, 每个元素由一个存储元素本身的节点和一个指向下一个元素的指针组成.

Screen Shot 2019-08-29 at 4.54.01 PM

相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,数组可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,需要从起点开始迭代, 直到找到所需的元素。

# 创建链表

function LinkedList() {
  this.head = null;
}

# 添加节点

function LinkedNode(value) {
  this.value = value;
  this.next = null;
}

LinkedList.prototype.insertNode = function(value, index) {
  const node = new LinkedNode(value);
  if (this.head === null) this.head = node;
  else {
    let current = this.head;
    let curIndex = 0;
    while (current.next && index !== curIndex) {
      current = current.next;
      curIndex++;
    }
    node.next = current.next;
    current.next = node;
  }
  return this.head;
};

# 删除节点

LinkedList.prototype.deleteNode = function(index) {
  let current = this.head;
  let previous = null;
  let curIndex = 0;
  if (index === 0) this.head = current.next;
  else {
    while (current.next && curIndex !== index) {
      previous = current;
      current = current.next;
      curIndex++;
    }
    previous.next = current.next;
  }
};

# 反转链表

# 迭代实现

LinkedList.prototype.reverse = function() {
  if (this.head === null || this.head.next === null) return this.head;
  let current = this.head;
  let previous = null;
  let next = current.next;
  current.next = null;

  while (next) {
    previous = current;
    current = next;
    next = current.next;
    current.next = previous;
  }
  this.head = current;
};

# 递归实现

LinkedList.prototype.reverse = function() {
  if (this.head === null || this.head.next === null) return this.head;
  const current = this.head;
  const reverse = function(previous, current) {
    let head = null;
    const next = current.next;

    if (next) head = reverse(current, next);
    else head = current;
    current.next = previous;

    return head;
  };
  this.head = reverse(current, current.next);
  current.next = null;
};

# 链表排序

# 拼接链表, 并排序

# 双向链表

在双向链表中,链接是双向的:一个链向下一个元素, 另一个链向前一个元素.

Screen Shot 2019-08-31 at 2.15.15 PM

# 创建双向链表

function DoubleLinkedList() {
  this.head = null;
  this.tail = null;
}

function DoubleLinkedNode(value) {
  this.value = value;
  this.prev = null;
  this.next = null;
}

# 插入节点

DoubleLinkedList.prototype.insertNode = function(value, index) {
  const node = new DoubleLinkedNode(value);
  let current = this.head;
  let curIndex = 0;

  // 插入到头部
  if (index <= 0 || current === null) {
    node.next = current;
    this.head = node;
    return this.head;
  }

  //找到目标节点
  while (current.next && index !== curIndex) {
    current = current.next;
    curIndex++;
  }

  //插入到尾部
  if (current.next === null) {
    node.prev = current;
    node.next = current.next;
    current.next = node;
    this.tail = node;
    // 插入到目标节点处
  } else {
    current.prev.next = node;
    node.prev = current.prev;
    node.next = current;
    current.prev = node;
  }

  return this.head;
};

# 删除节点

DoubleLinkedList.prototype.deleteNode = function(index) {
  if (this.head === null || this.head.next === null) {
    this.head = null;
    return this.head;
  }

  if (index <= 0) {
    this.head = this.head.next;
    this.head.prev = null;
    return this.head;
  }

  let current = this.head;
  let curIndex = 0;

  while (current.next && index !== curIndex) {
    current = current.next;
    curIndex++;
  }

  if (current.next === null) {
    current.prev.next = null;
    this.tail = current.prev;
  } else {
    current.prev.next = current.next;
    current.next.prev = current.prev;
  }

  return this.head;
};

# 排序

# 冒泡排序

思路: 比较任何两个相邻的项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序.

function bubble_sort(unsorted) {
  // 外循环: 每次循环确定一个项的正确顺序.
  for (let i = 0; i < unsorted.length; i++) {
    // 内循环: 每次循环对比相邻两个项的大小, 小的放在前, 大的放在后.
    for (let j = 0; j < unsorted.length - i - i; j++) {
      if (unsorted[j] > unsorted[j + 1]) {
        const temp = unsorted[j];
        unsorted[j] = unsorted[j + 1];
        unsorted[j + 1] = temp;
      }
    }
  }
}

Screen Shot 2019-08-28 at 10.41.16 AM

# 选择排序

思路: 是找到数据结构中的最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位, 以此类推.

function select_sort(unsorted) {
  for (let i = 0; i < unsorted.length; i++) {
    let minIndex = i;
    for (let j = i + 1; j < unsorted.length; j++) {
      minIndex = unsorted[j] < unsorted[minIndex] ? j : minIndex;
    }
    let temp = unsorted[i];
    unsorted[i] = unsorted[minIndex];
    unsorted[minIndex] = temp;
  }
}

Screen Shot 2019-08-29 at 10.14.36 AM

# 插入排序

思路: 每次排一个数组项,以此方式构建最后的排序数组. 假定第一个项是  已经排序的, 依次将后面的项与已经排序好的项做对比, 并将其插入到正确位置.

function insert_sort(unsorted) {
  for (let i = 1; i < unsorted.length; i++) {
    let temp = unsorted[i];
    let j = i;
    while (j > 0 && unsorted[j - 1] > temp) {
      unsorted[j] = unsorted[j - 1];
      j--;
    }
    unsorted[j] = temp;
  }
}

Screen Shot 2019-08-29 at 10.12.28 AM

# 归并排序

思路: 采用分治策略, 将原始数组切分成较小的数组,直到每个小数组只有一个项,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。

function merge_sort(unsorted) {
  const merge = function(left, right) {
    const result = [];
    let i = 0,
      j = 0;

    while (i < left.length && j < right.length) {
      if (left[i] <= right[j]) {
        result.push(left[i]);
        i++;
      } else {
        result.push(right[j]);
        j++;
      }
    }
    while (i < left.length) {
      result.push(left[i]);
      i++;
    }
    while (j < right.length) {
      result.push(right[j]);
      j++;
    }

    return result;
  };

  const divide = function(arr) {
    if (arr.length <= 1) return arr;
    const mid = Math.floor(arr.length / 2);
    const left = arr.slice(0, mid);
    const right = arr.slice(mid, arr.length);
    return merge(divide(left), divide(right));
  };

  return divide(unsorted);
}

Screen Shot 2019-08-29 at 3.09.42 PM

# 快速排序

思路:

  • 采用分治策略. 首先,从数组中选择中间一项作为主元.
  • 创建两个指针,左边一个指向数组第一个项,右边一个指向数组最后一个项。移动左指 针直到我们找到一个比主元大的元素,接着,移动右指针直到找到一个比主元小的元素,然后交换它们,重复这个过程,直到左指针超过了右指针. 这个过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后。这一步叫作划分操作.
  • 接着,算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的子数组)重复之前的两个步骤,直至数组已完全排序
function quick_sort(unsorted) {
  const partition = function(arr, left, right) {
    const index = Math.floor((left + right) / 2);
    const pivot = arr[index];
    let i = left;
    let j = right;
    while (i <= j) {
      while (arr[i] < pivot) {
        i++;
      }
      while (arr[j] > pivot) {
        j--;
      }
      if (i <= j) {
        const temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
        i++;
        j--;
      }
    }
    return i;
  };
  const quick = function(arr, left, right) {
    if (right - left < 1) return;
    let index = partition(arr, left, right);
    if (left < index - 1) {
      quick(arr, left, index - 1);
    }
    if (right > index) {
      quick(arr, index, right);
    }
  };
  quick(unsorted, 0, unsorted.length - 1);
}

Screen Shot 2019-08-29 at 3.37.46 PM

下图中, 对有较小值的子数组执行划分操作

Screen Shot 2019-08-29 at 3.54.56 PM

继续创建子数组,下图针对上图中有较大值的子数组

Screen Shot 2019-08-29 at 3.56.31 PM

# 正则

正则表达式可视化工具

Screen Shot 2019-09-07 at 5.35.02 PM

Screen Shot 2019-09-07 at 5.36.17 PM

// 判断字符串是否包含数字
var regx = /\d/;

// 判断电话号码
var regx = /^1[34578]\d{9}$/;

// 判断是否符合指定格式 XXX-XXX-XXXX, X 为 Number 类型
var regx = /^(\d{3}-){2}\d{4}&/;

// 判断连续重复字母
var regExp = /([a-zA-Z])\1/;

// 获取 url 参数
var regx = /\??(\w+)=(\w+)&?/g;

// 验证邮箱
var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/;

// 去除首尾的'/'
var regx = /^\/*|\/*$/g;

// 写一个正则表达式,匹配 "<OPTION value="待处理">"
var str = '<OPTION value="待处理">待处理</OPTION>';
var regx = /^<.*?>/;

// 敏感词过滤
var str = '我草你妈哈哈背景天胡景涛哪肉涯剪短发欲望';
var regExp = /草|肉|欲|胡景涛/g;

# 模块化

# CommonJS

Node.js 的模块机制实现参照了 CommonJS 标准.

CommonJS 用同步的方式加载模块

模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中.

// moduleA.js
module.exports = function( value ){
    return value * 2;
}

// moduleB.js
var multiplyBy2 = require('./moduleA');
var result = multiplyBy2(4);

# AMD

AMD 规范采用异步方式加载模块. 模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

这里介绍用 require.js 实现AMD规范的模块化:用 require.config() 指定引用路径等,用 define() 定义模块,用 require() 加载模块。

/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 执行基本操作
require(["jquery","underscore"], function($,_){
  // some code here
});

// 定义math.js模块
define(function () {
    var basicNum = 0;
    var add = function (x, y) {
        return x + y;
    };
    return {
        add: add,
        basicNum :basicNum
    };
});

// 定义一个依赖underscore.js的模块
define(['underscore'], function(_){
  var classify = function(list){
    _.countBy(list,function(num){
      return num > 30 ? 'old' : 'young';
    })
  };
  return {
    classify :classify
  };
})

// 引用模块,将模块放在[]内
require(['jquery', 'math'], function($, math){
  var sum = math.add(10,20);
  $("#sum").html(sum);
});

# CMD

CMD 是另一种模块化方案,它与 AMD 很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD 推崇依赖就近、延迟执行。Sea.js 遵从此方案.

/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); // 依赖可以就近书写
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});

// 加载模块
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

# ES6 Module

ES6 在语言标准的层面上,实现了模块功能. 旨在成为浏览器和服务器通用的模块解决方案。

export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };

/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

如上例所示,使用 import 命令的时候,用户需要知道所要加载的变量名或函数名. ES6 还提供了 export default 命令,为模块指定默认输出,对应的 import 语句不需要使用大括号。

/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}

import 命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。

# ES6 模块与 CommonJS 模块的差异

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用.

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值.

ES6 中, JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”.

ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”.

CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

# 移动端适配

# 网络协议

网路中的两台设备想要互相通信, 必须要通过一套统一的通信协议.

通常使用的网络(包括互联网)是在 TCP/IP 协议族的基础上运作的. TCP/IP 协议族按层次分别分为以下 4 层:应用层、传输层、网络层 和 数据链路层

  • 应用层决定了向用户提供应用服务时通信的活动
  • 传输层提供处于网络连接中的两台计算机之间的数据传输;
  • 网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数据单位;
  • 链路层处理连接网络的硬件部分;

# TCP

TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议. 在收发数据前,必须和对方建立可靠的连接。一个 TCP 连接必须要经过三次握手才能建立起来

# TCP 三次握手过程

Screen Shot 2019-09-08 at 9.53.50 AM

img

在 TCP 协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。TCP 连接建立完后双方都能发送和接收数据,所以 TCP 也是一个全双工的协议。

起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。

第一次握手

客户端向服务端发送带 SYN 标志的数据包。请求发送后,客户端便进入 SYN-SENT 状态

第二次握手

接收端收到后,回传一个带有 SYN/ACK 标志的数据包以示传达确认信息. 发送完成后便进入 SYN-RECEIVED 状态。

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个带 ACK 标志的确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

PS:第三次握手可以包含数据,通过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,都可以使用类似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减少 RTT 的目的。

# 明明两次握手就可以建立起连接,为什么还需要第三次应答

这是为了防止失效的 "连接请求报文段" 被服务端接收,从而产生错误.

如果客户端发送了连接请求 A, 但是发送完客户端就关闭了. 此时请求顺利到达服务端,服务端应答了该请求, 并进入 ESTABLISHED 状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费。

# 断开链接四次握手

img

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送带 FIN 标志的连接释放请求。

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送带 FIN 的连接释放请求,然后 B 便进入 LAST-ACK 状态。

第四次握手

A 收到释放请求后,向 B 发送带 ACK 的确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续一段时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

为什么 A 要进入 TIME-WAIT 状态,等待一段时间后才进入 CLOSED 状态?

为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭.

# UDP

UDP(User Data Protocol,用户数据报协议)

UDP 是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是 UDP 只是报文的搬运工,不会对报文进行任何拆分和拼接操作。

  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识,然后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

UDP 是无连接的,也就是说通信不需要建立和断开连接

UDP 没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能

# HTTP

简单来说, HTTP 协议用于规定 服务器 和 客户端 之间通信的规则.

# HTTP 请求方法

HTTP 定义了一组请求方法, 以表明要对给定资源执行的操作。

GET GET 方法请求一个指定资源的表示形式. 使用GET的请求应该只被用于获取数据. HEAD HEAD 方法请求一个与 GET 请求的响应相同的响应,但没有响应体. POST POST 方法用于将实体提交到指定的资源,通常导致在服务器上的状态变化或副作用. PUT PUT 使用请求中的负载创建或者替换目标资源

PUT 与 POST 方法的区别在于,PUT方法是幂等的:调用一次与连续调用多次是等价的(即没有副作用),而连续调用多次 POST 方法可能会有副作用,比如将一个订单重复提交多次。 DELETE DELETE 方法删除指定的资源。 CONNECT CONNECT 方法建立一个到由目标资源标识的服务器的隧道。 OPTIONS OPTIONS 方法用于描述目标资源的通信选项。 TRACE TRACE 方法沿着到目标资源的路径执行一个消息环回测试。 PATCH PATCH 方法用于对资源应用部分修改。

# HTTP 状态码

2XX 成功

  • 200 OK,表示从客户端发来的请求在服务器端被正确处理
  • 204 No content,表示请求成功,但响应报文不含实体的主体部分
  • 205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
  • 206 Partial Content,进行范围请求

3XX 重定向

  • 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
  • 302 found,临时性重定向,表示资源临时被分配了新的 URL
  • 303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
  • 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
  • 307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

4XX 客户端错误

  • 400 bad request,请求报文存在语法错误
  • 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
  • 403 forbidden,表示对请求资源的访问被服务器拒绝
  • 404 not found,表示在服务器上没有找到请求的资源

5XX 服务器错误

  • 500 internal sever error,表示服务器端在执行请求时发生了错误
  • 501 Not Implemented,表示服务器不支持当前请求所需要的某个功能
  • 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

# HTTPs

HTTP 存在的问题:

  • HTTP 本身不具备加密的功能, HTTP 报文使用明文方式发送
  • 无法确认你发送到的服务器就是真正的目标服务器(可能服务器是伪装的)
  • 无法确定返回的客户端是否是按照真实意图接收的客户端(可能是伪装的客户端)
  • 无法确定正在通信的对方是否具备访问权限
  • 请求或响应在传输途中,遭攻击者拦截并篡改内容的攻击被称为中间人攻击

HTTPS (常称为 HTTP over TLS,HTTP over SSL 或 HTTP Secure)是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。

传输层安全性协议(英语:Transport Layer Security,缩写作 TLS),及其前身安全套接层(Secure Sockets Layer,缩写作 SSL)是一种安全协议,目的是为互联网通信,提供安全及数据完整性保障。

# 对称加密 & 非对称加密

在 TLS 中使用了两种加密技术,分别为:对称加密和非对称加密。

对称加密

对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。

非对称加密

有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

# TLS 握手过程

  • 客户端发送一个随机值,需要的协议和加密方式
  • 服务端收到客户端的随机值,自己也产生一个随机值,并根据客户端需求的协议和加密方式来使用对应的方式,发送自己的证书(如果需要验证客户端证书需要说明)
  • 客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端,如果服务端需要验证客户端证书的话会附带证书
  • 服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值按照之前约定的加密方式生成密钥,接下来的通信就可以通过该密钥来加密解密

通过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端使用对称加密的方式通信。

# HTTP2.0

在 HTTP1 中, 当页面中需要请求很多资源的时候,队头阻塞(Head of line blocking)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

在 HTTP 2.0 中,多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

由于HTTP协议是无状态的,即不会记录客户端与服务端的连接信息。每一次的访问,都是没有任何关系的。 故而有时需要一种保存客户端对服务器的访问状态的机制.

cookie 是客户端请求服务器时,服务端记录的用户信息,存储在客户端, 下一次客户端发送请求时会将 cookie 一起发送。(就像令牌一样, 我是小钻风)

客户端第一次访问服务器的时候, 不携带 cookie, 服务器接到请求后, 会记录用户的信息, 然后在响应头中设置 set-cookie 头部. 客户端收到后进行储存,下一次再请求服务器时请求头中会加上 cookie 这一项,服务器通过 cookie 判断客户端是否曾经访问过该网站。

cookie 是有时限的,有一个属性 maxAge 可以设置 cookie 的存储时间,超过时间后 cookie 会被删除。

# session

session 和 cookie 的作用是一样的,也是存储用户信息,但是 session 是存储在服务器端的。

session 还需借助 cookie 将唯一标识 sessionID 存到客户端。当客户端访问服务器时,会生成一个全局唯一标识 sessionID,然后服务器会记录用户的信息然后存储到服务器 session 中,并将 id 响应给客户端.

服务器使用一种类似于散列表的结构来保存信息, 当程序需要为某个客户端的请求创建一个 session 时,服务器首先检查这个客户端的请求里是否已包含了一个 session 标识 (session id), 如果已包含则说明以前已经为此客户端创建过 session,服务器就按照session id 把对应的 session 检索出来, 如果检索不到,会新建一个. 如果客户端请求不包含 session id,则为此客户端创建一个session 并且生成一个与此 session 相关联的 session id,然后在本次响应中返回给客户端保存。

区别:

  • 最大的区别应该在于存储的地方不一样,cookie存储在客户端,session存储在服务器;
  • 从安全性方面来说,cookie 存储在客户端,很容易被窃取,暴露用户信息,而 session 存储在服务器,被窃取的机会小很多,故session 的安全性比 cookie 高;
  • 从性能方面来说,cookie 存储在浏览器端消耗的是用户的资源,相对比较分散,而 session 消耗的服务器的内存,会造成服务器端的压力;
  • cookie 可以长期的存储在客户端,但是数量和大小都是有限制的, 单个cookie保存的数据不能超过4K;session 存在服务器的时间较短,但是没有大小的限制

# 浏览器

# 事件机制

事件触发有三个阶段:

  • 最外层往事件触发处传播,遇到注册的捕获事件会触发;
  • 传播到事件触发处时触发注册的事件;
  • 从事件触发处往最外层传播,遇到注册的冒泡事件会触发;

通常我们使用 addEventListener 注册事件,该函数的第三个参数默认值为 false, 决定了注册的事件是捕获事件还是冒泡事件。false 对应的是冒泡事件.

# 跨域

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源.

因为浏览器出于安全考虑,有了同源策略, 这样就导致只要协议、域名、端口有任何一个不同,都被当作是不同的域。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求就会失败。

# JSONP

JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时.

<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
  console.log(data)
}
</script>

服务端返回如下(返回时即执行全局函数):

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

JSONP 使用简单且兼容性不错,但是只限于 GET 请求。

# CORS

CORS(Cross-Origin ResourceSharing)跨域资源共享,定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通。

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

CORS 背后的基本思想就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

Access-Control-Allow-Origin: *

# 储存

缺点:

  • 只能储存字符串;;
  • 存储量小。虽不同浏览器的存储量不同,但基本上都是在 4kb 左右;
  • 影响性能。Cookie 会由浏览器作为请求头发送,增加文档传输的负载;
  • 安全问题。存储在Cookie的任何数据可以被他人访问;
  • 由于第三方Cookie的滥用,所以很多老司机在浏览网页时会禁用Cookie,所以我们不得不测试用户是否支持Cookie;

使用起来很麻烦, 别用了.

# localStorage

通过 locaStorage 在浏览器端存储键值对数据,它相比于cookie而言,提供了更为直观的API,且在安全上相对好一点 ,而且虽然 locaStorage 只能存储字符串,但它也可以存储字符串化的 JSON 数据.

// 使用方法存储数据
locaStorage.setItem("name", "Srtian")
// 使用属性存储数据
locaStorage.say = "Hello world"
// 使用方法读取数据
const name = locaStorage.getItem("name")
// 使用属性读取数据
const say = locaStorage.say
// 删除数据
locaStorage.removeItem("name")

通过 locaStorage 存储的数据时永久性的,除非我们使用 removeItem 来删除或者用户通过设置浏览器配置来删除,负责数据会一直保留在用户的电脑上,永不过期。

# sessionStorage

localStorage 里面存储的数据没有过期时间设置,而 session Storage 只存储当前会话页的数据,且只有当用户关闭当前会话页或浏览器时,数据才会被清除。

// 保存数据到sessionStorage
sessionStorage.setItem('name', 'Srtian');

// 从sessionStorage获取数据
var data = sessionStorage.getItem('name');

// 从sessionStorage删除保存的数据
sessionStorage.removeItem('name');

// 从sessionStorage删除所有保存的数据
sessionStorage.clear();

# IndexedDB

IndexedDB 由 HTML5 所提供的一种本地存储,用于在浏览器中储存较大数据结构的 Web API.

一个单独的数据库项目的大小没有限制。

# 渲染机制

浏览器的渲染机制一般分为以下几个步骤

  1. 解析HTML,构建DOM树
  2. 解析CSS,生成CSS规则树
  3. 合并DOM树和CSS规则,生成render树
  4. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  5. 绘制render树(paint),绘制页面像素信息
  6. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上

img

在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢。

当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。

Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。 DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载。

重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘

回流是布局或者几何属性需要改变就称为回流。

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多

# 从输入URL到看到页面发生的全过程

  • 通过 DNS 解析 URL 对应的 IP
  • 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
  • TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
  • 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
  • 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400 或 500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
  • 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
  • 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
  • 初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded 事件
  • CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
  • 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了

# 前端安全

# XSS

跨站脚本 (Cross-Site Scripting, XSS) 是一种代码注入方式. 早期常见于网络论坛, 起因是网站没有对用户的输入进行严格的限制, 使得攻击者可以将脚本上传到帖子让其他人浏览到有恶意脚本的页面.

当其他用户浏览到这些网页时, 就会执行这些恶意脚本, 对用户进行 Cookie 窃取/会话劫持/钓鱼欺骗等各种攻击.

XSS 分为三种:反射型,存储型和 DOM-based

  • 反射型: 非持久化, 欺骗用户去点击链接,攻击代码包含在 url 中,被用户点击之后执行攻击代码;
  • 存储型: 持久型, 攻击提交恶意代码到服务器,服务器存储该段代码,这样当其他用户请求后,服务器返回并发给用户,用户浏览此类页面时就可能受到攻击;

例如通过 URL 获取某些参数:

<!-- http://www.domain.com?name=<script>alert(1)</script> -->
<div>{{name}}</div>

最普遍的做法是转义输入输出的内容,对于引号,尖括号,斜杠进行转义:

function escape(str) {
  str = str.replace(/&/g, '&amp;')
  str = str.replace(/</g, '&lt;')
  str = str.replace(/>/g, '&gt;')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, '&#39;')
  str = str.replace(/`/g, '&#96;')
  str = str.replace(/\//g, '&#x2F;')
  return str
}

# CPS 策略

内容安全策略 (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS) 和数据注入攻击等。

CSP 本质上也是建立白名单,规定了浏览器只能够执行特定来源的代码。

通常可以通过 HTTP Header 中的 Content-Security-Policy 来开启 CSP.

// 只允许加载本站资源
Content-Security-Policy: default-src ‘self’
// 只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*

# CSRF

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding. CSRF 就是利用用户的登录态发起恶意请求.

XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

2009040916453171

防范 CSRF 可以遵循以下几种规则:

  • Get 请求不对数据进行修改
  • 不让第三方网站访问到用户 Cookie
  • 阻止第三方网站请求接口
  • 请求时附带验证信息,比如验证码或者 token

SameSite

可以对 Cookie 设置 SameSite 属性。该属性设置 Cookie 不随着跨域请求发送,该属性可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览器都兼容。

验证 Referer

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址. 在通常情况下,访问一个安全受限页面的请求来自于同一个网站,比如需要访问 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件。这时,该转帐请求的 Referer 值就会是转账按钮所在的页面的 URL,通常是以 bank.example 域名开头的地址。而如果黑客要对银行网站实施 CSRF 攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的 Referer 是指向黑客自己的网站。

Token

服务器下发一个随机 Token(算法不能复杂),每次发起请求时将 Token 携带上,服务器验证 Token 是否有效。然后在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求

上次更新: 7/4/2020, 4:14:54 AM