frontEnd

11.03-11.09

数据结构与算法

链表反转

思路: 迭代法, 首先将后一个指针节点存在一个临时节点中,然后将当前节点的next节点指向前指针节点,让前指针节点指向当前节点,更新当前节点.

时间复杂度O(n) 空间复杂度O(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let reverseList = function(head) {

let cur = head; // 当前指针节点
let pre = null; // 前指针节点

while(cur !== null) { // 当指针节点不为空时
let temp = cur.next; // 创建临时节点存放当前节点的下一节点
cur.next = pre; // 改变当前指针节点的指向
pre = cur; // 前指针节点指向当前指针节点
cur = temp; // 当前指针节点变成下一个节点,进入下一个循环
}

return pre

};

环形链表

思路: 判断是否有环形链表,比较暴力的方法是hashmap,遍历所有结点并在哈希表中存储每个结点的引用,判断是否有空节点,这样空间复杂度就比较高.
推荐”快慢指针法”,两个指针同时跑,如果有环则一定相遇.
但需要证明双指针不会陷入死循环。假设链表从头开始,以0,1,2…m…n计,然后到节点n后下一个节点又引回到节点m,需要证明一定存在步数k,使得快指针运行了2k步,慢指针运行k步后指到同一个节点。显然k>=m,且两个指针在m和n之间的某点相遇。于是慢指针走k步后到达的节点序列为m +(k-m)%(n-m+1),快指针走2k步的位置是m +(2k-m)%(n-m+1),两者相等,有 (k-m)%(n-m+1)= (2k-m)%(n-m+1),只要k能被(n-m+1)

时间复杂度O(n) 空间复杂度O(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let hasCycle = function(head) {
let slow = head
let fast = head

if (head === null) {
return false
}

while (fast !== null && fast.next !== null) {
slow = slow.next
fast = fast.next.next
if (slow === fast) {
return true
}
}

return false

};

两数之和

思路: 用object代替数组存储,降低时间复杂度和空间复杂度

时间复杂度O(n)

1
2
3
4
5
6
7
8
9
10
let twoSum = function(nums, target) {
let obj = {}
for (let i = 0; i < nums.length; i++) {
if (obj[nums[i]] !== undefined) {
return [obj[nums[i]], i]
} else {
obj[target - nums[i]] = i
}
}
};

二叉树的层次遍历

思路:

方法一(推荐):利用队列实现BFS(广度优先搜索),迭代(循环函数)
使用队列存放每一层的节点, 然后在循环中记录值, 并且把子节点添加进去
外循环负责遍历层级结构, 内循环负责遍历每一层的子节点

时间复杂度:O(n),因为每个节点恰好会被运算一次。
空间复杂度:O(n),保存输出结果的数组包含 N 个节点的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
let levelOrder = function(root) {
if (!root) return []
let result = []
let queue = [root]
while(queue.length) {
let currLevel = []
for (let i = 0; i < queue.length; i++) {
let curr = queue.shift()
curr.left && queue.push(curr.left)
curr.right && queue.push(curr.right)
currLevel.push(curr.val)
}
result.push(currLevel)
}
return result
};

方法二: DFS(深度优先搜索),递归(深层调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let levelOrder = function(root, floor = 0, arr = []) {
if (!root) return arr

levelOrder(root.left, floor + 1, arr)

if (arr[floor]) {
arr[floor].push(root.val)
} else {
arr[floor] = [root.val]
}

levelOrder(root.right, floor + 1, arr)
return arr

};

排序算法

常用的排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序

冒泡排序

平均时间复杂度O(n^2)
最快情况: O(n)
最慢情况: O(n^2)
空间复杂度: O(1)
in-place(占用常数内存)
稳定

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j + 1]
arr[j + 1] = arr[j]
arr[j] = temp
}
}
}
return arr
}

快速排序

平均时间复杂度O(n log n)
最快情况: O(n log n)
最慢情况: O(n^2)
空间复杂度: O(log n)
in-place(占用常数内存)
不稳定

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function quickSort(arr) {
if (arr.length <= 1) {
return arr
}
// pivot基准索引
let pivotIndex = Math.floor(arr.length / 2)
// 找到基准,并且把基准从原数组中删除
let pivot = arr.splice(pivotIndex, 1)[0]
// 定义左右数组
let left = []
let right = []
// 比基准小的放在left,基准大的放在right
arr.forEach(el => {
if (el < pivot) {
left.push(el)
} else {
right.push(el)
}
})
return quickSort(left).concat([pivot], quickSort(right))
}

插入排序

平均时间复杂度O(n^2)
最快情况: O(n)
最慢情况: O(n^2)
空间复杂度: O(1)
in-place(占用常数内存)
稳定

原理: 通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function insertionSort(arr) {
var len = arr.length
var preIndexm, current
for (let i = 1; i < len; i++) {
preIndex = i - 1
current = arr[i]
while(preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex]
preIndex --
}
arr[preIndex + 1] = current
}
return arr
}

前端

JS

ES5的原型和原型链

1
2
var foo = function() {}
f1 = new foo()

原型链: f1 ->proto-> foo.prototype ->proto-> Object.prototype ->proto-> null

Javascript中所有的对象都是Object的实例, 并继承Object.prototype的属性和方法, Object.prototype是所有对象的爸爸。

对象:proto和 constructor
函数:proto和 constructor 和 prototype

proto和constructor属性是对象所独有的, prototype属性是函数所独有的, 因为函数是一种对象,所以函数也拥有proto和constructor属性 proto属性的作用就是当访问一个对象的属性时,如果对象内部不存在这个属性时,那就会去它的proto属性指向的那个对象(父对象)里面找,直到proto属性的终点null,然后返回undefined, 通过proto属性将对象连接起来的这条链路就是原型链.

  • prototype 属性的作用是让该函数所实例化的对象们都可以找到共用的属性和方法,将f1.ptoto=== foo.prototype
  • constructor属性的含义就是指向该对象的构造函数,所有函数(视为对象)最终的构造函数都指向Function

继承的方式

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

  1. 原型链继承
    思想: 利用原型让一个引用类型继承另一个引用类型的属性和方法。
    注意: 原型链继承的本质就是将B的原型重写为A的实例,这时,B的原型不仅具有作为一个A的实例所拥有的全部属性和方法, 而且其内部的proto指向了A的原型。最终结果就是:生成的实例1指向B的原型,B的原型又指向A的原型。很少会单独使用原型链继承这种方式。
    优点: 容易实现,父类新增原型属性和方法,子类都能访问到
    缺点: 原型属性在所有实例中是共享的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function A() { this.a = true }
    A.prototype.getValue = function() {
    return this.a
    }

    function B() {}
    // 继承A
    B.prototype = new A()
    B.prototype.getValue = function() {
    return this.a
    }
    new B()
  2. 构造函数继承
    思想: 在子类型构造函数内部调用超类型构造函数
    注意: 每个B的实例在创建的时候都会调用A构造函数,B的每个实例就都会具有自己的属性的副本了
    优点: (1) 每个实例不会共享属性,实例1的属性改变不会影响实例2的属性值;
    (2) 相比原型链的方法,构造函数方法可以传递参数。
    缺点: (1) 方法都在构造函数中定义,无法复用函数
    (2) A的原型方法对子类型和实例不可见。

1
2
3
4
5
function A() { this.a = [1,2,3] }
function B() {
A.call(this)
}
new B()
  1. 组合式继承
    思路: 其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

优点: 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点。
缺点: B的构造函数会被调用两次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function A(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}

A.prototype.sayName = function(){
console.log(this.name);
};

function B(name, age){
A.call(this, name);
this.age = age;
}

B.prototype = new A("hehe");

B.prototype.sayAge = function(){
console.log(this.age);
};
  1. 原型式继承
    思路: 在object()函数内部,先创建了一个临时性构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
    优点: 不用在执行和建立person的实例
    缺点: 同原型链继承一样,引用类型值的属性是被共享的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function object(o){
function F(){}
F.prototype = o
return new F()
}

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

var anotherPerson = object(person)
anotherPerson.name = "Greg"
anotherPerson.friends.push("Rob")
  1. 寄生式继承
    创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地做了所有工作一样返回对象。
1
2
3
4
5
function createAnother(original) {
var clone = object(original);
clone.sayHi = function(){ alert('1')}
return clone;
}

基于 person 返回了一个新对象——anotherPerson。新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi()方法。

缺点:
(1) 在createAnother()中为对象添加函数,不能做到函数复用
(2) 由于是浅复制person对象,anotherPerson的改变会影响person对象

  1. 寄生组合式继承
    通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。基本思路:不必为了指定子类型的原型而调用超类型的构造函数。
    本质:使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function A(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}

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

function B(name, age){
A.call(this, name);
this.age = age;
}

function inheritPrototype(b, a){
var prototype = object(a.prototype); //create object
prototype.constructor = b; //augment object
b.prototype = prototype; //assign object
}

inheritPrototype(B, A);

B.prototype.sayAge = function(){
alert(this.age)
};

B.prototype. proto指向Object.prototype,因为在inheritPrototype()中,副本是由基类型Object()创建的。

优点
(1) 既能具有组合继承的优点,又可以不必两次调用超类型的构造函数
(2) 避免了在 B.prototype 上面创建不必要的、多余的属性(在原型链继承时,B.prototype被重写为A的实例,因此具有了他的实例属性)

ES6的继承

主要要注意的是class的继承。

基本用法:Class之间通过使用extends关键字,这比通过修改原型链实现继承,要方便清晰很多

1
2
3
4
5
6
7
8
9
10
class Colorpoint extends Point {
constructor(x,y,color){
super(x,y); //调用父类的constructor(x,y)
this.color = color
}
toString(){
//调用父类的方法
return this.color + ' ' + super.toString();
}
}

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工,如果不调用super方法,子类就得不到this对象。因此,只有调用super之后,才可以使用this关键字。

prototype 和proto

一个继承语句同时存在两条继承链:一条实现属性继承,一条实现方法的继承

1
2
3
class A extends B{}
A._proto_=== B; //继承属性
A.prototype._proto_== B.prototype;//继承方法

new的实现原理

基本现象:
例如: function A(name) { this.name = name } a = new A(‘xxx’)

  • a是一个普通的对象
  • a instanceof A // true
  • a.constructor === A // true
  • a.proto=== A.prototype // true

实现原理:

  • 1、创建一个空对象obj
  • 2、拿到第一个参数(构造函数),将该对象obj的原型链proto指向构造函数的原型prototype,并在原型链proto上设置构造函数constructor为要实例化的constructorFunction
  • 3、改变constructorFunction的this指向到obj,并执行constructorFunction函数
  • 4、如果构造函数返回一个对象,则返回构造函数的对象, 否则返回 obj

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 第一个参数是构造函数,后面的是构造函数的参数
function MyNew() {
// 创建一个空对象
var obj = {}
// 拿到构造函数,因为构造函数是第一个参数,arguments里面的其余的参数就是构造函数的参数
// 拿出参数中第一个值
var constructorFunction = [].shift.call(arguments)
obj.__proto__= constructorFunction.prototype
// 此时的arguments已经是去掉了第一个参数的arguments
// 改变constructorFunction的this指向到obj并且执行constructorFunction这个函数
var res = constructorFunction.apply(obj, arguments)
return res instanceof Object ? res : obj
}
function a(name) {
this.name = name
}
b = MyNew(a, 'a')
c = new a('a')

实例:

1
2
3
4
5
6
7
8
function Super(age) {
this.age = age;
let obj = {a: '2'};
return obj;
}

let instance = new Super('hello');
console.log(instance.age); // undefined

结论: 上述返回undefined, 如果要返回hello, 去掉return.

构造函数不需要显示的返回值。使用new来创建对象(调用构造函数)时,如果return的是非对象(数字、字符串、布尔类型等)会忽而略返回值;如果return的是对象,则返回该对象。

执行上下文和调用堆栈

概述: 每当JavaScript代码运行时,它都在执行上下文中运行;调用栈则可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回的控制点。

执行上下文: 是评估和执行js代码的环境抽象概念,js在运行的时候,都是在执行上下文中运行.

执行上下文的类型:

  • 全局执行上下文: 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文: 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • Eval函数执行上下文: 执行在 eval 函数内部的代码也会有它属于自己的执行上下文。

调用栈: 调用栈是解析器(如浏览器中的的javascript解析器)的一种机制,可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回控制的点。(如什么函数正在执行,什么函数被这个函数调用,下一个调用的函数是谁)

  • 当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数。
  • 任何被这个函数调用的函数会进一步添加到调用栈中,并且运行到它们被上个程序调用的位置。
  • 当函数运行结束后,解释器将它从堆栈中取出,并在主代码列表中继续执行代码。
  • 如果栈占用的空间比分配给它的空间还大,那么则会导致“栈溢出”错误。

JS引擎创建执行上下文:
阶段: 1)创建阶段 2)执行阶段

1)创建阶段

  • this 值的决定,即我们所熟知的 this 绑定。
    a. 在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。
    b. 在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined(在严格模式下)

  • 创建词法环境组件
    定义: 词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。

    词法环境类型: (a) 全局环境下没有外部引用的词法环境 (b) 函数内部用户定义的变量存储在环境记录器中

    词法环境的内部有两个组件:(a) 环境记录器和 (b) 一个外部环境的引用。
    a. 环境记录器是存储变量和函数声明的实际位置。
    b. 外部环境的引用意味着它可以访问其父级词法环境(作用域)。

    结论:

    • 在全局环境中,环境记录器是对象环境记录器。
    • 在函数环境中,环境记录器是声明式环境记录器。
  • 创建变量环境组件
    它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

    在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。

变量提升(hositing)与作用域(scoping)

作用域: 全局作用域和函数作用域

全局作用域: 在当前的script标签内定义的变量,所有的函数都可以使用,它在打开时创建,关闭时销毁。在全局作用域中有一个全局对象window,window是由浏览器创建的,它里面的方法所有对象都可以使用。
e.g 全局作用域下的变量提升(输出undfined,说明a已经定义了,没有赋值)

  • 变量提升只会提升声明,而不会提升赋值,也就是说在执行第一个打印操作时,只是存在变量a,a却没有值;
  • 打印结果是undefined说明变量存在,只是没有值,而如果是报错说a is not defined的话,就是没有变量a。
  • 变量提升是发生在创建变量对象的过程中,会先扫描函数声明,再扫描变量声明,如果变量名与已经声明的函数相同,此时什么都不会发生,变量声明不会干扰已经存在的这个同名属性
1
2
3
console.log(a) // undefined
var a = 10
console.log(a) // 10
  1. 变量提升发生时机: JS预编译的过程中(只适用于var)

  2. 变量提升的优势:

  • 容错性更好: 过程是JS解析与执行,在解析阶段会检查语法,并对函数进行预编译,当函数的代码有语法错误的时候,在函数执行前就会报错(SyntaxError)
  • 声明提升可以提高性能: 如果没有预编译,那么每次执行函数前都必须重新解析一遍该函数,而这是没有必要的,因为函数的代码并不会改变,解析一遍就够了。解析的过程中,还会为函数生成预编译代码。在预编译时,会统计该函数声明了哪些变量、创建了哪些函数(注:这里就是声明提升),并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取函数中声明了哪些变量,注:这也是声明提升的好处),并且代码执行更快(因为压缩而变短了)。
  1. 变量提升的规则:
    a) var 声明的变量,提升时只声明,不赋值,默认为undefined;不用关键字直接赋值的变量不存在提升;(demo1)
    b) 函数提升会连带函数体一起提升,不执行;(deom2)
    c) 预解析的顺序是从上到下;(demo4)
    d) 函数的优先级高于变量,函数声明提前到当前作用域最顶端;(deom3)
    e) 变量重名,提升时不会重复定义;在执行阶段后面赋值的会覆盖上面的赋值;(demo4)
    f) 函数重名,提升时后面的会覆盖前面;(demo5)
    g) 函数和变量重名,提升函数,不会重复定义,变量不会覆盖函数;在执行阶段后面赋值的会覆盖上面的赋值;(demo8)
    h)用函数表达式声明函数,会按照声明变量规则进行提升;(deom6)
    i) 函数执行时,函数内部的变量声明和函数声明也按照以上规则进行提升;(deom7)
    j) let、const不存在提升; (PS: 只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。这就是暂时性死区。在暂时性死区内,不仅无法提前使用let声明的变量,他也不会获取函数体外部作用域内的同名变量。暂时性死区会到该变量被声明后结束。) (demo9、demo10)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**demo1**/
console.log('a=',a) //a=undefined
console.log('b=',b) // Uncaught ReferenceError: b is not defined
var a=1
b=6

/**deom2**/
console.log('a=',a) // a=function a() {console.log("func a()")}
function a() {
console.log("func a()")
}

/**deom3**/
console.log('a=',a) // a=function a() {console.log("fun a")}
var a=3
var a=4

function a(){
console.log("fun a")
}
var a=5
var a=6
console.log("a=",a) // a=6

/**deom4**/
console.log('a=',a) // a=undefined
var a =2
console.log('a=',a) //
var a =3
var a =4
console.log('a=',a) // a=4
console.log('b=',b) //b= undefined
var b='b1'

/**deom5**/
console.log('a=',a) // a=function a() {console.log("a2")}
function a(){
console.log("a1")
}
function a(){
console.log("a2")
}
console.log('a=',a) // a=function a() {console.log("a2")}

/**deom6**/
console.log('a=',a) // a=undefined
var a=function(){console.log('a1')}
var a=3
var a=4
var a=5
console.log(a)
var a=function(){console.log('a2')}
console.log('a=',a) // a= ƒ (){console.log('a2')}

/**deom7**/
console.log('b=',b)
var a=3
function b(i){
console.log('a=',a)
var a=4
function a(){
console.log('fun a')
}
console.log('a=',a)
}
b()

/**demo8**/
console.log('a=',a) //a= function a(){ console.log('fun a')}
var a=2
function a(){
console.log('fun a')
}
console.log('a=',a) // a=2
var a=3
var a=4
var a=5
console.log('a=',a) // a=5

/**demo9**/
console.log('a=',a) //Uncaught ReferenceError: a is not defined
let a=4

/****/
<!--demo10-->
console.log('b=',b) // Uncaught ReferenceError: b is not defined
const b=5

==和===

区别:

  • == 抽象相等,比较时,会先进行类型转换,然后再比较值。
  • === 严格相等,会比较两个值的类型和值。

抽象相等比较过程:

  • 如果Type(x)和Type(y)相同,返回x===y的结果
  • 如果Type(x)和Type(y)不同
  • 如果x是null,y是undefined,返回true
  • 如果x是undefined,y是null,返回true
  • 如果Type(x)是Number,Type(y)是String,返回 x==ToNumber(y) 的结果
  • 如果Type(x)是String,Type(y)是Number,返回 ToNumber(x)==y 的结果
  • 如果Type(x)是Boolean,返回 ToNumber(x)==y 的结果
  • 如果Type(y)是Boolean,返回 x==ToNumber(y) 的结果
  • 如果Type(x)是String或Number或Symbol中的一种并且Type(y)是Object,返回 x==ToPrimitive(y) 的结果
  • 如果Type(x)是Object并且Type(y)是String或Number或Symbol中的一种,返回 ToPrimitive(x)==y 的结果
  • 其他返回false

严格相等比较过程:

  • 如果Type(x)和Type(y)不同,返回false
  • 如果Type(x)和Type(y)相同
  • 如果Type(x)是Undefined,返回true
  • 如果Type(x)是Null,返回true
  • 如果Type(x)是String,当且仅当x,y字符序列完全相同(长度相同,每个位置上的字符也相同)时返回true,否则返回false
  • 如果Type(x)是Boolean,如果x,y都是true或x,y都是false返回true,否则返回false
  • 如果Type(x)是Symbol,如果x,y是相同的Symbol值,返回true,否则返回false
  • 如果Type(x)是Number类型
  • 如果x是NaN,返回false
  • 如果y是NaN,返回false
  • 如果x的数字值和y相等,返回true
  • 如果x是+0,y是-0,返回true
  • 如果x是-0,y是+0,返回true
  • 其他返回false

ToPrimitive() : 转换成原始类型方法。

隐式转换

定义: js中当运算符在运算时,如果两边数据不统一,CPU就无法计算,这时我们编译器会自动将运算符两边的数据做一个数据类型转换,转成一样的数据类型再计算.由编译器自动转换的方式就称为隐式转换

隐式转换规则:

  1. 转成string类型: +(字符串连接符)
  2. 转成number类型:++/–(自增自减运算符) + - * / %(算术运算符) > < >= <= == != === !=== (关系运算符)
  3. 转成boolean类型:!(逻辑非运算符)

实例:

  • 字符串连接符+: 会把其他数据类型调用String()方法转成字符串然后拼接, 算数运算符+会把其他数据类型调用Number方法转成数字然后做加法计算

    1
    2
    3
    4
    console.log(1 + 'true')  // '1true'
    console.log(1 + true) // 2
    console.log(1 + undefined) // NaN
    console.log(1 + null) // 1
  • 关系运算符:会把其他数据类型转换成number之后再比较关系

    1
    2
    3
    4
    5
    6
    console.log('2' > 10)   // false
    console.log('2' > '10') // true 通过unicode来比较(charCodeAt)
    console.log('abc' > 'b') // false
    console.log('abc' > 'aad') // true
    console.log(NaN == NaN) // false
    console.log(null == undefined) // true
  • 复杂数据类型在隐式转换时会先转成String,然后再转成Number运算

    1
    2
    3
    4
    var a = ?         // a = {i: 0, valueOf: function() { return ++a.i}}
    if (a == 1 && a == 2 && a == 3) {
    console.log(1)
    }
  • 空数组的toString()方法会得到空字符串,而空对象的toString()方法会得到字符串[object Object]

    1
    2
    3
    4
    5
    6
    7
    8
    console.log([] == 0)  // true
    console.log(![] == 0) // true

    console.log([] == ![]) // true
    console.log([] == []) // false

    console.log({} == {}) // false
    console.log({} == !{}) // false

call和apply

每个函数都包含两个非继承而来的方法: call和apply
用途: 改变函数作用域,实现继承

相同点: 都是为了改变某个函数运行时的上下文(context)而存在的(为了改变函数体内部this的指向)。
不同点: 参数书写方式不同

1
2
call(thisObj, arg1, arg2, arg3, arg4)
apply(thisObj, [args])

性能对比: call方法永远比apply方法执行速度要快

用call来模拟apply方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (args.length) {
case 0:
g = gen.call(ctx);
break;
case 1:
g = gen.call(ctx, args[0]);
break;
case 2:
g = gen.call(ctx, args[0], args[1]);
break;
default:
g = gen.apply(ctx, args);
}

手动实现bind函数

bind函数的核心作用:绑定this、初始化参数

语法:fun.bind(thisArg[, arg1[, arg2[, …]]])
bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值(thisArg)。
被调用时,arg1、arg2等参数将置于实参之前传递给被绑定的方法。
它返回由指定的this值和初始化参数改造的原函数拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== 'function') {
return
}

let self = this
let args = Array.prototype.slice.call(arguments, 1)
let fBound = function() {
let _this = this instanceof self ? this : oThis //检测是否使用new创建
return self.apply(_this, args.concat(Array.prototype.slice.call(arguments)))
}

if (this.prototype) {
fBound.prototype = this.prototype
}
return fBound
}
}

由于arguments是类数组对象,不拥有slice方法,所以通过call来将slice的this指向arguments
args就是调用bind时传入的初始化参数(剔除了第一个参数oThis)。将args与绑定函数执行时的实参arguments通过concat连起来作为参数传入,就实现了bind函数初始化参数的效果。

  • 当使用new操作符时, 它会永远地为绑定函数固定this为指定的对象.
    不适用: thisArg:当使用new 操作符调用绑定函数时,该参数无效。
    一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

MDN的实现方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}

var aArgs = Array.prototype.slice.call(arguments, 1),//这里的arguments是跟oThis一起传进来的实参
fToBind = this,
fNOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof fNOP
? this
: oThis,
// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
aArgs.concat(Array.prototype.slice.call(arguments)));
};

// 维护原型关系
if (this.prototype) {
// Function.prototype does not have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();

return fBound;
};
}

优势: 最主要的差异就在于它定义了一个空的function fNOP,通过fNOP来传递原型对象给fBound(通过实例化的方式)。这时,修改fBound的prototype对象,就不会影响到self的prototype对象啦~而且fNOP是空对象,所以几乎不占内存。

柯里化函数(Currying)

  1. 性能:
  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 支持多参数传递
function progressCurrying(fn, args) {

var _this = this
var len = fn.length;
var args = args || [];

return function() {
var _args = [].slice.call(arguments);
Array.prototype.push.apply(args, _args);

// 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
if (_args.length < len) {
return progressCurrying.call(_this, fn, _args);
}

// 参数收集完毕,则执行fn
return fn.apply(this, _args);
}
}

实例:
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);

// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function() {
_args.push(...arguments);
return _adder;
};

// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}

add(1)(2)(3) // 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15
add(2, 6)(1) // 9

reduce函数实现

reduce方法: 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,接受四个参数:初始值(或者上一次回调函数的返回值),当前元素值,当前索引,调用 reduce 的数组。

  1. 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var arr = [1, 2, 3, 4];
    var sum = arr.reduce(function(prev, cur, index, arr) {
    console.log(prev, cur, index);
    return prev + cur;
    })
    console.log(arr, sum);

    // result
    1 2 1
    3 3 2
    6 4 3
    [1, 2, 3, 4] 10
  2. 手动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
const reduce = (array, fn, initialValue) => {
let sum;
if (initialValue) {
sum = initialValue;
for (const value of array) // 这里用了es6 在for of
sum = fn(sum, value)
} else {
sum = array[0];
for (let i = 1; i < array.length; i++)
sum = fn(sum, array[i])
}
return sum;
}

0.1 + 0.2 !== 0.3

0.1 + 0.2 === 0.30000000000000004

计算结果转成二进制
0.1 => 0.0001 1001 1001 1001…(无限循环)
0.2 => 0.0011 0011 0011 0011…(无限循环)

双精度浮点数的小数部分最多支持 52 位,所以两者相加之后得到这么一串 0.0100110011001100110011001100110011001100110011001100因浮点数小数位的限制而截断的二进制数字,这时候,我们再把它转换为十进制,就成了0.30000000000000004。

解决方法:

1
2
3
var numA = 0.1;
var numB = 0.2;
alert( parseFloat((numA + numB).toFixed(2)) === 0.3 ); // true

[0, 1, 2].map(parseInt)

结果: [0, NaN, NaN]

  1. map函数
    将数组的每个元素传递给指定的函数处理,并返回处理后的数组,所以 [0, 1, 2].map(parseInt) 就是将字符串0,1,2作为元素;0,1,2作为下标分别调用 parseInt 函数。即分别求出 parseInt(0,0), parseInt(1, 1), parseInt(2, 2)的结果。

  2. 以第二个参数为基数来解析第一个参数字符串,通常用来做十进制的向上取整(省略小数)如:parseInt(2.7) //结果为2

因此parseInt(0, 0) 十进制解析,结果为0; parseInt(1, 1) 以1进制来解析1,超出范围,结果为NaN; parseInt(2, 2) 以2进制来解析2,超出范围,结果为NaN;

11.10

数据结构与算法

前端

js

js的事件循环(event-loop)

  1. js的运行机制
    a. 所有的同步任务都在主线程上执行,形成一个执行栈(excution context stack)
    b. 主线程之外有个”任务队列”(task queue).只要异步任务有了运行结果,任务队列里面就会放置一个事件
    c. 执行栈的同步任务执行完毕,系统读取”任务队列”,看里面事件对应的异步任务,进入执行栈,开始执行.

概括: 主线程先执行调用栈的同步任务,执行完了以后清空栈,然后去任务队列按照顺序读取任务放入栈中执行,每当栈被清空,都会读取任务队列有没有任务,有就读取执行,一直去循环读取-执行操作.

  1. 一个事件循环有一个或者是多个任务队列

  2. js中有两种异步任务:
    a. 宏任务: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
    b. process.nextTick(node.js), Promises, Object.observe, MutationObserver

  3. 事件循环(event-loop)

主线程从”任务队列”中读取执行事件,这个过程循环不断,这个机制就是事件循环.

机制: 主线程不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查microtask队列是否为空(执行完一个任务的具体标志是函数执行栈为空),如果不为空则一次性执行完所有microtask,然后进入下一循环去任务队列中取下一个任务执行.

  1. 事件循环的详细过程
    a. 选择当前要执行的宏任务队列,选择一个最先进入队列的宏任务,如果没有宏任务,则跳转到微任务(microtask)的执行步骤
    b. 将事件循环的当前运行宏任务设置为已选择的宏任务
    c. 运行宏任务.
    d. 将事件循环的当前任务设置为null
    e. 将运行完成的宏任务从宏任务队列移除.
    f. microtasks步骤:进入microtask检查点
    g. 更新界面渲染
    h. 返回第一步

关于f步骤: 执行进入microtask检查的具体步骤如下:

a. 设置进入microtask检查点的标志为true。
b. 当事件循环的微任务队列不为空时:选择一个最先进入microtask队列的microtask;设置事件循环的当前运行任务为已选择的microtask;运行microtask;设置事件循环的当前运行任务为null;将运行结束的microtask从microtask队列中移除。
c. 对于相应事件循环的每个环境设置对象(environment settings object),通知它们哪些promise为rejected。
d. 清理indexedDB的事务。
e. 设置进入microtask检查点的标志为false。

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

  2. 为什么需要event-loop
    因为 JavaScript 是单线程的。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。

  3. 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    setTimeout(()=>{
    console.log("setTimeout1");
    Promise.resolve().then(data => {
    console.log(222);
    });
    });
    setTimeout(()=>{
    console.log("setTimeout2");
    });
    Promise.resolve().then(data=>{
    console.log(111);
    });

    // 111
    // setTimeout1
    // 222
    // setTimeout2

过程: 主线程上没有需要执行的代码
接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在下一次的事件循环中执行)。
接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在再下一次的事件循环中执行)。
首先检查微任务队列, 即 microtask队列,发现此队列不为空,执行第一个promise的then回调,输出 ‘111’。
此时microtask队列为空,进入下一个事件循环, 检查宏任务队列,发现有 setTimeout的回调函数,立即执行回调函数输出 ‘setTimeout1’,检查microtask 队列,发现队列不为空,执行promise的then回调,输出’222’,microtask队列为空,进入下一个事件循环。
检查宏任务队列,发现有 setTimeout的回调函数, 立即执行回调函数输出’setTimeout2’。

  1. 例子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    console.log('script start');

    setTimeout(function () {
    console.log('setTimeout---0');
    }, 0);

    setTimeout(function () {
    console.log('setTimeout---200');
    setTimeout(function () {
    console.log('inner-setTimeout---0');
    });
    Promise.resolve().then(function () {
    console.log('promise5');
    });
    }, 200);

    Promise.resolve().then(function () {
    console.log('promise1');
    }).then(function () {
    console.log('promise2');
    });
    Promise.resolve().then(function () {
    console.log('promise3');
    });
    console.log('script end');

    // script start
    // script end
    // promise1
    // promise3
    // promise2
    // setTimeout---0
    // setTimeout---200
    // promise5
    // inner-setTimeout---0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log('1');

setTimeout(function() {
console.log('2');
}, 0);

new Promise(resolve => {
console.log('3');
resolve();
}).then(() => console.log('4'));

Promise.resolve().then(function() {
console.log('5');
}).then(function() {
console.log('6');
});

console.log('7');

// 1 3 7 4 5 6 2
  1. js的事件机制和node的事件循环的区别

区别:
a. 浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。
b. Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

nodejs循环的阶段:
a. timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
b. I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
c. idle, prepare 阶段:仅node内部使用
d. poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
e. check 阶段:执行 setImmediate() 的回调
f. close callbacks 阶段:执行 socket 的 close 事件回调

总结:
1.Node.js 的事件循环分为6个阶段
2.浏览器和Node 环境下,microtask 任务队列的执行时机不同
Node.js中,microtask 在事件循环的各个阶段之间执行
浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
3.递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()

process.nextTick() VS setImmediate()
官方文档从语义角度看,setImmediate() 应该比 process.nextTick() 先执行才对,而事实相反,命名是历史原因也很难再变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
setTimeout(()=>{
console.log('timer1')

Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)

setTimeout(()=>{
console.log('timer2')

Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)

//浏览器输出结果
timer1
promise1
timer2
promise2

//Node输出结果
timer1
timer2
promise1
promise2

let/var/const

  1. 概念:
    var 声明一个变量,并可选地将其初始化为一个值。
    let 声明一个块级作用域的本地变量,并且可选的将其初始化为一个值。
    const 声明一个只读的常量。一旦声明,常量的值就不能改变。

  2. 区别

  • 只有var存在变量提升
  • 暂时性死区(用let/const命令声明变量之前,该变量是不可用的):只要块级作用域内存在let/const命令,它所声明的变量就“绑定”这个区域,不再受外部的影响。
  • let不允许重复声明
  • let块级作用域(优势: 防止内层变量可能会覆盖外层变量,变量泄露)
  • const命令跟let命令一样不存在变量提升、具有块级作用域、存在暂时性死区
  • const命令的本质是保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。
  • var如果注册在全局就是顶层对象,是挂载在window上的,但是const和let是不会挂载在window上的

深拷贝和浅拷贝

浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象;

区别:浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制;

  1. 实现深拷贝

a. 递归实现深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
deepClone = (obj) => {
let objClone = Array.isArray(obj)?[]:{};
if(obj && typeof obj==="object"){
// for...in 会把继承的属性一起遍历
for(let key in obj){
// 判断是不是自有属性,而不是继承属性
if(obj.hasOwnProperty(key)){
//判断ojb子元素是否为对象或数组,如果是,递归复制
if(obj[key]&&typeof obj[key] ==="object"){
objClone[key] = this.deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}

b. 用JSON.stringify和JSON.parse实现

1
2
3
4
5
function deepCopy(obj1){
let _obj = JSON.stringify(obj1);
let obj2 = JSON.parse(_obj);
return obj2;
}

c. Object.assign()实现浅拷贝及一层的深拷贝

1
let obj2 = Object.assign({},obj1)

d. 新建一个对象取属性值

e. jquery 和 zepto 里的 $.extend 方法可以用作深拷贝。

1
2
var $ = require('jquery');
var newObj = $.extend(true, {}, obj);

f. lodash

1
2
var _ = require('lodash');
var newObj = _.cloneDeep(obj);

  1. 实现浅拷贝

a. slice()和concat()(非深拷贝)

b. ES6 的对象扩展 let newObj = {…obj};

c. 自定义函数

1
2
3
4
5
6
7
function shallowClone (initalObj) {
   var obj = {};
   for ( var i in initalObj) {
    obj[i] = initalObj[i];
   }
   return obj;
}

箭头函数和普通函数

  • 箭头函数相当于匿名函数,并且简化了函数定义。
  • 箭头函数是匿名函数,不能作为构造函数,不能使用new
  • 箭头函数不绑定arguments,用…代替
  • 箭头函数通过 call() 或 apply() 方法调用一个函数时,只传入了一个参数,对 this 并没有影响
  • 箭头函数没有原型属性

this的区别

普通函数:

  1. this总是代表它的直接调用者, 例如 obj.func ,那么func中的this就是obj
  2. 在默认情况(非严格模式下,未使用 ‘use strict’),没找到直接调用者,则this指的是 window
  3. 在严格模式下,没有直接调用者的函数中的this是 undefined
  4. 使用call,apply,bind(ES5新增)绑定的,this指的是 绑定的对象

箭头函数:

  1. 默认指向在定义它时,它所处的对象,而不是执行时的对象, 定义它的时候,可能环境是window(即继承父级的this)

总结: 箭头函数的 this 永远指向其上下文的 this ,任何方法都改变不了其指向,如 call() , bind() , apply() , 普通函数的this指向调用它的那个对象


普通函数: 根据调用我的人(谁调用我,我的this就指向谁)

箭头函数:根据所在的环境(我再哪个环境中,this就指向谁)

箭头函数是匿名函数,不能作为构造函数,不能使用new

箭头函数有注意点:

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
  • this是继承而来; 默认指向在定义它时所处的对象(宿主对象),而不是执行时的对象

原理: this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

  • 箭头函数的 this 永远指向其上下文的 this ,任何方法都改变不了其指向,如 call() , bind() , apply()
  • 普通函数的this指向调用它的那个对象

正则表达式

1、 匹配电话号码

1
2
    (/^1(3|4|5|6|7|8|9)\d{9}$/.test(phone)
}

2、 匹配邮箱

1
2
//名称允许汉字、字母、数字,下划线、中划线域名只允许英文域名
^[A-Za-z0-9_-\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$

GET和POST的区别

1、GET在浏览器回退时是无害的,而POST会再次提交请求。
2、GET产生的URL地址可以被Bookmark,而POST不可以。
3、GET请求会被浏览器主动cache,而POST不会,除非手动设置。
4、GET请求只能进行url编码,而POST支持多种编码方式。
5、GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
6、GET请求在URL中传送的参数是有长度限制的,而POST没有。(谷歌是8k,IE是2k)
7、对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
8、GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
9、GET参数通过URL传递,POST放在Request body中。

从本质上来说,GET/POST都是TCP链接。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。
GET和POST能做的事情是一样的。

GET和POST还有一个重大区别,简单的说:
GET产生一个TCP数据包;POST产生两个TCP数据包。
有些文章中提到,post 会将 header 和 body 分开发送,先发送 header,服务端返回 100 状态码再发送 body。
HTTP 协议中没有明确说明 POST 会产生两个 TCP 数据包,而且实际测试(Chrome)发现,header 和 body 不会分开发送。
所以,header 和 body 分开发送是部分浏览器或框架的请求方法,不属于 post 必然行为。

target和currentTarget的区别

  • currentTarget 其事件处理程序当前正在处理事件的那个元素
  • target 事件的目标

结论: currentTarget指的是事件触发后,冒泡到绑定处理程序的元素,就是绑定事件处理程序的元素,target指的是触发事件的元素。

自己理解: currentTarget是绑定事件的元素,但是target是冒泡后实际触发的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let div1 = document.getElementById('div1'),
div2 = document.getElementById('div2'),
div3 = document.getElementById('div3');

div1.addEventListener('click', (ev) => {
console.log(`target: ${ev.target.id} currentTarget: ${ev.currentTarget.id}`);
});

div2.addEventListener('click', (ev) => {
console.log(`target: ${ev.target.id} currentTarget: ${ev.currentTarget.id}`);
});

div3.addEventListener('click', (ev) => {
console.log(`target: ${ev.target.id} currentTarget: ${ev.currentTarget.id}`);
});

// target: div3 div3 div3 currentTarget: div3 div2 div1

getBoundingClientRect和offset的区别

getBoundingClientRect参照是视窗顶端,而JQ的offset().top参照的是文档,两者参照对象不同。
当监听的是window的滚动条时,元素的getBoundingClientRect().top会原来越小,而offset().top一直不变。

计算机网络

http状态码

  1. 1xx: (临时响应)
  • 100: 状态码说明服务器收到了请求的初始部分,并且请客户端继续发送。在服务器发送了 100 Continue 状态码之后,如果收到客户端的请求,则必须进行响应。
    应用场景:客户端有一个较大的文件需要上传并保存,但是客户端不知道服务器是否愿意接受这个文件,所以希望在消耗网络资源进行传输之前,先询问一下服务器的意愿。实际操作为客户端发送一条特殊的请求报文,报文的头部应包含Expect: 100-continue
    此时,如果服务器愿意接受,就会返回 100 Continue 状态码,反之则返回 417 Expectation Failed 状态码。
  • 101: (切换协议) 请求者已要求服务器切换协议,服务器已确认并准备切换。
  1. 2xx: (成功)
  • 200: 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
  • 201: (已创建) 请求成功并且服务器创建了新的资源。
  • 202: (已接受) 服务器已接受请求,但尚未处理。
  • 203: (非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源。
  • 204: (无内容) 服务器成功处理了请求,但没有返回任何内容。
  • 205: (重置内容) 服务器成功处理了请求,但没有返回任何内容。
  • 206: (部分内容) 服务器成功处理了部分 GET 请求。
  1. 3xx: (重定向)
  • 300 (多种选择) 针对请求,服务器可执行多种操作。 服务器可根据请求者 (useragent)选择一项操作,或提供操作列表供请求者选择。
  • 301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或HEAD请求的响应)时,会自动将请求者转到新位置。
  • 302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
  • 303 (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。
  • 304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
  • 305 (使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。
  • 307 (临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
  1. 4xx: (请求错误)
  • 400 (错误请求) 服务器不理解请求的语法。
  • 401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
  • 402 该状态码是为了将来可能的需求而预留的。
  • 403 (禁止) 服务器拒绝请求。
  • 404 (未找到) 服务器找不到请求的网页。
  • 405 (方法禁用) 禁用请求中指定的方法。
  • 406 (不接受) 无法使用请求的内容特性响应请求的网页。
  • 407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。
  • 408 (请求超时)服务器等候请求时发生超时。
  • 409 (冲突) 服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。
  • 410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应。
  • 411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。
  • 412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。
  • 413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。
  • 414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。这比较少见,通常的情况包括:本应使用POST方法的表单提交变成了GET方法,导致查询字符串(Query String)过长(超过2k或者4k)。
  • 415 (不支持的媒体类型) 请求的格式不受请求页面的支持。
  • 416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。
  • 417 (未满足期望值) 服务器未满足”期望”请求标头字段的要求。
  1. 5xx: (服务器错误)
  • 500 (服务器内部错误) 服务器遇到错误,无法完成请求。
  • 501 (尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。
  • 502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。
  • 503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。
  • 504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。
  • 505 (HTTP 版本不受支持)服务器不支持请求中所用的 HTTP 协议版本
  1. 600 源站没有返回响应头部,只返回实体内容

  2. 302/303/307
    浏览器对303状态码的处理跟原来浏览器对HTTP1.0的302状态码的处理方法一样;浏览器对307状态码处理则跟原来HTTP1.0文档里对302的描述一样。
    303和307的存在,归根结底是由于POST方法的非幂等属性引起的。
    第二次post的时候环境有可能发生变化,POST操作会不符合用户预期。但是,很多浏览器(user agent我描述为浏览器以方便介绍)在这种情况下都会把POST请求变为GET请求。

302标准禁止post变化get,但实际使用时大家不遵守。
307会遵照浏览器标准,不会从post变为get。

TCP连接3次握手和四次挥手

第一次握手
客户端向服务端发送连接请求报文段。该报文段的头部中SYN=1,ACK=0,seq=x。请求发送后,客户端便进入SYN-SENT状态。

PS1:SYN=1,ACK=0表示该报文段为连接请求报文。
PS2:x为本次TCP通信的字节流的初始序号。
TCP规定:SYN=1的报文段不能有数据部分,但要消耗掉一个序号。

第二次握手
服务端收到连接请求报文段后,如果同意连接,则会发送一个应答:SYN=1,ACK=1,seq=y,ack=x+1。
该应答发送完成后便进入SYN-RCVD状态。

PS1:SYN=1,ACK=1表示该报文段为连接同意的应答报文。
PS2:seq=y表示服务端作为发送者时,发送字节流的初始序号。
PS3:ack=x+1表示服务端希望下一个数据报发送序号从x+1开始的字节。

第三次握手
当客户端收到连接同意的应答后,还要向服务端发送一个确认报文段,表示:服务端发来的连接同意应答已经成功收到。
该报文段的头部为:ACK=1,seq=x+1,ack=y+1。
客户端发完这个报文段后便进入ESTABLISHED状态,服务端收到这个应答后也进入ESTABLISHED状态,此时连接的建立完成!

  1. 为什么连接建立需要三次握手?

防止失效的连接请求报文段被服务端接收,从而产生错误。

PS:失效的连接请求:若客户端向服务端发送的连接请求丢失,客户端等待应答超时后就会再次发送连接请求,此时,上一个连接请求就是『失效的』。

若建立连接只需两次握手,客户端并没有太大的变化,仍然需要获得服务端的应答后才进入ESTABLISHED状态,而服务端在收到连接请求后就进入ESTABLISHED状态。此时如果网络拥塞,客户端发送的连接请求迟迟到不了服务端,客户端便超时重发请求,如果服务端正确接收并确认应答,双方便开始通信,通信结束后释放连接。此时,如果那个失效的连接请求抵达了服务端,由于只有两次握手,服务端收到请求就会进入ESTABLISHED状态,等待发送数据或主动发送数据。但此时的客户端早已进入CLOSED状态,服务端将会一直等待下去,这样浪费服务端连接资源。

  1. 四次挥手过程:
  • 首先进行关闭的一方(即发送第一个FIN)将执行主动关闭,而另一方(收到这个FIN)执行被动关闭。
  • 当服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。
  • 同时TCP服务器还向应用程序(即丢弃服务器)传送一个文件结束符。接着这个服务器程序就关闭它的连接,导致它的TCP端发送一个FIN。
  • 客户必须发回一个确认,并将确认序号设置为收到序号加1。
  1. 为什么4次挥手
    TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。

  2. 为什么要设置时间为 2MSL
    MSL 是Maximum Segment Lifetime,译为“报文最大生存时间”,任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

等待 2MSL 时间主要目的是怕最后一个 ACK 对方没收到,那么对方在超时后将重发第三次握手的 FIN ,主动关闭端接到重发的 FIN 包后,系统收到该分组后,可以再发一个 ACK 应答包。还有就是等来该连接在网络上的所有报文都传输完毕,所以处于 TIME_WAIT 状态时候,两端的端口都是不可用的,迟到的报文都会被废弃。

TCP和UDP的区别

TCP是传输控制协议,提供的是面向连接、可靠的字节流服务。通信双方彼此交换数据前,必须先通过三次握手协议建立连接,之后才能传输数据。TCP提供超时重传,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。
UDP是用户数据报协议,是一个简单的面向无连接的协议。UDP不提供可靠的服务。在数据数据前不用建立连接故而传输速度很快。UDP主要用户流媒体传输,IP电话等对数据可靠性要求不是很高的场合。

HTTP常见的请求头

  • Host:主机和端口号
  • Connection:连接类型
  • Upgrade-lnsecure-Requests:升级为https请求
  • User-Agent:浏览器名称
  • Accept:传输文件类型
  • Referer:页面跳转处
  • Accept-Encoding:文件编解码格式
  • Cookie:Cookie
  • x-requested-with :XMLHttpRequest(是Ajax异步请求)

HTTPS

HTTPS是由两部分组成: HTTP + SSL / TLS, 在HTTP层加了处理加密信息的模块. 服务端和客户端的信息传输都会通过TLS加密.

工作过程:

  1. 在使用HTTPS是需要保证服务端配置正确了对应的安全证书
  2. 客户端发送请求到服务端
  3. 服务端返回公钥和证书到客户端
  4. 客户端接收后会验证证书的安全性,如果通过则会随机生成一个随机数,用公钥对其加密,发送到服务端
  5. 服务端接受到这个加密后的随机数后会用私钥对其解密得到真正的随机数,随后用这个随机数当做私钥对需要发送的数据进行对称加密
  6. 客户端在接收到加密后的数据使用私钥(即生成的随机值)对数据进行解密并且解析数据呈现结果给客户
  7. SSL加密建立

加密算法:

  1. 非对称加密算法: RSA,DSA/DSS
  2. 对称加密算法: AES, RC4, 3DES
  3. HASH算法: MD5, SHA1, SHA256

用途: 解决http的三个缺点(被监听/被篡改/被伪装)

对称加密: 即加密的密钥和解密的密钥相同
非对称加密: 非对称加密将密钥分为公钥和私钥,公钥可以公开,私钥需要保密,客户端公钥加密的数据,服务端可以通过私钥来解密

HTTP2.0与HTTP1.1的区别

与HTTP1.1相比, 主要区别包括:

  1. HTTP/2采用二进制格式而非文本格式 (相对于HTTP1.x, 二进制协议解析更高效,线上更紧凑,错误更少)
  2. HTTP/2是完全多路复用的,而非有序并阻塞的——只需一个连接即可实现并行 (需要多路传输的原因: http1.x有个问题是线端阻塞, 一个连接一次只提交一个请求的效率比较高,多了就会变慢,多路传输能很好的解决这个问题,因为它能同时处理多个消息的请求和响应,甚至可以在传输过程中将一个消息跟另一个掺杂在一起,所以客户端只需要一个连接就能加载一个页面)
  3. 使用报头压缩,HTTP/2降低了开销(由于慢启动机制,会基于对已知的包,还要来回的包,限制了最初的几个来回可以发送的数据包的数量,轻微的压缩头部让那些请求只需一个来回就能搞定)
  4. HTTP/2让服务器可以将响应主动“推送”到客户端缓存中(服务器推送服务通过“推送”那些它认为客户端将会需要的内容到客户端的缓存中,以此来避免往返的延迟)

TCP相应问题

  1. 现代浏览器在与服务器建立了一个 TCP 连接后是否会在一个 HTTP 请求完成后断开?什么情况下会断开?
    在http1.1之后,默认情况下(Connection: keep-alive)建立TCP 连接不会断开,只有在请求报头中声明Connection: close 才会在请求完成后关闭连接。

  2. 一个 TCP 连接可以对应几个 HTTP 请求?
    如果维持连接,一个TCP 连接是可以发送多个HTTP 请求的。

  3. 一个TCP 连接中HTTP 请求发送可以一起发送么(比如一起发三个请求,再三个响应一起接收)?
    在HTTP/1.1 存在Pipelining(流水线) 技术可以完成这个多个请求同时发送,但是由于浏览器默认关闭,所以可以认为这是不可行的。在HTTP2 中由于Multiplexing(多路传输特性) 特点的存在,多个HTTP 请求可以在同一个TCP 连接中并行进行。

在HTTP/1.1 时代,浏览器是如何提高页面加载效率的呢?主要有下面两点:

  • 维持和服务器已经建立的TCP 连接,在同一连接上顺序处理多个请求。
  • 和服务器建立多个TCP 连接。
  1. 为什么有的时候刷新页面不需要重新建立SSL 连接?
    TCP 连接有的时候会被浏览器和服务端维持一段时间。TCP 不需要重新建立,SSL 自然也会用之前的。

  2. 浏览器对同一Host 建立TCP 连接到数量有没有限制?
    有。Chrome 最多允许对同一个Host 建立六个TCP 连接。不同的浏览器有一些区别。

  3. 收到的HTML 如果包含几十个图片标签,这些图片是以什么方式、什么顺序、建立了多少连接、使用什么协议被下载下来的呢?
    如果图片都是HTTPS 连接并且在同一个域名下,那么浏览器在SSL 握手之后会和服务器商量能不能用HTTP2,如果能的话就使用Multiplexing 功能在这个连接上进行多路传输。不过也未必会所有挂在这个域名的资源都会使用一个TCP 连接去获取,但是可以确定的是Multiplexing(多路传输) 很可能会被用到。
    如果发现用不了HTTP2 呢?或者用不了HTTPS(现实中的HTTP2 都是在HTTPS 上实现的,所以也就是只能使用HTTP/1.1)。那浏览器就会在一个HOST 上建立多个TCP 连接,连接数量的最大限制取决于浏览器设置,这些连接会在空闲的时候被浏览器用来发送新的请求,如果所有的连接都正在发送请求呢?那其他的请求就只能等等了。

手写XHR请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function createXMLHTTPRequest() {     
//1.创建XMLHttpRequest对象
//这是XMLHttpReuquest对象无部使用中最复杂的一步
//需要针对IE和其他类型的浏览器建立这个对象的不同方式写不同的代码
var xmlHttpRequest;
if (window.XMLHttpRequest) {
//针对FireFox,Mozillar,Opera,Safari,IE7,IE8
xmlHttpRequest = new XMLHttpRequest();
} else if (window.ActiveXObject) {
//针对IE6
try {
//取出一个控件名进行创建,如果创建成功就终止循环
//如果创建失败,回抛出异常,然后可以继续循环,继续尝试创建
xmlHttpRequest = new ActiveXObject("Microsoft.XMLHTTP");
if(xmlHttpRequest){
break;
}
} catch (e) {
console.log(e)
}
}
return xmlHttpRequest;
}

GET请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function get(url){  
var req = createXMLHTTPRequest();
if(req){
req.open("GET", url, true);
req.onreadystatechange = function(){
if(req.readyState == 4){
if(req.status == 200){
alert("success");
}else{
alert("error");
}
}
}
req.send(null);
}
}

POST请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function post(url){  
var req = createXMLHTTPRequest();
if(req){
req.open("POST", url, true);
req.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=gbk;"); req.send('');
req.onreadystatechange = function(){
if(req.readyState == 4){
if(req.status == 200){
alert("success");
}else{
alert("error");
}
}
}
}
}

浏览器

base64图片利弊

优势:

  • 减少http请求
  • 模块封装

弊端:

  • base64编码的长度有些长(base64一般比原图大1/3), 尽管图片请求少了,但是 HTML 文件本身尺寸会变大,会影响首屏加载,所以要权衡
  • 获取修改比较麻烦
  • IE的兼容性问题,IE8下不支持Data url,iE8开始支持data url,却有大小限制,32k
  • 如果构建工具比较落后(或者没有构建工具),手动插入 base64 是很蛋疼的,编辑器会卡到哭
  • base64 无法缓存,要缓存只能缓存包含 base64 的文件,比如 HTML 或者 CSS,这相比直接缓存图片要弱很多,一般 HTM 会改动频繁,所以等同于得不到缓存效益

使用场景:

  • 图片很少或者不会更新
  • 图片实际尺寸很小(一般10k以下)
  • 图片在网站多次使用

DNS预加载是什么?为什么需要DNS预加载

  • DNS预加载背景
    所在域名外的域名文件时会遇到请求延时非常严重的情况,如调用百度联盟广告和谷歌联盟广告。

  • DNS预加载用法

    1
    2
    <meta http-equiv="x-dns-prefetch-control" content="on" />
    <link rel="dns-prefetch" href="{url}" />
  • DNS预加载优势
    DNS 作为互联网的基础协议,其解析的速度似乎容易被网站优化人员忽视。现在大多数新浏览器已经针对 DNS 解析进行了优化,典型的一次 DNS 解析耗费 20-120 毫秒,减少 DNS 解析时间和次数是个很好的优化方式。DNS Prefetching 是具有此属性的域名不需要用户点击链接就在后台解析,而域名解析和内容载入是串行的网络操作,所以这个方式能减少用户的等待时间,提升用户体验。

  • DNS注意要点
    dns-prefetch 需慎用,多页面重复DNS预解析会增加重复DNS查询次数

预加载(preload)与预读取(prefetch)

简单理解: preload是异步的, prefetch是空闲时加载

区别:

  • preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源
  • prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源
Prefetch

如下所示,prefetch是link元素中的rel属性值

1
<link rel=“prefetch”>

它的作用是告诉浏览器加载下一页面可能会用到的资源,注意,是下一页面,而不是当前页面。因此该方法的加载优先级非常低,也就是说该方式的作用是加速下一个页面的加载速度

Preload

使用 preload 后,不管资源是否使用都将提前加载。若不确定资源是必定会加载的,则不要错误使用 preload,以免本末导致,给页面带来更沉重的负担
Preload 有 as 属性,浏览器可以设置正确的资源加载优先级,这种方式可以确保资源根据其重要性依次加载, 所以,Preload既不会影响重要资源的加载,又不会让次要资源影响自身的加载;浏览器能根据 as 的值发送适当的 Accept 头部信息;浏览器通过 as 值能得知资源类型,因此当获取的资源相同时,浏览器能够判断前面获取的资源是否能重用

如果忽略 as 属性,或者错误的 as 属性会使 preload 等同于 XHR 请求,浏览器不知道加载的是什么,因此会赋予此类资源非常低的加载优先级
Preload 的与众不同还体现在 onload 事件上。也就是说可以定义资源加载完毕后的回调函数

在VUE SSR生成的页面中,首页的资源均使用preload,而路由对应的资源,则使用prefetch

从输入 url 到浏览器显示页面,中间经历什么过程?

  1. 在浏览器地址栏输入url
  2. 浏览器进行DNS查询是否有缓存,如果缓存中有,则直接读取缓存,显示页面内容
  3. 如果无缓存,则DNS解析(域名解析),解析获取相应的IP地址
  4. 浏览器器向服务器发起TCP连接,与浏览器进行三次握手
  5. 握手成功后,浏览器向服务器发送http请求,请求数据
  6. 服务器处理收到的请求,将数据返回至浏览器
  7. 浏览器收到http响应,读取页面内容,浏览器渲染,解析html源码
  8. 生成DOM树,解析css样式,生成css树,js交互
  9. 客户端和服务器交互
  10. ajax查询
HTTP缓存

DNS的查询顺序(是否有缓存):浏览器缓存-系统缓存-路由器缓存-ISP DNS缓存 - (递归搜索)

递归查询

  • 浏览器查询缓存,是否有百度的ip,如果有结束
  • hosts文件中是否有百度的ip地址,如果有结束
  • 如果本地DNS有百度的ip地址,如果有,本地DNS将其返回给请求主机,然后结束
  • 根服务器根据com后缀,将请求转发给顶级域名服务器
  • 顶级域名服务器查询自己的权威DNS服务器
  • 权威DNS域名服务器查询到百度的IP,将结果返回给顶级,顶级返回给根,根返回给本地,本地返回给请求主机,结束。
  1. 浏览器缓存机制
    a. 缓存位置 (特点: 各自是有优先级的)
  • Service Worker: 运行在浏览器背后的独立线程。(缓存是持续性的,传输协议必须https,因为涉及到请求拦截)
  • Memory Cache: 内存中的缓存。(特点: 读取比较快,但持续性很短,一旦关闭tab他页面,缓存会被释放。内存比较小)(from memory cache)
    内存缓存中有一块重要的缓存资源是preloader相关指令(例如)下载的资源。
    注意: 内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。
  • Disk Cache: 存储在硬盘中的缓存,读取速度会慢一些,但是什么都能存储到磁盘中,比之Memory Cache优势在于容量和存储时效上。
  • Push Cache: 推送缓存是HTTP/2的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在谷歌浏览器中只有5分钟左右。

浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?

  • 对于大文件来说,大概率是不存储在内存中的,反之优先
  • 当前系统内存使用率高的话,文件优先存储进硬盘

b. 缓存过程
浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

根据是否需要向服务器重新发起HTTP请求缓存过程分为两个部分,分别是强缓存协商缓存

c. 缓存策略(强缓存和对比缓存,并且缓存策略都是通过HTTP Header来实现的)

强缓存:不会向服务器发送请求,直接从缓存中读取资源,并且size显示from disk cache或from memory cache。强缓存可以通过设置两种HTTP Header实现:Expires 和 Cache-Control

  • Expires: 缓存过期时间,用来指定资源到期的时间,是服务端的具体时间点。(Expires = max-age(单位是s) + 请求时间,需要和last-modified结合使用)。Expires 是web服务器响应的消息头字段,可以在浏览器缓存中读取数据。 Expires是HTTP/1的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

  • Cache-Control 在HTTP/1.1中,Cache-control是最重要的规则,主要用于控制网页缓存,Cache-Control 可以在请求头或者响应头中设置。
    参数: public: 所有内容将被缓存(客户端和代理服务器);private: 所有内容只有客户端可以缓存,Cache-Control的默认值。no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control的缓存控制方式做前置验证,而是使用 Etag 或者Last-Modified字段来控制缓存。no-store:所有内容都不会被缓存

  • 对比Expires和Cache-Control
    区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。

强缓存判断缓存的依据来自于是否超出摸个时间或者时间段,而不关心服务器端的文件是否已经跟新

协商缓存:强制缓存失效后,浏览器带缓存标示向服务器发起请求,由服务器根据缓存标识是否使用缓存的过程

  • 协商缓存生效,返回304和Not Modified
  • 协商缓存失效,返回200和请求结果。

协商缓存可以通过设置两种HTTP Header实现: ETag和Last-Modified

  • Last-Modified和If-Modified-Since
    会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200

产生的问题:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
  • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源

因为根据文件修改时间来决定是否缓存尚有不足,能否根据内容是否修改来决定缓存策略,所以在 HTTP / 1.1 出现了 ETag 和If-None-Match

  • ETag和If-None-Match
    Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成。

对比:

  • 精确度:Etag要优于Last-Modified。
    Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
  • 性能: Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
  • 优先级: 服务器校验优先考虑Etag

d. 缓存机制
强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。

问题: 如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。

e. 实际场景应用缓存策略

  • 频繁变动的资源: Cache-Control: no-cache
    对于频繁变动的资源,首先需要使用Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

  • 不常变化的资源
    Cache-Control: max-age=(特别大的值)
    这样浏览器请求相同的URL会强制命中缓存,为了解决问题,就需要在路由或者文件名中加hash

因特网协议

因特网协议:应用层、传输层、网络层、链接层、实体层

http报文结构
  1. Http请求包括: 状态行、请求头、请求正文三部分组成
  • 状态行:
    Request URL: https://www.baidu.com/
    Request Method: GET
    Remote Address(远程地址,一般会自动转成IP)
    Referrer Policy: unsafe-url

  • 请求头(request Header)
    Accept: 接收类型,表示浏览器支持的MIME类型(对标服务端返回的Content-Type)
    Accept-Encoding:浏览器支持的压缩类型,如gzip等,超出类型不能接收
    Content-Type:客户端发送出去实体内容的类型
    Cache-Control: 指定请求和响应遵循的缓存机制,如no-cache
    If-Modified-Since:对应服务端的Last-Modified,用来匹配看文件是否变动,只能精确到1s之内,http1.0中
    Expires:缓存控制,在这个时间内不会请求,直接使用缓存,http1.0,而且是服务端时间
    Max-age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存,http1.1中
    If-None-Match:对应服务端的ETag,用来匹配文件内容是否改变(非常精确),http1.1中
    Cookie: 有cookie并且同域访问时会自动带上
    Connection: 当浏览器与服务器通信时对于长连接如何进行处理,如keep-alive
    Host:请求的服务器URL
    Origin:最初的请求是从哪里发起的(只会精确到端口),Origin比Referer更尊重隐私
    Referer:该页面的来源URL(适用于所有类型的请求,会精确到详细页面地址,csrf拦截常用到这个字段)
    User-Agent:用户客户端的一些必要信息,如UA头部等

  • 请求正文:
    1、一些参数(例如POST请求)

  1. HTTP返回:状态行,响应头,响应正文
  • 响应头部
    Access-Control-Allow-Headers: 服务器端允许的请求Headers
    Access-Control-Allow-Methods: 服务器端允许的请求方法
    Access-Control-Allow-Origin: 服务器端允许的请求Origin头部(譬如为*)
    Content-Type:服务端返回的实体内容的类型
    Date:数据从服务器发送的时间
    Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档
    Last-Modified:请求资源的最后修改时间
    Expires:应该在什么时候认为文档已经过期,从而不再缓存它
    Max-age:客户端的本地资源应该缓存多少秒,开启了Cache-Control后有效
    ETag:请求变量的实体标签的当前值
    Set-Cookie:设置和页面关联的cookie,服务器通过这个头部把cookie传给客户端
    Keep-Alive:如果客户端有keep-alive,服务端也会有响应(如timeout=38)
    Server:服务器的一些相关信息

canvas

调用过 imageData = ctx.getImageData(sx, sy, sw, sh); 这个 API。这个 API 返回的是一个 ImageData 数组,包含了 sx, sy, sw, sh 表示的矩形的像素数据。
而且这个数组是 Uint8 类型的,且四位表示一个像素。

我们在定义颜色的时候就是使用 rgba(r,g,b,a) 四个维度来表示,而且每个像素值就是用十六位 00-ff 表示,即每个维度的范围是 0~255,即 2^8 位,即 1 byte, 也就是 Uint8 能表示的范围。

所以 100 100 canvas 占的内存是 100 100 * 4 bytes = 40,000 bytes。

CSS

盒式模型

在一个文档中,每个元素都被表示为一个矩形的盒子。确定这些盒子的尺寸, 属性 — 像它的颜色,背景,边框方面 — 和位置是渲染引擎的目标。
在CSS中,使用标准盒模型描述这些矩形盒子中的每一个。这个模型描述了元素所占空间的内容。每个盒子有四个边:外边距边, 边框边, 内填充边与内容边。

最外面橙色的就是外边距区域(margin area ),往里黄色的是边框区域(border area),再往里的绿色的是内边距区域(padding area ),最里面绿色的就是内容区域(content area)了。

1
box-sizing: border-box

position的属性

  • absolute
    生成绝对定位的元素,相对于 static 定位以外的第一个父元素进行定位。
    元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。

  • fixed
    生成绝对定位的元素,相对于浏览器窗口进行定位。

元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。

  • relative
    生成相对定位的元素,相对于其正常位置进行定位。
    因此,”left:20” 会向元素的 LEFT 位置添加 20 像素。

  • static 默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明)。

  • inherit 规定应该从父元素继承 position 属性的值。

flex布局

单行排布:justify-content: flex-start
多行排布: align-content: flex-start

两栏布局:父级元素设置display: flex 左边flex: 80px 0; 右边flex:1

CSS两栏布局

  • float+margin-left
  • absolute+margin-left
  • float+BFC(overflow:hidden)
  • flex布局
  • display:inline-block
  • 浮动布局+负外边距
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <div class="aside"></div>
    <div class="main">
    <div class="content"></div>
    </div>
    .aside{
    width: 300px;
    height: 100px;
    background:darkcyan;
    margin-right: -100%;
    float: left;
    }
    .main{
    width: 100%;
    float: left;
    }
    .content{
    margin-left: 300px;
    background: salmon;
    }

清除浮动

  • 给前面一个父元素设置高度
  • 给后面的盒子添加clear属性(clear:both;zoom:1)
  • 外墙法,在两个盒子中间添加一个额外的块级元素,并给这个添加的元素设置clear:both属性。
  • 利用伪元素添加块级元素清除浮动

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .box {
    zoom:1
    }
    .box:after {
    content: '',
    display: block,
    height: 0,
    clear:both,
    visibility: hidden,
    }
  • overflow:hidden;
    1.可以将超出标签范围的内容裁剪掉
    2.清除浮动
    3.两个嵌套的盒子,可以让里面的盒子在设置margin-top时,外边的盒子不被顶下来。

CSS三栏布局

  • float
  • position
  • display:inline-block

BFC及其特性

BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响。创建 BFC 的方式有:

  • html 根元素
  • float 浮动
  • 绝对定位
  • overflow 不为 visiable
  • display 为表格布局或者弹性布局

BFC 主要的作用是:

  • 清除浮动
  • 防止同一 BFC 容器中的相邻元素间的外边距重叠问题

div实现水平垂直居中

1
2
3
<div class="parent">
<div class="child"></div>
</div>

方法1.

1
2
3
4
5
.parent {
display: flex;
justify-content: center;
align-items: center;
}

方法2.

1
2
3
4
5
6
7
8
9
10
.parent {
position: relative;
}

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

方法3.

1
2
3
4
5
6
7
8
.parent {
display: grid;
}

.child {
justify-self: center;
align-self: center;
}

方法4.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.parent {
font-size: 0;
text-align: center;
&::before {
content: '';
display: inline-block;
width: 0;
height: 100%;
vertical-align: middle;
}
}
.child {
display: inline-block;
vertical-align: middle;
}

方法5.

1
2
3
4
5
6
.parent {
display: flex;
}
.child {
margin: auto;
}

opacity:0/visibility:hidden/display:none优劣及适用场景

  • 结构:
    display:none会让元素从渲染树中消失,渲染的时候不占据任何内容,无法点击。
    visibility:hidden不会让元素从渲染树中消失,渲染元素继续占据空间,只是内容不可见,不能点击。
    opacity: 0不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,可点击。

  • 继承: display: none和opacity: 0:是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示。 visibility: hidden:是继承属性,子孙节点消失由于继承了hidden,通过设置visibility: visible;可以让子孙节点显式。

  • 性能: display:none 修改元素会造成文档回流,读屏器不会读取display: none元素内容,性能消耗较大 visibility:hidden: 修改元素只会造成本元素的重绘,性能消耗较少读屏器读取visibility: hidden元素内容 opacity: 0 : 修改元素会造成重绘,性能消耗较少

如何修改才能让图片宽度为300px

1
<img src="1.jpg" style="width:480px!important;”>
1
2
3
<img src="1.jpg" style="width:480px!important; max-width: 300px">
<img src="1.jpg" style="width:480px!important; transform: scale(0.625, 1);" >
<img src="1.jpg" style="width:480px!important; width:300px!important;">
坚持原创技术分享,您的支持将鼓励我继续创作!