js

334 阅读1小时+

1. 变量提升

先声明后赋值 代码经过编译会生成两部分,执行上下文和可执行代码、 执行上下文是代码的运行环境

console.log(f);function f (){};var f = 'g'; 
/// ƒ f (){}
console.log(f);var f = 'g'; function f (){};
/// ƒ f (){}
var f = 'g';function f (){}; console.log(f);
// g
function f (){};var f = 'g'; console.log(f);
// g

函数变量提升:

  • 找到function声明的变量,在环境中创建
  • 将这些变量初始化并赋值, function(){ console.log(2) }。
  • 开始执行代码 fn(); Var 变量提升:
  • 将这些变量初始化为undefined
  • 开始执行代码 同一个标识符的情况下,变量声明与函数声明都会提升;函数声明会覆盖变量声明,但不会覆盖变量赋值 同名变量和函数,函数会提升到最前边,变量其次,那为什么结果不是我们人工执行的undefined呢?原因是 变量会被忽略
function a(){ console.log(a) }
var a;//忽略
a = 1
a()// a is not a function

2. 类型判断

  • 基本类型 Boolean Null Undefine Number String Symbol 和 Object
  • typeof //"undefined" "object" "boolean" "number" "string" "symbol" "function" (1) NaN / Infinity 都是数字类型的,检测结果都是“number”;

(2) typeof null 的结果是“object”;(这是浏览器的BUG:所有的值在计算中都以二进制编码储存,浏览器中把前三位000的当作对象,而null的二进制前三位是000,所以被识别为对象,但是他不是对象,他是空对象指针,是基本类型值) (000:对象,010:浮点数,100:字符串,110:布尔,1:整数)

(3) typeof 普通对象/数组对象/正则对象..., 结果都是“object”,这样就无法基于typeof 区分是普通对象还是数组对象``...等了

  • constructor 当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性,并让其指向 F 的引用
    function F(){}   var f = new F()
    typeof F   //"function"
   typeof f // 'object'
   f.constructor === F;// true

但是 constructor 属性易变,不可信赖,这个主要体现在自定义对象上,当开发者重写prototype后,原有的constructor会丢失。

function F() {}
F.prototype = {
	_name: 'Eric',
};
var f = new F();
f.constructor === F; // false

function F() {}
F.prototype = {
    constructor: F, 
   _name: 'Eric',
};
var f = new F();
f.constructor === F; // true 
  • instanceof 是用来判断 A 是否为 B 的实例,当 A 的 proto 指向 B 的 prototype 时,就认为 A 就是 B 的实例 instanceof操作符采用了另一种方式来判断对象类型:原型链。如 a instanceof b只要能在a对象的原型链上找到b,则认为a是b类型的一个对象。
   function myInstanceof(left, right) {
    if (typeof left !== 'object' || left === null) return false // 基础类型一律为 false
    let proto = Object.getPrototypeOf(left) // 获取对象的原型
    while(true) {
    	if (proto === null) return false
        if (proto === right.prototype) return true
        proto = Object.getPrototypeOf(proto)
    }
}

局限性 (1) 要求检测的实例必须是对象数据类型的 (2) 基本数据类型的实例是无法基于它检测出来的: 字面量方式创建的不能检测,构造函数创建的就可以检测 (3) 不管是数组对象还是正则对象,都是 Object 的实例,检测结果都是 TRUE ,所以无法基于这个结果判断是否为普通对象

  • toString ,toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,更严格的讲,是 toString运行时this指向的对象类型 缺点是对用户自定义的类型,它只会返回[object Object]

2 原理:

  1. 每一种数据类型的构造函数的原型上都有toString方法;
  2. 除了Object.prototype上的toString是用来返回当前实例所属类的信息(检测数据类型的),其余的都是转换为字符串的
  3. 对象实例.toString() :toString方法中的THIS是对象实例,也就是检测它的数据类型,也就是THIS是谁,就是检测谁的数据类型
  4. Object.prototype.toString.call([value]) 所以我们是把toString执行,基于call改变this为要检测的数据值

JS 隐式转换,显示转换

一般非基础类型进行转换时会先调用 valueOf,如果 valueOf 无法返回基本类型值,就会调用 toString

  1. 字符串和数字
  • "+" 操作符,如果有一个为字符串,那么都转化到字符串然后执行字符串拼接
  • "-" 操作符,转换为数字,相减 (-a, a * 1 a/1) 都能进行隐式强制类型转换
  1. 其他类型与布尔类型 先把布尔类型转换为数字,然后继续进行比较

symbol 有什么用处

  • 用来表示一个独一无二的变量防止命名冲突,不知道对象原有属性名的情况下,扩展对象属性很有用。
  • 不会被常规的方法(除了 Object.getOwnPropertySymbols 外)遍历到,所以可以用来模拟私有变量。

js 为什么是单线程

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

  • JS单线程,指的是在JS引擎中,解析执行JS代码的调用栈是唯一的,所有的JS代码都在这一个调用栈里按照调用顺序执行,不能同时执行多个函数

setTimeout、Promise、Async/Await 的区别

  • 其中settimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行;
  • promise.then里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行;
  • async函数表示函数里面可能会有异步方法,await后面跟一个表达式,async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。

async await 原理

async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。可以说async 是Generator函数的语法糖,并对Generator函数进行了改进。 async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。 async函数对 Generator 函数的改进,体现在以下四点:

(1)内置执行器。Generator 函数的执行必须依靠执行器,而 async 函数自带执行器,无需手动执行 next() 方法。 (2)更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。 (3)更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。 (4)返回值是 Promise。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用。

interface 和 type 区别

  • 都可以描述一个对象或者函数
  • 都允许拓展(extends):interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 extends interface 。
  1. 原始类型: type 可以用来定义原始类型的别名,如 type Name = string;。而 interface 只能用于定义对象类型。
  2. 联合类型: type 可以用来定义联合类型,如 type Transport = 'Bus' | 'Car' | 'Bike' | 'Walk';。interface 无法直接定义联合类型。
  3. 函数类型: type 可以更简洁地定义函数类型,如 type AddFn = (num1: number, num2: number) => number;。interface 也可以定义函数类型,但语法稍微复杂一些。
  4. 扩展: interface 可以使用 extends 关键字进行扩展,而 type 需要使用交叉类型 & 来实现扩展。此外,interface 还支持声明合并,而 type 不支持。
  5. 实现: 类可以使用 implements 关键字来实现 interface 或 type 定义的契约。但 type 不支持实现联合类型。

3. 作用域链

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

4. 闭包

闭包是一个函数读取其它函数变量的桥梁 由于在JS中,变量的作用域属于函数作用域,在函数执行后作用域就会被清理、内存也随之回收,但是由于闭包是建立在一个函数内部的子函数,由于其可访问上级作用域的原因,即使上级函数执行完,作用域也不会随之销毁,这时的子函数——也就是闭包,便拥有了访问上级作用域中的变量的权限,即使上级函数执行完后作用域内的值也不会被销毁。

优点:

  • 当需要一个变量常驻内存时,闭包可以实现一个变量常驻内存 (如果多了就占用内存了)
  • 避免全局变量的污染
  • 私有化变量

缺点:

  • 因为闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存
  • 引起内存泄露

5. 原型 原型链

  • 在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。
  • 当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。
  • JavaScript对象通过__proto__ 指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。
  • 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性
  • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true

JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。 每个对象都会在其内部初始化一个属性,就是 prototype (原型),当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去 prototype 里找这个属性,这个prototype 又会有自己的 prototype ,于是就这样一直找下去,也就是我们平时所说的原型链的概念

关系: instance.constructor.prototype = instance.proto 绝大部分的函数(少数内建函数除外,如Math.abs函数)都有一个prototype属性,这个属性是原型对象用来创建新对象实例,而所有被创建的对象都会共享原型对象,因此这些对象便可以访问原型对象的属性,例如hasOwnProperty()方法存在于Obejct原型对象中,它便可以被任何对象当做自己的方法使用 在解析一个对象实例的时候为对象实例添加一个__proto__属性,此属性指向原型对象,我们便可以通过此属性找到原型对象.

我们需要牢记两点:①__proto__和constructor属性是对象所独有的;② prototype属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__和constructor属性。

__proto__属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,再往上找就相当于在null上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。

prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即f1.proto === Foo.prototype。

constructor属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function。

字面量与new的区别

var s = '12' //typeof s === 'string'

var str = new Srting('12') // typeof str === 'object'

var a = []; 您要告诉解释器创建一个新的运行时数组。根本不需要额外的处理。完成。

如果使用:var a = new Array(); 您要告诉解释器,我想调用构造函数"Array"并生成一个对象。然后,它通过执行上下文查找要调用的构造函数,并调用它,从而创建数组。

Array()是一个对象,[]是一个数据原型。使用new Array()系统每次都会新生成一个对象(浏览器每生成一个对象都会耗费资源去构造他的属性和方法),他的子集是[];

[undefined, undefined].map(e => 1) // [1, 1]

new Array(2).map(e => 1) //"(2) [empty × 2]" in Chrome

Object.create()、new Object()和{}的区别

字面量和new关键字创建的对象是Object的实例,原型指向Object.prototype,继承内置对象Object

Object.create(arg, pro)创建的对象的原型取决于arg,arg为null,新对象是空对象,没有原型,不继承任何对象;arg为指定对象,新对象的原型指向指定对象,继承指定对象

6. this

要判断一个运行中函数的 this 绑定, 就需要找到这个函数的直接调用位置。

  • new 调用:绑定到新创建的对象,注意:显示return函数或对象,返回值不是新创建的对象,而是显式返回的函数或对象。
  • call 或者 apply( 或者 bind) 调用绑定到指定的第一个参数
  • 对象上的函数调用:绑定到那个对象。
  • 普通函数调用: 在严格模式下绑定到 undefined,否则绑定到全局对象。 箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined。 juejin.cn/post/684490… 如果你把一个函数当成参数传递到另一个函数的时候,也会发生隐式丢失的问题,且与包裹着它的函数的this指向无关。在非严格模式下,会把该函数的this绑定到window上,严格模式下绑定到undefined。

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

  • 箭头函数不会创建自己的this,会捕获自己在定义时(注意,是定义时,不是调用时)所处的外层执行环境的this,
  • call()/.apply()/.bind()无法改变箭头函数中this的指向,
  • 箭头函数不能作为构造函数使用,
  • 箭头函数没有原型prototype,
  • 箭头函数没有自己的arguments,需要用剩余参数的形式访问箭头函数的参数列表

map 和 object 区别

es6 新功能

  • let const var
  • 数组,字符串,object的方法
  • 箭头函数
  • Promise
  • async/await
  • class
  • 解构赋值: 本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
  • Set/WeakSet Map/WekMap WeakSet 的成员只能是对象,而不能是其他类型的值。 WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,

Map是键值对的集合,各种类型的值(包括对象)都可以当作键。 WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。 WeakMap键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内

  • Symbol
  • proxy

es6 继承

constructor 方法是类的构造函数,是一个默认方法,通过 new 命令创建对象实例时,自动调用该方法,一般 constructor 方法返回实例对象 this

先创建父类实例this 通过class丶extends丶super关键字定义子类,并改变this指向,super本身是指向父类的构造函数但做函数调用后返回的是子类的实例,实际上做了父类.prototype.constructor.call(this),做对象调用时指向父类.prototype,从而实现继承。

子类在构造函数中使用super, 必须放到 this 前面 (必须先调用父类的构造方法,再使用子类构造方法),否则新建实例时会报错(this is not defined)。因为子类没有自己的 this 对象,而是继承父类的 this 对象。如果不调用 super 方法,子类就得不到 this 对象。

super 这个关键字,既可以当做函数使用,也可以当做对象使用。

当做函数使用 : 在 constructor 中必须调用 super 方法,因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工,而 super 就代表了父类的构造函数。super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B,因此 super() 在这里相当于A.prototype.constructor.call(this, props)

当做对象使用:在普通方法中,指向父类的原型对象;在静态方法中,指向父类。通过 super 调用父类的方法时,super 会绑定子类的 this。

7. 浏览器缓存

  • 强缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。Cache-Control(http1.1)和Expires(http1.0)
  • 协商缓存,让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,将缓存信息中的Etag和Last-Modified 通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。Last-Modified/If-Modified-since(http1.0)和Etag/If-None-match(http1.1)

github.com/lgwebdream/…

async defer prefetch preload

  • defer: 异步获取脚本, 不会停止 HTML 渲染, 在 DOM 事件 domInteractive 之后, 开始执行脚本, 执行完成之后, 触发 domComplete 事件, 然后是 onLoad 事件。标记了 defer 的脚本在执行时会按照页面标记的顺序执行

  • async: 异步获取脚本, 之后如果 HTML 没有渲染完毕, 中断 HTML 渲染, 执行脚本, 然后继续渲染后续的 HTML 内容。标记了 async 的脚本在执行时不会按照页面标记的顺序执行。

-preload 在遇到如下link标签时,立刻开始下载main.js(不阻塞parser),并放在内存中,但不会执行其中的JS语句。 只有当遇到script标签加载的也是main.js的时候,浏览器才会直接将预先加载的JS执行掉。

  • Prefetch 专注于下一个页面将要加载的资源并以低优先级加载。

for in / for of

for...in 循环主要是为了遍历对象而生,不适用于遍历数组

for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象

  1. for...in 循环:只能获得对象的键名,不能获得键值

    for...of 循环:允许遍历获得键值

  2. 对于普通对象,没有部署原生的 iterator 接口,直接使用 for...of 会报错 可以使用 for...in 循环遍历键名 也可以使用 Object.keys(obj) 方法将对象的键名生成一个数组,然后遍历这个数组

  3. for...in 循环不仅遍历键名,还会遍历手动添加的其它键,甚至包括原型链上的键。for...of 则不会这样

  4. forEach 循环无法中途跳出,break 命令或 return 命令都不能奏效 for...of 循环可以与break、continue 和 return 配合使用,跳出循环

  5. 无论是 for...in 还是 for...of 都不能遍历出 Symbol 类型的值,遍历 Symbol 类型的值需要用 Object.getOwnPropertySymbols() 方法

JS中的垃圾回收机制,

标记清除:垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,如果变量再被标记则表示此变量准备被删除。 2008年为止,IE,Firefox,opera,chrome,Safari的javascript都用使用了该方式;

引用计数:跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个值的引用次数就是1,如果这个值再被赋值给另一个变量,则引用次数加1。相反,如果一个变量脱离了该值的引用,则该值引用次数减1,当次数为0时,就会等待垃圾收集器的回收。

这个方式存在一个比较大的问题就是循环引用,就是说A对象包含一个指向B的指针,对象B也包含一个指向A的引用。 这就可能造成大量内存得不到回收(内存泄露),因为它们的引用次数永远不可能是 0

浏览器垃圾回收

  • 第一步:标记空间中「可达」值。
  • V8 采用的是可达性 (reachability) 算法来判断堆中的对象应不应该被回收。这个算法的思路是这样的:
  • 从根节点(Root)出发,遍历所有的对象。
  • 可以遍历到的对象,是可达的(reachable)。
  • 没有被遍历到的对象,不可达的(unreachable)。
  • 在浏览器环境下,根节点有很多,主要包括这几种:
  • 全局变量 window,位于每个 iframe 中;
  • 文档 DOM 树;
  • 存放在栈上的变量;
  • ...
  • 这些根节点不是垃圾,不可能被回收。
  • 第二步:回收「不可达」的值所占据的内存。
  • 在所有的标记完成之后,统一清理内存中所有不可达的对象。
  • 第三步,做内存整理。
  • 在频繁回收对象后,内存中就会存在大量不连续空间,专业名词叫「内存碎片」。
  • 当内存中出现了大量的内存碎片,如果需要分配较大的连续内存时,就有可能出现内存不足的情况。

什么时候垃圾回收

  • 浏览器进行垃圾回收的时候,会暂停 JavaScript 脚本,等垃圾回收完毕再继续执行。
  • 对于普通应用这样没什么问题,但对于 JS 游戏、动画对连贯性要求比较高的应用,如果暂停时间很长就会造成页面卡顿。
  • 这就是我们接下来谈的关于垃圾回收的问题:什么时候进行垃圾回收,可以避免长时间暂停。
  • 所以最后一步是整理内存碎片。(但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如接下来我们要介绍的副垃圾回收器。)
  • 分代收集
  • 浏览器将数据分为两种,一种是「临时」对象,一种是「长久」对象。
  • 临时对象:
  • 大部分对象在内存中存活的时间很短;
  • 比如函数内部声明的变量,或者块级作用域中的变量。当函数或者代码块执行结束时,作用域中定义的变量就会被销毁;
  • 这类对象很快就变得不可访问,应该快点回收。
  • 长久对象:
  • 生命周期很长的对象,比如全局的 window、DOM、Web API 等等;
  • 这类对象可以慢点回收。
  • 这两种对象对应不同的回收策略,所以,V8 把堆分为新生代和老生代两个区域, 新生代中存放临时对象,老生代中存放持久对象。
  • 并且让副垃圾回收器、主垃圾回收器,分别负责新生代、老生代的垃圾回收。这样就可以实现高效的垃圾回收啦。
  • 主垃圾回收器
  • 负责老生代的垃圾回收,有两个特点:对象占用空间大;对象存活时间长。
  • 它使用「标记-清除」的算法执行垃圾回收。
  • 首先是标记。从一组根元素开始,递归遍历这组根元素;
  • 在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。然后是垃圾清除。直接将标记为垃圾的数据清理掉。多次标记-清除后,会产生大量不连续的内存碎片,需要进行内存整理。
  • 副垃圾回收器
  • 负责新生代的垃圾回收,通常只支持 1~8 M 的容量。新生代被分为两个区域:一般是对象区域,一半是空闲区域。
  • 新加入的对象都被放入对象区域,等对象区域快满的时候,会执行一次垃圾清理。先给对象区域所有垃圾做标记;标记完成后,存活的对象被复制到空闲区域,并且将他们有序的排列一遍;
  • 这就回到我们前面留下的问题 -- 副垃圾回收器没有碎片整理。因为空闲区域里此时是有序的,没有碎片,也就不需要整理了;复制完成后,对象区域会和空闲区域进行对调。将空闲区域中存活的对象放入对象区域里。这样,就完成了垃圾回收。因为副垃圾回收器操作比较频繁,所以为了执行效率,一般新生区的空间会被设置得比较小。一旦检测到空间装满了,就执行垃圾回收。
  • 一句话总结分代回收就是:将堆分为新生代与老生代,多回收新生代,少回收老生代。这样就减少了每次需遍历的对象,从而减少每次垃圾回收的耗时。
  • 增量收集
  • 如果脚本中有许多对象,引擎一次性遍历整个对象,会造成一个长时间暂停。所以引擎将垃圾收集工作分成更小的块,每次处理一部分,多次处理。
  • 这样就解决了长时间停顿的问题。
  • 闲时收集
  • 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。

Javascritp 中类型:值类型,引用类型。 引用类型: 在没有引用之后,通过 V8 自动回收。 值类型: 如果处于闭包的情况下,要等闭包没有引用才会被 V8 回收; 非闭包的情况下,等待 V8 的新生代切换的时候回收。

HTTP中常用的请求方法有哪些?哪些请求方法是安全的?为什么?

答:常用的请求方法有GET,POST,HEAD,PUT,DELETE。

其中GET和HEAD是安全的,因为其他的三个方法都会对服务器产生动作,GET和HEAD只是请求数据,POST和POUT都会给服务端发送报文主体。

简单请求和复杂请求

跨域资源共享,或者进行跨域接口访问的情况。跨域资源共享( CORS)机制允许 Web 应用服务器进行跨域访问控制。 跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是GET以外的 HTTP 请求,或者搭配某些 MIME 类型的POST请求),浏览器必须首先使用OPTIONS方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括Cookies和 HTTP 认证相关数据)。

在涉及到CORS的请求中,我们会把请求分为简单请求和复杂请求。

  • 简单请求 满足以下条件的请求即为简单请求:

请求方法:GET、POST、HEAD 除了以下的请求头字段之外,没有自定义的请求头

Accept Accept-Language Content-Language Content-Type DPR Downlink Save-Data Viewport-Width Width Content-Type的值只有以下三种(Content-Type一般是指在post请求中,get请求中设置没有实际意义)

text/plain multipart/form-data application/x-www-form-urlencoded 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器 (未验证)

XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问 请求中没有使用 ReadableStream 对象 (未验证)

  • 复杂请求 非简单请求即为复杂请求。复杂请求我们也可以称之为在实际进行请求之前,需要发起预检请求的请求。

HTTP和HTTPS的关系以及HTTPS的实现原理

答:

  • HTTPS和HTTP的关系  HTTPS在HTTP的基础上加上了TLS/SSL协议,使得HTTP通信更加安全。针对HTTP无法验证通信方的身份,无法验证报文的完整性以及容易被窃听等安全方面的缺点,HTTPS添加了加密和认证机制,使得HTTP通信更加安全。

  • HTTPS通信原理(加密机制,不包括认证机制以及验证报文完整性机制,可参考这里)

  1. 客户端发送请求
  2. 服务端接收请求返回数字证书
  3. 客户端使用内置的CA解密证书,拿到公钥
  4. 如果证书没有问题,就将自己的对称秘钥使用公钥加密发送给服务端
  5. 服务端使用私钥进行解密
  6. 客户端和服务端使用对称秘钥进行通信

HTTPS的整个详细过程

SSL/TLS协议就是为了解决上面提到的HTTP存在的问题而生的

  • 所有的信息都是加密传输的,第三方无法窃听
  • 配备身份验证,防止身份被冒充
  • 具有校验机制,一旦被篡改,通信双方会立刻发现

HTTPS使用对称加密和非对称加密结合

传输数据阶段依然使用对称加密,但是对称加密的秘钥我们采用非对称加密传输。

1.客户端请求 HTTPS 网址,然后连接到 server 的 443 端口 (HTTPS 默认端口,类似于 HTTP 的80端口)。

2.采用 HTTPS 协议的服务器必须要有一套数字 CA (Certification Authority)证书,证书是需要申请的,并由专门的数字证书认证机构(CA)通过非常严格的审核之后颁发的电子证书 (当然了是要钱的,安全级别越高价格越贵)。颁发证书的同时会产生一个私钥和公钥。私钥由服务端自己保存,不可泄漏。公钥则是附带在证书的信息中,可以公开的。证书本身也附带一个证书电子签名,这个签名用来验证证书的完整性和真实性,可以防止证书被篡改。

3.服务器响应客户端请求,将证书传递给客户端,证书包含公钥和大量其他信息,比如证书颁发机构信息,公司信息和证书有效期等。Chrome 浏览器点击地址栏的锁标志再点击证书就可以看到证书详细信息。

4.客户端解析证书并对其进行验证。如果证书不是可信机构颁布,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信如果证书没有问题,客户端就会从服务器证书中取出服务器的公钥A。然后客户端还会生成一个随机码 KEY,并使用公钥A将其加密。

5.客户端把加密后的随机码 KEY 发送给服务器,作为后面对称加密的密钥。

6.服务器在收到随机码 KEY 之后会使用私钥B将其解密。经过以上这些步骤,客户端和服务器终于建立了安全连接,完美解决了对称加密的密钥泄露问题,接下来就可以用对称加密愉快地进行通信了。

7.服务器使用密钥 (随机码 KEY)对数据进行对称加密并发送给客户端,客户端使用相同的密钥 (随机码 KEY)解密数据。

8.双方使用对称加密愉快地传输所有数据。

使用对称和非对称混合的方式,实现了数据的加密传输。但是这种仍然存在一个问题,服务器可能是被黑客冒充的。这样,浏览器访问的就是黑客的服务器,黑客可以在自己的服务器上实现公钥和私钥,而对浏览器来说,它并不完全知道现在访问的是这个是黑客的站点。 服务器需要证明自己的身份,需要使用权威机构颁发的证书,这个权威机构就是 CA(Certificate Authority), 颁发的证书就称为数字证书 (Digital Certificate)。 对于浏览器来说,数字证书有两个作用:

  • 通过数字证书向浏览器证明服务器的身份;
  • 数字证书里面包含了服务器公钥

POST和GET方法有什么区别?

答:主要有以下几点区别: GET 和 POST 本质上就是 TCP 链接,并无差别。但是由于 HTTP 的规定和浏览器 / 服务器的限制,导致他们在应用过程中体现出一些不同。

  • 从缓存的角度,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
  • 从编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。
  • 从参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。
  • 从幂等性的角度,GET是幂等的,而POST不是。(幂等表示执行相同的操作,结果也是相同的)
  • 从TCP的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)
  • 大小限制

在浏览器中输入URL到页面进行渲染的过程中发生了什么?

答:可以参考下面这张图理解:(图片来自《HTTP权威指南》) 图片描述

语言描述:经过了以下几步

  • 浏览器解析主机名
  • DNS进行域名解析,即将语义化的主机名转换成IP地址
  • 浏览器获得端口号
  • 浏览器根据得到的ip地址和端口号发起TCP连接
  • 浏览器发起HTTP请求
  • 浏览器读取服务器返回的响应报文
  • 浏览器对返回的HTML进行渲染
  • 浏览器断开TCP连接

1.4 输入 URL 到页面过程中经历了什么

这是一道老生常谈的面试题,现在我们从多进程架构角度去看中间的历经过程。

(1)用户输入

地址栏会判断输入的关键字是搜索内容,还是请求的 URL。

  • 搜索内容:使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL;

  • URL:会根据规则,把这段内容加上协议,合成为完整的 URL。

在加载新页面之前,浏览器还给了当前页面一次执行 beforeunload 事件的机会。

(2)URL 请求过程

浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程。

  • 网络进程会查找本地缓存是否缓存了该资源,如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程:

  • 进行 DNS 解析,以获取请求域名的服务器 IP 地址;

  • 如果请求协议是 HTTPS,那么还需要建立 TLS 连接;

  • 利用 IP 地址和服务器建立 TCP 连接;

  • 连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息;

  • 服务器接收到请求信息后,会根据请求信息生成响应数据,并发给网络进程;

  • 等网络进程接收之后,就开始解析响应头的内容;

  • 如果发现返回的状态码是 301 或者 302,那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始;如果是 200,那么继续;

  • 处理响应数据类型 Content-Type,值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是 HTML,那么浏览器则会继续进行导航流程。

(3)准备渲染进程

相同的协议和根域名,所以它们属于同一站点,并运行在同一个渲染进程中;

不属于同一站点,会运行在两个不同的渲染进程之中。

(4)提交文档

浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,浏览器进程在收到渲染进程的“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。

(5)渲染阶段

  • 构建 DOM 树:因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树;

  • 样式计算:

1)把 CSS 转换为浏览器能够理解的结构 styleSheets;

2)转换样式表中的属性值,使其标准化;

3)计算出 DOM 树中每个节点的具体样式:css 继承和 css 层叠;

  • 布局阶段:计算出 DOM 树中可见元素的几何位置

1)创建布局树:遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;而不可见的节点会被布局树忽略掉;

2)布局计算

  • 分层:页面中有很多复杂的效果,如 3D 变换、页面滚动、使用 z-index 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树。并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。

  • 图层绘制:绘制操作是由渲染引擎中的合成线程(将图层划分为图块)来完成的,比较复杂,还涉及栅格化(将图块转换为位图,中间涉及名词也比较多)还涉及 GPU 进程(帮忙加速栅格化)。

重排:需要更新完整的渲染流水线,所以开销也是最大的。

重绘:省去了布局(Layout)和分层(Layer)阶段,所以执行效率会比重排操作要高一些。

合成:更改一个既不要布局也不要绘制的属性,比如 transform 动画,渲染引擎将跳过布局和绘制,直接执行后面非主线程(合成线程)的更新流程。所以它的效率最高。

请简单描述一下TCP的三次握手和四次挥手的过程,两次握手可以吗?

segmentfault.com/a/119000001…

  • TCP的三次握手:

客户端发送一个SYN报文请求连接,变为SYN_SEND状态 服务端接收到客户端发送SYN包,进行确认过后,发送ACK报文,变为SYN_RECV状态 客户端接收到服务端的SYN和ACK报文后,发送ACK包进行确认,然后客户端和服务端都变成ESTABLISHED状态

  • TCP的四次挥手:

客户端没有数据要发送了,请求关闭连接,发送一个FIN报文,并进入FIN_WAIT_1状态 服务端接收到FIN报文后,发送ACK报文,然后进入CLOSE_WAIT状态;客户端接收到ACK报文后,进入FIN_WAIT_2状态 服务端判断是否有数据发送给客户端,如果有的话,就将数据发送给客户端,再发送FIN报文;没有的话就直接发送FIN报文,请求关闭连接,然后进入LAST_ACK状态; 客户端接收到服务端的FIN报文后,发送ACK报文,然后客户端进入TIME_WAIT状态;服务端接收到ACK报文后,关闭连接,变为CLOSED状态,客户端在2MSL后依然没有收到回复,也可以关闭连接了。

  •  两次握手可以吗?

 不可以,三次握手主要是防止已经过期的请求再次连接到服务端而占用资源造成浪费。如果是两次握手的话,假设主机A在发送第一次请求时,由于网络滞留的问题卡住了,很久后没有收到主机B的确认信息,于是又发送了第二次请求。过了一段时间后,第一个请求到达了主机B,主机B以为是一次新的请求,就返回确认信息,但是由于没有第三次握手,只要主机B发出确认信息,就会连接,这个时候主机B一直等待着主机A发送信息,就会造成资源浪费。(参考资料)

TCP和UDP的区别是什么?

答:主要有以下几个方面的区别:

  • 连接方面:TCP是面向连接的,而UDP是无连接的,即UDP在传输数据之前不需要像TCP那样3次握手建立连接
  • 可靠性:TCP比UDP更可靠,TCP可以保证不丢包,会按照顺序传输数据,这也是导致
  • 资源消耗:TCP对系统资源要求比较高,并且消耗资源比较大;UDP要求不高,但是在网络质量不好的情况下比较容易丢包,消耗资源相对比较小
  • 适用场景:TCP适用于HTTP,FTP以及邮件传输等等;而UDP比较适合于语音,视频等
  • 速度问题:TCP传输速度比较慢,效率低,在握手和挥手的过程中会占用很多时间;UDP传输速度比较快,由于是无连接的,只有传输数据的过程
  • 安全性:安全性和可靠性是不同的概念,由于TCP的机制比较多,更容易受到攻击;UDP相对来说就比较安全,但是也不能避免受到攻击
  • 连接形式:TCP是只能一对一的发送,而UDP可以是一对一,一对多,多对多

HTTP中常见的状态码有哪些?分别表示什么意思?

答:常见的状态码有:

  • 200:接收并响应成功
  • 301:请求的资源已经换了URL,需要重新请求(永久重定向)
  • 302:请求的资源临时换了URL,需要重新请求(临时重定向)
  • 304:在缓存中有该资源,可以直接获取
  • 307 307要求客户端不改变原先的请求方法,对在Location头部中规定的URI进行访问。对于302,很多客户端的实现是,直接使用GET方式访问重定向地址。
  • 4xx
  • 400 Bad Request: 开发者经常看到一头雾水,只是笼统地提示了一下错误,并不知道哪里出错了。
  • 403 Forbidden: 这实际上并不是请求报文出错,而是服务器禁止访问,原因有很多,比如法律禁止、信息敏感。
  • 404 Not Found: 资源未找到,表示没在服务器上找到相应的资源。
  • 405 Method Not Allowed: 请求方法不被服务器端允许。
  • 406 Not Acceptable: 资源无法满足客户端的条件。
  • 408 Request Timeout: 服务器等待了太长时间。
  • 409 Conflict: 多个请求发生了冲突。
  • 413 Request Entity Too Large: 请求体的数据过大。
  • 414 Request-URI Too Long: 请求行里的 URI 太大。
  • 429 Too Many Request: 客户端发送的请求过多。
  • 431 Request Header Fields Too Large请求头的字段内容太大。
  • 5xx
  • 500 Internal Server Error: 仅仅告诉你服务器出错了,出了啥错咱也不知道。
  • 501 Not Implemented: 表示客户端请求的功能还不支持。
  • 502 Bad Gateway: 服务器自身是正常的,但访问的时候出错了,啥错误咱也不知道。
  • 503 Service Unavailable: 表示服务器当前很忙,暂时无法响应服务。

HTTP 报文头字段

  • 请求头(Request):
  • Accept:text/html application/xml 告诉服务器客户端浏览器这边可以出里什么数据;
  • Accept-Encodeing:gzip 告诉服务器我能支持什么样的压缩格式
  • accept-language:告诉服务器浏览器支持的语言
  • Cache-control:告诉服务器是否缓存
  • Connection:keep-alive 告诉服务器当前保持活跃(与服务器处于链接状态)
  • Host:远程服务器的域名
  • User-agent:客户端的一些信息,浏览器信息 版本
  • referer:当前页面上一个页面地址。一般用于服务器判断是否为同一个域名下的请求
  • 返回头(response-header):
  • cache-control:private/no-cache; 私有的不需要缓存/no-cache也不需要缓存
  • connection:keep-live; 服务器同意保持连接
  • content-Enconding:gzip;除去头的剩余部分压缩返回格式
  • content-length:内容长度
  • content-type:text/css;返回内容支持格式
  • Date: 时间
  • server:ngnix 服务器类型
  • set-Cookie:服务器向客户端设置cookie 第一次访问服务器会下发cookie当作身份认证信息,第二次访问服务器再把cookie送给服务器,可以当作认证信息
  • last-modified: 时间戳 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。
  • expires 告诉浏览器把回送的资源缓存多长时间 -1或0则是不缓存
  • etag:版本专有的加密指纹。(有的网站不用,并非必须)优先检查etag再检查last-modified的时间戳。向服务器请求带if-none-match,服务器判断是否过期未过期返回304,过期返回200

HTTP有什么缺点?

答:有以下缺点:

一条连接上只能发送一个请求 请求只能从客户端开始,客户端不可以接收除响应之外的指令 请求/响应首部未经压缩就发送,首部信息越多造成的延迟越大 发送冗长的首部,每次互发相同的首部造成很大的浪费 通信使用明文,内容可能会被窃听 不验证通信方的身份,有可能会遭遇伪装 无法验证报文的完整性,有可能遭遇篡改

请描述AJAX实现原理

答:AJAX的实现核心在于XMLHttpRequest这个API,主要分为以下几个步骤(参考文章):

  • 创建XMLHttpRequst对象
  • 初始化传进来的参数
  • 使用open方法发送请求
  • 接收请求并调用onreadystatechange方法对返回成功以及失败的情况进行定义

什么是HTTP持久化和管线化?

答:HTTP持久化是针对HTTP无连接的特点进行改进的,使用的是keep-live首部字段保持长连接 HTTP管线化是针对HTTP每次只能是请求一次回答一次的模式进行改进,使得可以连续发送多次请求。

webScoket是什么?主要作用是什么?

答:webScoket也是一种协议,概括地说就是:支持双向通信,更灵活,更高效,可扩展性更好。用来解决要实现实时更新时,只能通过AJAX轮询等方式的问题。使用这个协议就改变了只能客户端发送请求,服务端接收请求的模式,在该协议中客户端和服务端都可以发送请求和接收请求,从而实现实时推送(服务端有数据更新后就发送数据)的功能,基于该协议可以大大的减少通信量。

建立TCP连接 WebSocket是基于TCP协议的,所以第一步就是要建立一个TCP连接,关于TCP的知识,本文中就不赘述了,不熟悉的同学可以搜索相关资料或者书籍学习下。这里我们直接用NodeJS的net模块来创建一个TCP服务,监听7002端口。如下代码:

//index.js
var net = require('net');

net.createServer(function(socket) {
    console.info('tcp client connected');

    socket.on('data', function(data) {

    }); 

    socket.on('end', function() {
        console.info('client disconnected');
    }); 
}).listen(7002);

建立TCP连接之后,开始建立WebSocket连接,上文说过WebSocket连接只需一次成功握手即可建立

1)首先客户端会发送一个握手包。这里就体现出了WebSocket与Http协议的联系,握手包的报文格式必须符合HTTP报文格式的规范。其中:

  • 方法必须位GET方法

  • HTTP版本不能低于1.1

  • 必须包含Upgrade头部,值必须为websocket

  • 必须包含Sec-WebSocket-Key头部,值是一个Base64编码的16字节随机字符串。

  • 必须包含Sec-WebSocket-Version头部,值必须为13

  • 其他可选首部可以参考:tools.ietf.org/html/rfc645… 2)服务端验证客户端的握手包符合规范之后也会发送一个握手包给客户端。格式如下:

  • 必须包含Connection头部,值必须为Upgrade

  • 必须包含一个Upgrade头部,值必须为websocket

  • 必须包含一个Sec-Websocket-Accept头部,值是根据如下规则计算的:

首先将一个固定的字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接到Sec-WebSocket-Key对应值的后面。 对拼接后的字符串进行一次SHA-1计算 将计算结果进行Base-64编码

3)客户端收到服务端的握手包之后,验证报文格式时候符合规范,以2)中同样的方式计算Sec-WebSocket-Accept并与服务端握手包里的值进行比对。

//index.js
var net = require('net');
var crypto = require('crypto');

var wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

net.createServer(function(socket) {
    console.info('tcp client connected');

    socket.on('data', function(data) {
        var dataString = data.toString(),
            key = getWebSocketKey(dataString),
            acceptKey;
        if (key) {
            console.info(key);
            acceptKey = genAcceptKey(key);
            socket.write('HTTP/1.1 101 Switching Protocols\r\n');
            socket.write('Upgrade: websocket\r\n');
            socket.write('Connection: Upgrade\r\n');
            socket.write('Sec-WebSocket-Accept: ' + acceptKey + '\r\n');
            socket.write('\r\n');
        }
    });

    socket.on('end', function() {
        console.info('client disconnected');
    });
}).listen(7002);

function getWebSocketKey(dataStr) {
    var match = dataStr.match(/Sec\-WebSocket\-Key:\s(.+)\r\n/);
    if (match) {
         return match[1];
    }
    return null;
}

function genAcceptKey(webSocketKey) {
    return crypto.createHash('sha1').update(webSocketKey + wsGUID).digest('base64');
}
var ws = new WebSocket("ws://localhost:7002");
ws.onopen = function() {
    console.info('connected');
};

SSL有几次握手?具体过程是怎样的?

答:这个问题和HTTPS的实现原理可以看做是一样的,但是比较有针对性,以下是回答:  SSL有4次握手,握手过程为:

TLS/SSL中使用了非对称加密,对称加密以及HASH算法。握手过程的具体描述如下:

  1. 浏览器将自己支持的一套加密规则发送给网站。 

  2. 网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。 

  3. 浏览器获得网站证书之后浏览器要做以下工作: 

    a) 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。 

    b) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。 

    c) 使用约定好的HASH算法计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。 

  4. 网站接收浏览器发来的数据之后要做以下的操作: 

    a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。 

    b) 使用密码加密一段握手消息,发送给浏览器。 

  5. 浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。

False Start

False Start 有抢跑的意思,TLS False Start 是指客户端在发送 Change Cipher Spec Finished 同时发送应用数据(如 HTTP 请求),服务端在 TLS 握手完成时直接返回应用数据(如 HTTP 响应)。这样,应用数据的发送实际上并未等到握手全部完成,故谓之抢跑。 还有就是 ,TLS 缓存,快速握手。以及域名证书查询。客户端查询改为服务端查询,返回给客户端

CDN的工作原理

Content Delivery Network”,中文叫内容分发网络。 CDN的工作原理,下面以客户端发起对"join.qq.com/video.php"的HTTP请求为例进行说明:

  1. 用户发起对"join.qq.com/video.php"的HTTP请求,首先需要通过本地DNS通过"迭代解析"的方式获取域名"join.qq.com"的IP地址;
  2. 如果本地DNS(域名系统) 的缓存中没有该域名的记录,则向根DNS发送DNS查询报文;
  3. 根DNS发现域名的前缀为"com",则给出负责解析com的顶级DNS的IP地址;
  4. 本地DNS向顶级DNS发送DNS查询报文;
  5. 顶级DNS发现域名的前缀为"qq.com",在本地记录中查找负责该前缀的权威DNS的IP地址并进行回复;
  6. 本地DNS向权威DNS发送DNS查询报文;
  7. 权威DNS查找到一条NAME字段为"join.qq.com"的CNAME记录(由服务提供者配置),该记录的Value字段为"join.qq.cdn.com";并且还找到另一条NAME字段为"join.qq.cdn.com"的A记录,该记录的Value字段为GSLB的IP地址;
  8. 本地DNS向GSLB发送DNS查询报文;
  9. GSLB(全局负载均衡) 根据本地DNS的IP地址判断用户的大致位置为深圳,筛选出位于华南地区且综合考量最优的SLB的IP地址填入DNS回应报文,作为DNS查询的最终结果;
  10. 本地DNS回复客户端的DNS请求,将上一步的IP地址作为最终结果回复给客户端;
  11. 客户端根据IP地址向SLB发送HTTP请求:"join.qq.com/video.php";
  12. SLB综合考虑缓存服务器集群中各个节点的资源限制条件、健康度、负载情况等因素,筛选出最优的缓存节点后回应客户端的HTTP请求(状态码为302,重定向地址为最优缓存节点的IP地址);
  13. 客户端接收到SLB的HTTP回复后,重定向到该缓存节点上;
  14. 缓存节点判断请求的资源是否存在、过期,将缓存的资源直接回复给客户端,否则到源站进行数据更新再回复。

其中较为关键的步骤为6~9,与普通的DNS过程不同的是,这里需要服务提供者(源站)配置它在其权威DNS中的记录,将直接指向源站的A记录修改为一条CNAME记录及其对应的A记录,CNAME记录将目标域名转换为GSLB的别名,A记录又将该别名转换为GSLB的IP地址。通过这一系列的操作,将解析源站的目标域名的权力交给了GSLB,以致于GSLB可以根据地理位置等信息将用户的请求引导至距离其最近的"缓存节点",减缓了源站的负载压力和网络拥塞。

CDN 获取最近节点资源的算法是什么

智能 DNS 分线路或者分地区解析

跨域解决方案

同源是指"协议+域名+端口"三者相同,

同源策略限制以下几种行为: (1) Cookie、LocalStorage 和 IndexDB 无法读取。(2) DOM 无法获得。(3) AJAX 请求不能发送。

  • 1、 通过jsonp跨域 通过jsonp跨域通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。jsonp正是利用这个特性来实现的。 优缺点:JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。只能实现get一种请求、不安全 容易遭到xss攻击

JSONP 是一种非正式传输协议,允许用户传递一个callback给服务端,然后服务端返回数据时会将这个callback 参数作为函数名来包裹住 JSON 数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。当GET请求从后台页面返回时,可以返回一段JavaScript代码,这段代码会自动执行,可以用来负责调用后台页面中的一个callback函数。

  • 2、CORS 普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。

目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。CORS与JSONP的使用目的相同,但是比JSONP更强大。JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

  • 3、 document.domain + iframe跨域 此方案仅限主域相同,子域不同的跨域应用场景(网页一级域名相同,只是二级域名不同)。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
  • 4、 location.hash + iframe 实现原理: a与b跨域相互通信,通过中间页c来实现(且c与a是同域)。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
  • 5、 window.name + iframe跨域
  • 6、 postMessage跨域
  • HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。它可用于解决以下方面的问题:
  • a.) 页面和其打开的新窗口的数据传递
  • b.) 多窗口之间消息传递
  • c.) 页面与嵌套的iframe消息传递
  • d.) 上面三个场景的跨域数据传递
  • 7、 nginx代理跨域
  1. 浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。location / { add_header Access-Control-Allow-Origin *;}
  2. nginx反向代理接口跨域
  • 8、 nodejs中间件代理跨域 http-proxy-middleware
  • 9、 WebSocket协议跨域

前端工程化

  1. 制定各项规范,让工作有章可循统一团队成员的编码规范,便于团队协作和代码维护
  • 目录结构,文件命名规范
  • 编码规范:eslint, stylelint
  • 开发流程的规范使用敏捷,增强开发进度管理和控制
  • 应对各项风险,需求变更等
  • code review 机制
  • UAT 提升发布的需求的质量前后端接口规范,
  • 其他文档规范
  1. 使用版本控制工具,高效安全的管理源代码使用
  • git 版本控制工具Git分支管理
  • Commit描述规范,例如:task-number + task 描述创建 merge request,
  • code review 完毕之后方可合并代码
  1. 使用合适的前端技术和框架,提高生产效率,降低维护难度

目标: 职责分离、降低耦合,增强代码的可读性、维护性和测试性。

采用模块化的方式组织代码

  • JS 模块化:AMD、CommonJS、UMD、ES6 Module
  • CSS 模块化:less、sass、stylus、postCSS、css module 采用组件化的编程思想,处理 UI 层react、vue、angular

将数据层分离管理redux、mbox使用面向对象或者函数编程的方式组织架构

  1. 提高代码的可测试性,引入单元测试,提高代码质量5. 通过使用各种自动化的工程工具,提升整个开发、部署效率

SSR和客户端渲染区别

客户端渲染和服务器端渲染的最重要的区别就是究竟是谁来完成html文件的完整拼接,

  • SPA(single page application) 单页面应用,是前后端分离时提出的一种解决方案。 优点:页面之间切换快;减少了服务器压力; 缺点:首屏打开速度慢,不利于 SEO 搜索引擎优。
  • SEO(search engine optimization)搜索引擎优化,利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。 我们之前说 SPA 单页面应用,通过 ajax 获取数据,这就难保证我们的页面能被搜索引擎收到。并且有一些搜索引擎不支持的 js 和通过 ajax 获取的数据,那就更不用提 SEO 了,为解决这个问题,SSR 登场了···
  • SSR (server side rendering)服务端渲染,SSR 的出现一定程度上解决了 SPA 首屏慢的问题,又极大的减少了普通 SPA 对于 SEO 的不利影响。 优点: 前端耗时少。因为后端拼接完了html,浏览器只需要直接渲染出来。 有利于SEO。因为在后端有完整的html页面,所以爬虫更容易爬取获得信息,更有利于seo。 无需占用客户端资源。即解析模板的工作完全交由后端来做,客户端只要解析标准的html页面即可,这样对于客户端的资源占用更少,尤其是移动端,也可以更省电。 后端生成静态化文件。即生成缓存片段,这样就可以减少数据库查询浪费的时间了,且对于数据变化不大的页面非常高效 。 缺点: 占用更多的 CUP 和内存资源; 一些常用的浏览器的 api 可能无法正常使用,比如 window,document,alert等,如果使用的话需要对运行环境加以判断。

浏览器渲染流程

  • 解析HTML生成DOM树 - 渲染引擎首先解析HTML文档,生成DOM树
  • 构建Render树 - 接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM树,根据DOM树与CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree),
  • 布局Render树 - 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置
  • 绘制Render树 - 最后遍历渲染树并用UI后端层将每一个节点绘制出来

CDN

CDN 的核心功能,一个是缓存,一个是回源。

缓存就是说我们把资源复制一份到 CDN 服务器上,回源就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),重新向向web 服务器(或者它的上层服务器)重新取这个资源。

CDN中一方面有很多的缓存策略,从而可以提高加载速度。 另一方面CDN有路由算法,可以选择用户访问速度最快的节点进行接入,从而加快RTT(Roud Trip Time)

  • 资源调度:CDN会根据用户接入网络的ip寻找距离用户最优路径的服务器。调度的方式主要有DNS调度、http 302调度、使用 HTTP 进行的 DNS 调度(多用于移动端);
  • 缓存策略和数据检索:CDN服务器使用高效的算法和数据结构,快速的检索资源和更新读取缓存;
  • 网络优化:从OSI七层模型进行优化,达到网络优化的目的。

HTTP2, HTTP2的性能优化方面,真的优化很多么?

  • HTTP/2做了什么
  1. HTTP/2是二进制协议
  2. 多路复用技术
  3. 压缩HTTP头
  4. 服务端推送
  5. 协议层面提供的传输控制能力,如流量控制、优先级等 需要重点说的是多路复用,前面说过Pipelining技术会有阻塞的问题,HTTP/2的多路复用可以粗略的理解为非阻塞版的Pipelining。即可以同时通过一个HTTP连接发送多个请求,谁先响应就先处理谁,这样就充分的压榨了TCP这个全双工管道的性能。加载性能会是HTTP1.1的几倍,需要加载的资源越多越明显。当然多路复用是建立在加载的资源在同一域名下,不同域名神仙也复用不了。

HTTP头压缩。以前我们的头部信息很小,没有压缩的必要,但是现在我们发送的头信息已经大的惊人了,可以看看现在的cookie。

HTTP2

HTTP2 相比 HTTP1.1 有如下几个优点:

解析速度快 服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段。

多路复用 HTTP1.1 如果要同时发起多个请求,就得建立多个 TCP 连接,因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。

在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。同一个请求和响应用一个流来表示,并有唯一的流 ID 来标识。 多个请求和响应在 TCP 连接中可以乱序发送,到达目的地后再通过流 ID 重新组建。

首部压缩 HTTP2 提供了首部压缩功能,HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送。

优先级 HTTP2 可以对比较紧急的请求设置一个较高的优先级,服务器在收到这样的请求后,可以优先处理。

流量控制 由于一个 TCP 连接流量带宽(根据客户端到服务器的网络带宽而定)是固定的,当有多个请求并发时,一个请求占的流量多,另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制。

服务器推送 HTTP2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。

性能优化

  • 减少 HTTP 请求
  • 使用 HTTP2
  • 使用服务端渲染
  • CDN
  • 将 CSS 放在文件头部,JavaScript 文件放在底部
  • 降低 CSS 选择器的复杂性
  • 使用字体图标 iconfont 代替图片图标
  • 用缓存,不重复加载相同的资源
  • 压缩文件
  • 图片优化。webp
  • 通过 webpack 按需加载代码,提取第三库代码,减少 ES6 转为 ES5 的冗余代码
  • 减少重绘重排
  • 使用 requestAnimationFrame 来实现视觉变化 浏览器使用与 requestAnimationFrame 类似的机制,requestAnimationFrame比起setTimeout,setInterval设置动画的优势主要是:1)requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。2)在隐藏或不可见的元素中requestAnimationFrame不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
  • 使用 transform 和 opacity 属性更改来实现动画

Object.defineProperty和proxy的区别

  • Object.defineProperty的问题有三个:不能监听数组的变化(数组的这些方法不能触发set:push、pop、shift、unshift、splice、sort、reverse)。Vue对其变异方法进行重写。必须遍历对象的每个属性(Object.defineProperty劫持对象的属性)
  • proxy针对对象:针对整个对象,而不是对象的某个属性,可以监听数组的变化,嵌套支持,可劫持的方法更多,但是兼容性不好

ajax

// get请求
var xhr = new XMLHttpRequest();

xhr.open('GET', '/api/user?id=333', true);
xhr.send();

xhr.onreadystatechange = function (e) {
  if (xhr.readyState == 4 && xhr.status == 200) {
    console.log(xhr.responseText);
  }
};
var xhr = new XMLHttpRequest();

xhr.open('POST', '/api/user', true);
// POST请求需要设置此参数
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
xhr.send('name=33&ks=334');

xhr.onreadystatechange = function (e) {
  if (xhr.readyState == 4 && xhr.status == 200) {
    console.log(xhr.responseText);
  }
};
// Promise
let request = obj => {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest();
        xhr.open(obj.method || "GET", obj.url);
        if (obj.headers) {
            Object.keys(obj.headers).forEach(key => {
                xhr.setRequestHeader(key, obj.headers[key]);
            });
        }
        xhr.onload = () => {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(xhr.response);
            } else {
                reject(xhr.statusText);
            }
        };
        xhr.onerror = () => reject(xhr.statusText);
        xhr.send(obj.body);
    });
};

// 进度条
function fetchProgress(url, opts={}, onProgress){
    return new Promise(funciton(resolve, reject){
        var xhr = new XMLHttpRequest();
        xhr.open(opts.method || 'get', url);
        for(var key in opts.headers || {}){
            xhr.setRequestHeader(key, opts.headers[key]);
        }

        xhr.onload = e => resolve(e.target.responseText)
        xhr.onerror = reject;
        if (xhr.upload && onProgress){
            xhr.upload.onprogress = onProgress; //上传
        }
        if ('onprogerss' in xhr && onProgress){
            xhr.onprogress = onProgress; //下载
        }
        xhr.send(opts.body)
    })
}
fetchProgress('/upload').then(console.log)

跨端

WebView 跨平台技术 Weex/React-Native 跨平台技术 Flutter

小程序

  1. 提供了JavsScript运行环境,由JavaScript编写的业务代码完成逻辑层的处理

  2. 通过数据传输接口(注册Page时的data属性及后续的setData方法调用)将逻辑层的数据传输给视图层 数据在逻辑层与视图层间如何传输? 视图基本是在webview环境中渲染的,只有少数组件如map video为了性能native化。 而逻辑层如上所述,是跑在JsCore中的JavaScript代码。 有了JsCore,微信小程序框架的native端与js端就可以通过JsCore来相互通信了

  3. 视图层由WXML语言编写的模板通过“数据绑定”与逻辑层传输过来的数据merge成展现结果并展现

  4. 视图的样式控制由WXSS语言编写的样式规则进行配置

表单可以跨域吗

同源策略主要是限制js行为,form表单提交的结果js是无法拿到,所以没有去限制. 当然不限制也是有漏洞了,csrf攻击就能利用form表单能带cookie的特点. 而cookie的新属性SameSite就能用来限制这种情况

GraphQL

GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

  • 多终端的出现,APP、小程序、PC端都需要相同的接口,但是又略有差异,常规接口需要提供几套,GraphQL的话只需要写好查询语句即可
  • 天生的聚合接口,以前一个页面需要请求不同的数据,我们可以请求多个接口,我们可以让服务端进行聚合,有了GraphQL后我们可以自己去聚合想要的数据
  • 不用被版本困扰,之前写接口的时候为了兼容老的项目可以正常被访问,尤其是APP,线上的项目,我们接口肯定是不能影响线上的,所以有比较大的改变的时候,只能升级版本,有了GraphQL后就无需关心版本问题了,接口还是那个接口查询语句变一下就好了
  • 迁移很简单,服务端在之前的接口上稍加改造就好,前端写查询语句

使用js如何改变url,并且页面不刷新?

改变URL的目的是让js拿到不同的参数,进行不同的页面渲染,其实就是vue-router的原理 最简单的就是改变hash,改变hash是并不会刷新页面的,也会改变URL,也能监听hashchange事件进行页面的渲染

还有一种就是使用history.pushState()方法,该方法也可以改变url然后不刷新页面,但是该方法并不能够触发popstate事件,不过pushState使我们手动触发的,还能不知道url改变了么,其实这时候并不需要监听popstate我们就能够知道url改变拿到参数并渲染页面

window.requestAnimationFrame()

告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行 浏览器帧渲染的钩子函数,一般不渲染不触发 做动画比较合适因为这个函数在每帧渲染的开始与上一帧结束的时候触发,能最大限度的利用浏览器的渲染流程 window.requestAnimationFrame() 告诉浏览器,你想执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。 requestAnimationFrame 优势: 1、使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源 2、而requestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的requestAnimationFrame也会停止渲染 3、当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。 4、函数节流:为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销 5、一个刷新间隔内函数执行多次时没有意义的,因为显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。

使用css3动画代替js的动画有什么好处?

css和js动画各有优劣

  • 不占用JS主线程
  • 可以利用硬件加速
  • 浏览器可对动画做优化(元素不可见时不动画,减少对FPS的影响)

XSS和CSRF的理解,他们之间的区别是啥

XSS: 通过客户端脚本语言(最常见如:JavaScript) 在一个论坛发帖中发布一段恶意的JavaScript代码就是脚本注入,如果这个代码内容有请求外部服务器,那么就叫做XSS!

CSRF:又称XSRF,冒充用户发起请求(在用户不知情的情况下),完成一些违背用户意愿的请求(如恶意发帖,删帖,改密码,发邮件等)。

xss前端主要是控制好输入,cookie设置http-only

csrf需要严格的cookie策略,增加token校验等

cookie localStorage session

cookie

在cookie赋值语法部分,我们知道document.cookie = ''不能用来清除cookie; 描述obj1的hello属性的是value和writable,用于描述obj2的hello属性的是get和set。简单的说,获取和修改data property是直接的,获取和修改accessor property是间接的, document的cookie属性也是accessor property。

Object.getOwnPropertyDescriptor(document, 'cookie') // undefined
Object.getOwnPropertyDescriptor(document.__proto__.__proto__, 'cookie')

在cookie赋值算法部分,我们知道:

同时设置了expires和max-age,max-age有效; 假设已经有一个名为hello的cookie,再次设置名为hello的cookie的时候,如果name,domain和path属性完全相同就是修改,否则是新增; 在cookie取值算法部分,我们知道:

如何把字符串形式的cookie改成json的形式的时候,可以先通过; 分割一次,在通过=分割一次;

var cookieStr = document.cookie
var cookieArray = cookieStr.split('; ') // 获得每个cookie的字符串形式组成的数组
var cookieObj = {}
cookieArray.forEach(item => {
    var index = item.indexOf('=') // 获取每个cookie字符串的key和value
    if (index !== -1) { // 没有使用split的原因是value里面也是可以包含=号的
        cookieObj[item.slice(0, index)] = item.slice(index + 1)
    }
})
console.log(cookieObj)

如果设置了多个同名cookie,所有同名的cookie都会出现在最终的cookie字符串中,按照path属性的长度和creation-times排序。

JS引擎

juejin.cn/post/684490…

不像其他编译型语言,编译之后成为机器码可以直接在主机上运行。 JS是运行在一个宿主环境中的(一个可以识别并且执行JS代码的程序),这个容器一般必须要做2件事情:

  • 解析JS代码,转换成可执行的机器语言
  • 暴露一些额外的对象(API),可以与JS代码做交互

做第一部分工作的就是Javascript引擎
做第二部分功能的实际上就是我们本节所说的 Javascript Runtime, 因此我们可以粗略地理解为, Javascript Runtime 就是JS宿主环境创建的一个scope, 在这个scope内JS可以访问宿主环境提供的一系列特性 那么在这两个环境中,对应的引擎和runtime分别如下:

宿主环境JS引擎运行时特性
浏览器chrome V8引擎DOM、 window对象、用户事件、Timers等
node.jschrome V8引擎require对象、 Buffer、Processes、fs 等

JS源代码是需要经过JS引擎解析、转化之后才执行的。
通常认为,JS引擎主要有两部分组成:

  • 内存堆 引用类型实际值、内存分配的地方
  • 调用栈 基本类型存储、引用类型地址名存储、代码逻辑执行的地方

源代码进入JS引擎之后,顺序读取代码,按照变量声明、函数执行等不同规则,分配到堆、或者栈内。