共计 30540 个字符,预计需要花费 77 分钟才能阅读完成。
前言
是时候撸一波 JS 基础啦, 撸熟了, 银十速拿 offer;
本文不从传统的问答方式梳理, 而是从知识维度梳理, 以便形成知识网络
包括函数, 数组, 对象, 数据结构, 算法, 设计模式和 http.
1. 函数
1.1 函数的 3 种定义方法
1.1.1 函数声明
//ES5
function getSum(){}
function (){}// 匿名函数
//ES6
()=>{}// 如果 {} 内容只有一行 {} 和 return 关键字可省,
1.1.2 函数表达式(函数字面量)
//ES5
var sum=function(){}
//ES6
let sum=()=>{}// 如果 {} 内容只有一行 {} 和 return 关键字可省,
1.1.3 构造函数
var sum=new GetSum(num1,num2)
1.1.4 三种方法的对比
1. 函数声明有预解析, 而且函数声明的优先级高于变量;
2. 使用 Function 构造函数定义函数的方式是一个函数表达式, 这种方式会导致解析两次代码,影响性能。第一次解析常规的 JavaScript 代码,第二次解析传入构造函数的字符串
1.2.ES5 中函数的 4 种调用
在 ES5 中函数内容的 this 指向和调用方法有关
1.2.1 函数调用模式
包括函数名 () 和匿名函数调用,this 指向 window
function getSum() {console.log(this) //window
}
getSum()
(function() {console.log(this) //window
})()
var getSum=function() {console.log(this) //window
}
getSum()
1.2.2 方法调用
对象. 方法名(),this 指向对象
var objList = {
name: 'methods',
getSum: function() {console.log(this) //objList 对象
}
}
objList.getSum()
1.2.3 构造器调用
new 构造函数名(),this 指向构造函数
function Person() {console.log(this); // 指向构造函数 Person
}
var personOne = new Person();
1.2.4 间接调用
利用 call 和 apply 来实现,this 就是 call 和 apply 对应的第一个参数, 如果不传值或者第一个值为 null,undefined 时 this 指向 window
function foo() {console.log(this);
}
foo.apply('我是 apply 改变的 this 值');// 我是 apply 改变的 this 值
foo.call('我是 call 改变的 this 值');// 我是 call 改变的 this 值
1.3 ES6 中函数的调用
箭头函数不可以当作构造函数使用,也就是不能用 new 命令实例化一个对象,否则会抛出一个错误
箭头函数的 this 是和定义时有关和调用无关
调用就是函数调用模式
(() => {console.log(this)//window
})()
let arrowFun = () => {console.log(this)//window
}
arrowFun()
let arrowObj = {arrFun: function() {(() => {console.log(this)//arrowObj
})()}
}
arrowObj.arrFun();
1.4.call,apply 和 bind
1.IE5 之前不支持 call 和 apply,bind 是 ES5 出来的;
2.call 和 apply 可以调用函数, 改变 this, 实现继承和借用别的对象的方法;
1.4.1 call 和 apply 定义
调用方法, 用一个对象替换掉另一个对象 (this)
对象.call(新 this 对象, 实参 1, 实参 2, 实参 3 …..)
对象.apply(新 this 对象,[实参 1, 实参 2, 实参 3 …..])
1.4.2 call 和 apply 用法
1. 间接调用函数, 改变作用域的 this 值
2. 劫持其他对象的方法
var foo = {
name:"张三",
logName:function(){console.log(this.name);
}
}
var bar={name:"李四"};
foo.logName.call(bar);// 李四
实质是 call 改变了 foo 的 this 指向为 bar, 并调用该函数
3. 两个函数实现继承
function Animal(name){
this.name = name;
this.showName = function(){console.log(this.name);
}
}
function Cat(name){Animal.call(this, name);
}
var cat = new Cat("Black Cat");
cat.showName(); //Black Cat
4. 为类数组 (arguments 和 nodeList) 添加数组方法 push,pop
(function(){Array.prototype.push.call(arguments,'王五');
console.log(arguments);//['张三','李四','王五']
})('张三','李四')
5. 合并数组
let arr1=[1,2,3];
let arr2=[4,5,6];
Array.prototype.push.apply(arr1,arr2); // 将 arr2 合并到了 arr1 中
6. 求数组最大值
Math.max.apply(null,arr)
7. 判断字符类型
Object.prototype.toString.call({})
1.4.3 bind
bind 是 function 的一个函数扩展方法,bind 以后代码重新绑定了 func 内部的 this 指向, 不会调用方法, 不兼容 IE8
var name = '李四'
var foo = {
name: "张三",
logName: function(age) {console.log(this.name, age);
}
}
var fooNew = foo.logName;
var fooNewBind = foo.logName.bind(foo);
fooNew(10)// 李四,10
fooNewBind(11)// 张三,11 因为 bind 改变了 fooNewBind 里面的 this 指向
1.4.4 call,apply 和 bind 原生实现
call 实现:
Function.prototype.newCall = function(context, ...parameter) {
context.fn = this;
context.fn(...parameter);
delete context.fn;
}
let person = {name: 'Abiel'}
function sayHi(age,sex) {console.log(this.name, age, sex);
}
sayHi.newCall (person, 25, '男'); // Abiel 25 男
apply 实现:
Function.prototype.newApply = function(context, parameter) {if (typeof context === 'object') {context = context || window} else {context = Object.create(null)
}
let fn = Symbol()
context[fn] = this
context[fn](parameter);
delete context[fn]
}
bind 实现:
Function.prototype.bind = function (context,...innerArgs) {
var me = this
return function (...finnalyArgs) {return me.call(context,...innerArgs,...finnalyArgs)
}
}
let person = {name: 'Abiel'}
function sayHi(age,sex) {console.log(this.name, age, sex);
}
let personSayHi = sayHi.bind(person, 25)
personSayHi('男')
1.4.5 三者异同
同: 都是改变 this 指向, 都可接收参数
异:bind 和 call 是接收单个参数,apply 是接收数组
1.5. 函数的节流和防抖
类型 | 概念 | 应用 |
---|---|---|
节流 | 某个时间段内, 只执行一次 | 滚动条,resize 事件一段时间触发一次 |
防抖 | 处理函数截止后一段时间依次执行 | scroll,resize 事件触发完后一段时间触发 |
节流:
1.5.1 节流
let throttle = function(func, delay) {
let timer = null;
return function() {if (!timer) {timer = setTimeout(function() {func.apply(this, arguments);
timer = null;
}, delay);
}
};
};
function handle() {console.log(Math.random());
}
window.addEventListener("scroll", throttle(handle, 1000)); // 事件处理函数
1.5.2 防抖
function debounce(fn, wait) {
var timeout = null;
return function() {if (timeout !== null) clearTimeout(timeout);// 如果多次触发将上次记录延迟清除掉
timeout = setTimeout(function() {fn.apply(this, arguments);
timer = null;
}, wait);
};
}
// 处理函数
function handle() {console.log(Math.random());
}
// 滚动事件
window.addEventListener("onscroll", debounce(handle, 1000));
1.6. 原型链
1.6.1 定义
对象继承属性的一个链条
1.6.2 构造函数, 实例与原型对象的关系
var Person = function (name) {this.name = name;}//person 是构造函数
var o3personTwo = new Person('personTwo')//personTwo 是实例
原型对象都有一个默认的 constructor 属性指向构造函数
1.6.3 创建实例的方法
1. 字面量
let obj={'name':'张三'}
2.Object 构造函数创建
let Obj=new Object()
Obj.name='张三'
3. 使用工厂模式创建对象
function createPerson(name){var o = new Object();
o.name = name;
};
return o;
}
var person1 = createPerson('张三');
4. 使用构造函数创建对象
function Person(name){this.name = name;}
var person1 = new Person('张三');
1.6.4 new 运算符
1. 创了一个新对象;
2.this 指向构造函数;
3. 构造函数有返回, 会替换 new 出来的对象, 如果没有就是 new 出来的对象
4. 手动封装一个 new 运算符
var new2 = function (func) {var o = Object.create(func.prototype); // 创建对象
var k = func.call(o); // 改变 this 指向,把结果付给 k
if (typeof k === 'object') { // 判断 k 的类型是不是对象
return k; // 是,返回 k
} else {return o; // 不是返回返回构造函数的执行结果}
}
1.6.5 对象的原型链
1.7 继承的方式
JS 是一门弱类型动态语言, 封装和继承是他的两大特性
1.7.1 原型链继承
将父类的实例作为子类的原型
1. 代码实现
定义父类:
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {console.log(this.name + '正在吃:' + food);
};
子类:
function Cat(){}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name);//cat
console.log(cat.eat('fish'));//cat 正在吃:fish undefined
console.log(cat.sleep());//cat 正在睡觉!undefined
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
2. 优缺点
简单易于实现, 但是要想为子类新增属性和方法,必须要在 new Animal()这样的语句之后执行, 无法实现多继承
1.7.2 构造继承
实质是利用 call 来改变 Cat 中的 this 指向
1. 代码实现
子类:
function Cat(name){Animal.call(this);
this.name = name || 'Tom';
}
2. 优缺点
可以实现多继承, 不能继承原型属性 / 方法
1.7.3 实例继承
为父类实例添加新特性,作为子类实例返回
1. 代码实现
子类
function Cat(name){var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
2. 优缺点
不限制调用方式, 但不能实现多继承
1.7.4 拷贝继承
将父类的属性和方法拷贝一份到子类中
1. 子类:
function Cat(name){var animal = new Animal();
for(var p in animal){Cat.prototype[p] = animal[p];
}
Cat.prototype.name = name || 'Tom';
}
2. 优缺点
支持多继承, 但是效率低占用内存
1.7.5 组合继承
通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
1. 子类:
function Cat(name){Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
1.7.6 寄生组合继承
function Cat(name){Animal.call(this);
this.name = name || 'Tom';
}
(function(){
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
// 将实例作为子类的原型
Cat.prototype = new Super();})();
1.7.7 ES6 的 extends 继承
ES6 的继承机制是先创造父类的实例对象 this(所以必须先调用 super 方法),然后再用子类的构造函数修改 this, 链接描述
// 父类
class Person {
//constructor 是构造方法
constructor(skin, language) {
this.skin = skin;
this.language = language;
}
say() {console.log('我是父类')
}
}
// 子类
class Chinese extends Person {constructor(skin, language, positon) {//console.log(this);// 报错
super(skin, language);
//super(); 相当于父类的构造函数
//console.log(this); 调用 super 后得到了 this,不报错,this 指向子类,相当于调用了父类.prototype.constructor.call(this)
this.positon = positon;
}
aboutMe() {console.log(`${this.skin} ${this.language} ${this.positon}`);
}
}
// 调用只能通过 new 的方法得到实例, 再调用里面的方法
let obj = new Chinese('红色', '中文', '香港');
obj.aboutMe();
obj.say();
1.8. 高阶函数
1.8.1 定义
函数的参数是函数或返回函数
1.8.2 常见的高阶函数
map,reduce,filter,sort
1.8.3 柯里化
1. 定义: 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数
fn(a,b,c,d)=>fn(a)(b)(c)(d)
2. 代码实现:
let currying = function(fn) {
// args 获取第一个方法内的全部参数
var args = Array.prototype.slice.call(arguments, 1)
return function() {
// 将后面方法里的全部参数和 args 进行合并
var newArgs = args.concat(Array.prototype.slice.call(arguments))
// 把合并后的参数通过 apply 作为 fn 的参数并执行
return fn.apply(this, newArgs)
}
}
1.8.4 反柯里化
1. 定义:
obj.func(arg1, arg2)=>func(obj, arg1, arg2)
2. 代码实现:
Function.prototype.uncurrying = function() {
var that = this;
return function() {return Function.prototype.call.apply(that, arguments);
}
};
function sayHi () {return "Hello" + this.value +" "+[].slice.call(arguments);
}
let sayHiuncurrying=sayHi.uncurrying();
console.log(sayHiuncurrying({value:'world'},"hahaha"));
1.8.5 偏函数
1. 定义: 指定部分参数来返回一个新的定制函数的形式
2. 例子:
function foo(a, b, c) {return a + b + c;}
function func(a, b) {return foo(a,b,8);
}
2. 对象
2.1. 对象的声明方法
2.1.1 字面量
var test2 = {x:123,y:345};
console.log(test2);//{x:123,y:345};
console.log(test2.x);//123
console.log(test2.__proto__.x);//undefined
console.log(test2.__proto__.x === test2.x);//false
2.1.2 构造函数
var test1 = new Object({x:123,y:345});
console.log(test1);//{x:123,y:345}
console.log(test1.x);//123
console.log(test1.__proto__.x);//undefined
console.log(test1.__proto__.x === test1.x);//false
new 的作用:
1. 创了一个新对象;
2.this 指向构造函数;
3. 构造函数有返回, 会替换 new 出来的对象, 如果没有就是 new 出来的对象
2.1.3 内置方法
Obejct.create(obj,descriptor),obj 是对象,describe 描述符属性(可选)
let test = Object.create({x:123,y:345});
console.log(test);//{}
console.log(test.x);//123
console.log(test.__proto__.x);//3
console.log(test.__proto__.x === test.x);//true
2.1.4 三种方法的优缺点
1. 功能: 都能实现对象的声明, 并能够赋值和取值
2. 继承性: 内置方法创建的对象继承到__proto__属性上
3. 隐藏属性: 三种声明方法会默认为内部的每个成员(属性或方法)生成一些隐藏属性,这些隐藏属性是可以读取和可配置的, 属性分类见下面
4. 属性读取:Object.getOwnPropertyDescriptor() 或 getOwnPropertyDescriptor()
5. 属性设置:Object.definePropertype 或 Object.defineProperties
2.2. 对象的属性
2.2.1 属性分类
1. 数据属性 4 个特性:
configurable(可配置),enumerable(可枚举),writable(可修改),value(属性值)
2. 访问器属性 2 个特性:
get(获取),set(设置)
3. 内部属性
由 JavaScript 引擎内部使用的属性;
不能直接访问, 但是可以通过对象内置方法间接访问, 如:[[Prototype]]可以通过 Object.getPrototypeOf()访问;
内部属性用 [[]] 包围表示, 是一个抽象操作, 没有对应字符串类型的属性名, 如[[Prototype]].
2.2.2 属性描述符
1. 定义: 将一个属性的所有特性编码成一个对象返回
2. 描述符的属性有: 数据属性和访问器属性
3. 使用范围:
作为方法 Object.defineProperty, Object.getOwnPropertyDescriptor, Object.create 的第二个参数,
2.2.3 属性描述符的默认值
1. 访问对象存在的属性
特性名 | 默认值 |
---|---|
value | 对应属性值 |
get | 对应属性值 |
set | undefined |
writable | true |
enumerable | true |
configurable | true |
所以通过上面三种声明方法已存在的属性都是有这些默认描述符
2. 访问对象不存在的属性
特性名 | 默认值 |
---|---|
value | undefined |
get | undefined |
set | undefined |
writable | false |
enumerable | false |
configurable | false |
2.2.3 描述符属性的使用规则
get,set 与 wriable,value 是互斥的, 如果有交集设置会报错
2.2.4 属性定义
1. 定义属性的函数有两个:Object.defineProperty 和 Object.defineProperties. 例如:
Object.defineProperty(obj, propName, desc)
2. 在引擎内部, 会转换成这样的方法调用:
obj.[[DefineOwnProperty]](propName, desc, true)
2.2.5 属性赋值
1. 赋值运算符 (=) 就是在调用[[Put]]. 比如:
obj.prop = v;
2. 在引擎内部, 会转换成这样的方法调用:
obj.[[Put]](“prop”, v, isStrictModeOn)
2.2.6 判断对象的属性
名称 | 含义 | 用法 |
---|---|---|
in | 如果指定的属性在指定的对象或其原型链中,则 in 运算符返回 true | ‘name’ in test //true |
hasOwnProperty() | 只判断自身属性 | test.hasOwnProperty(‘name’) //true |
. 或[] | 对象或原型链上不存在该属性,则会返回 undefined | test.name //”lei” test[“name”] //”lei” |
2.3.Symbol
2.3.1 概念
是一种数据类型;
不能 new, 因为 Symbol 是一个原始类型的值,不是对象。
2.3.2 定义方法
Symbol(), 可以传参
var s1 = Symbol();
var s2 = Symbol();
s1 === s2 // false
// 有参数的情况
var s1 = Symbol("foo");
var s2 = Symbol("foo");
s1 === s2 // false
2.3.3 用法
1. 不能与其他类型的值进行运算;
2. 作为属性名
let mySymbol = Symbol();
// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
var a = {[mySymbol]: 'Hello!'
};
// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!'});
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
3. 作为对象属性名时,不能用点运算符, 可以用[]
let a = {};
let name = Symbol();
a.name = 'lili';
a[name] = 'lucy';
console.log(a.name,a[name]);
4. 遍历不会被 for…in、for…of 和 Object.keys()、Object.getOwnPropertyNames()取到该属性
2.3.4 Symbol.for
1. 定义: 在全局中搜索有没有以该参数作为名称的 Symbol 值,如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值
2. 举例:
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true
2.3.5 Symbol.keyFor
1. 定义: 返回一个已登记的 Symbol 类型值的 key
2. 举例:
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
2.4. 遍历
2.4.1 一级对象遍历方法
方法 | 特性 |
---|---|
for … in | 遍历对象自身的和继承的可枚举属性(不含 Symbol 属性) |
Object.keys(obj) | 返回一个数组, 包括对象自身的 (不含继承的) 所有可枚举属性(不含 Symbol 属性) |
Object.getOwnPropertyNames(obj) | 返回一个数组, 包括对象自身的所有可枚举属性(不含 Symbol 属性) |
Object.getOwnPropertySymbols(obj) | 返回一个数组, 包含对象自身的所有 Symbol 属性 |
Reflect.ownKeys(obj) | 返回一个数组, 包含对象自身的所有 (不枚举、可枚举和 Symbol) 属性 |
Reflect.enumerate(obj) | 返回一个 Iterator 对象, 遍历对象自身的和继承的所有可枚举属性(不含 Symbol 属性) |
总结:1. 只有 Object.getOwnPropertySymbols(obj)和 Reflect.ownKeys(obj)可以拿到 Symbol 属性
2. 只有 Reflect.ownKeys(obj) 可以拿到不可枚举属性
2.4.2 多级对象遍历
数据模型:
var treeNodes = [
{
id: 1,
name: '1',
children: [
{
id: 11,
name: '11',
children: [
{
id: 111,
name: '111',
children:[]},
{
id: 112,
name: '112'
}
]
},
{
id: 12,
name: '12',
children: []}
],
users: []},
];
递归:
var parseTreeJson = function(treeNodes){if (!treeNodes || !treeNodes.length) return;
for (var i = 0, len = treeNodes.length; i < len; i++) {var childs = treeNodes[i].children;
console.log(treeNodes[i].id);
if(childs && childs.length > 0){parseTreeJson(childs);
}
}
};
console.log('------------- 递归实现 ------------------');
parseTreeJson(treeNodes);
2.5. 深度拷贝
2.5.1 Object.assign
1. 定义: 将源对象(source)的所有可枚举属性,复制到目标对象(target)
2. 用法:
合并多个对象
var target = {a: 1, b: 1};
var source1 = {b: 2, c: 2};
var source2 = {c: 3};
Object.assign(target, source1, source2);
3. 注意:
这个是伪深度拷贝, 只能拷贝第一层
2.5.2 JSON.stringify
1. 原理: 是将对象转化为字符串, 而字符串是简单数据类型
2.5.3 递归拷贝
function deepClone(source){const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
for(let keys in source){ // 遍历目标
if(source.hasOwnProperty(keys)){if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
targetObj[keys] = source[keys].constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]);
}else{ // 如果不是,就直接赋值
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
2.6. 数据拦截
定义: 利用对象内置方法, 设置属性, 进而改变对象的属性值
2.6.1 Object.defineProterty
1.ES5 出来的方法;
2. 三个参数: 对象(必填), 属性值(必填), 描述符(可选);
3.defineProterty 的描述符属性
数据属性:value,writable,configurable,enumerable
访问器属性:get,set
注: 不能同时设置 value 和 writable, 这两对属性是互斥的
4. 拦截对象的两种情况:
let obj = {name:'',age:'',sex:''},
defaultName = ["这是姓名默认值 1","这是年龄默认值 1","这是性别默认值 1"];
Object.keys(obj).forEach(key => {
Object.defineProperty(obj, key, {get() {return defaultName;},
set(value) {defaultName = value;}
});
});
console.log(obj.name);
console.log(obj.age);
console.log(obj.sex);
obj.name = "这是改变值 1";
console.log(obj.name);
console.log(obj.age);
console.log(obj.sex);
let objOne={},defaultNameOne="这是默认值 2";
Object.defineProperty(obj, 'name', {get() {return defaultNameOne;},
set(value) {defaultNameOne = value;}
});
console.log(objOne.name);
objOne.name = "这是改变值 2";
console.log(objOne.name);
5. 拦截数组变化的情况
let a={};
bValue=1;
Object.defineProperty(a,"b",{set:function(value){
bValue=value;
console.log("setted");
},
get:function(){return bValue;}
});
a.b;//1
a.b=[];//setted
a.b=[1,2,3];//setted
a.b[1]=10;// 无输出
a.b.push(4);// 无输出
a.b.length=5;// 无输出
a.b;//[1,10,3,4,undefined];
结论:defineProperty 无法检测数组索引赋值, 改变数组长度的变化;
但是通过数组方法来操作可以检测到
6. 存在的问题
不能监听数组索引赋值和改变长度的变化
必须深层遍历嵌套的对象, 因为 defineProterty 只能劫持对象的属性, 因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历, 显然能劫持一个完整的对象是更好的选择
2.6.2 proxy
1.ES6 出来的方法, 实质是对对象做了一个拦截, 并提供了 13 个处理方法
13 个方法详情请戳, 阮一峰的 proxy 介绍
2. 两个参数: 对象和行为函数
let handler = {get(target, key, receiver) {console.log("get", key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {console.log("set", key, value);
return Reflect.set(target, key, value, receiver);
}
};
let proxy = new Proxy(obj, handler);
proxy.name = "李四";
proxy.age = 24;
3. 问题和优点
reflect 对象没有构造函数
可以监听数组索引赋值, 改变数组长度的变化,
是直接监听对象的变化, 不用深层遍历
2.6.3 defineProterty 和 proxy 的对比
1.defineProterty 是 es5 的标准,proxy 是 es6 的标准;
2.proxy 可以监听到数组索引赋值, 改变数组长度的变化;
3.proxy 是监听对象, 不用深层遍历,defineProterty 是监听属性;
3. 利用 defineProterty 实现双向数据绑定 (vue2.x 采用的核心)
请戳, 剖析 Vue 原理 & 实现双向绑定 MVVM
4. 利用 proxy 实现双向数据绑定(vue3.x 会采用)
3. 数组
数组基本上考察数组方法多一点, 所以这里就单纯介绍常见的场景数组的方法, 还有很多场景后续补充;
本文主要从应用来讲数组 api 的一些骚操作;
如一行代码扁平化 n 维数组、数组去重、求数组最大值、数组求和、排序、对象和数组的转化等;
上面这些应用场景你可以用一行代码实现?
3.1 扁平化 n 维数组
1. 终极篇
[1,[2,3]].flat(2) //[1,2,3]
[1,[2,3,[4,5]].flat(3) //[1,2,3,4,5]
[1,[2,3,[4,5]]].toString() //'1,2,3,4,5'
[1[2,3,[4,5[...]].flat(Infinity) //[1,2,3,4...n]
Array.flat(n)是 ES10 扁平数组的 api,n 表示维度,n 值为 Infinity 时维度为无限大
2. 开始篇
function flatten(arr) {while(arr.some(item=>Array.isArray(item))) {arr = [].concat(...arr);
}
return arr;
}
flatten([1,[2,3]]) //[1,2,3]
flatten([1,[2,3,[4,5]]) //[1,2,3,4,5]
实质是利用递归和数组合并方法 concat 实现扁平
3.2 去重
1. 终极篇
Array.from(new Set([1,2,3,3,4,4])) //[1,2,3,4]
[...new Set([1,2,3,3,4,4])] //[1,2,3,4]
set 是 ES6 新出来的一种一种定义不重复数组的数据类型
Array.from 是将类数组转化为数组
… 是扩展运算符, 将 set 里面的值转化为字符串
2. 开始篇
Array.prototype.distinct = nums => {const map = {}
const result = []
for (const n of nums) {if (!(n in map)) {map[n] = 1
result.push(n)
}
}
return result
}
[1,2,3,3,4,4].distinct(); //[1,2,3,4]
取新数组存值, 循环两个数组值相比较
3.3 排序
1. 终极篇
[1,2,3,4].sort((a, b) => a - b); // [1, 2,3,4], 默认是升序
[1,2,3,4].sort((a, b) => b - a); // [4,3,2,1] 降序
sort 是 js 内置的排序方法, 参数为一个函数
2. 开始篇
冒泡排序:
Array.prototype.bubleSort=function () {
let arr=this,
len = arr.length;
for (let outer = len; outer >= 2; outer--) {for (let inner = 0; inner <= outer - 1; inner++) {if (arr[inner] > arr[inner + 1]) {
// 升序
[arr[inner], arr[inner + 1]] = [arr[inner + 1], arr[inner]];
console.log([arr[inner], arr[inner + 1]]);
}
}
}
return arr;
}
[1,2,3,4].bubleSort() //[1,2,3,4]
选择排序
Array.prototype.selectSort=function () {
let arr=this,
len = arr.length;
for (let i = 0, len = arr.length; i < len; i++) {for (let j = i, len = arr.length; j < len; j++) {if (arr[i] > arr[j]) {[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
}
return arr;
}
[1,2,3,4].selectSort() //[1,2,3,4]
3.4 最大值
1. 终极篇
Math.max(...[1,2,3,4]) //4
Math.max.apply(this,[1,2,3,4]) //4
[1,2,3,4].reduce((prev, cur,curIndex,arr)=> {return Math.max(prev,cur);
},0) //4
Math.max()是 Math 对象内置的方法, 参数是字符串;
reduce 是 ES5 的数组 api, 参数有函数和默认初始值;
函数有四个参数,pre(上一次的返回值),cur(当前值),curIndex(当前值索引),arr(当前数组)
2. 开始篇
先排序再取值
3.5 求和
1. 终极篇
[1,2,3,4].arr.reduce(function (prev, cur) {return prev + cur;},0) //10
2. 开始篇
function sum(arr) {
var len = arr.length;
if(len == 0){return 0;} else if (len == 1){return arr[0];
} else {return arr[0] + sum(arr.slice(1));
}
}
sum([1,2,3,4]) //10
利用 slice 截取改变数组, 再利用递归求和
3.6 合并
1. 终极篇
[1,2,3,4].concat([5,6]) //[1,2,3,4,5,6]
[...[1,2,3,4],...[4,5]] //[1,2,3,4,5,6]
let arrA = [1, 2], arrB = [3, 4]
Array.prototype.push.apply(arrA, arrB))//arrA 值为[1,2,3,4]
2. 开始篇
let arr=[1,2,3,4];
[5,6].map(item=>{arr.push(item)
})
//arr 值为[1,2,3,4,5,6], 注意不能直接 return 出来,return 后只会返回[5,6]
3.7 判断是否包含值
1. 终极篇
[1,2,3].includes(4) //false
[1,2,3].indexOf(4) //-1 如果存在换回索引
[1, 2, 3].find((item)=>item===3)) //3 如果数组中无值返回 undefined
[1, 2, 3].findIndex((item)=>item===3)) //2 如果数组中无值返回 -1
includes(),find(),findIndex()是 ES6 的 api
2. 开始篇
[1,2,3].some(item=>{return item===3}) //true 如果不包含返回 false
3.8 类数组转化
1. 终极篇
Array.prototype.slice.call(arguments) //arguments 是类数组(伪数组)
Array.prototype.slice.apply(arguments)
Array.from(arguments)
[...arguments]
类数组: 表示有 length 属性, 但是不具备数组的方法
call,apply: 是改变 slice 里面的 this 指向 arguments, 所以 arguments 也可调用数组的方法
Array.from 是将类似数组或可迭代对象创建为数组
… 是将类数组扩展为字符串, 再定义为数组
2. 开始篇
Array.prototype.slice = function(start,end){var result = new Array();
start = start || 0;
end = end || this.length; //this 指向调用的对象,当用了 call 后,能够改变 this 的指向,也就是指向传进来的对象,这是关键
for(var i = start; i < end; i++){result.push(this[i]);
}
return result;
}
3.9 每一项设置值
1. 终极篇
[1,2,3].fill(false) //[false,false,false]
fill 是 ES6 的方法
2. 开始篇
[1,2,3].map(() => 0)
3.10 每一项是否满足
[1,2,3].every(item=>{return item>2}) //false
every 是 ES5 的 api, 每一项满足返回 true
3.11 有一项满足
[1,2,3].some(item=>{return item>2}) //true
some 是 ES5 的 api, 有一项满足返回 true
3.12. 过滤数组
[1,2,3].filter(item=>{return item>2}) //[3]
filter 是 ES5 的 api, 返回满足添加的项的数组
3.13 对象和数组转化
Object.keys({name:'张三',age:14}) //['name','age']
Object.values({name:'张三',age:14}) //['张三',14]
Object.entries({name:'张三',age:14}) //[[name,'张三'],[age,14]]
Object.fromEntries([name,'张三'],[age,14]) //ES10 的 api,Chrome 不支持 , firebox 输出{name:'张三',age:14}
3.14 对象数组
[{count:1},{count:2},{count:3}].reduce((p, e)=>p+(e.count), 0)
4. 数据结构篇
数据结构是计算机存储、组织数据的方式, 算法是系统描述解决问题的策略。了解基本的数据结构和算法可以提高代码的性能和质量。
也是程序猿进阶的一个重要技能。
手撸代码实现栈, 队列, 链表, 字典, 二叉树, 动态规划和贪心算法
4.1 栈
栈的特点:先进后出
class Stack {constructor() {this.items = [];
}
// 入栈
push(element) {this.items.push(element);
}
// 出栈
pop() {return this.items.pop();
}
// 末位
get peek() {return this.items[this.items.length - 1];
}
// 是否为空栈
get isEmpty() {return !this.items.length;}
// 长度
get size() {return this.items.length;}
// 清空栈
clear() {this.items = [];
}
}
// 实例化一个栈
const stack = new Stack();
console.log(stack.isEmpty); // true
// 添加元素
stack.push(5);
stack.push(8);
// 读取属性再添加
console.log(stack.peek); // 8
stack.push(11);
console.log(stack.size); // 3
console.log(stack.isEmpty); // false
4.2 队列
队列:先进先出
class Queue {constructor(items) {this.items = items || [];
}
enqueue(element) {this.items.push(element);
}
dequeue() {return this.items.shift();
}
front() {return this.items[0];
}
clear() {this.items = [];
}
get size() {return this.items.length;}
get isEmpty() {return !this.items.length;}
print() {console.log(this.items.toString());
}
}
const queue = new Queue();
console.log(queue.isEmpty); // true
queue.enqueue("John");
queue.enqueue("Jack");
queue.enqueue("Camila");
console.log(queue.size); // 3
console.log(queue.isEmpty); // false
queue.dequeue();
queue.dequeue();
4.3 链表
链表: 存贮有序元素的集合,
但是不同于数组, 每个元素是一个存贮元素本身的节点和指向下一个元素引用组成
要想访问链表中间的元素, 需要从起点开始遍历找到所需元素
class Node {constructor(element) {
this.element = element;
this.next = null;
}
}
// 链表
class LinkedList {constructor() {
this.head = null;
this.length = 0;
}
// 追加元素
append(element) {const node = new Node(element);
let current = null;
if (this.head === null) {this.head = node;} else {
current = this.head;
while (current.next) {current = current.next;}
current.next = node;
}
this.length++;
}
// 任意位置插入元素
insert(position, element) {if (position >= 0 && position <= this.length) {const node = new Node(element);
let current = this.head;
let previous = null;
let index = 0;
if (position === 0) {this.head = node;} else {while (index++ < position) {
previous = current;
current = current.next;
}
node.next = current;
previous.next = node;
}
this.length++;
return true;
}
return false;
}
// 移除指定位置元素
removeAt(position) {
// 检查越界值
if (position > -1 && position < length) {
let current = this.head;
let previous = null;
let index = 0;
if (position === 0) {this.head = current.next;} else {while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = current.next;
}
this.length--;
return current.element;
}
return null;
}
// 寻找元素下标
findIndex(element) {
let current = this.head;
let index = -1;
while (current) {if (element === current.element) {return index + 1;}
index++;
current = current.next;
}
return -1;
}
// 删除指定文档
remove(element) {const index = this.indexOf(element);
return this.removeAt(index);
}
isEmpty() {return !this.length;}
size() {return this.length;}
// 转为字符串
toString() {
let current = this.head;
let string = "";
while (current) {string += ` ${current.element}`;
current = current.next;
}
return string;
}
}
const linkedList = new LinkedList();
console.log(linkedList);
linkedList.append(2);
linkedList.append(6);
linkedList.append(24);
linkedList.append(152);
linkedList.insert(3, 18);
console.log(linkedList);
console.log(linkedList.findIndex(24));
4.4 字典
字典:类似对象,以 key,value 存贮值
class Dictionary {constructor() {this.items = {};
}
set(key, value) {this.items[key] = value;
}
get(key) {return this.items[key];
}
remove(key) {delete this.items[key];
}
get keys() {return Object.keys(this.items);
}
get values() {
/*
也可以使用 ES7 中的 values 方法
return Object.values(this.items)
*/
// 在这里我们通过循环生成一个数组并输出
return Object.keys(this.items).reduce((r, c, i) => {r.push(this.items);
return r;
}, []);
}
}
const dictionary = new Dictionary();
dictionary.set("Gandalf", "gandalf@email.com");
dictionary.set("John", "johnsnow@email.com");
dictionary.set("Tyrion", "tyrion@email.com");
console.log(dictionary);
console.log(dictionary.keys);
console.log(dictionary.values);
console.log(dictionary.items);
4.5 二叉树
特点:每个节点最多有两个子树的树结构
class NodeTree {constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
class BinarySearchTree {constructor() {this.root = null;}
insert(key) {const newNode = new NodeTree(key);
const insertNode = (node, newNode) => {if (newNode.key < node.key) {if (node.left === null) {node.left = newNode;} else {insertNode(node.left, newNode);
}
} else {if (node.right === null) {node.right = newNode;} else {insertNode(node.right, newNode);
}
}
};
if (!this.root) {this.root = newNode;} else {insertNode(this.root, newNode);
}
}
// 访问树节点的三种方式: 中序, 先序, 后序
inOrderTraverse(callback) {const inOrderTraverseNode = (node, callback) => {if (node !== null) {inOrderTraverseNode(node.left, callback);
callback(node.key);
inOrderTraverseNode(node.right, callback);
}
};
inOrderTraverseNode(this.root, callback);
}
min(node) {
const minNode = node => {return node ? (node.left ? minNode(node.left) : node) : null;
};
return minNode(node || this.root);
}
max(node) {
const maxNode = node => {return node ? (node.right ? maxNode(node.right) : node) : null;
};
return maxNode(node || this.root);
}
}
const tree = new BinarySearchTree();
tree.insert(11);
tree.insert(7);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(10);
tree.insert(13);
tree.insert(12);
tree.insert(14);
tree.inOrderTraverse(value => {console.log(value);
});
console.log(tree.min());
console.log(tree.max());
5. 算法篇
5.1 冒泡算法
冒泡排序,选择排序,插入排序,此处不做赘述,请戳 排序
5.2 斐波那契
特点:第三项等于前面两项之和
function fibonacci(num) {if (num === 1 || num === 2) {return 1}
return fibonacci(num - 1) + fibonacci(num - 2)
}
5.3 动态规划
特点:通过全局规划, 将大问题分割成小问题来取最优解
案例:最少硬币找零
美国有以下面额(硬币):d1=1, d2=5, d3=10, d4=25
如果要找 36 美分的零钱,我们可以用 1 个 25 美分、1 个 10 美分和 1 个便士(1 美分)
class MinCoinChange {constructor(coins) {
this.coins = coins
this.cache = {}}
makeChange(amount) {if (!amount) return []
if (this.cache[amount]) return this.cache[amount]
let min = [], newMin, newAmount
this.coins.forEach(coin => {
newAmount = amount - coin
if (newAmount >= 0) {newMin = this.makeChange(newAmount)
}
if (newAmount >= 0 &&
(newMin.length < min.length - 1 || !min.length) &&
(newMin.length || !newAmount)) {min = [coin].concat(newMin)
}
})
return (this.cache[amount] = min)
}
}
const rninCoinChange = new MinCoinChange([1, 5, 10, 25])
console.log(rninCoinChange.makeChange(36))
// [1, 10, 25]
const minCoinChange2 = new MinCoinChange([1, 3, 4])
console.log(minCoinChange2.makeChange(6))
// [3, 3]
5.4 贪心算法
特点:通过最优解来解决问题
用贪心算法来解决 2.3 中的案例
class MinCoinChange2 {constructor(coins) {this.coins = coins}
makeChange(amount) {const change = []
let total = 0
this.coins.sort((a, b) => a < b).forEach(coin => {if ((total + coin) <= amount) {change.push(coin)
total += coin
}
})
return change
}
}
const rninCoinChange2 = new MinCoinChange2 ([ 1, 5, 10, 25])
console.log (rninCoinChange2. makeChange (36))
6 设计模式
设计模式如果应用到项目中,可以实现代码的复用和解耦,提高代码质量。本文主要介绍 14 种设计模式
写 UI 组件, 封装框架必备
6.1 简单工厂模式
1. 定义:又叫静态工厂方法,就是创建对象,并赋予属性和方法
2. 应用:抽取类相同的属性和方法封装到对象上
3. 代码:
let UserFactory = function (role) {function User(opt) {
this.name = opt.name;
this.viewPage = opt.viewPage;
}
switch (role) {
case 'superAdmin':
return new User(superAdmin);
break;
case 'admin':
return new User(admin);
break;
case 'user':
return new User(user);
break;
default:
throw new Error('参数错误, 可选参数:superAdmin、admin、user')
}
}
// 调用
let superAdmin = UserFactory('superAdmin');
let admin = UserFactory('admin')
let normalUser = UserFactory('user')
// 最后得到角色, 可以调用
6.2 工厂方法模式
1. 定义:对产品类的抽象使其创建业务主要负责用于创建多类产品的实例
2. 应用: 创建实例
3. 代码:
var Factory=function(type,content){if(this instanceof Factory){var s=new this[type](content);
return s;
}else{return new Factory(type,content);
}
}
// 工厂原型中设置创建类型数据对象的属性
Factory.prototype={Java:function(content){console.log('Java 值为',content);
},
PHP:function(content){console.log('PHP 值为',content);
},
Python:function(content){console.log('Python 值为',content);
},
}
// 测试用例
Factory('Python','我是 Python');
6.3 原型模式
1. 定义: 设置函数的原型属性
2. 应用: 实现继承
3. 代码:
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {console.log(this.name + '正在吃:' + food);
};
function Cat(){}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name);//cat
console.log(cat.eat('fish'));//cat 正在吃:fish undefined
console.log(cat.sleep());//cat 正在睡觉!undefined
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
6.4 单例模式
1. 定义: 只允许被实例化依次的类
2. 应用: 提供一个命名空间
3. 代码:
let singleCase = function(name){this.name = name;};
singleCase.prototype.getName = function(){return this.name;}
// 获取实例对象
let getInstance = (function() {
var instance = null;
return function(name) {if(!instance) {// 相当于一个一次性阀门, 只能实例化一次
instance = new singleCase(name);
}
return instance;
}
})();
// 测试单体模式的实例, 所以 one===two
let one = getInstance("one");
let two = getInstance("two");
6.5 外观模式
1. 定义: 为子系统中的一组接口提供一个一致的界面
2. 应用: 简化复杂接口
3. 代码:
外观模式
6.6 适配器模式
1. 定义: 将一个接口转换成客户端需要的接口而不需要去修改客户端代码,使得不兼容的代码可以一起工作
2. 应用: 适配函数参数
3. 代码:
适配器模式
6.7 装饰者模式
1. 定义: 不改变原对象的基础上, 给对象添加属性或方法
2. 代码
let decorator=function(input,fn){
// 获取事件源
let input=document.getElementById(input);
// 若事件源已经绑定事件
if(typeof input.onclick=='function'){
// 缓存事件源原有的回调函数
let oldClickFn=input.onclick;
// 为事件源定义新事件
input.onclick=function(){
// 事件源原有回调函数
oldClickFn();
// 执行事件源新增回调函数
fn();}
}else{
// 未绑定绑定
input.onclick=fn;
}
}
// 测试用例
decorator('textInp',function(){console.log('文本框执行啦');
})
decorator('btn',function(){console.log('按钮执行啦');
})
6.8 桥接模式
1. 定义: 将抽象部分与它的实现部分分离,使它们都可以独立地变化
2. 代码
桥接模式
6.9 模块方法模式
1. 定义: 定义一个模板, 供以后传不同参数调用
2. 代码:
模块方法模式
6.10. 观察者模式
1. 作用: 解决类与对象, 对象与对象之间的耦合
2. 代码:
let Observer=
(function(){let _message={};
return {
// 注册接口,
//1. 作用: 将订阅者注册的消息推入到消息队列
//2. 参数: 所以要传两个参数, 消息类型和处理动作,
//3. 消息不存在重新创建, 存在将消息推入到执行方法
regist:function(type,fn){
// 如果消息不存在, 创建
if(typeof _message[type]==='undefined'){_message[type]=[fn];
}else{
// 将消息推入到消息的执行动作
_message[type].push(fn);
}
},
// 发布信息接口
//1. 作用: 观察这发布消息将所有订阅的消息一次执行
//2. 参数: 消息类型和动作执行传递参数
//3. 消息类型参数必须校验
fire:function(type,args){
// 如果消息没有注册, 则返回
if(!_message[type]) return;
// 定义消息信息
var events={
type:type, // 消息类型
args:args||{} // 消息携带数据},
i=0,
len=_message[type].length;
// 遍历消息
for(;i<len;i++){
// 依次执行注册消息
_message[type][i].call(this,events);
}
},
// 移除信息接口
//1. 作用: 将订阅者注销消息从消息队列清除
//2. 参数: 消息类型和执行的动作
//3. 消息参数校验
remove:function(type,fn){
// 如果消息动作队列存在
if(_message[type] instanceof Array){
// 从最后一个消息动作序遍历
var i=_message[type].length-1;
for(;i>=0;i--){
// 如果存在该动作在消息队列中移除
_message[type][i]===fn&&_message[type].splice(i,1);
}
}
}
}
})()
// 测试用例
//1. 订阅消息
Observer.regist('test',function(e){console.log(e.type,e.args.msg);
})
//2. 发布消息
Observer.fire('test',{msg:'传递参数 1'});
Observer.fire('test',{msg:'传递参数 2'});
Observer.fire('test',{msg:'传递参数 3'});
6.11 状态模式
1. 定义: 一个对象状态改变会导致行为变化
2. 作用: 解决复杂的 if 判断
3. 代码
状态模式
6.12 策略模式
1. 定义: 定义了一系列家族算法,并对每一种算法单独封装起来,让算法之间可以相互替换,独立于使用算法的客户
2. 代码
策略模式
6.13. 访问模式
1. 定义: 通过继承封装一些该数据类型不具备的属性,
2. 作用: 让对象具备数组的操作方法
3. 代码:
访问者模式
6.14 中介者模式
1. 定义: 设置一个中间层, 处理对象之间的交互
2. 代码:
中介者模式
7. HTTP
1.1 什么是 HTTP
HTTP 是一个连接客户端,网关和服务器的一个协议。
7.2 特点
支持客户 / 服务器模式:可以连接客户端和服务端;
简单快速:请求只需传送请求方法,路径和请求主体;
灵活:传输数据类型灵活;
无连接:请求结束立即断开;
无状态:无法记住上一次请求。
7.3 怎么解决无状态和无连接
无状态:HTTP 协议本身无法解决这个状态,只有通过 cookie 和 session 将状态做贮存,常见的场景是登录状态保持;
无连接:可以通过自身属性 Keep-Alive。
7.4 请求过程
HTTP(S) 请求地址 → DNS 解析 → 三次握手 → 发送请求 → 四次挥手
三次握手过程(图片来源 CSDN)
在这里插入图片描述
- 四次挥手过程(图片来源 CSDN)
在这里插入图片描述
7.5 HTTP 0.9~3.0 对比
7.5.1 HTTP 0.9
只允许客户端发送 GET 这一种请求;
且不支持请求头,协议只支持纯文本;
无状态性,每个访问独立处理,完成断开;
无状态码。
7.5.2 HTTP 1.0
有身份认证,三次握手;
请求与响应支持头域;
请求头内容;
属性名 | 含义 |
---|---|
Accept | 可接受的 MIME 类型 |
Accept-Encoding | 数据可解码的格式 |
Accept-Language | 可接受语言 |
Connection | 值 keep-alive 是长连接 |
Host | 主机和端口 |
Pragma | 是否缓存, 指定 no-cache 返回刷新 |
Referer | 页面路由 |
If-Modified-Since | 值为时间 |
响应头内容;
属性名 | 含义 |
---|---|
Connection | 值 keep-alive 是长连接 |
Content-Type | 返回文档类型, 常见的值有 text/plain,text/html,text/json |
Date | 消息发送的时间 |
Server | 服务器名字 |
Last-Modified | 值为时间,s 返回的最后修改时间 |
Expires | 缓存过期时间,b 和 s 时间做对比 |
注意
expires 是响应头内容,返回一个固定的时间, 缺陷是时间到了服务器要重新设置。
请求头中如果有 If-Modified-Since,服务器会将时间与 last-modified 对比,相同返回 304。
响应对象以一个响应状态行开始
响应对象不只限于超文本
支持 GET、HEAD、POST 方法
有状态码
支持长连接(但默认还是使用短连接)、缓存机制以及身份认证。
7.5.3 HTTP 1.1
请求头增加 Cache-Control
属性名 | 含义 |
---|---|
Cache-Control | 在 1.1 引入的方法, 指定请求和响应遵循的缓存机制, 值有:public(b 和 s 都缓存),private(b 缓存),no-cache(不缓存),no-store(不缓存),max-age(缓存时间,s 为单位),min-fresh(最小更新时间),max-age=3600 |
If-None-Match | 上次请求响应头返回的 etag 值响应头增加 Cache-Control,表示所有的缓存机制是否可以缓存及哪种类型 etag 返回的哈希值, 第二次请求头携带去和服务器值对比 |
注意
Cache-Control 的 max-age 返回是缓存的相对时间
Cache-Control 优先级比 expires 高
缺点:不能第一时间拿到最新修改文件
7.5.4 HTTP 2.0
采用二进制格式传输
多路复用,其实就是将请求数据分成帧乱序发送到 TCP 中。TCP 只能有一个 steam,所以还是会阻塞
报头压缩
服务器推送主动向 B 端发送静态资源,避免往返延迟。
7.5.5 HTTP 3.0
1. 是基于 QUIC 协议,基于 UDP
2. 特点:
自定义连接机制:TCP 以 IP/ 端口标识, 变化重新连接握手,UDP 是一 64 位 ID 标识,是无连接;
自定义重传机制:TCP 使用序号和应答传输,QUIC 是使用递增序号传输;无阻塞的多路复用:同一条 QUIC 可以创建多个 steam。
7.5.6 HTTPS
1.https 是在 http 协议的基础上加了个 SSL;
2. 主要包括:握手 (凭证交换和验证) 和记录协议(数据进行加密)。
7.5.7 缓存
1. 按协议分:协议层缓存和非 http 协议缓存:
1.1 协议层缓存:利用 http 协议头属性值设置;
1.2 非协议层缓存:利用 meta 标签的 http-equiv 属性值 Expires,set-cookie。
2. 按缓存分:强缓存和协商缓存:
2.1 强缓存:利用 cache-control 和 expires 设置,直接返回一个过期时间,所以在缓存期间不请求,If-modify-since;
2.2 协商缓存:响应头返回 etag 或 last-modified 的哈希值,第二次请求头 If-none-match 或 IF-modify-since 携带上次哈希值,一致则返回 304。
3. 协商缓存对比:etag 优先级高于 last-modified;
4.etag 精度高,last-modified 精度是 s,1s 内 etag 修改多少次都会被记录;last-modified 性能好,etag 要得到 hash 值。
5. 浏览器读取缓存流程:
会先判断强缓存;再判断协商缓存 etag(last-modified)是否存在;
存在利用属性 If-None-match(If-Modified-since)携带值;
请求服务器, 服务器对比 etag(last-modified),生效返回 304。
F5 刷新会忽略强缓存不会忽略协商缓存,ctrl+f5 都失效
7.5.8 状态码
序列 | 详情 |
---|---|
1XX(通知) | |
2XX(成功) | 200(成功)、201(服务器创建)、202(服务器接收未处理)、203(非授权信息)、204(未返回内容)、205(重置内容)、206(部分内容) |
3XX(重定向) | 301(永久移动)、302(临时移动)、303(查看其他位置)、304(未修改)、305(使用代理)、307(临时重定向) |
4XX(客户端错误) | 400(错误请求)、401(未授权)、403(禁止)、404(未找到)、405(方法禁用)、406(不接受)、407(需要代理授权) |
5XX(服务器错误) | 500(服务器异常)、501(尚未实施)、502(错误网关)、503(服务不可用)、504(网关超时)、505(HTTP 版本不受支持) |
7.5.9 浏览器请求分析
在这里插入图片描述
7.5.10 总结
协议
版本 | 内容 |
---|---|
http0.9 | 只允许客户端发送 GET 这一种请求; 且不支持请求头, 协议只支持纯文本; 无状态性, 每个访问独立处理, 完成断开; 无状态码 |
http1.0 解决 0.9 的缺点, 增加 If-modify-since(last-modify)和 expires 缓存属性 | |
http1.x | 增加 cache-control 和 If-none-match(etag)缓存属性 |
http2.0 | 采用二进制格式传输; 多路复用; 报头压缩; 服务器推送 |
http3.0 | 采用 QUIC 协议, 自定义连接机制; 自定义重传机制; 无阻塞的多路复用 |
缓存
类型 | 特性 |
---|---|
强缓存 | 通过 If-modify-since(last-modify)、expires 和 cache-control 设置,属性值是时间,所以在时间内不用请求 |
协商缓存 | 通过 If-none-match(etag)设置,etag 属性是哈希值,所以要请求和服务器值对比 |
8. 总结
这只是 JS 原生梳理, 后续会再出 react,node, 小程序相关的梳理;
原创码字不易, 欢迎 star!