ES6一些结构
set&weakset
set
- 类似于数组,但是元素不能重复,向set加入值时,不会发生类型转换,5和“5”算是不重复的两个值
- 创建set需要通过set构造函数
- size属性可用于返回set中元素的个数
Set的遍历顺序就是插入顺序- 常用方法
-
- add(value):添加某个元素,返回set对象本身
- delete(value):从set中删除和这个值相等的与元素,返回boolean类型
- has(value):判断set中是否存在某个元素,返回boolean类型
- clear():清空set中所有元素,没有返回值
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries:返回键值对的遍历器
- forEach(callback,[,thisArg]):同forEach遍历set
- 支持for...of
weakset
- 只能存放对象类型和symbol值,不能存放其他类型
- 对元素的引用是弱引用,垃圾回收的时候不可会考虑weakset的引用,gc会回收,所以WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。
- weakset不能遍历:WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
- 当传入一个数组时,数组中的所有成员会成为weakset的成员而不是元素本身
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
// 因为b中的元素不是对象
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
- 常用方法
-
- add(value):添加某个元素,返回weakset对象本身
- delete(value):从weakset中删除和这个值相等的与元素,返回boolean类型
- has(value):判断weakset中是否存在某个元素,返回boolean类型
- 不能遍历
- 应用场景:不能使用非构造方法创建出来的对象调用running方法
map&weakmap
map
- 用于存储映射关系,对象存储映射关系只能用字符串作为属性名,map可以使用其他类型作为属性名
- size属性可用于返回map中元素的个数
Map构造函数接受数组作为参数,实际上执行的是下面的算法。
任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数
const items = [
['name', '张三'],
['title', 'Author']
];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
- 常见方法
-
- size属性:返回 Map 结构的成员总数
- Map.prototype.get(key):读取
key对应的键值,如果找不到key,返回undefined - Map.prototype.set(key, value):
set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。 - Map.prototype.delete(key):从map中删除和这个值相等的与元素,返回boolean类型
- Map.prototype.has(key):判断map中是否存在某个元素,返回boolean类型
- Map.prototype.clear():清空map中所有元素,没有返回值
- Map.prototype.keys():返回键名的遍历器
- Map.prototype.values():返回键值的遍历器
- Map.prototype.entries():返回所有成员的遍历器
- Map.prototype.forEach(callback,[,thisArg]):遍历 Map 的所有成员
- 支持for...of
- Map与其他类型的相互转换
-
- Map转为数组
[...myMap] - 数组转为Map
- Map转为数组
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
-
- Map转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
- Map转为对象
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
-
- 对象转为Map
-
-
- 使用Object.entries()
-
let map = new Map(Object.entries(obj));
-
-
- 实现一个转换函数
-
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
-
- Map转为Json
// 第一种情况:Map 的键名都是字符串,这时可以选择转为对象 JSON
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
// 第二种情况:Map 的键名有非字符串,这时可以选择转为数组 JSON
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
-
- Json转为map
weakmap
- key只能使用对象(null除外)和Symbol值,不接受其他类型作为key
- 对元素的引用是弱引用,gc会回收
- 常见方法:
-
- set(key,value):在map中添加key,value,并且返回整个map对象
- get(key):根据key获取map中的value
- has(key):判断是否包括某一个key,返回boolean类型
- delete(key):根据key删除一个键值对,返回boolean类型
- 应用场景:
-
- 在网页的 DOM 元素上添加数据,就可以使用
WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除
- 在网页的 DOM 元素上添加数据,就可以使用
weakRef:直接创建对象的弱引用
- 使用方法
let target = {};
let wr = new WeakRef(target);
- 常用方法:
-
- deref():如果原始对象存在,该方法返回原始对象;如果原始对象已经被垃圾回收机制清除,该方法返回
undefined
- deref():如果原始对象存在,该方法返回原始对象;如果原始对象已经被垃圾回收机制清除,该方法返回
let target = {};
let wr = new WeakRef(target);
let obj = wr.deref();
if (obj) { // target 未被垃圾回收机制清除
// ...
}
Symbol
- 原始数据类型
- 通过symbol函数生成,可以作为属性名
- 不可以使用new命令
- 接受一个字符串作为参数,表示对symbol实例的描述;如果参数是对象则通过tostring将其转为字符串;相同参数的symbol函数的返回值是不相等的
- 不能与其他类型的值进行运算,可以显示转换为字符串,也可以转换为布尔值,但是不能转为数值
- 可以通过descriptioin直接返回symbol的描述——ES10 Symbol([description])
- 由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
- 不能使用点运算符
- 还可以使用定义一组常量,保证这组常量的值是不相等的
- symol作为属性名,遍历对象的时候不会出现在for...in和for...of循环中,也不会被Object.keys(),Object.getOwnPropertySymbols(),JSON.stringify()返回,但是他也不是私有属性,Object.getOwnPropertySymbols() 可以获取对象的所有Symbol属性名,该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
- Reflect.ownKeys()也可以返回所有类型的键名,包括常规键名和symbol键名
- Symbol.for()——接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。
Proxy——代理器
- Proxy用于修改某些操作的默认行为,可以理解成在目标对象之前架设一层拦截,可以对外界的访问进行过滤和改写
- 生成Proxy实例的方法
let proxy = new Proxy(target, handler);
- target:表示所要拦截的目标对象
- handler:也是一个对象,用来定制拦截行为。如果handler没有设置任何拦截,那就等同于直接通向原对象
- 支持的拦截操作
- Proxy.revocable():返回一个可取消的Proxy实例对象,使用场景为目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问
- this问题
- 虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的 this 关键字会指向 Proxy 代理
- Proxy拦截函数内部的this指向的是handler对象
Reflect
设计目的
- 将Object对象的一些明显属于语言内部的方法放到Reflect对向上,现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
- 修改某些Object方法的返回结果,让其变得合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
- 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
- Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法,这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
Promise
【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理) - 掘金
一篇文章解决Promise...then,async/await执行顺序类型题 - 掘金
是异步编程的一种解决方案,是一个对象,从它可以获取异步操作的消息。有了promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层回调嵌套的回调函数
const promise = new Primise(function(resolve,reject){
// some code
if(异步操作成功){
resolve(value);
}else{
reject(error)
}
})
promise.then(function(value){
//success
},function(error){
//failure
})
Promise对象的特点:
- 对象的状态不受外界影响。Promise对象的三种状态:pending、fulfilled、rejected
- 状态一旦改变,就不会再变,任何时候都可以得到这个结果。从pending变为fulfilled和从pending变为rejected两种可能。
promise的缺点:
- 无法取消promise,一旦新建立即执行,无法中途取消
- 如果不设置回调函数,promise内部抛出的错误,不会反映到外部
- 当处于pending状态时,无法知道当前进展到哪一阶段。是刚刚开始还是即将完成
方法
Promise.prototype.then()
then方法返回的是一个新的Promise实例,可以采用链式调用
一般来说不要再then()方法里面定义Reject状态的回调函数,即then方法的第二个参数,总是使用catch方法
Promise.prototype.catch()
是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
promise抛出一个错误,就被catch()方法指定的回调函数捕获。reject()方法的作用,等同于抛出错误
promise对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止,也就是说,错误总是会被写一个catch语句捕获。
跟传统的 try/catch 代码块不同的是,如果没有使用 catch() 方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应
Promise.prototype.finally()
finally方法用于指定不管promise对象最后状态如何,都会执行的操作,不依赖于Promise的执行结果
Promise.all()
const p = Promise.all([p1, p2, p3]);
用于将多个Promise实例包装成一个新的Promise实例,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1,p2,p3的返回值组成一个数组,传递给p的回调函数
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数
Promise.race()
const p = Promise.race([p1, p2, p3]);
将多个Promise实例,包装成一个新的Promise实例,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数
Promise.allSettled()
用来确定一组异步操作是否都结束了,不管成功或失败。该方法返回一个新的Promise对象,只有等到参数数组的所有Promise对象都发生状态变更,返回的Promise对象才会发生状态变更,状态总是fulfilled,不会变成rejected。
状态变成fulfilled后,他的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个Promise对象
Promise.any()
该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。
只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有的promise实例变成rejected状态才会结束。
Promise.resolve()
将现有对象转为Promise对象
- 参数是一个Promise实例:如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
- 参数是一个thenable对象:thenable对象指的是具有then方法的对象。Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。
- 参数不是具有then()方法的对象,或根本就不是对象:如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved。
- 不带有任何参数:Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时
Promise.reject()
返回一个新的Promise实例,该实例的状态为rejected
Class
- class中的constructor对应的是构造函数,this关键字代表实例对象;class底层其实还是构造函数,class内部的方法都是不可枚举的。class类必须使用new进行调用,否则会报错
- class中的方法都等同于定义在Class类的Prototype上,Object.assign()可以很方便的一次向类添加多个方法
- 类的内部所有定义的方法,都是不可枚举的
- constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。constructor()方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
- ES2022中实例属性现在除了可以定义在constructor()方法里面的this上面,也可以定义在类内部的最顶层。此时实例属性前面不需要加上this
- 静态方法:如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。静态方法中this指向类而不是实例。静态方法也可以从super对象上调用,父类的静态方法子类可以调用
- 私有属性:ES2022正式为class添加了私有属性,方法是在属性名之前使用#表示,只能在类内部使用(ES2022),之前的区分方式:命名时前缀为下划线 /call改变this指向/使用Symbol,子类无法继承父类的私有属性
- 静态块:允许在类的内部设置一个代码块,在类生成时运行且只运行一次,主要作用是对静态属性进行初始化,以后,新建类的实例时,这个块就不运行了,静态块的内部不能有return语句
- js引擎在解析子类的时候有要求,如果我们有实现继承,那么子类的构造方法中,在使用this之前需要使用super()或者在return之前调用super()——调用父类的constructor
子类必须在constructor()方法中调用super(),否则就会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象
为什么子类的构造函数,一定要调用super()?原因就在于 ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类
- 类的注意点:
-
- 严格模式:类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式
- 不存在变量提升,必须保证子类在父类之后定义
- 如果某个方法之前加上*号,就表示该方法是一个Generator函数
- 类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
解决办法:
- 在构造方法中绑定this,这样就不会找不到print方法了
- 是用箭头函数
- 使用proxy
-
- new.target:该属性一般用在构造函数之中,返回new命令作用于的那个构造函数,如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined
iterator和for...of循环
- 任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
- 作用:
-
- 为各种数据结构,提供一个统一的、简便的访问接口
- 使得数据结构的成员能够按某种次序排列
- ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费
- 遍历过程:
每一次调用指针对象的next方法,都会返回数据结构的当前成员信息。即一个包含value(当前成员的值)和done(表示遍历是否结束的布尔值)两个属性的对象
-
- 创建一个指针对象,指向当前数据结构的起始位置,也就是说,就是一个指针对象
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置
- ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性,或者说一个数据结构只要具有Symbol.iterator属性,就可以认为是可遍历的。Symbol.iterator属性本身就是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};
- 原生具备Iterator接口的数据结构如下:Array、Map、Set、String、TypedArray、函数的arguments对象、NodeList对象。除了这些数据结构的Iterator接口,都需要自己在Symbol。iterator属性上面部署,这样才会被for...of循环遍历
var iterableObj = {
data: [1, 2, 3],
[Symbol.iterator]: function (){
let self = this;
let index = 0;
return {
next: {
if(index < self.data.length){
return {
value: self.data[index++],
done: false,
}
}
return { value: undefined, done: true };
}
}
}
}
for(let item of iterableObj){
console.log(item); // 1, 2, 3
}
- 调用Iterator接口的场合
-
- 解构赋值:对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法
- 扩展运算符
- yield*
- 任何接受数组作为参数的场合:
- 字符串是一个类似数组的对象,也原生具有Iterator接口
- 遍历器对象除了具有next方法,还可以具有return方法和throw方法,return和throw方法可选
-
- return()方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return()方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return()方法
- throw()方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法
- 与其他遍历语法的比较
-
- for循环:最原始的写法,比较麻烦
- forEach:问题在于无法中途跳出forEach循环,break和return语句都不奏效
- for...in循环可以遍历数组的键名,主要是为遍历对象设计的,不适用于遍历数组
缺点:
-
-
- 数组的键名是数字,但是for...in是以字符串作为键名"0", "1"等
- for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键
- 某些情况下,for...in循环会以任意顺序遍历键名
-
-
- for...of循环:
优点:
-
-
- 语法和for...in一样简洁,没有for...in的那些缺点
- 不同于forEach,可以与break和return结合使用
- 提供了遍历所有数据结构的统一操作接口
-
Generator
- Generator 函数是 ES6 提供的一种异步编程解决方案。可以理解为一个状态机,封装了多个内部状态,执行generator函数会返回一个遍历器对象,该对象可以依次遍历generator函数内部的每一个状态。
- 形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
- yield表达式就是一个暂停标志,只有调用next方法时,内部的指针指向该语句时才会执行,相当于提供了手动的惰性求值的语法功能
-
- 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
- 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
- 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
- 如果该函数没有return语句,则返回的对象的value属性值为undefined。
- generator函数可以不用yield表达式,这是就变成了以恶个暂缓执行函数
- yield表达式如果用在另一个表达式之中,必须放在圆括号里。yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
- 由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。下面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
- next方法可以带一个参数,该参数会被当做上一个yield表达式的返回值,yield表达式本身没有返回值,或者说总是返回undefiend。
- for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
- thunk函数:编译器的传名调用实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就是Thunk函数
- co模块:用于generator函数的自动执行,co函数返回一个Promise对象,因此可以用then方法添加回调函数。
Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权
async函数
- async函数就是generator函数的语法糖,async函数将generator函数的星号*替换成async,将yield替换成await
- 较generator函数的改进:
-
- 内置执行器:generator函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器
- 更好的语义:async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
- 更广的适用性:co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
- 返回值是promise:async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。async函数内部return语句返回的值,会成为then方法回调函数的参数
- async函数返回的Promise对象,必须等到内部所有await命令后面的Promise对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
- 任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
数据类型
基本数据类型和引用数据类型——共8种
- 基本数据类型:undefined,null(空对象指针),boolean,string,number,symbol,bigint
symbol代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题
bigint是一种数据类型的数据,它可以表示任意精度格式的数据,使用bigint可以安全的存储和操作大整数,即使这个数已经超出了number能够表示的安全整数范围;可以用在一个整数字面量后面加n的 方式定义一个bigint,例10n,或者调用函数BigInt()并传递一个整数值或字符串值。原本js能存储的最 大数为2^53 - 1
- bigint不能用于math对象中的方法
- 不能和任何number实例混合运算,两者必须转换成同一类型
- 引用数据类型:object
object类型包括:
- 普通对象{}
- 数组对象[]
- 函数对象Function
- 正则对象RegExp
- 日期Date
- 数学对象Math
- 原始数据类型直接存储在栈中,占据空间小,大小固定
引用数据类型同时存储在栈和堆中,占据空间大,大小不固定,在栈中存储指针,该指针指向堆中该实体的起始地址,堆中存储实体
基本数据类型是值传递,即在拷贝时,会创建一个完全相等的变量,在栈中重新开辟一块内存空间存储原变量的副本
引用数据类型是引用传递,即在拷贝时,会创建一个指针指向原有变量,在栈中重新开辟一块内存空间存储指针以指向原变量
- 每个object实例都拥有的属性和方法:
-
- constructor:用于创建对象的函数
- hasOwnProperty(proeprtyName):用于判断当前对象实例上是否存在给定的属性,要检查的属性名必须是字符串
- isPropertypeof(object):用于判断当前对象是否为另一个对象的原型
- propertyEnumerable(propertyName):用于判断给定的属性是否可以使用for-in语句枚举,要检查的属性名必须是字符串
- toLocalString():返回对象的字符串表示
- toString():返回对象的字符串表示
- valueof:返回对象对应的字符串,数值或布尔值表示
判断数据类型
JavaScript 深入系列之数据类型检测 · Issue #94 · yuanyuanbyte/Blog
a. typeof:常用于检测基本类型和Function,除null外
typeof返回一个字符串,表示未经计算的操作数的类型,用于区分基本数据类型,但无法区分null,无法区分引用数据类型,但能区分Function
因为js的第一个版本,在这个版本中单个值在栈中占用32位的存储单元,32位存储单位又划分为(1-3)位和实际数据,类型标签存储在低位中,而null 0~31位均为0,所以typeof会判断其为object
typeof原理:不同的对象在底层都表示为二进制,在js中二进制前三位存储其类型信息
-
- 000:对象
- 010:浮点数
- 100:字符串
- 110:布尔值
- 111:整数
| 类型 | 结果 |
|---|---|
| undefined | “undefined” |
| null | “object” |
| boolean | “boolean” |
| Number | “number” |
| bigint | “bigint” |
| string | “string” |
| Symbol | “symbol” |
| 宿主对象 | 取决于具体实现 |
| Function对象 | “function” |
| 其他任何对象 | “object” |
typeof NaN // 'number'
NaN和自身不相等
b. instanceof:常用于检测引用类型,无法区分基本类型
用来比较一个对象是否为某一个构造函数的实例,在运行时检测constructor.prototype是否存在于参数object的原型链上
object:某个实例对象 constructor:某个构造函数
c. construtor:常用于检测引用类型,无法区分基本类型
如果创建一个对象改变他的原型,constructor就不能用来判断数据类型了
d. Object.prototype.toString.call:常用于检测基本类型和引用类型
将参数转化为字符串
String(val)会依次调用val.toString, val.valueOf()
讲透Object.prototype.toString.call(val)
每个对象都有一个toString()方法,默认情况下该方法被每个Object对象继承,如果此方法在自定义对象中未被覆盖,toString()返回“[object type]”,其中type是对象的类型
e. Reflect.apply(Object.prototype.toString, val, []):常用于检测基本类型和引用类型
developer.mozilla.org/zh-CN/docs/…
静态方法Reflect.apply()通过指定的参数列表发起对目标函数的调用
Reflect.apply(target, thisArgument, argumentsList)
- target:目标函数
- thisArgument:target函数调用时绑定的this对象
- argumentList:target函数调用时传入的实参列表,该参数应该是一个类数组的对象
数据类型转换
字符串转数组
- split()
- 扩展运算符
- Array.from()
数组转字符串
- join()
- toString(), toLocaleString(), String()
toLocaleString()调用每个数组的toLocaleString()方法,然后使用地区特定的分隔符将生成的字符串连接起来,形成一个字符串。如果是为了返回时间类型的数据,推荐使用toLocaleString()
toString()方法是最保险的,返回唯一值得方法,他不会因为本地环境的改变而发生变化
类数组转数组
类数组:类数组是具有length属性,但是不具备数组原型上的方法,常见的有arguments和dom操作返回的结果
- Array.from(类数组)
- Array.prototype.slice.call(类数组) // 由于类数组不具有数组原型上的方法,所以这样写,这样写可以将具有length的对象转为数组
- 扩展运算符:内部是for...of循环
- 使用concat,Array.prototype.concat.apply([], 类数组)
转换为数字
- Number():
-
- true=>1, false=>0
- 数值直接返回
- null返回0
- undefined返回NaN
- 字符串
-
-
- 字符串包含数值,包括数值字符前面带加减号的情况,则转换为一个十进制数值
- 如果字符串包含有效的浮点值格式,则会转换为相应的浮点值
- 字符串包含有效的十六进制格式,转换为十进制整数
- 空字符串返回0
-
-
- 除上述情况返回NaN
- 对象:调用valueof()方法,并按照上述规则转换返回的值,如果转换结果为NaN,则调用toString()方法,再按照转换字符串的规则转换
- parseInt():字符串最前面的空格会被忽略,从第一个非空格字符串开始转换,如果第一个字符不是数字字符,加号或减号,parseInt()立即返回NaN,parseInt()的第二个参数用于指定底数,即按照第二个参数的进制转换为10进制
数值分隔符_(ES2021)
使用数值分隔符的几个注意点:
- 不能放在数值的最前面或最后面
- 不能两个或两个以上的分隔符连在一起
- 小数点的前后不能有分隔符
- 科学计数法里面,表示指数的e或E前后不能有分隔符
- 分隔符不能紧跟着进制的前缀0b、0B、0o、0O、0x、0X
- Number(),parseInt(),parseFloat()不支持数值分隔符
js浮点误差
计算机编程语言里的浮点数会存在精度丢失问题,根本原因是二进制和实现位数限制有些数无法有限表示。js以IEEE-754标准格式64位双精度浮点数形式存储数字类型,在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的需要舍去,超出部分会舍去以下是十进制小数对应的二进制表示。IEEE-754标准是目前最广泛使用的浮点数运算标准
解决办法:
将小数放大为整数进行算数运算,再缩小为小数
IEEE-754标准提供了如何在计算机内存中,以二进制的方式存储十进制浮点数的具体标准
IEEE754详解(最详细简单有趣味的介绍)_明月几时有666的博客-CSDN博客
undefined,undeclared,null的区别
已在作用域中声明但是还没有赋值的变量是undefined
没声明过的变量是undeclared
null表示空对象
对于undeclared变量的引用,浏览器会报引用错误
==和===
==会发生强制类型转换
- 如果任一操作数为布尔值,将其转换为数值再比较
- 如果一个操作数为字符串,另一个操作符为数值,则尝试将字符串转换为数值再比较
- 如果一个操作数为对象,另一个操作数不是,则调用对象的valueOf()方法取得其原始值,再根据之前的规则进行比较
- null == undefined,null和undefined不能再转换成其他类型的值再作比较
- 任一操作符为NaN,则相等操作符返回false,不相等操作符返回true,NaN == NaN => false
- 如果两个操作符都为对象,则比较是否为同一个对象,如果都指向同一个对象,则相等操作符返回true。虽然存储的值相等,但是如果属于不同的内存区域,那么两者仍不相等
isNaN和Number.isNaN函数的区别
- 函数isNaN接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的值都会返回true,因此传入非数字值也会返回true,会影响NaN的判断
- 函数Number.isNaN会首先判断传入参数是否为数字,如果是再继续判断是否为NaN,不会进行数据类型的转换,这种方法对于NaN的判断更为准确
函数的尾调用
setTimeout和setInterval的区别
setTimeout是延迟执行,setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式,常用于延迟执行某些事务或方法
setInterval是定时执行,用于在指定的毫秒数间隔调用函数或计算表达式,一般用于一般定时刷新业务,用于业务数据同步等
setTimeout()倒计时为什么会出现误差
为什么 setTimeout 有最小时延 4ms ? - 掘金
setTimeout()只是将事件插入了任务队列,必须等当前代码(执行栈)执行完,主线程才会去执行他指定的回调函数,要是当前代码消耗时间很长,也有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()指定的时间执行,所以setTimeout()的第二个参数表示的是最少时间,并非是确切是时间
HTML5标准规定了 setTimeout() 的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒。在此之前。老版本的浏览器都将最短时间设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常是间隔16毫秒执行。这时使用 requestAnimationFrame() 的效果要好于 setTimeout()
ajax&axios
ajax——asynchronous javascript and xml
主要用于实现客户端与服务端的异步通信效果,实现页面的局部刷新
- 创建xmlhttprequest异步调用对象
- 创建一个新的http请求,并指定http请求的方法,url及验证信息
- 设置响应http请求状态变化的参数
- 发送http请求
var xhr = new XMLHttpRequest();//创建对象
xhr.open('method','url','async');//配置请求,初始化设置请求方法和url
xhr.send();//构建请求体,发送到服务器
xhr.setRequestHeader('Content-Type','application/json');//设置请求头
//事件绑定,处理服务端返回的结果
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){[
if(xhr.status >= 200 && xhr.status < 300){
// some code
}
]}
}
readyState的状态从0到4变化
- 0:请求未初始化,xmlhttprequest对象还没有完成初始化
- 1:服务器连接已建立,xmlhttprequest对象开始发送请求
- 2:请求已接收,xmlhttprequest对象的请求发送完成
- 3:请求处理中,xmlhttprequest对象开始读取服务器的响应
- 4:请求已完成且响应就绪,xmlhttprequest对象读取服务器响应结束
promise封装ajax
const getJson = function(url){
return new Promise(function(resolve,reject){
const handler = fucntion(){
if(this.readyState !== 4){
return;
}
if(this.status == 200){
resolve(this.response);
}else{
reject(new Error(this.statusText));
}
};
const client = new XHRHttpRequest();
client.open('GET',url);
client.onreadystatechange = handler;
client.responseType = 'json';
client.setRequestHeader('Accept','application/json');
client.send();
});
}
getJson("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
axios
- axios是通过promise实现的对ajax技术的一种封装,支持promise所有的api
- 在服务端使用node.js http模块,在客户端中使用xmlhttprequests
- 支持请求响应拦截器
- 自动转换json数据
- 客户端防止csrf
解构赋值
解构赋值的用途
- 交换变量的值
[x, y] = [y, x]
- 从函数返回多个值
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
- 函数参数的定义
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
- 提取json数据
- 函数参数的默认值
- 遍历map结构
- 输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
Number的一些方法
Number.isNaN&Number.isFinite
传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false。
Number.isInteger()
用来判断一个数值是否为整数,如果参数不是数值,返回false
JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。
如果一个数值的绝对值小于Number.MIN_VAlUE(5E-324),即小于js能够分辨的最小值,会被自动转为0。
Number.EPSILON
es6在Number对象上面,新增的一个极小的常量Number.EPSLION。根据规格,它表示1与大于1的最小浮点数之间的差。Number.EPSLION实际上是js能够表示最小精度。误差如果小于这个值,就可以认为没有意义了,即不存在误差了
安全整数和Number.isSafeInteger()
js能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。
Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER用来表示这个范围的上下限
Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内
验证运算结果是否落在安全整数的范围内,不要只验证运算结果,而要同时验证参与运算的每个值
Math对象的扩展
Math.trunc()
用于去除一个数的小数部分,返回整数部分。对于非数值,Math.trunc内部使用Number方法将其先转换为数值。对于空值和无法截取整数的值,返回NaN。对于没有部署这个方法的环境,可以用下面的代码模拟。
Math.sign()
用来判断一个数到底是正数,负数还是零,对于非数值,会先将其转换为数值。返回值如下:
- 参数为整数返回+1
- 参数为负数,返回-1
- 参数为0,返回0
- 参数为-0,返回-0
- 其他值,返回NaN
如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回NaN
Math.cbrt()
用于计算一个数的立方根.对于非数值,Math.cbrt()方法内部也是先使用Number()方法将其转为数值
函数扩展
函数的length属性
函数的length属性的含义是该函数预期传入的参数个数,某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数,也就是说指定了默认值后,length属性将失真
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
将参数默认值设为undefined,表明这个参数是可以省略的
函数的rest参数
arguments对象不是数组,是一个类似数组的对象,所以当我们想使用数组的方法时必须使用Array.from先将其转为数组,rest参数本身就是一个真正的数组,数组特有的方法都可以使用
rest参数之后不能再有其他参数,否则会报错
函数的length属性,不包括rest参数
严格模式
只要函数参数使用了默认值,解构赋值,或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
原因:函数内部的严格模式,同时适用于函数体和函数参数,但是,函数执行的时候,先执行函数参数,再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
解决方案:
- 全局设定严格模式
- 将函数包在一个无参数的立即执行函数里面
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
箭头函数
- 箭头函数本身是没有prototype的,所以箭头函数本身没有this,箭头函数的this指向在定义的时候继承自外层第一个普通函数的this。(call,bind,apply等方法不能改变箭头函数的this指向)
- 不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误
- 没有arguments,箭头函数中访问arguments实际上获取的是他外层函数的arguments 。如果要用,可以使用rest参数代替
- 没有super和new.target
- 定义对象的大括号{}是无法形成单独的执行环境,他依旧是处于全局环境中
- 不可以使用yield命令,因此箭头函数不能用作generator函数
不适用的场合:
- 定义对象的方法,且该方法内部包括this
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。
- 需要动态this的时候,也不应该使用箭头函数
var button = document.getElementById('press');
button.addEventListener('click', () => {
// 这里的this时全局对象
// 如果是普通函数,this就会动态指向被点击的按钮对象
this.classList.toggle('on');
});
函数柯里化
柯里化函数应用 | Heying Ye’s Personal Website
彻底搞懂闭包,柯里化,手写代码,金九银十不再丢分! - 掘金
将多参数的函数转换成单参数的形式
作用及特点:
- 参数复用——复用最初函数的第一个参数
- 提前返回——返回接受余下的参数且返回结果的新函数
- 延迟执行——返回新函数,等待执行
柯里化的应用
- 兼容浏览器事件监听方法,只进行一次兼容判断
var addEvent = function(ele, type, fn, isCapture) {
if(window.addEventListener) {
ele.addEventListener(type, fn, isCapture)
} else if(window.attachEvent) {
ele.attachEvent("on" + type, fn)
}
}
// 柯里化后可以写成这样
var addEvent = (function() {
if(window.addEventListener) {
return function(ele, type, fn, isCapture) {
ele.addEventListener(type, fn, isCapture)
}
} else if(window.attachEvent) {
return function(ele, type, fn) {
ele.attachEvent("on" + type, fn)
}
}
})()
- 性能优化:防抖节流
- 兼容低版本的IE的bind方法
封装简单柯里化函数
function createCurry(fn){
if(typeof fn !== 'function'){
throw TyepError("fn is not function");
}
// 复用第一个参数
var args = [].slice.call(arguments, 1);
// 返回新函数
return funciton (){
// 收集剩余参数
var _args = [].slice.call(arguments);
// 返回结果
return fn.apply(this, args.concat(_args));
}
}
http缓存
缓存的优点:减少不必要的数据传输,减轻服务器压力;加快客户端展示网页的速度,用户体验更友好
缓存的缺点:资源如果有更改但是客户端不进行更新造成用户获取信息滞后
一般用http头信息控制缓存
强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存,主要过程如下:
缓存过程:
- 浏览器首次加载资源成功时,服务器返回200,此时除了下载资源之外将response的header一并缓存
- 下一次加载资源时,首先要经过强缓存的处理,cache-control的优先级最高,比如cache-control:no-cache,就直接进入到协商缓存的步骤了,如果cache-control:max-age=xxx,就会先比较当前时间和上一次返回200时的时间差,如果没有超过max-age,命中强缓存,不发请求直接从本地缓存读取该文件(这里需要注意,如果没有cache-control,会取expires的值,来对比是否过期),过期的话会进入下一个阶段,协商缓存
- 协商缓存阶段,则向服务器发送header带有If-None-Match和If-Modified-Since的请求,服务器会比较Etag,如果相同,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;
- 协商缓存第二个重要的字段是,If-Modified-Since,如果客户端发送的If-Modified-Since的值跟服务器端获取的文件最近改动的时间,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;
强制缓存
向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程
浏览器直接从本地获取数据,不与服务器进行交互,命中时http返回码为200,size显示为from cache,其通过http返回头中的Expires或者cache-Control(优先级高)来控制
- Expires指缓存过期时间,是服务器的具体时间点,其返回一个代表资源失效的时间
缺点:失效时间是一个绝对时间,当客户端时间被修改或客户端与服务器时间偏差较大就会导致缓存混乱 - Cache-Control为相对客户端的时间,服务器与客户端时间偏差也不会有影响,其拥有一些字段
-
- max-age:指定一个时间长度,在时间长度内资源有效(单位为s)
- public:响应可以被任何对象(客户端,代理服务器)缓存
- private:表明响应只能被单个用户缓存,代理服务器不能缓存
- no-cache:该指令目的是为了防止从缓存中返回过期的数据,不是不缓存,客户端使用该指令表示客户端不接收缓存过的响应,缓存服务器必须把客户端请求发送给源服务器,服务器使用该指令时缓存服务器不能对该资源进行缓存
- no-store:禁止缓存
- Immutable:若页面命中强缓存,就算用户刷新页面,浏览器也不会发起服务请求
强制缓存的缓存规则:当浏览器向服务器发起请求时,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是Expires和Cache-Control,其中Cache-Control优先级比Expires高。
彻底理解浏览器的缓存机制 | Heying Ye’s Personal Website
协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。
浏览器发送请求到服务器,服务器根据http头信息中的条件判断文件是否更改,如果未发生改变则返回304,浏览器从缓存中加载资源,改变则返回文件与状态码200。其通过两对值来判断文件是否被改变
- Last-modified/if-modified-since:
(1) Last-modified为浏览器向服务端发起请求后服务器放在响应头返回的文件最后更改时间
(2) If-modified-since即为被保存的last-modified值,其会在浏览器协商缓存时被发送到服务器,服务器通过读取这个字段并与文件最后改变时间比较,若相同则返回空,不同则将更新后的文件与文件最新更新时间返回至浏览器
缺点: last-modified保存的是精确到秒的绝对时间,如果资源在一秒内多次修改则无法识别。而且对于只修改了修改时间,并没有修改内容的文件也会去重新请求
- 弱Etag/if-none-match: //强etag无论资源发生多细微的变化也会改变
(1) Etag为每个文件唯一存在的hash值
(2) 在浏览器向服务器发起请求时if-none-match字段会被设置为该文件的etag值,服务器在接收该字段后将其与本地文件的etag值对比,若一致则代表文件内容没有被改变
缺点: 由于要生成hash所以性能较低
总结:etag精度更高且优先级大于last-modified,但是性能差于后者。同时配置时服务器优先验证etag,一致后才会验证last-modified·
- 协商缓存生效,返回304
- 协商缓存失效,返回200和请求结果
内存缓存from memory cache和硬盘缓存from disk cache
内存=》硬盘=》网络请求
- 先查找内存,如果内存中存在,从内存中加载;
- 如果内存中未查找到,选择硬盘获取,如果硬盘中有,从硬盘中加载;
- 如果硬盘中未查找到,那就进行网络请求;
- 加载到的资源缓存到硬盘和内存;
内存缓存:快速读取和时效性
- 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。
- 时效性:硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行I/O操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。
启发式缓存
如果响应中未显示Expires,Cache-Control:max-age或Cache-Control:s-maxage,并且响应中不包含其他有关缓存的限制,缓存可以使用启发式方法计算新鲜度寿命。通常会根据响应头中的2个时间字段 Date 减去 Last-Modified 值的 10% 作为缓存时间。
// Date 减去 Last-Modified 值的 10% 作为缓存时间。
// Date:创建报文的日期时间, Last-Modified 服务器声明文档最后被修改时间
response_is_fresh = max(0,(Date - Last-Modified)) % 10
cookie、session、token
cookie
- cookie指的是某些网站为了识别用户身份而存储在用户本地终端的数据,cookie是服务端生成,客户端进行维护和存储的纯文本形式内容, 以键值对形式存在。
- cookie是与特定域绑定的,服务端通过响应头里的Set-Cookie设置cookie后,默认情况下,domain被设置为设置cookie页面的主机名,cookie会与请求一起发送到创建它的域,这个限制能保证cookie中存储的信息只对被认可的接收者开放,不被其他域访问
- 构成:
-
- 名称:不区分大小写,需经过url编码
- 值:存储在cookie里的字符串值,这个值必须经过url编码
- 域:cookie有效的域,发送到这个域的所有请求都会包含对应的cookie
- 路径:请求url中包含这个路径才会把cookie发送到服务器
- 过期时间:表示何时删除cookie的时间戳
- 安全标志:设置之后,只适用ssl安全连接的情况才会把cookie发送到服务器
- 使用场景: 用户登录状态类的会话状态管理;自定义设置,主题等个性化设置;跟踪分析用户行为类的浏览器行为跟踪
- 缺点:
-
- cookie不够大,大小限制在4kb左右,很多浏览器限制一个站点最多保存20个cookie(ie6或者更低版本),ie7和之后的版本最多可以有50个cookie,firefox也可以有50个cookie,chrome和safari没有硬性限制
- 过多的cookie会带来性能浪费:一旦服务器端向客户端发送了设置cookie的意图,除非cookie过期,否则客户端每次请求都会发送这些cookie到服务器端,一旦设置的cookie过多,将会导致报头较大,大多数的cookie并不需要每次都用上,会造成宽带的浪费
-
-
- 减少cookie的大小
- 为静态组件使用不同的域名
-
多个域名的优点:
- 为不需要cookie的组件换个域名可以减少无效cookie的传输,所以很多网站的静态文件会有特别的域名,使得业务相关的cookie不影响静态资源
- 不仅可以减少cookie的发送,还可以突破浏览器下载线程数量的限制,因为域名不同,下载线程限制数量翻倍
多个域名的缺点:
- 将域名转换为IP需要进行DNS查询,多一个域名就多一次dns查询,页面的性能规则上就有一条:减少DNS查询,但是大多数浏览器都会进行DNS查 询,以削弱这个副作用的影响
-
- 不安全,http中cookie是明文传递的,所以具有安全问题
- 每次http请求都会发送到服务器,影响效率
- 需要自行封装增删改查的方法
- cookie什么时候会被销毁?
Max-age:以秒为单位,cookie会在Max-age秒之后被删除,当Max-age为负数的时候,表示的是临时存储,不会生成cookie文件,只会存在浏览器内存中,且只会在打开浏览器窗口或者子窗口有效,一旦浏览器关闭就会消失,当Max-age为0时,就会删除cookie
- 限制访问cookie:两种方法可以确保cookie被安全发送,并且不会被意外的参与者或脚本访问
-
- secure属性:标记为secure的cookie只应通过被https协议加密过的请求发送给服务端,他永远不会使用不安全的http发送,这意味着中间人攻击者无法轻松访问他,不安全的站点无法使用secure属性设置cookie,但是,secure不会阻止对cookie中敏感信息的访问
- httponly属性:httponly不支持读写,浏览器不允许脚本操作document.cookie去更改cookie,所以为避免跨域脚本攻击(xss),通过js的document.cookie API无法访问带有httponly标记的cookie,他们只应该发送给服务端,如果包含服务端session信息的cookie不想被客户端js脚本调用,那么就应该为其设置httponly标记
- SameSite属性
用来限制第三方cookie,从而减少安全风险
-
- strict:完全禁止第三方cookie,跨站点时,任何情况都不会发送cookie,只有当前网页的url与请求目标一致,才会携带cookie
- lax:导航到目标网址的get请求除外,这意味着 cookie 不会在跨站请求中被发送
| 请求类型 | 正常情况 | 示例 | lax |
|---|---|---|---|
| 链接 | 发送cookie | 发送cookie | |
| 预加载 | 发送cookie | 发送cookie | |
| get表单 | 发送cookie | 发送cookie | |
| post表单 | 发送cookie | 不发送 | |
| iframe | 发送cookie | 不发送 | |
| ajax | 发送cookie | $.get("...") | 不发送 |
| image | 发送cookie | 不发送 |
-
- none:Chrome 计划将 Lax 变为默认设置。这时,网站可以选择显式关闭 SameSite 属性,将其设为 None 。不过,前提是必须同时设置 Secure 属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
只要两个 URL 的 eTLD+1 有效顶级域名相同即可,不需要考虑协议和端口。其中,eTLD 表示有效顶级域名。同源是协议域名端口,同站的话是只考虑域名,不考虑协议和端口
session
我们将一个用户对服务的访问成为会话
session是保存在服务器的一种数据结构,是用户的唯一标识,用于跟踪用户状态
当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否已包含了一个session标识(称为session id),如果已包含则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(检索不到,会新建一个),如果客户端请求不包含session id,则为客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应中返回给客户端保存。保存这个session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发送给服务器。
泄漏后如何补救:
- 立即让所有受影响的用户重新生成SessionID: 停止使用原来的SessionID,让用户强制重新登陆
- 通知用户: 及时告知用户,提醒他们采取相应措施,例如:更改密码等等
- 检测异常行为: 检测到用户的异常行为后,即视采取错误,例如禁止某些操作等
token
访问资源接口时所需要的资源凭证
简单token的组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token的前几位以哈希算法压缩成的一定长度的十六进制字符串)
jwt——json web token
是一种开源标准(RFC 7519),用来定义通信双方如何安全地交换信息的格式。
JSON Web Token 入门教程 - 阮一峰的网络日志
放在http请求头信息的 authorization 字段里,使用bearer模式添加jwt,跨域的时候将jwt放在post请求的数据体里,通过url传输
原理: 服务器认证以后,生成一个 JSON 对象,发回给用户,以后用户与服务端通信的时候,都要发回这个json对象,服务器完全只靠这个对象认定用户身份,为了防止用户篡改数据,服务器在生成这个对象的时候会加上签名
当服务端拿到JWT后,会解析出其中的 Header 、Playload、Signature 三部分,解析完成后根据服务端保存的密钥再次生成一个Signature。拿到新生成的Signature和解析出来的进行对比,如果一样则表示未被篡改即可使用Header和Playload中的信息
Authorization:Bearer token
authorization 字段用于提供服务器验证用户代理身份的凭据,允许访问受保护的资源
Authorization: <authorization-parameters>
JWT授权为啥要在 Authorization标头里加个Bearer 呢 - 掘金
常见的type授权类型为:
- Basic用于http-basic认证
- Bearer常见于OAuth和JWT认证
- Digest MD5哈希的http-basic认证(已弃用)
- AWS4-HMAC-SHA256 AWS授权
jwt的三个部分依次如下:
- Header: 头部
- payload:负载
- signature:签名
特点:
-
JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次
-
JWT 不加密的情况下,不能将秘密数据写入 JWT
-
JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数
-
JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑
-
JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证
-
为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输
泄漏后如何补救:
- 使用黑名单或令牌撤销机制: 在服务器端维护一个黑名单,将已泄露或被盗的JWT加入其中,以阻止未经授权的访问。
- 重新颁发JWT: 要求用户或客户端重新登录以获取新的JWT令牌,这样之前泄露的JWT将无法继续使用。
cookie和session的区别
- cookie存储在客户端,session存储在服务端
- session是基于cookie实现的,session id会存储在客户端的cookie中,session比cookie安全
- cookie只支持存字符串数据,想要设置其他类型的数需要将其转换成字符串,session可以存储任意数据类型
- cookie可设置为长时间保持,session一般失效时间较短,客户端关闭(默认情况下)或者session超时都会失效
- 存储大小不同,单个cookie保存的数据不能超过4kb,session可存储数据远高于cookie,但是当访问量过多,会占用过多的服务器资源
session和token的区别
- session是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。
- session和token并不矛盾,作为身份认证token安全性比session好, 因为每一个请求都有签名还 能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
SSO登录(单点登录)
一个大型系统中包含了n个子系统,用户在操作不同系统时,需要多次登录就会导致用户体验感不好,那么SSO就可以很好的解决这个问题,即在多个应用系统中,只需要登录一次即可访问其他相互信任应用系统 。
我们以 tmall.com 和 taobao.com 为例,我们只需要登录其中一个系统,另一个系统就会默认登录
单点登录下的CAS验证流程:
- 客户端访问系统A , 系统A发现用户并未登录 ,重定向至 CAS服务中心
- CAS服务中心发现请求的cookie中并没有携带登录的票据证明(TGC),则CAS判定用户处于未登录状态,重定向至CAS的登录页面
- 用户在CAS登录页面输入账号密码进行认证
- CAS校验用户信息,生成TGC放入自己的Session中 ,并同时以 set-Cookie的形式写入 Domain为sso.com的域下,并生成一个授权令牌ST ,然后重定向到系统A的地址,并在url中携带ST
- 系统A保存携带的ST(token),然后再次携带token请求资源,CAS系统验证ST信息后告诉服务器已经登陆,服务器进行资源的响应。
- 当我们此时访问系统B时,发现用户未登录,重定向至CAS服务中心
- 客户端携带cookie中含有TGC,CAS服务中心校验成功后,生成一个新的token (含有ST)返还给客户端
- 客户端拿到token后,携带token发送请求,系统B会拿着token向CAS认证,校验成功后CAS返还给系统B一个成功标识,系统B返还数据
CAS 生成的票据:
- TGT(Ticket Grangting Ticket) :TGT 是 CAS 为用户签发的 登录票据,拥有了 TGT,用户就可以证明自己在 CAS 成功登录过。
- TGC:Ticket Granting Cookie : CAS Server 生成TGT放入自己的 Session 中,而 TGC 就是这个 Session 的唯一标识(SessionId),以 Cookie 形式放到浏览器端,是 CAS Server 用来明确用户身份的凭证。
- ST(Service Ticket) :ST 是 CAS 为用户签发的访问某个 Service 的票据。
OAuth2.0(第三方登录)
OAuth是一种授权机制,数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据 。 系统生成一个短期的进入Token,用来代替密码,提供给第三方使用
- 第三方应用发起微信授权登录请求后,用户授权登录第三方应用后,微信会重定向到第三方网站,并携带临时票据(code)
code是用来获取 access_token,并且code具有时间限制 - 通过code 加上AppId等参数,调用API获取 access_token
- 通过携带token进行获取用户的基本数据资源或帮助用户实现基本操作
Access token和refresh token
Access Token & Refresh Token 详解以及使用原则 - 掘金
Access token(访问令牌)是一种用于认证和授权的令牌,用于访问受保护的资源。它通常由身份验证服务器颁发,并且具有一定的有效期。Access Token应该维持在较短有效期,过长不安全,过短也会影响用户体验,因为频繁去刷新带来没有必要的网络请求。可以参考我们常常在某些网站停止操作一段时间之后就会掉线,这个时间是Refresh Token的有效期,Access Token不应长过这个时间。
Access Token(访问令牌)是在OAuth 2.0认证和授权框架中使用的一种令牌
Refresh token(刷新令牌)是一种用于认证和授权的令牌,通常与访问令牌(access token)一起使用。它是在用户进行身份验证后由身份验证服务器颁发的,并具有相对较长的有效期。Refresh Token的有效期就是允许用户在多久时间内不用重新登录的时间,可以很长,视业务而定。我们在使用某些APP的时候,即使一个月没有开过也是登录状态的,这就是Refresh Token决定的。授权服务在接到Refresh Token的时候还要进一步做客户端的验证,尽可能排除盗用的情况。
- 客户端向授权服务器请求Access Token(整个认证授权的流程,可以是多次请求完成该步骤)
- 授权服务器验证客户端身份无误,且请求的资源是合理的,则颁发Access Token 和 Refresh Token,可以同时返回Access Token的过期时间等附加属性。
- 带着Access Token请求资源
- 资源服务器验证Access Token有效则返回请求的内容
- 上面的3、4步骤可以反复进行,直到Access Token过期。 如果客户端在请求之前就能判断Access Token已过期或临近过期(下发过期时间),就可以直接跳到步骤7。否则,就会再请求一次,也就产生了本步骤。
- 当Access Token无效的时候,资源服务器会拒绝响应资源并返回Token无效的错误。
- 客户端重新向授权服务器请求Access Token,但是这次只需带着Refresh Token即可,而不需要用户再执行认证和授权的流程。这样就可以做到用户无感。
- 授权服务器验证Refresh Token,如果有效,则签发新的Access Token(或者同时下发一个新的Refresh Token)
- Refresh Token的其中一个目的是让用户在较长的时间保持登录状态,那么可否直接让Access Token具有更长的有效期,从而可以省去许多没用的步骤。答案是不安全,理由参考上面问题的答案。
举个例子,某个用户登录成功,获得了一个可以发帖的Access Token,这时管理员发现他发布垃圾内容吊销了发帖权限,而这个信息一般属于授权服务管理,也就是说他下次向授权服务请求Access Token将不会得到发帖权限。但是如果用户之前拿到的Access Token是长期有效的,那么这个用户就可以发帖很长时间。如果Access Token在短时间内失效,那么他必须重新去授权服务请求,这时授权服务将不会颁发具备发帖权限的Access Token。
第二个例子,如果Access Token具有较长的有效期,一旦被盗用,攻击者就可以拿Access Token使用很长时间。聪明的你可能会想到,攻击者可以同时盗取Refresh Token。
RFC6749第10节中有说明,授权服务必须维护Refresh Token与客户端的绑定关系,也就是说只有合法用户的客户端(可通过IP,UA等资料判断)来请求是可以通过的。退一步讲,如果攻击者模拟了客户端可以执行刷新请求,那么就要看谁先刷。由于授权服务可以设置Refresh Token一次有效,因此不管哪个先刷新,另一个人刷新就会报错。如果用户先刷新,攻击者以Access Token和Refresh Token的双重失效结束游戏。如果攻击者先刷新了,合法用户就会收到报错信息,授权服务会引导用户从上图的步骤1重新开始认证,从而把有效的Refresh Token拿回到合法用户这里。
唯一登录
- 用户在客户端A登录时,输入用户名和密码后,服务端校验用户名和密码,并创建出Token和一个已登录状态并保存,并将token返还给用户端
- 用户在客户端B登录时,服务端校验后清除服务端缓存的token,并生成一个新的token并重新保存一个已登陆状态
- 用户重新在客户端A登录时,校验token已经过期,并提示客户端
webstorage
为了解决客户端存储不需要频繁发送回服务器的数据时使用cookie问题
localStorage
- localstorage:永久存储机制,存储空间大,除非通过js删除,否则数据永远不会过期
- 如何设置localstorage的过期时间?www.jianshu.com/p/50b4c89d3…
检测某一个网页下localStoragr的剩余容量
使用json.stringify(localStorage).length和最大容量进行比较
if(window.localStorage) {
var aa= 1024*1024*5 -
unescape(encodeURIComponent(JSON.stringify(localStorage))).length;
console.log(aa);
}
-
- 为localstorage设置过期时间
- 惰性删除:可以在每一次get的时候判断是否过期,过期就删除,但是可能有一些永远也不会用到,就永远不会删除。所以也可以采用刷新就删除。
- 刷新删除:每次刷新进入页面就调用一次删除过期localstorage的函数
方法:
- removeItem:移除 localStorage 项
存储新手引导问题:
- 确定要存储的问题和相关信息:首先,确定您想要存储的新手引导问题及其相关信息。这些信息可以包括问题的文本、提示、步骤、状态等。
- 将问题和信息转换为JavaScript对象:将问题和相关信息组织成一个JavaScript对象,以便能够方便地进行存储和检索。例如:
var guideQuestion = {
question: "这是一个新手引导问题吗?",
hint: "请按照以下步骤操作...",
steps: ["第一步:...", "第二步:...", "第三步:..."],
status: "未完成"
};
- 将对象存储到localStorage中:使用localStorage.setItem()方法将问题对象存储到localStorage中。
- 从localStorage中检索问题对象:如果您需要在以后的时间点检索存储的问题对象,可以使用localStorage.getItem()方法。
var storedQuestion = localStorage.getItem("guideQuestion");
var questionObject = JSON.parse(storedQuestion);
- 更新问题对象的信息:如果用户完成了新手引导的某个步骤,您可以更新问题对象的相应信息,并将其重新存储到localStorage中。
sessionstorage
只存储会话信息,数据会保存到浏览器关闭,存在sesionstorage中的数据不受页面刷新影响,可以在浏览器崩溃后重启恢复
indexdb
浏览器数据库 IndexedDB 入门教程 - 阮一峰的网络日志
用于客户端存储大量结构化数据,该api使用索引来实现对该数据的高性能搜索,IndexedDB 是一个运行在浏览器上的非关系型数据库理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250M)。它不仅可以存储字符串,还可以存储二进制数据
- 采用键值对存储
- 异步,防止大量数据读写,拖慢网页
- 支持事务:这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
- 受到同源限制
- 存储空间大,理论上没有上限
- 支持二进制存储
包装类
注:在通过new实例化引用类型后,得到的实例会在离开作用域时销毁,而自动创建的原始值包装对象则只存在于访问
var str="hello word";
// var str = new String("hello world"); // 1.创建出一个和基本类型值相同的对象
// var long = str.length; // 2.这个对象就可以调用包装对象下的方法,并且返回结给long变量
// str = null; // 3.之后这个临时创建的对象就被销毁了
var long=str.length; //因为str没有length属性 所以执行这步之前后台会自动执行以上三步操作
console.log(long); // (结果为:10)
// 1.因为下面有输出创建出str.length 而str不应该具有length这个属性 所以再次开辟空间创建出一个和基本类型值相同的对象
// var str = new String("hello word");
// str.length=nudefined; // 2.因为包装对象下面没有length这个属性没有值,所以值是未定
// str = null; // 3.这个对象又被销毁了
console.log(str.length) // (结果为:undefined)
步骤:
- 创建一个基本类型对应的对象
- 调用该实例上的特定方法
- 销毁该对象即该实例
number,string,boolean构造函数可以与new操作符搭配,symbol,bigint使用call进行隐式装箱
function createSymbolObject(description){
return function (){
return this
}.call(Symbol(description))
}
function createSymbolObject(description){
return function (){
return this
}.call(BigInt(description))
}
js内置对象
- 本地对象:与宿主无关,独立于宿主环境的ecmascript实提供的对象,这些引用类型在本地运行过程中需要通过new来创建所需的实例对象
包含object,array,date,regexp,function,boolean,number,string等
- 内置对象:与宿主无关,独立于宿主环境的ecmascript实现提供的对象,本身就是实例化内置对象,包含Global和Math,json
- 宿主对象:由ecmascript实现的宿主环境提供的对象,包含两大类,素数提供以及自定义类对象,对于嵌入到网页中的js来说其宿主对象就是浏览器提供的对象例如window和document,所有的dom以及bom都属于宿主对象
数组方法
数组拍平
- 使用flat()拍平数组:该方法可传入一个数字作为拍平的层数,默认为1,设置为infinity后可实现全部拍平
- 使用正则表达式:
// 转为字符串后拼接为数组样式最后将其转换为数组对象
Json.parse(‘[’+ json.stringify(arr).replace(/[|]/g ,‘ ’) + ‘]’)
- 使用reduce:
let flat = arr => {
return arr.reduce((prev,cur) => {
return prev.concat(Array.isArray(cur) ? flat(cur) : cur)
},[])
}
- 递归
const res = [];
const fns = arr => {
for(let i = 0; i < arr.length; i++){
if(Array.isArray(arr[i])){
fns(arr[i])
}else{
res.push(arr[i])
}
}
}
数组去重
- 基本去重
Array.prototype.unique = function (){
let temp = {};
let arr = [];
let len = this.length;
for(let i = 0; i < len; i++){
if(!temp[this[i]){
temp[this[i]] = 'abc';
arr.push(this[i])
}
}
return arr;
}
- indexOf()——返回某个指定的字符串值在字符串中首次出现的位置
function unique(arr){
if(!Array.isArray(arr)){
console.log('type error');
return ;
}
let res = [];
let len = arr.length;
for(let i = 0; i < len; i++){
let cur = arr[i];
if(res.indexOf(cur) === -1){
res.push(cur);
}
}
return res;
}
- sort()先将数组进行排序,然后判断当前元素与前一个元素是否相同,相同说明重复,不相同就添加进res
function unique(array){
array = array.sort();
let res = [array[0]]
for(let i = 0;i<len;i++){
if(array[i] !== array[i-1]){
res.push(array[i]);
}
}
return res;
}
- 利用splice
function unique(arr){
let len = arr.length;
for(let i = 0; i < len; i++){
for(let j = i + 1; j < len; j++){
if(arr[i] === arr[j]){
arr.splice(j, l);
j--;
}
}
return arr;
}
}
- filter
function unique(arr){
return arr.filter(function (item, index, arr){
return arr.indexOf(item, 0) === index;
})
}
- 利用set
function unique(arr){
return Array.from(new Set(arr))
}
- map
function unique(arr){
const seen = new Map();
return arr.filter(a => !seen.has(a) && seen.set(a, 1))
}
语义版本控制
版本控制:Major.Minor.Patch
- Major:主版本号,当你做了不兼容的API修改
- Minor:次版本号,当你做了向下兼容的功能性新增
- Patch:修订号,当你做了向下兼容的问题修正
ES6 Module
- ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
- ES6模块的好处
-
- 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。
- 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性
- 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
- 模块module功能主要有两个命令构成:
- export:用于规定模块的对外接口
-
- 可以使用as关键字进行重命名
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
-
- 导出写法,目前export命令能够对外输出的三种接口类型:函数,类,var/let/const声明的变量
// 报错
export 1;
// 报错
var m = 1;
export m;
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
- import:用于输入其他模块提供的功能
-
- import命令具有提升效果,会提升到整个模块的头部,首先执行
- import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构
- 特点:
-
- 很多现代浏览器可以使用
- 具有Commonjs的简单语法以及AMD的异步
- 得益于 ES6 的静态模块结构,可以进行 Tree Shaking
- ESM 允许像 Rollup 这样的打包器,删除不必要的代码,减少代码包可以获得更快的加载
import和import()的区别
import 是 ES6 中用于在静态环境中导入模块的关键字,它是在代码加载阶段执行的。import 的语法如下:
import defaultExport from "module-name";
import * as name from "module-name";
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name";
import { export1 , export2 } from "module-name";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export1 [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import()是 ES6 中用于在动态环境中导入模块的函数,它是在运行时执行的,而不是在代码加载阶段执行。import() 函数的语法如下:
import(moduleName)
.then((module) => {
// 使用模块中的内容
})
.catch((error) => {
// 处理错误
});
let module = await import('/modules/my-module.js');
import()函数返回一个promise,可以在 Promise 的 then 方法中使用导入的模块。与 import 不同,import() 可以动态地加载模块,即可以在运行时根据需要动态加载模块,而不需要在代码加载阶段就加载所有模块
CommonJS
- Commonjs是Nodejs专用的,语法上最明显的差异为,CommonJS 模块使用require()和module.exports,ES6 模块使用import和export
// importing
const doSomething = require('./doSomething.js');
// exporting
module.exports = function doSomething(n) {
// do something
}
- commonjs是同步导入模块
- 当commonjs导入的时候,会给你一个导入对象的副本
- commonjs不能在浏览器中工作,必须经过转换和打包
- CommonJS的加载原理
CommonJS的一个模块,就是一个脚本文件,require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存
{
id: '...', // 模块名
exports: { ... }, // 模块输出的各个接口
loaded: true, // 表示该模块的脚本是否执行完毕
...
}
- 一旦出现CommonJS模块被循环加载,就只输出已经执行的部分,还未执行的部分不会输出
// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
// 执行main.js后的输出
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
AMD
异步加载模块,模块的加载不影响后面语句运行,所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成后,这个回调函数才会运行,包装模块的函数是全局define的参数
AMD模块可以使用字符串表示指定自己的依赖,而AMD加载器会在所有依赖模块加载完毕后立即调用模块工厂函数。
define(['dep1', 'dep2'], function (dep1, dep2) {
//Define the module value by returning a value.
return function () {};
});
// 或者
define(function (require) {
var dep1 = require('dep1'),
dep2 = require('dep2');
return function () {};
});
UMD
UMD用于创建这两个系统都可以使用的模块代码。本质上,UMD定义的模块会在启动时检测要使用哪个模块系统然后进行配置,并把所有逻辑包装在一个立即调用函数表达式中
(function (root, factory) {
if (typeof define === "function" && define.amd) {
// AMD 规范
define(["b"], factory); }
else if (typeof module === "object" && module.exports) {
// 类 Node 环境,并不支持完全严格的 CommonJS 规范,但是属于 CommonJS-like 环境,支持 module.exports 用法
module.exports = factory(require("b"));
} else {
// 浏览器环境 root.returnExports = factory(root.b);
}
})(this, function (b) {
// 返回值作为 export 内容
return {};
});
module,AMD,CommonJS
- ES6的模块是尽量静态化的,编译时就能确定模块的依赖关系以及输入和输出的变量,但是CommonJS和AMD模块都只能在运行时确定这些东西
- export语句输出的接口,与其对应的值是动态绑定的关系,输出的是值得引用;但是Commonjs模块输出的是值的缓存,是值得拷贝
- commonjs模块的require()是同步加载模块,ES6模块的import命令是异步加载,有一个独立的模块依赖的解析阶段
严格模式
单位
- rem:根据网页的根元素来设置页面大小
- em:根据父元素的字体大小来设置
- vm/vh:很好的弥补了rem需要js辅助的缺点
- 1vw:window.innerWidth的1%
- 1vh:window.innerHeight的1%
懒加载&预加载
预加载
将所有所需资源提前请求加载到本地,这样后面再需要的时候就直接从缓存获取资源
作用:
在网页全部加载之前,对一些主要内容进行加载,以提供给用户更好的体验,减少等待时间,如果一个页面的内容过庞大,没有使用预加载技术的页面就会长时间的展现为一片空白
- 图片预加载
- img标签:img标签会在Html渲染解析到的时候,如果解析到img中src的值,则浏览器会立即开启一个线程去请求该资源,所以我们可以先将img标签隐藏但src写上对应的链接,这样皆可以把资源先请求回来了
- image对象,原理同上
- js预加载:
-
- 添加async或defer属性
- 使用module:浏览器会对其内部的 import 引用发起 HTTP 请求,获取模块内容。这时 script 的行为会像是 defer 一样,在后台下载,并且等待 DOM 解析
<script type="module">import { a } from './a.js'</script>
-
- link标签的preload属性:用于提前加载一些需要的依赖,这些资源会优先加载;vue2 项目打包生成的 index.html 文件,会自动给首页所需要的资源,全部添加 preload,实现关键资源的提前加载
<link rel="preload" as="script" href="index.js">
-
- link标签的prefetch属性:prefetch 是利用浏览器的空闲时间,加载页面将来可能用到的资源的一种机制;通常可以用于加载其他页面(非首页)所需要的资源,以便加快后续页面的打开速度
<link rel="prefetch" as="script" href="index.js">
懒加载
图片加载不出来的时候可以通过监听图片的error事件做出对应的操作
也叫做延迟加载,指的是在网页中延迟加载图像,是一种很好优化网页性能的方式
作用:
- 提升用户体验
- 减少无效资源的加载
- 防止并发加载资源过度会阻塞js的加载
步骤:
- 加载loading图片
- 判断哪些图片要加载
- 隐形加载图片
- 替换真图片
一张图片就是一个img标签,浏览器是否发起请求是根据img的src属性,懒加载的关键是在图片没有进入可视区域时,先不给img的src赋值,即浏览器不会发送请求
window.innerHeight:是浏览器可视区的高度
document.documentElement.scrollTop || document.body.scrollTop:浏览器滚动过的距离
imgs.offsetTop:元素顶部距离文档顶部的高度
imgs.offsetHeight:图片自身高度
内容到达显示区域:
img.offsetTop < window.innerHeight + document.body.scrollTop && img.offsetTop + img.offsetHeight > document.body.scrollTop
// 等所有的资源文件加载完毕之后再绑定事件
window.onload = function (){
// 获取图片列表,即img标签列表
var imgs = document.querySelectorAll('img');
function lazyLoad(imgs){
// 可视区域高度
let innerHeight = window.innerHeight
// 滚动区域高度
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
for(let i = 0;i < imgs.length;i++){
// 图片距离顶部的距离大于可视区域和滚动区域之和时懒加载
if((innerHeight + scrollTop) > imgs[i].offsetTop){
// 不加立即执行函数i会等于9
(function(i){
// 真实情况是页面开始有2秒空白,所以使用setTimeout定时2s
setTimeout(function (){
//创建一个临时图片,这个图片在内存中不会到页面上去。实现隐形加载
let temp = new Image();
//只会请求一次
temp.src = imgs[i].getAttribute('data-src');
temp.onload = function (){
// 获取自定义属性data-src,用真图片替换假图片
imgs[i].src = imgs[i].getAttribute('data-src');
}
},2000)
})(i)
}
}
}
lazyLoad(imgs);
window.onscroll = function(){
lazyLoad(imgs)
}
}
rgba()透明度和opacity透明度
rgba()只作用于元素的颜色或其背景
opacity作用于元素以及元素内所有内容的透明度
客户端渲染与服务端渲染
客户端渲染的页面是js负责进行的,html仅仅作为静态文件,客户端在请求时,服务端不做任何处理,直接以源文件的形式返回客户端,然后根据html上的js生成dom插入html,而服务端渲染是服务器直接返回html让浏览器直接渲染,服务端在返回html之前,在特定的区域符号里用数据填充,再给客户端,客户端只负责解析html
| 客户端渲染 | 服务端渲染 |
|---|---|
| - 前后端分离,前端专注于UI,后端专注于逻辑 |
- 局部刷新,无需每次都请求完整页面,体验更好
- 节省服务器性能,部署简单
- 交互性好,可以实现各种效果 | - 首屏渲染快,客户端只负责解析html
- 利于SEO
- 可以生成存储片段,生成静态化文件
- 节能 | | - SEO问题,爬虫看不到完整的呈现源码
- 首屏渲染慢,渲染前,需要下载一堆js和css文件 | - 用户体验较差
- 不容易维护,通常前端改了部分html或者css后端也需要更改 |
代码同构
- 服务端生成html
- 发送html给浏览器
- 浏览器接到内容显示
- 浏览器加载js文件
- js代码执行并接管页面的操作
- 浏览器对服务器发起请求
- 服务器node server render
- 拿到请求中的path
- 根据path查找到具体的组件
- 数据获取调用组件的getInitialProp方法
- 数据注入组件,将数据注入到组件props或者通过context传递
- 得到组件的html字符串结果
- 数据注水,将预取的数据注入到页面,是浏览器可以访问到
- 加载静态资源,服务端进行输出组件html和预取数据
- 数据脱水,浏览器得到数据,将数据传入组件
- ReactDOM.hydrate浏览器执行渲染并进行节点对比
- 完成事件的绑定和交互处理
数据注水
将服务端的store数据注入到window全局环境中
数据脱水
将window上绑定的数据给到客户端的store
创建对象
工厂模式
可以解决创建多个类似对象的问题,但没有解决对象标识的问题,即不知道对象是什么类型——所有实例都指向一个原型
function create(name, age, obj){
let o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name);
}
return o;
}
let person1 = create('tyx', 18);
let person2 = create('ftf', 19);
构造函数模式
确保实例被标识为特定类型,但定义的方法会在每个实例上都创建一遍
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = function (){
console.log(this.name);
}
}
let person1 = new Person('tyx',18);
let person2 = new Person('txj',19);
// 为了解决的定义方法会在每个实例上都创建一个可以优化成如下代码
// 但如果需要多个方法,需要在全局作用域上定义多个函数
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName(){
console.log(this.name);
}
let person1 = new Person('tyx',18);
let person2 = new Person('txj',19);
与工厂模式的区别:
- 没有显式的创建对象
- 属性和方法都直接赋值给了this
- 没有return
原型模式
原型对象上定义的属性和方法可以被对象实例共享,解决了构造函数中的问题,实例与构造函数的原型之间有直接的关系,但实例与构造函数之间没有,即person与Person.prorotype之间有关系,再通过对象访问属性时,会按照这个属性的名称开始搜索,搜索开始于对象实例本身,如果没有找到会在原型对象上继续查找
function Person(){}
Person.prototype.name = 'tyx';
Person.prototype.age = 18
Person.prototype.job = 'teacher'
Person.prototype.sayName = function(){
console,log(this.name);
}
let person1 = new Person();
let person2 = new Person();
person1.sayName();//'tyx'
person2.sayName();//'tyx'
组合使用原型模式和构造函数模式
原型模式创建方法,构造函数模式用于属性
缺点:代码封装性一般
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype = {
constructor: Person
sayName : function (){}
}
动态原型模式
只有在sayname()不存在的时候才会将其放入原型,减少了新建时的操作,不能使用对象字面量重写prototype属性,否则原先在原型上的属性与方法会被覆盖,但是可以用delete操作符去掉实例与原型的联系
function Person(){
Person.prototype.age = 18;
Person.prototype.name = 'tyx';
if(typeof this.sayname !== 'function'){
Person.prototype.sayName = function(){
console.log('tyx');
}
}
}
寄生构造函数模式
- 返回的对象与构造函数或者构造函数的原型无关
- 不能使用instanceof操作符判断对象类型
function Create(name, age, job){
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
console.log(this.name);
}
return o;
}
let person1 = new Create('tyx',18,'doctor');
let person2 = new Create('ftf',19,'teacher');
稳妥构造函数模式
没有公共属性,而且其方法也不引用this的对象。无法识别对象所属类型
function Create(name,age,job){
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
console.log(name)
}
return o;
}
let person1 = Create('tyx',18,'doctor');
let person2 = Create('ftf',19,'teacher');
继承
1. 原型链继承
引用值共享,改变引用值会影响所有实例;创建子类型实例时无法向超类型传参。
构造函数.prototype = new 被继承构造函数
function Parent(){
this.name = 'tyx'
}
Parent.prototype.getName = function (){
console.log(this.name);
}
function Child(){}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // 'tyx'
2. 构造函数继承
方法都在构造函数中定义,无法复用函数,在超类型中定义的方法,子类型中不可见
function Parent(){
this.names = ['kevin','daisy'];
}
function Child(){
Parent.call(this);
}
var child1 = new Child();
child1.names.push('ftf');
console.log(child.names); // ['kevin','daisy','ftf']
var child2 = new Child();
console.log(child2.names);
3. 组合继承
会调用两次构造函数
function Parent(name){
this.name = name;
this.colors = ['red','blue','green'];
}
4. 原型式继承
引用值共享
function createObj(o){
function F(){};
F.prototype = o;
return new F();
}
5. 寄生式继承
function createObj(o){
var clone = Object.create(o);
clone.sayName = function (){
console.log('hi');
}
return clone;
}
6. 寄生组合式继承
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function prototype(child, parent){
var prototype = object(parnet.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
属性的可枚举型和遍历
对象的每个属性都有一个描述对象,用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象
目前四个操作会忽略enumerable为false的属性
- for...in循环:之遍历对象自身的和继承的可枚举的属性
- Object.keys():返回对象自身的所有可枚举的属性的键名
- JSON.stringify():只串行化对象自身的可枚举的属性
- Object.assign():忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性
可以遍历对象属性的方法:
super
super关键字指向当前对象的原型对象,只能用在对象的方法之中,用在其他地方会报错
js引擎内部super.foo等同于Object.getPrototypeOf(this).foo或Object.getPrototypeOf(this).foo.call(this)
AggregateError错误对象
AggregateError 在一个错误对象里面,封装了多个错误。如果某个单一操作,同时引发了多个错误,需要同时抛出这些错误,那么就可以抛出一个 AggregateError 错误对象,把各种错误都放在这个对象里面。
AggregateError()可以接受两个参数:
- errors:数组,它的每个成员都是一个错误对象。该参数是必须的。
- message:字符串,表示 AggregateError 抛出时的提示信息。该参数是可选的
从输入url页面加载发生了什么即浏览器渲染机制
浏览器渲染过程
- 对url解析:url只能是字母或者数字还有一些其他特殊符号(-_.~ ! * ' ( ) ; : @ & = + $ , / ? # [ ],不转义的话会出现歧义。比如
http:www.baidu.com?key=value,假如我的key本身就包括等于=符号,比如ke=y=value,就会出现歧义,你不知道=到底是连接key和value的符号,还是说本身key里面就有=。
-
- 对url进行编码,URL编码只是简单的在特殊字符的各个字节前加上%
- url编码的格式采用的是ascii码
- 查找是否有缓存(强制缓存,协商缓存)
- DNS解析(网址=>ip地址),计算机在互联网中唯一的标识是ip地址
dns优化
在html头部写入dns缓存地址 <link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />
- tcp连接(三次握手)
- 发送http请求
- 服务端处理请求,并且返回http报文
- 浏览器解析和渲染页面:
浏览器工作流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。
-
- 浏览器通过http协议请求到服务器,获取到html样式后,自上而下进行解析,构建dom树,document.readystate = "loading" ——正在加载
浏览器得到一部分就会开始构建dom,不会等到整个文档就位才开始渲染
-
- 遇到link外部css,创建线程加载,并继续解析文档
- 遇到外部js,并且没有设置async或defer,浏览器阻塞并加载,等待js加载完成并执行该脚本,然后继续解析文档,对于设置async,defer的浏览器创建线程加载,并继续解析文档,async脚本加载完后立即执行
多个带async属性的标签,不能保证加载的顺序,多个带defer属性的标签,按照加载顺序执行
-
- 遇到img,先正常解析dom架构,然后 浏览器异步加载src,并继续解析文档
- 文档解析完成后,document.readystate =“interactive” ——可交互,并触发domcontentloaded事件,所有设置defer的脚本还会按顺序执行,dom树和cssom树进行关联形成渲染树——rendertree,并绘制在页面上
- 所有defer的脚本加载完成并执行后,img等加载完成,document.readystate ="complete" ——完成,window对象触发事件。
- CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。通常情况下DOM和 CSSOM是并行构建的,但是当浏览器遇到一个不带defer或async属性的script标签时,DOM构建将暂 停,如果此时又恰巧浏览器尚未完成CSSOM的下载和构建,由于JavaScript可以修改CSSOM,所以需 要等CSSOM构建完毕后再执行JS,最后才重新DOM构建。
- 连接结束——四次挥手=>为什么三次不可以?简易理解:发完了,知道发完了,收完了,知道收完了
由于tcp连接时全双工的,每个方向都需要单独进行关闭,收到一个fin后,意味着这个方向没有数 据流动,一个tcp连接在收到一个fin后仍能发送数据。首先进行关闭的一方执行主动关闭,另一方 执行被动关闭。防止服务器还在传输数据,不能断连
将html文件转换为dom树:字节数据=>字符串=>Token=>Node=>Dom。token中会表示当前token是开始标签或结束标签或文本等信息。有结束标签标识的Token不会创建节点对象
构建cssom树:字节数据=>字符串=>Token=>Node=>cssom
CSSOM 提供了接口让JS动态操作 CSS,DOM 提供了接口让JS修改 HTML。cssom会阻塞页面渲染
document.readyState属性描述了document的加载状态
三种状态:
- loading:正在加载
- interactive:文档已经被解析,正在加载状态结束,但是图像,样式表或框架类的子资源仍在加载
- complete:文档和所有子资源已完成加载,表示load状态的事件即将被触发
根据浏览器渲染过程的优化:
- 优化JS:JavaScript文件加载会阻塞DOM树的构建,可以给
<script>标签添加异步属性async,这样浏览器的HTML解析就不会被js文件阻塞。 - 优化CSS:浏览器每次遇到
<link>标签时,浏览器就需要向服务器发出请求获得CSS文件,然后才继续构建DOM树和CSSOM树,可以合并所有CSS成一个文件,减少HTTP请求,减少关键资源往返加载的时间,优化渲染速度。 - 加载部分HTML浏览器先加载主要HTML初始化静态部分,动态变化的HTML内容通过Ajax请求加载。这样可以减少浏览器构建DOM树的工作量,让用户感觉页面加载速度很快。
- 压缩:对HTML、CSS、JavaScript这些文件去除冗余字符(例如不必要的注释、空格符和换行符等),再进行压缩,减小文件数据大小,加快浏览器解析文件编码。
- 图片加载优化
1)小图标合并成雪碧图,进而减少img的HTTP请求次数;
2)图片加载较多时,采用懒加载的方案,用户滚动页面可视区时再加载渲染图片。 - http缓存
- gzip压缩
- 去除console.log
- 预渲染:将浏览器解析
javascript动态渲染页面的这部分工作,在打包阶段就完成了,(只构建了静态数据)换个说法在构建过程中,webpack通过使用prerender-spa-plugin插件生成静态结构的html - 资源预加载:提前加载资源,当用户需要查看时可直接从本地缓存中渲染。
防抖和节流
防抖
触发事件后在n秒内函数只能执行一次,如果在n秒内又触发了事件,则会重新计算函数执行时间。单位时间内,操作n次,选中最后一次。
特点:延迟==》无限后延,不断刷新定时器
// 防抖
function debounce(fn, delay) {
let timeout = null;
return function () {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn.apply(this, arguments);
}, delay)
}
}
节流
连续触发事件但是在n秒内只执行一次函数,节流会稀释函数的执行频率。单位时间内,操作n次,执行第一次。
特点:只执行一次。设置标识位,看能不能触发事件
function throttle(fn, delay) {
let canrun = false;
return function () {
if(!canrun) return;
canrun = false;
setTimeout(() => {
fn.apply(this, arguments);
canrun = true;
}, delay)
}
}
同步异步
同步:一条线程按顺序执行指令,当客户端发送请求给服务端,在等待服务端响应的请求时,客户端不做其他的事情。当服务端做完了才返回到客户端。这样的话客户端需要一直等待。用户使用起来会有不友好。
异步:创建多条线程执行不同指令,当客户端发送给服务端请求时,在等待服务端响应的时候,客户端可以做其他的事情,这样节约了时间,提高了效率。
this指向
this指向执行环境也就是指向最后调用它的那个对象,在标准函数中,this引用的是把函数当成方法调用的上下文对象;在箭头函数中,this引用的是定义箭头函数的上下文。当this遇到return时如果返回值是一个对象则this指向返回的对象,如果不是一个对象,this还是指向函数的实例,虽然null也是一个对象,但是其中的this还是指向函数的实例。 (在全局执行上下文中,this指向全局对象;在函数执行上下文中,this的值取决于该函数是如何调用的,如果被一个引用对象调用,那么this会被设置为那个对象,否则this的值被设置为全局对象或者undefined)
改变this指向:
- 可以通过call,bind,apply方式改变。
-
- call(),第一个参数为函数内this的值,参数需要一个一个列出来
- apply(),第一个参数为函数内this的值, 第二个参数可以是Array的实例,也可以是arguents对象
- bind(),会创建一个新的函数实例,其this值会被绑定到传给bind对象。例如,f.bind(obj),实际上可以理解为obj.f(),这时,f函数体内的this自然指向的是obj,bind是创建一个新的函数,必须手动去调用
- 使用箭头函数
- 在函数内部使用_this = this,即使用变量保存下来
- new实例化一个对象
new有什么作用,如何用代码实现new
- 在内存中创建一个新对象
- 这个新对象内部的[[prototype]]特性被赋值为构造函数的prototype属性
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)
- 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回空对象,则返回该对象;否则,返回刚创建的新对象
new一个构造函数,创建一个新对象
将构造函数的原型对象绑定到新对象上,将构造函数的作用域给新对象——this指向这个新对象
执行构造函数的代码
返回新对象
Fucntion create(con, ...args){
let obj = {};
obj._proto_ = con.prototype;
let result = con.apply(obj, args);
return result instanceof object ? result : obj;
}
let a = create(构造函数名称,需要传入的构造函数参数)
js特点
- js是一种解释性脚本语言,代码不进行预编译;
-
- 编译型语言:在代码运行前编译器将人类可以理解的语言装换成集器可以理解的语言,会先转成可执行文件
- 解释型语言:将人类可以理解的语言转换成机器可以理解的语言,但是是在运行时转换的
- 跨平台,在绝大多数浏览器的支持下,可以在多种平台下运行
- 弱类型脚本语言:对使用的数据类型未作出严格要求,可以进行类型转换
- 单线程,事件驱动:js对用户的响应,是以事件驱动的方式进行的。再网页中执行了某种操偶做所产生的动作,被称为事件。事件发生后,可能会引起响应事件响应,执行某些对应的脚本,这种机制被称为事件驱动
- 面向对象:将一切都看成是对象,而对象一般都由属性和方法组成。
面向过程可以看作是蛋炒饭,面向对象可以看作是盖浇饭。
-
- 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;编程语言有:汇编语言,c语言等
-
-
- 优点:编码流程化,便于分析
- 缺点:代码重用性低,扩展性差
-
-
- 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。编程语言有java,js,python等
-
-
- 优点:代码结构分析,具备结构化;耦合度低,可重用,易扩展
- 缺点:性能低(没有蛋炒饭香🤭)
-
- 安全性:JavaScript是一种安全性语言,它不允许访问本地的硬盘,并不能将数据存入到服务器上,不允许对网络文档进行修改和删除,只能通过浏览器实现信息浏览或动态交互。从而有效地防止数据的丢失。
v8怎么实现的
- parse将js代码转换成ast(抽象语法树)
- ignition[ɪɡˈnɪʃn] 会将ast转换为bytecode字节码同时会收集turbofan优化所需要的信息(比如函数参数的类型信息)
- turbofan是一个编译器,可以将字节码编译为cpu可以直接执行的机器码。如果一个函数被多次调用,那么就会标记为热点函数,就会经过turbofan转换成优化的机器码,提高代码的执行性能;
机器码实际上也会被还原成bytecode,因为在后续执行函数的过程中,类型发生了变化,之前优化的机器码不能正确的处理,就会逆向转成字节码
作用域和执行上下文
词法作用域:作用域是由书写代码时函数声明的位置决定的。js使用的是词法作用域
动态作用域:作用域链是基于调用栈的,而不是代码中的作用域嵌套
变量或函数的上下文决定它可以访问哪些数据,以及他们的行为。作用域则保护内部的变量与环境不被外部访问,定义了变量的代码使用范围,避免命名冲突,为模块化开发提供了便利。
在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用arguments和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域一直向外串起所有包含函数的活动对象,直到全局上下文才终止。
全局上下文中的叫变量对象,他会在代码执行期间始终存在,而函数局部上下文中的叫活动对象,只在函数执行期间存在。作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。
执行上下文:当前js代码被解析和执行时所在的环境
闭包
闭包指的是那些那些引用了另一个函数作用域中变量的函数。
将执行环境想象为一个栈,栈底为全局执行环境,每检测到一个函数便将其执行环境入栈,由此栈顶的执行环境可以通过向栈底查找的方式寻找到本执行域总不存在的值。每执行一个函数便将其从栈中弹出,导致后续的函数无法访问被弹出函数内部的变量。闭包就是将需要保存执行域的函数通过return的方式返回到外部并保存其函数执行域,使外部可以访问函数内部的值
解决:
再退出函数之前,将不使用的全局变量全部删除,即手动接触引用赋值为null
使用场景:
- return 返回函数
- 函数作为参数
- iife立即执行函数
- 定时器setTimeout
- 所有的回调函数
作用:
- 模拟块级作用域,使用立即执行函数IIFE即可
- 模块化,可以实现对私有变量的封装
- 可以在函数外部读取到函数内部的变量、
- 将内部变量始终保存在内存中
注:
- 引用变量可能会发生变化
- this指向问题,闭包函数是在window作用域下执行的,this指向window
- 内存泄漏问题:解决需要主动释放不需要的闭包。对内存消耗有负面影响,闭包会导致原始作用域链不是放,造成内存泄露
- 会污染全局变量
内存泄漏
不在用到的内存,没有及时释放就叫做内存泄漏。
产生原因:
- 意外声明全局变量。函数中的局部变量在函数执行结束之后这些变量已经不再被需要,所以垃圾回收会识别他们并释放,但是对于全局变量,垃圾回收器很难判断什么时候变量才不被需要,所以全局变量通常不会被回收。在使用全局变量做持续存储大量数据的缓存时,需要设置存储上线并及时清理,不然的话数据量越来越大,内存压力也会越来越高
- 定时器的回调函数中通过闭包引用了外部变量,定时器存在,则会产生内存泄露
- 使用js闭包
- 没有清理的dom元素引用:DOM 元素的生命周期正常是取决于是否挂载在 DOM 树上,当从 DOM 树上移除时,也就可以被销毁回收了
- 未清理的console.log输出
内存泄漏带来的影响:
- 频繁GC——garbage collect,垃圾回收,gc会阻塞主进程的执行使得页面卡顿
- 当内存不足以为某些对象分配所需要的空间,会导致程序崩溃,造成体验差
垃圾回收
找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。
在chrome中,v8被限制了内存的使用(64位约1.4G/1464MB , 32位约0.7G/732MB),限制原因:
- v8最初是为浏览器设计的,不太可能会用到使用大量内存的情况
- v8的垃圾回收的限制——如果清理大量的内存垃圾是很消耗时间的,会引起js线程暂停执行的时间,性能和应用直线下降
垃圾回收器都有一些必须定期完成的任务:
- 标记:确定存活/死亡对象
- 清除:回收/再利用死亡对象所占用的内存
- 整理:压缩/整理内存
执行环境负责在代码执行时管理内存,垃圾回收程序每个一定时间会自动运行,确定不再使用的变量并释放其内存。常见的两种标记策略:标记清理和引用计数。
weakset,weakmap是弱引用,即垃圾回收机制不考虑weakset对该对象的引用,如果其他对象都不在引用该对象,那么垃圾回收机制会自动回收改对象所占用的内存。
标记清理:当变量进入上下文,这个变量会被加上存在于上下文中的标记,当变量离开上下文时,也会被加上离开上下文的标记。eg:变量进入上下文时,反转某一位
标记清除:垃圾回收器从根节点开始,标记根直接引用的对象,然后递归标记这些对象的直接引用对象。对象的可达性将作为是否“存活”的依据。
引用计数:对每个值都记录他被引用的次数。声明变量并给他赋一个引用值时,这个值的引用数为1,如果同一个值又被赋给另外一个变量,引用数加1,如果保存该值引用的变量被其他值覆盖了,引用数减1。引用值为0时,就没办法再访问这个值了。
v8采用三色标记来识别内存垃圾,三种颜色通过两个标志位来区分,即白色(00)、灰色(10)、黑色(11)。最初,所有对象都是白色的。标记将从根节点出发,每遍历到一个节点,便将该节点变为灰色。如果某个灰色节点的所有直接子节点都遍历完成,该灰色节点将变为黑色。如果不再有新的灰色节点,则标记结束,剩余的白色节点不可访问,可以被安全回收。
世代假说(弱分代假说)
垃圾回收基于世代假说,将内存分为新生代和老生代;
新生代又分为Nursery和Intermediate
新生对象会被分配到新生代的Nursery子世代,若对象在第一次垃圾回收中存活,它的标志位将发生改变,就如逻辑上的Intermediate子世代,在物理存储上仍存在于新生代中,如果对象再下一次垃圾回收中再次存活,就会进入老生代。对象从新生代到老生代的过程叫做晋升
v8在新生代使用Parallel scavenge[ˈpærəlel] [ˈskævɪndʒ]算法核心是复制算法——以空间换时间
在老生代中使用标记清除和标记整理进行垃圾回收
标记整理能够让堆利用的更充分,但是需要额外的扫描时间和对象移动时间,花费的时间与堆的大小成正比
执行垃圾回收时,不可避免会暂停 JavaScript 的执行。另一方面,为了页面流畅运行,我们通常希望页面能以每秒 60 帧的帧率运行,即每帧约 16ms 渲染间隔。这意味着如果在垃圾回收加上代码执行时间超过 16ms,用户将感受到卡顿的情况。Orinoco 利用了并行、增量和并发的技术进行垃圾回收,以释放主线程的压力,使其有更多的时间用于正常的 JavaScript 代码执行。v8使用的是并发
- 并行是指将垃圾回收任务分配成工作量大致相等的若干任务,交给主线程和辅助线程同时执行。由于执行过程没有 JavaScript 运行,所以实现较为简单,只需确保线程之间进行同步即可。
- 增量是指主线程将原本大量、集中的垃圾回收任务进行拆分,少量、多次间歇性地运行。
- 并发是指主线程保持 JavaScript 执行不中断,辅助线程完全在后台执行垃圾回收。由于涉及主线程和辅助线程的读写竞争,是三种策略中最复杂的一种。
代码缓存
代码缓存分为cold,warm,hot三个等级
- 用户首次请求js文件时,chrome将下载该文件并将其提供给v8进行编译,并将该文件缓存到磁盘中
- 当用户第二次请求这个 JS 文件时(即 warm run),chrome将从浏览器缓存中获取该文件,并将其再次交给v8进行编译,在warm run阶段编译完成后,编译的代码会被反序列化,作为元数据附加到缓存的脚本文件中。
- 当用户第三次请求这个文件时,chrome从缓存中获取文件和元数据,并将两者交给v8.v8将跳过编译阶段,直接反序列化元数据
尾调用优化
尾调用指的是某个函数的最后一步是调用另一个函数,只能是直接调用。函数调用会在内存中产生调用帧,所有的调用帧会形成调用栈。尾调用优化即不保存之前的调用帧,只保存最内层的调用帧。
尾递归:函数调用自身称为递归,如果尾调用自身,则称为尾递归
webworker
它允许在 Web 程序中并发执行多个 JavaScript脚本,每个脚本执行流都称为一个线程,彼此间互相独立,并且有浏览器中的 JavaScript引擎负责管理。这将使得线程级别的消息通信成为现实。使得在 Web 页面中进行多线程编程成为可能。
- 能够长时间运行(响应)
- 快速启动和理想的内存消耗
- 天然的沙箱环境
规范中有三种类型的web workers
- Dedicated Workers:由主进程实例化并且只能与之进行通信
- Shared Workrers:以被运行在同源的所有进程访问(不同的浏览的选项卡,内联框架及其它shared workers)。
- Service Workers:是一个由事件驱动的 worker,它由源和路径组成。它可以控制它关联的网页,解释且修改导航,资源的请求,以及一种非常细粒度的方式来缓存资源以让你非常灵活地控制程序在某些情况下的行为(比如网络不可用)。
原理:
以加载 .js 文件的方式实现的,这些文件会在页面中异步加载。
为了在 Web Worker 和 创建它的页面间进行通信,你得使用 postMessage 方法或者一个广播信道。
局限性: (不能访问一些非常关键的元素)
- dom
- window对象
- document对象
- parent对象
浏览器内核
浏览器内核指的是浏览器的排版引擎,也称为浏览器引擎,页面渲染引擎或者样板引擎
其构成:
- GUI渲染线程:
-
- HTMLparse解析html
- css parser解析style数据
- layout过程,为每个可见节点的几何信息
- painting过程,遍历rendertree调用ui接口绘制每个节点
- js引擎线程:负责解析js脚本,运行代码
- 定时器触发线程:浏览器定时计数器并不是由js引擎技术的,因为js引擎是单线程的,如果出于阻塞状态就会影响计时的准确,因此通过单独线程来即使并触发计时
- 事件触发线程:当一个事件被触发时该线程会把事件添加到处理队列的末尾,等待js引擎的处理
- 异步http请求过程:XMLHttpRequest 请求会在浏览器中新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中
Gecko:早期被netscape和mozilla firefox浏览器使用
Trident:微软开发,被IE4~IE11浏览器使用,但是edge浏览器已经转向blink。
webkit:苹果基于KHTML开发,开源的,用于safari,google chrome之前也在使用
Blink:是webkit的一个分支,google开发,目前应用于google chrome,edge,opera等
我们编写的js无论交给浏览器执行还是node执行,最后都是被cpu执行的,所以需要js引擎讲js代码翻译成cpu来执行
常见的js引擎:
- spiderMonkey:第一款js引擎
- chakra[ˈtʃʌkrə]:微软开发,用于it浏览器
- jscore:webkit中的js引擎,apple公司开发
- v8:google开发的js引擎
webkit由两部分组成:
- webcore:负责解析html,布局,渲染等
- jscore:解析,执行js代码
小程序中编写的js代码就是被jscore执行的
js执行机制
js引擎的执行过程(一) | Heying Ye’s Personal Website
js引擎执行过程分为三个阶段:
- 语法分析:分析该js脚本代码块的语法是否正确,如果出现不正确,则向外抛出一个语法错误(SyntaxError) ,停止该js代码块的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入预编译
- 预编译阶段:创建go对象,找形参和变量声明,赋值为undefined;将实参和形参相统一;找函数声明并赋值函数体
运行环境:
-
- 全局环境:js代码加载完毕后,进入代码预编译即进入全局环境
- 函数环境:函数调用时,进入该函数环境,不同的函数则函数环境不同
- eval:不建议使用,由安全,性能等问题
函数调用栈:使用栈存取的方式进行管理运行环境
创建执行上下文:
-
- 创建变量对象:创建变量对象发生在预编译阶段,但尚未进入执行阶段,该变量对象都是不能访问的,因为此时的变量对象中的变量属性尚未赋值,值仍为undefined,只有进入执行阶段,变量对象中的变量属性进行赋值后,变量对象(Variable Object)转为活动对象(Active Object)后,才能进行访问,这个过程就是VO –> AO过程。vo:变量对象,存储了在上下文中定义的变量和函数声明,无法访问,必须是js中以var声明的变量才会记录在这里,let或者const声明的变量不会存在,必须是显式声明的函数,函数表达式不会被记录
-
-
- 创建arguments对象,检查当前上下文的参数,建立该对象的属性与属性值,仅在函数环境(非箭头函数)中进行,全局环境没有此过程
- 检查当前上下文的函数声明
- 检查当前上下文的变量声明
-
-
- 建立作用域链:作用域链由当前执行环境的变量对象(未进入执行阶段前)与上层环境的一系列活动对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。作用域链的第一项永远是当前作用域(当前上下文的变量对象或活动对象),最后一项永远是全局作用域(全局执行上下文的活动对象)
- 确定this指向
- 执行阶段:
事件循环:
首先,整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务,异步任务两部分,同步任务会直接进入主线程依次执行,异步任务会再分为宏任务和微任务,宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中。微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中。当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务,该过程不断重复。event table:异步流程
await 以前的代码,相当于与 new Promise 的同步代码,await 以后的代码相当于 Promise.then的异步
常见微任务:process.nextTick ()-Node;Promise.then();catch;finally;Object.observe;MutationObserver
常见宏任务:主代码块;setTimeout;setInterval;setImmediate ()-Node;requestAnimationFrame ()-浏览器
const { resolve } = require("path");
console.log('1');
setTimeout(function(){
console.log('2');
process.nextTick(function(){
console.log('3');
})
new Promise(function(reslove){
console.log('4');
reslove();
}).then(function(){
console.log('5');
})
});
process.nextTick(function(){
console.log('6');
})
new Promise(function(reslove){
console.log('7');
resolve()
}).then(function(){
console.log('8');
})
setTimeout(function(){
console.log('9');
process.nextTick(function(){
console.log('10');
})
new Promise(function(reslove){
console.log('11');
resolve()
}).then(function(){
console.log('12');
})
});
// 1 7 6 8 2 4 3 5 9 11 10 12
var,let与const
- var声明的变量存在变量提升的情况,let,const声明的变量不存在变量提升,只在let或const声明的代码块内有效
- let不允许在相同作用域内重复声明,会报错
- 存在暂时性死区,即在代码块内,let,const之前使用变量会报错
- const声明之后需要初始化,const声明的变量即指向其地址,修改地址会出错
为什么let,const不能重复声明
词法环境分为两个部分:
环境记录以及对外部词法环境引用
es6中存在全局环境变量记录Global Environment Records,其中包括两部分:
- object environment record:对象式环境记录:主要用于with和global的词法环境。
- declarative environment record:声明式环境记录。用来记录直接有标识符定义的元素,比如变量、常量、let、class、module、import以及函数声明。
-
- function environment record:函数环境记录
- module environment record:模块环境记录
函数声明和使用var声明的变量会添加进入Object Enviroment Record中。
使用let声明和使用const声明的变量会添加入Declarative Enviroment Record中。
使用了let和const声明时,引擎会同时检查Object Enviroment Record和Declarative Enviroment Record是否有该变量,如果有,则报错,否则将将变量添加入Declarative Enviroment Record中。
js性能优化
- 作用域链深层次对象局部化:如果某个跨作用域的值被引用一次以上,则将其存储于局部变量
- 不改变执行时的作用域链:避免使用with语句,再catch语句中将错误委托给一个函数来处理,永远不要使用eval语句,如果一定要使用eval,用window.Function代替
- 避免使用闭包
- 对象深层次成员局部化:如果某个对象的属性被多次读取,则将属性值存储于局部变量
- 适当在原型上增加方法:在实例调用方法时,若频繁创建实例,则应在原型上调用目标方法;若只创建一次,但需频繁执行内置方法,则应将目标方法创建于构造函数。
- DOM方法代替innerHTML
- 文档碎片增加节点:使用createDocumentFragment创建文档碎片,批量修改DOM,减少重排和重绘。
- 事件绑定
- 流程控制:4种循环类型:for,while,do-while,for-in,避免使用for-in,会搜索实例和原型,长生更多性能开销,唯一使用for-in的场景是迭代一个属性数量未知的对象
-
- 使用forEach循环:数据量较少时,优先使用forEach,其次是for,便秘那使用for-in,大量数据使用for
- switch总比if-else快:条件数量大时用switch,数量少时用if-else
- 避免使用+=,因为会在内存中创建一个临时字符串,进行字符串操作后讲结果赋值给目标变量
- 数组项合并推荐使用join
- 提高加载性能。因为js下载会阻塞其他资源的下载,尽管脚本下载不会相互影响,但是页面必须等待所有js代码下载并执行完成才能继续,推荐将所有js文件放在body标签底部以减少对整个页面的影响
json
json是一种轻量级的数据交换格式,完全独立于语言的文本格式
json中有三种结构:
- 简单值:字符串,数值,布尔值和null,undefined不可以
- 对象:
var packjson = {
"name" = "tyx",
"age" = "18"
}
- 数组:
var packjson = [ { "name":"tpf", "age":"19" },{ "name":"tyx", "age":"18" }]
json.parse(str)——字符串转对象
json.stringify(要序列化的对象,过滤器,用于缩进结果的json字符串的选项)——对象转字符串
json.parse(json.stringify)——深克隆
- 对象中不能有函数,否则无法序列化,会被忽略
- 对象中不能有undefined,否则无法序列化,会被忽略
- 对象中不能有正则,否则无法序列化,克隆后为空
- date数据类型会被转化成字符串类型
- 对象不能是环状结构,否则会报错
dom事件流
什么是流:流是对输入输出设备的抽象,程序的角度说流是有方向的数据
事件流所描述的是从页面中接收事件的顺序
dom事件流三个阶段:
- 事件捕获
- 到达目标
- 事件冒泡
现在chrome使用的是事件冒泡处理事件流,如果使用事件捕获需将addeventlistener最后的参数设置为true,默认为false事件冒泡
阻止事件冒泡:event.stopPropagation()
事件委托:一般来讲,会把一个或者一组元素的事件委托到它的浮层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
事件冒泡:event.stopPropagation()——阻止事件冒泡,事件的触发响应会从最底层目标一层层地向外到最外层(根节点)
事件捕获:事件会从最外层开始发生,直到最具体的元素。
dom0,dom2,dom3
- dom0是通过onclick卸载html中的事件
清理该事件只需要给事件赋值null,同一个元素的同种事件只能绑定一个函数
- dom2是通过addeventlistener绑定的事件
清除时使用removeeventlistener
参数一为事件名,二为事件处理函数,第三个参数true表示捕获阶段,false为冒泡阶段
- dom3在dom2事件的基础上加了很多事件类型
跨域
跨域是为了防止用户读取到另一个域名下的内容,ajax可以获取响应,浏览器认为不安全,所以拦截了响应。同源策略:是一种约定,浏览器最核心最基本的安全功能,如果缺少了同源策略,浏览器容易受到xss,csrf等攻击,同源指的是协议+域名+端口三者相同,即便两个不同的域名指向同一个ip地址,也非同源
限制内容:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
但是有三个标签是允许跨域加载资源:
注:
- 如果协议和端口造化的跨域问题前台是无能为力的
- 在跨域问题上,仅仅是通过url首部来识别而不会根据对应的ip地址是否相同来判断。url首部可以了理解为协议,域名和端口必须匹配
CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
日常工作中,用得比较多的跨域方案是cors和nginx反向代理
jsonp
利用
网页通过添加一个
jsonp与ajax相比:都是客户端向服务端发送请求,从服务端获取数据的方式,但ajax属于同源策略,jsonp属于非同源策略
优缺点:简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题,缺点是仅支持get方法具有局限性,不安全可能会受到xss攻击,难以确定jsonp请求是否失败,可以通过使用定时器指定响应的允许时间,超出时间认为相应失败
cors
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
服务端设置Access-Control-Allow-Origin 就可以开启 CORS。盖属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源
简单请求:条件一:使用get/post/head条件3二:Content-Type的值为text/plain或multipart/form-data或application/x-www-form-urlencoded
复杂请求:复杂请求得cors会在正式通信前,增加一次http请求,成为预检请求,该请求是option方法的,通过该请求来指导服务端是否允许跨域请求
预检请求:浏览器先询问服务器,当前所在的域名是否在服务器的许可名单中,以及可以使用哪些http动词和头信息字段,只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
- 使用put或delete
- 发送json格式的数据
- 请求中带有自定义头部
Access-Control-Allow-Origin设置为*通配符时,表示接受任意域名的请求
Access-Control-Allow-Credentials值是一个布尔值,表示是否允许发送cookie,默认情况下,cookie不包括在cors请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
postmessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,允许来自不同源得脚本采用异步方式进行有限的通信,可以实现跨文档,多窗口,跨域消息传递。它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
otherWindow.postMessage(message, targetOrigin, [transfer]);
websocket
一文吃透 WebSocket 原理 刚面试完,趁热赶紧整理 - 掘金
h5提供的一种浏览器与服务器进行全双工通信的网络技术,属于应用层协议。
- 单向通信/单工通信:只能由一个方向的通信而没有反方向的交互
- 双向交替通信/半双工通信:通信双发都可以发送信息,但不能双方同时发送
- 双向同时通信/全双工通信:通信双发可以同时发送和接收信息
特点:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
一些api:
- webSocket.onopen:连接成功后的回调函数
- webSocket.onclose:用于指定连接关闭后的回调函数
- webSocket.onmessage:用于指定收到服务器数据后的回调函数
- webSocket.send():用于向服务器发送数据
原理
具体实现是通过http协议建立通道,然后再此基础上使用websocket协议进行通信。
过程:
客户端发起http请求,经过三次握手之后,建立起tcp链接,http请求里存放websocket支持的版本号信息,如:Upgrade、Connection、WebSocket-Version等;然后,服务器收到客户端的握手请求后,同样采用http协议反馈数据,最后,客户端收到连接成功的消息后,开始借助于tcp传输信道进行全双工通信。
断线重连
当客户端第一次发请求至服务端时会携带唯一标识,以及时间戳,服务端到db或者缓存去查询该请求的唯一标识,如果不存在就存入db或者缓存中
第二次客户端定时再次发送请求依旧携带唯一标识,以及时间戳,服务端到db或者缓存中去查询该请求的唯一标识,如果存在就把上次的时间戳拿取出来,使用当前的时间戳减去上次的时间,得出的毫秒数判断是否大于指定的时间,小于的话就是在线,否则就是离线
断线原因:
- websocket超时没有消息自动断开连接
- websocket异常包括服务端出现中断,交互切屏等等客户端异常中断等等
-
- 解决方法:引入reconnecting-websocket.min.js
解决:
- 修改nginx配置信息
- websocket发送心跳包
-
- 客户端每隔一个时间间隔发生一个探测包给服务器
- 客户端发包时启动一个超时定时器
- 服务器端接收到检测包,应该回应一个包
- 如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器
- 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了
node中间件代理
实现原理:同源策略式浏览器需要遵循的标准,而如果服务器向服务器请求就毋须遵循同源策略。代理服务器需要做以下几个步骤:
- 接收客户端请求
- 将请求转发给服务器
- 拿到服务器响应数据
- 将相应转发给客户端
nginx反向代理
搭建一个中转ngnix服务器,用于转发请求,使用nginx反向代理实现跨域是最简单的跨域方式,只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能】
实现:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
使用反向代理最主要的两个原因:
- 安全及权限。可以看出,使用反向代理后,用户端将无法直接通过请求访问真正的内容服务器,而必须首先通过Nginx。可以通过在Nginx层上将危险或者没有权限的请求内容过滤掉,从而保证了服务器的安全。
- 负载均衡。例如一个网站的内容被部署在若干台服务器上,可以把这些机子看成一个集群,那么Nginx可以将接收到的客户端请求“均匀地”分配到这个集群中所有的服务器上(内部模块提供了多种负载均衡算法),从而实现服务器压力的负载均衡。
nginx还带有健康检查功能(服务器心跳检查),会定期轮询向集群里的所有服务器发送健康检查请求,来检查集群中是否有服务器处于异常状态,一旦发现某台服务器异常,那么在以后代理进来的客户端请求都不会被发送到该服务器上(直到后面的健康检查发现该服务器恢复正常),从而保证客户端访问的稳定性。
window.name+iframe
name值在不同的页面加载后依旧存在,并且可以支持非常长的name值
location.hash+iframe
实现原理:a.html欲与c.html跨域相互通信,通过中间页b.html来实现,不同域之间利用iframe的location.hash船只,相同域之间直接js访问来通信
document.domain+iframe
该方式值用于二级域名相同的情况下。实现原理:两个页面都通过js强制设置document.domain为基础主语,就实现了同域
samesite
cookie的samesite属性用来限制第三方cookie,从而减少安全风险
可以设置三个值:
- strict:完全禁止第三方cookie,跨站点时,任何情况都不会发送cookie。只有当前网页的url与请求目标一致,才会携带上cookie
- lax:导航到目标网址的get请求除外
| 请求类型 | 示例 | 正常情况 | lax |
|---|---|---|---|
| 链接 | 发送 Cookie | 发送 Cookie | |
| 预加载 | 发送 Cookie | 发送 Cookie | |
| get表单 | 发送 Cookie | 发送 Cookie | |
| post表单 | 发送 Cookie | 不发送 | |
| iframe | 发送 Cookie | 不发送 | |
| ajax | $.get("...") | 发送 Cookie | 不发送 |
| image | 发送 Cookie | 不发送 |
- none:Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
只要两个 URL 的 eTLD+1 有效顶级域名相同即可,不需要考虑协议和端口。其中,eTLD 表示有效顶级域名。同源是协议域名端口,同站的话是只考虑域名,不考虑协议和端口
深浅克隆
深克隆
主要是将一个对象的属性拷贝并存储至自己开辟的内存区域,不受外界干扰:
- 判断是不是原始值
- 判断是数组还是对象instanceof/toString/constructor
- 建立相应的而数组或对象
- 递归
方法:
- json.parse(json.stringify)
json只支持object,array,string,number,true,false,null,其他的比如函数,undefined,Date,RegExp等数据类型都不支持。对于它不支持的数据都会直接忽略该属性
-
- 对象中不能有函数,否则无法序列化,会被忽略
- 对象中不能有undefined,否则无法序列化,会被忽略
- 对象中不能有正则,否则无法序列化,克隆后为空
- date数据类型会被转化成字符串类型
- 对象不能是环状结构,否则会报错
- 库函数lodash
- 手写递归
function deepClone(target,cache = new Map()){
if(cache.get(target)){
return cache.get(target)
}
if(target instanceof Object){
let dist ;
if(target instanceof Array){
// 拷贝数组
dist = [];
}else if(target instanceof Function){
// 拷贝函数
dist = function () {
return target.call(this, ...arguments);
};
}else if(target instanceof RegExp){
// 拷贝正则表达式
dist = new RegExp(target.source,target.flags);
}else if(target instanceof Date){
dist = new Date(target);
}else{
// 拷贝普通对象
dist = {};
}
// 将属性和拷贝后的值作为一个map
cache.set(target, dist);
for(let key in target){
// 过滤掉原型身上的属性
if (target.hasOwnProperty(key)) {
dist[key] = deepClone(target[key], cache);
}
}
return dist;
}else{
return target;
}
}
浅拷贝
值复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享一块内存,但深拷贝会另外创造一个一模一样的对象,新对象与源对象不共享内存,修改新对象不会改到原对象。
方法:
- object.assign()——当object只有一层时,是深拷贝。用于将所有可枚举属性的值从一个或多个元对象分配到目标对象,返回目标对象
- Array.prototype.concat()
- Array.prototype.slice()
//浅
function clone(origin,target){
var target = target || {};//如果用户不提供子集提供
for(let prop in origin){
target[prop]=origin[prop];
}
return target;
}
Restful
什么是REST | RESTful API 中文网
如果一个架构符合 REST 原则,就称它为 RESTful 架构。RESTful 架构可以充分的利用 HTTP 协议的各种功能,是 HTTP 协议的最佳实践
cdn(内容分发网络)
CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
优点:
- js体积变小,使用CDN的第三方资源的js代码,将不再打包到本地服务的js包中,减小本地js包的体积,提高加载速度
- 给网页加载提速
访问过程
主要依赖于DNS的重定向技术,即将用户重定向至离用户最近的CDN边缘节点中。
- 基于DNS调度
-
- 当终端用户向www.aliyundoc.com下的指定资源发起请求时,首先向Local DNS(本地DNS)发起请求域名www.aliyundoc.com对应的IP。
- Local DNS检查缓存中是否有www.aliyundoc.com的IP地址记录。如果有,则直接返回给终端用户;如果没有,则向网站授权DNS请求域名www.aliyundoc.com的解析记录。
- 当网站授权DNS解析www.aliyundoc.com后,返回域名的CNAME www.aliyundoc.com.example.com。
- Local DNS向阿里云CDN的DNS调度系统请求域名www.aliyundoc.com.example.com的解析记录,阿里云CDN的DNS调度系统将为其分配最佳节点IP地址。
- Local DNS获取阿里云CDN的DNS调度系统返回的最佳节点IP地址。
- Local DNS将最佳节点IP地址返回给用户,用户获取到最佳节点IP地址。
- 用户向最佳节点IP地址发起对该资源的访问请求。
-
-
- 如果该最佳节点已缓存该资源,则会将请求的资源直接返回给用户(步骤8),此时请求结束。
- 如果该最佳节点未缓存该资源或者缓存的资源已经失效,则节点将会向源站发起对该资源的请求。获取源站资源后结合用户自定义配置的缓存策略,将资源缓存到CDN节点并返回给用户(步骤8),此时请求结束。配置缓存策略的操作方法,请参见配置缓存过期时间。
-
存在的问题:
DNS的缓存时间较长,导致节点异常时自动调度延迟很大
- 302调度
- 访问url后,请求到了调度集群上,此时的算法和DNS调度的算法一样,但判断依据由本地域名服务器的IP变成了客户端的出口IP
- 浏览器接收到302响应,根据location中的url继续发起http请求,此时的请求目标IP时CDN边缘节点
302 调度的优势:
-
- 实时调度,因为没有 local DNS 缓存的,适合 CDN 的削峰处理,对于成本控制意义重大
- 准确性高,直接获取客户端出口 IP 进行调度
302 调度的劣势:
-
- 每次都要跳转,对于延时敏感的业务不友好。一般只适用于大文件。
- AnyCast BGP路由策略
CDN预热数据
上面说的访问模式,都是基于Pull模式,由用户决策哪部分热点数据会最终存留在CDN缓存中;对于大促场景,我们往往需要预先将活动相关资源预热 到 边缘节点(L1),避免大促开启后,大量用户访问,造成源站压力过大。这时候采用的是 Push模式。
CDN回源
当CDN本地缓存没有命中时,触发回源动作,
- 一级缓存 访问二级缓存是否有相关数据,如果有,返回一级缓存。
- 二级缓存 Miss,触发 二级缓存 回源请求,请求源站对应数据。获取结果后,缓存到本地缓存,返回数据到一级缓存。
- 一级缓存 获取数据,缓存本地后,返回给用户。
fetch
fetch()是xmlHttpRequest的升级版,用于在js里面发出http请求
// 基本使用
fetch(url,options).then(response => response.json()) //解析为可读数据
.then(data => console.log(data))//执行结果是 resolve就调用then方法
.catch(err => console.log("Oh, error", err))//执行结果是 reject就调用catch方法
fetch(url,options):
- 第一个参数:请求的url
- 第二个参数:定制http请求
-
- 请求方法
- 提交的json数据
- 请求头
const response = await fetch(url, {
method: 'POST',
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
body: 'foo=bar&lorem=ipsum',
});
const json = await response.json();
优点:
- 语法简介,语义化,业务逻辑更清晰
- 基于标准promise实现,支持async/await
- 通过数据流处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用,xmlhttpRequest对象不支持数据流,所有的数据必须放在缓存里,不支持分块读取,必须等待全部拿到后,再一次性吐出来
API:
大文件切片上传
获取文件后进行切片,切片整理好每个切片的参数并发请求即可
<template>
<div>
<input type="file" @change="handleFileChange" />
<el-button @click="handleUpload">上传</el-button>
</div>
</template>
<script>
const SIZE = 10 * 1024 * 1024; // 切片大小(10MB)
export default {
data: () => ({
// 存放文件信息
container: {
file: null
hash: null
},
data: [] // 用于存放加工好的文件切片列表
hashPercentage: 0 // 存放hash生成进度
}),
methods: {
// 获取上传文件
handleFileChange(e) {
const [file] = e.target.files;
if (!file) {
this.container.file = null;
return;
}
this.container.file = file;
},
// 生成文件切片
createFileChunk(file, size = SIZE) {
const fileChunkList = [];
let cur = 0;
while (cur < file.size) {
fileChunkList.push({ file: file.slice(cur, cur + size) });
cur += size;
}
return fileChunkList;
},
// 生成文件hash
calculateHash(fileChunkList) {
return new Promise(resolve => {
this.container.worker = new Worker("/hash.js");
this.container.worker.postMessage({ fileChunkList });
this.container.worker.onmessage = e => {
const { percentage, hash } = e.data;
// 可以用来显示进度条
this.hashPercentage = percentage;
if (hash) {
resolve(hash);
}
};
});
},
// 切片加工(上传前预处理 为文件添加hash等)
async handleUpload() {
if (!this.container.file) return;
// 切片生成
const fileChunkList = this.createFileChunk(this.container.file);
// hash生成
this.container.hash = await this.calculateHash(fileChunkList);
this.data = fileChunkList.map(({ file },index) => ({
chunk: file,
// 这里的hash为文件名 + 切片序号,也可以用md5对文件进行加密获取唯一hash值来代替文件名
hash: this.container.hash + "-" + index
}));
await this.uploadChunks();
}
// 上传切片
async uploadChunks() {
const requestList = this.data
// 构造formData
.map(({ chunk,hash }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
return { formData };
})
// 发送请求 上传切片
.map(async ({ formData }) =>
uploadRequest(formData) // 这里的uploadRequest是你封装好的上传文件切片接口请求方法
);
await Promise.all(requestList); // 等待全部切片上传完毕
await merge(this.container.file.name) // 发送请求合并文件
},
}
};
</script>
实现方案
- 读取文件计算hash值:
-
- 使用组件或者
input读取文件,使用sparkMd5计算大文件的hash值
- 使用组件或者
-
- 简单的做法:使用文件名 + 切片下标作为切片 hash,但这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化
- 根据文件内容生成hash:使用spark-md5,根据文件内容计算出文件的 hash 值,另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态,所以我们使用 web-worker在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互
-
- 由于file对象继承于Blob,即可以使用
slice()方法切片
- 由于file对象继承于Blob,即可以使用
- 创建文件上传列表:
-
- 根据预设的切片大小,对文件进行切片并构建上传列表
interface RequestItem {
index :number // 下标值
fileSection : Blob // 切片
}
type SectionList = RequestItem[];
- 上传文件 :
发送请求(文件hash值 、切片大小 、 文件类型 、 文件总大小 )获取已经上传的切片的下标
-
- 如果已经上传过该文件则后端直接返回url前端直接引用,实现秒传
- 如果未上传过,则将构建的上传列表进行上传
- 上传过部分切片则返回已经上传切片的下标值 ,根据该下标值筛选未上传的切片后进行上传
- 合并切片:
-
- 当切片前端全部上传完毕且后端返回成功响应后,发送合并请求让后端对切片进行合并校验
优化方案
- 构建上传列表后,上传列表时控制并发量减少浏览器负担
- 计算hash值时可以使用webworker开启新线程进行计算
- 抽样计算hash值 。 第一个和最后一个切片全部数据,中间的切片进行首、中、尾部进行定量的取样后对上述数据进行hash计算 。
文件秒传
在文件上传之前先计算出文件的hash,然后发送给后端进行验证,看后端是否存在这个hash,如果存在,则证明这个文件上传过,则直接提示用户秒传成功
暂停上传
将所有的切片存在一个数组中,每当一个切片上传完毕,从数组中移除,这样就可以实现用一个数组只保存上传中的文件。此外,因为要暂停上传,所以需要中断请求 axios中断请求可以利用AbortController