一. JS基础
1. 数据类型
总共8种数据类型,7种基础的数据类型String
,Number
,Boolean
,Null
,Undefined
,Symbol
,BigInt
,一个对象类型Object
Symbol
可以定义对象的唯一属性名BigInt
可以表示很大的数
1) typeof
判断数据类型
typeof可以判断基本的数据类型,可以判断function
,无法判断Null
,Object
和Array
,都为object
类型
typeof 'a' // string
typeof 1 // number
typeof true // boolean
typeof undefined // undefined
typeof Symbol() // symbol
typeof 12n // bigint
typeof function(){} // function
/** 无法判断 */
typeof null // object
typeof {} // object
typeof [] // object
2) instanceof
判断对象类型
instanceof可以判断对象类型,原理是判断对象的原型链是否有该类型的原型
class People {}
class Jay extends People {}
const jayChou = new Jay();
/** 因为实例jayChou顺着原型链可以找到Jay.prototype和People.prototype */
jayChou instanceof People // true
jayChou instanceof Jay // true
3) Object.prototype.toString.call()
判断所有的原始类型,包含内置类型Math
,Date
,Array
,Function
,Error
等
/** 原始数据类型 */
Object.prototype.toString.call('a') // [object String]
Object.prototype.toString.call(1) // [object Number]
Object.prototype.toString.call(true) // [object Boolean]
Object.prototype.toString.call(null) // [object Null
Object.prototype.toString.call(undefined) // [object Undefined]
Object.prototype.toString.call(Symbol()) // [object Symbol]
Object.prototype.toString.call(BigInt) // [object BigInt]
Object.prototype.toString.call({}) // [object Object]
/** 内置类型 */
Object.prototype.toString.call([]) // [object Array]
Object.prototype.toString.call(function(){}) // [object Function]
Object.prototype.toString.call(new Date()) // [object Date]
Object.prototype.toString.call(Math) // [object Math]
Object.prototype.toString.call(new Error()) // [object Error]
4) 判断Array类型的方法
4种
Array.isArray([]) // true
[].__proto__ === Array.prototype // true
[] instanceof Array // true
Object.prototype.toString.call([]) // [object Array]
5) 深拷贝的实现
- 递归遍历对象的属性
- 考虑对象,数组和基本类型的克隆方式不同,基本类型直接返回,对象和数组递归克隆;
- 使用while替换for…in优化速度
- 考虑循环引用,使用WeakMap判断当前对象是否已经被克隆,如果被克隆过直接返回
- 考虑对象保留原型,对于可遍历对象使用
Object.create(target.constructor.prototype)
创建对象 - 考虑引用类型中的可遍历类型Object,Array,Map,Set和不可遍历的类型的String,Number,Boolean,Date,Symbol,Regexp等内置对象的克隆
- 考虑创建引用类型的方式,从对象获取构造函数或
**Object.create**
- Symbol,Regexp的特殊克隆实现
- 考虑(个锤子)克隆函数function,lodash不做特殊处理
实现:
// 第二天重新实现
const objectType = '[object Object]';
const arrayType = '[object Array]';
const mapType = '[object Map]';
const setType = '[object Set]';
const argumentsType = '[object Arguments]';
const iterableType = [objectType, arrayType, mapType, setType, argumentsType];
const stringType = '[object String]';
const numberType = '[object Number]';
const booleanType = '[object Boolean]';
const errorType = '[object Error]';
const dateType = '[object Date]';
const symbolType = '[object Symbol]';
const regexpType = '[object Regexp]';
function deepclone(target, map = new WeakMap()) {
/** 基本类型 */
if (!isObject(target)) {
return target;
}
/** 循环引用 */
if (map.get(target)) {
return map.get(target);
}
/** 可遍历对象的原型继承 */
let clone = {};
const type = Object.prototype.toString.call(target);
if (iterableType.includes(type)) {
clone = Object.create(target.constructor.prototype);
} else {
/** 不可遍历对象的复制 */
return cloneOther(target)
}
map.set(target, clone);
/** 处理map */
if (type === mapType) {
target.forEach((value, key) => {
clone.set(key, deepclone(value));
});
return clone;
}
/** 处理set */
if (type === setType) {
target.forEach((value) => {
clone.add(clone(value));
});
return clone;
}
/** 普通对象,数组 */
for (let key in target) {
clone[key] = deepclone(target[key]);
}
return clone;
}
function isObject(target) {
const type = typeof target;
return (type === 'object' || type === 'function') && (target !== null);
}
function cloneOther(target) {
switch(Object.prototype.toString.call(target)) {
case booleanType:
case stringType:
case numberType:
case dateType:
case errorType:
return new target.constructor(target);
case symbolType:
return Object(Symbol.prototype.valueOf.call(target));
case regexpType:
const reFlags = /\w*$/;
const res = new target.constructor(target.source, reFlags.exec(target));
res.lastIndex = target.lastIndex;
return res;
default:
return null;
}
}
参考:如何写出一个惊艳面试官的深拷贝?
如何 clone 一个正则?
6) IEEE 754 Number数的表示、
IEEE 754规定浮点数由符号位(1),阶码exponent(11位),尾数mantissa(53位)组成;
符号位:没啥好说的
阶码:采用移码,阶码的真实数字需要手动-1023,阶码的范围-1023~1024
尾数:由于整数部分总是1,所以省去;尾数表示小数部分
问题:如何表示0?
答:因为1 * 2-1023 ,所以可以忽略不计
问题:**Number.MAX_SAFE_INTEGER**
是如何得来的?
答:Math.pow(2, 53) - 1
,长度16位,9007199254740991
问题:为什么0.1+0.2 != 0.3
答:因为进制转换和对阶运算会丢失数字精度,当差值小于Number.EPSILON
时可以认为他们相等
Number.EPSILON
表示1与Number可表示大于1的最小浮点数之间的差值
2. 原型和原型链
1) 原型:
每个js对象在创建的时候(null除外)都会与之关联一个对象,这个对象就是prototype
对象,即原型;每个对象都能从原型中继承一些属性。
2) 原型链:
3) 其他一些结论性概念:
- js对象分为普通对象和函数对象,两种对象都有
__proto__
属性- 普通对象:有
__proto__
属性 - 函数对象:有
__proto__
属性,独有prototype
属性
- 普通对象:有
Object
和Function
是内置的函数,Array,Date这些也是内置函数__proto__
是一个对象,有__proto__
和constructor
属性- 原型对象
prototype
有constructor
属性 - 原型对象的
constructor
属性指向构造函数本身(如:Person.prototype.constructor === Person) - 实例的
__proto__
和原型对象prototype
的指向同一个地方,指向原型(如:person.proto === Person.prototype) - 普通函数是Function的实例,Object函数也是Function的实例
3. 继承的实现
js没有类的概念,只能使用原型来实现继承1) 原型链继承
父类的实例是子类的原型,重点是这句SubType.prototype = new SuperType();
缺点:创建实例时不能给父类型传参,父类所有的引用类型子类共享 ```javascript function Parent() { this.name = ‘jay’; this.work = [‘cooker’, ‘programmer’]; } Parent.prototype.getName = function() { return this.name; }
function child(age) { this.age = age; } child.prototype = new Parent();
const me = new child(26); const you = new child(31); you.work[2] = ‘deliveryman’; console.log(‘me.work :>> ‘, me.work);
<a name="j2HG3"></a>
##### 2) 借用构造函数
调用父类的构造函数`Parent.call(this, name)`,将this指向子类。<br />优点:解决了原型继承父类引用对象共享的问题<br />缺点:无法继承父类的原型
```javascript
function Parent(name) {
this.name = name;
this.work = ['cooker', 'programmer'];
}
Parent.prototype.getName = function() {
return this.name;
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
const me = new Child('jay', 26);
console.log('me.name :>> ', me.name); 、// 可以访问普通属性
console.log('me.getName() :>> ', me.getName()); // 报错 无法继承父类原型
3) 组合继承
组合继承是原型链继承和借用构造函数继承的组合
优点:父类的普通属性,原型都可以继承
缺点:构造函数执行了2次,普通属性出现了2次,如me.name
,me.__proto__.name
都存在
function Parent(name) {
this.name = name;
this.work = ['cooker', 'programmer'];
}
Parent.prototype.getName = function() {
return this.name;
}
function Child(name, age) {
this.age = age;
Parent.call(this, name);
}
Child.prototype = new Parent();
const me = new Child('jay', 26);
console.log('me.getName() :>> ', me.getName());
4) 原型式继承
const me = Object.create(Parent)
,把Parent对象作为me的原型
缺点:父类引用类型的值共享
const Parent = {
name: 'jay',
age: 26,
work: ['cooker', 'programmer'],
getName() {
return this.name;
}
}
const me = Object.create(Parent);
const you = Object.create(Parent);
you.work[2] = 'deliveryman';
console.log('me.work :>> ', me.work);
5) 寄生组合式继承
比较完美的方案了,父类的引用对象不会共享
function inherit(subType, superType) {
const prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function Parent(name) {
this.name = name;
this.work = ['cooker', 'programmer'];
}
Parent.prototype.getName = function() {
return this.name;
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inherit(Child, Parent);
const me = new Child('jay', 26);
const you = new Child('wang', 31);
you.work[2] = 'deliveryman';
console.log('me.work :>> ', me.work);
4. 作用域和作用域链
js采用静态作用域(词法作用域),作用域是在创建的时候(解释阶段)确定。所以如果需要确定一个变量取值,需要到创建函数的作用域取值
1) 作用域
作用域规定了如何查找变量,函数;确定了当前执行环境对变量和函数的访问权限;作用域分为全局作用域,函数作用域和块级作用域
2) 作用域链
如果某一变量在当前作用域中没有定义,就会上级作用域层层查找知道全局作用域,这种层级关系就叫做作用域链
3) 全局作用域和函数作用域
var和function只有全局作用域和函数作用域,所以只有函数体会产生新的作用域;js引擎会在解释阶段对var和function进行变量提升,所以允许他们在声明前使用
var的特点:
- 只有全局作用域和函数作用域
- 只提升声明,不提升赋值
- 声明在代码不可达的区域也可以提升 ```javascript if(false) { var a = 1; } console.log(a); // 没有报错,undefined
4. 相同作用域中可以**重复声明**
**function的特点:**
1. 只有全局作用域和函数作用域
1. 既提升声明也提升赋值
1. 函数声明的提升**在变量提升之前**
1. 声明在不可达的区域不能提升
```javascript
if (false) {
function fn() {};
}
fn(); // 报错,fn未定义
4) 块级作用域
ES6增加了块级作用域,let、const变量可以声明块级作用域;块级作用域才是正常的作用域
- 不允许声明前使用
-
5. 执行上下文和执行上下文栈
1) 执行上下文
概念:执行上下文是评估和执行JavaScript代码的环境的抽象概念,每当JavaScript代码执行的时候,它都在执行上下文中运行。
类型: 全局执行上下文:在函数外面的代码都在全局执行上下文中,一个程序只有一个全局执行上下文,在程序运行时被压入执行上下文栈的栈底。全局执行上下文做了两件事情 :
- 创建全局对象(浏览器window,node环境global)
- this指向全局对象
- 函数执行上下文:函数被调用的时候会创建函数执行上下文,并把该上下文压入栈中,调用结束后上下文从栈中弹出
- eval执行上下文:忽略先
执行上下文中的三个重要的属性:
- 变量对象(variable object,vo)
- 作用域链(scope chain)
- this
2) 执行上下文栈
执行栈用于存储在代码执行期间创建的所有上下文,程序开始运行时会创建全局上下文压入栈中;在遇到函数调用的时候会创建函数执行上下文并压入栈中,函数执行完成会将该上下文弹出;控制流程始终保持在执行上下文栈的栈顶3) 变量对象(vo)
每一个执行上下文都有一个与之关联的变量对象,这个对象存储了上下文中定义的变量和函数。
函数调用时会立即创建一个活动对象(AO),并将这个活动对象作为变量对象;以下时活动对象的创建过程: ```javascript function foo(a, b) { var c = 10; function d() {
} var e = function () {console.log('d');
}; (function f() {}) if (true) {console.log('e');
} else {var g = 20;
} }var h = 30;
foo(10);
1. 初始化活动对象
活动对象会以`arguments`为属性初始化,`arguments`的值为arguments对象
```javascript
AO = {
arguments: <Args>
}
arguments对象是一个类数组对象,包含实参的值,对象有如下属性:
- length:实参的个数
- callee:指向函数本身
- 下标index:存储了传入实参的值
- 进入执行环境
进入执行环境后,会扫描所有的变量声明和函数声明,在活动对象中添加3类属性:
- 形参和实参:实参的值添加进arguments属性中,形参作为活动对象的属性被添加,如果传入了实参,则值为实参的值,如果没有传入实参,则为undefined;
- 函数声明:函数声明的名称作为属性名添加到对象中,值指向函数对象的引用;如果变量对象中已经有该属性则替换值
- 变量声明:变量声明的的名称作为属性名添加到对象中,值为undefined;如果变量对象中已经有该属性则跳过不添加
AO = {
arguments: {
callee: show,
length: 1,
0: 10,
},
a: 10,
b: undefined,
c: undefined,
d: <d reference>,
e: undefined,
g: undefined,
h: undefined
}
- 执行代码阶段
执行代码阶段所有的属性都会被赋值,活动对象包含arguments对象 + 形参 + 函数声明 + 局部变量(不包含表达式)
AO = {
arguments: {
callee: show,
length: 1,
0: 10,
},
a: 10,
b: undefined,
c: 10,
d: <d reference>,
e: <function reference>,
g: 20,
h: undefined
}
6) 作用域链
当查找一个变量的时候,会从当前的变量对象查找,如果查找不到就从(静态作用域的,编译阶段已经确认)父级执行上下文的变量对象查找直至全局变量对象,这样由多个执行上下文的变量对象组成的链表就叫作用域链。
作用域链形成的过程如下:
function foo() {
function bar() {
...
}
}
- 创建(编译)阶段,就已经确定每个函数的作用域,保存在各自的
[[scope]]
属性中 ```javascript foo.[[scope]] = [ globalContext.VO ]
bar.[[scope]] = [ fooContext.AO, globalContext.VO ]
2. 执行阶段,foo的执行上下文作用域链属性`Scope`复制一份foo函数的`[[scope]]` ,并添加自己的AO到作用域链顶端。查找变量的时候按照作用域链的顺序逐层向外查找
```javascript
// 执行到foo
fooContext = {
AO: ...,
Scope: [fooContext.AO, foo.[[scope]]]
}
// 执行到bar
barContext = {
AO: ...,
Scope: [barContext.AO, bar.[[scope]]]
}
7) this指向
属性:this是执行上下文中的一个属性,在函数被调用进去执行上下文时确定,在上下文运行代码期间不会改变
指向:this由激活上下文代码的调用者提供,即this指向函数的调用上下文(调用这个函数的父上下文)
在全局环境中,严格模式下this为undefined,非严格模式下为全局环境对象
this的指向有如下4个结论:
- 当作为对象被调用时,this指向对象
obj.b()
// this指向obj - 当作为函数被调用时,this指向全局环境
vat b = obj.b; b()
// this指向window、global - 当使用new调用时,this指向当前对象的实例
var b = new obj.b()
// this指向b,new调用了内部的[[call]]
- 当使用call,bind调用时,this指向绑定的对象
c = {}; obj.b.call(c)
// this指向c
箭头函数的this:箭头函数没有this,所以箭头函数中的this直接在作用域链中查找,直到全局作用域var obj = {
a: 1,
b: function(){
console.log(this);
}
}
参考:深入解析this
JavaScript的this原理—阮一峰6. 闭包
定义:函数和它周围环境(Lexical Enviroment词法环境)的引用绑定在一起,这样的组合叫做闭包;简而言之,闭包指那些能访问自由变量的函数。
自由变量:函数中既不是形参,又不是局部变量的对象
作用:闭包可以让内层的函数访问到外层函数的作用域
理论上:所有的js函数都是闭包的,因为函数(创建的时候就)包含了上层的上下文数据(应该就是作用域)
实际上:闭包函数是指那些:
- 即使创建函数的上下文已经被销毁,函数依然存在(函数作为变量被返回)
- 代码中包含自由变量的函数
用途:
- 缓存数据(使函数有状态)
```javascript
function createCache() {
const data = {};
return {
}; }set(key, value) {
data[key] = value;
},
get(key) {
return data[key];
}
const c = createCache();
c.set(‘a’, 12);
console.log(c.get('a') :>>
, c.get(‘a’));
2. 提供私有变量的公共访问方法
```javascript
function Hello(name) {
this.getName = function () {
return name;
}
}
const h = new Hello('Jay');
console.log('h.getName() :>> ', h.getName());
闭包和IIFE没啥关系,因为IIFE并没有访问外部的变量
let arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = (function (k) {
return function () {
return k;
}
})(i);
}
console.log('arr[0]() :>> ', arr[0]());
但是闭包可以配个IIFE做一些封装,防止污染,保证内部内部变量的安全
const Person = (function () {
let name = "Jay";
return {
getName() {
return name;
}
}
})();
console.log('Person.getName() :>> ', Person.getName());
console.log('Person.name :>> ', Person.name);
缺点:导致内存消耗增加,因为闭包导致已经执行完成的执行上下文没有被销毁
7. call,apply,bind实现
1) call的实现
实现call有两个目标:
改变this指向
执行函数
/**
* 在Funtcion.prototype上添加myCall
* 假设需要将this指向obj,就将函数变成obj的一个属性fn,myCall中this就是我们要执行的函数
* 再执行obj.fn
* 最后删除这个属性即可
*/
Function.prototype.myCall = function (context) {
// this即要执行的函数
if (typeof this !== 'function') {
throw new Error('Type Error');
}
let res;
const args = [...arguments].slice(1);
context = context || window;
const fn = Symbol('fn');
context[fn] = this;
res = context[fn](...args);
delete context[fn];
return res;
}
2) apply的实现
在call实现的基础上,apply支持类数组对象,需要使用
Apply.from
转换Function.prototype.myApply = function (context) {
// this即要执行的函数
if (typeof this !== 'function') {
throw new Error('Type Error');
}
let res;
let args = [...arguments].slice(1)[0];
if (!args) {
args = [];
}
args = Array.from(args);
context = context || window;
const fn = Symbol('fn');
context[fn] = this;
res = context[fn](...args);
delete context[fn];
return res;
}
3) bind的实现
bind返回一个函数,显示的绑定this为obj,它有如下特性永久绑定,原始函数
fn
绑定完的函数bindFn
指向永远指向bind的对象obj- 绑定时支持传递参数,也就是说原始函数被柯里化了
- 当作普通函数调用的时候,this指向绑定的对象
- 当作构造函数调用的时候,this指向创建出的实例
实现需要注意的点:
- 首先判断this是不是函数类型
- 可以使用
new.target
或this instanceof bindFn
判断是否是构造函数调用 - 可以使用
Array.from
将类数组对象转换成数组 - 无论是构造函数调用还是普通函数调用,绑定函数
bindFn
的原型都继承自原始函数的原型fn
为了防止修改绑定函数
bindFn
的原型影响原始函数的原型fn
,可以使用一个中继函数tempFn
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new Error('Type Error');
}
const fn = this;
// 支持柯里化
const args = Array.prototype.slice.call(arguments, 1);
const bindFn = function () {
const bindObj = new.target ? this : context;
return fn.apply(bindObj, args.concat(Array.from(arguments)));
}
// 中间函数
const tempFn = function () { };
tempFn.prototype = fn.prototype;
bindFn.prototype = new tempFn();
return bindFn;
}
参考:js 手动实现bind方法,超详细思路分析!
JavaScript深入之bind的模拟实现8. new的实现
new的实现思路也是我们使用
new
语法的时候引擎帮我们做了什么创建一个空对象obj
- 对象obj的原型对象指向构造函数的原型
- 调用构造函数并把this指向对象obj
判断函数的返回值,如果是对象则返回这个对象,否则返回创建的对象obj
function myNew(fn) {
const newObj = Object.create(fn.prototype);
const res = fn.apply(newObj, [...arguments].slice(1));
return res instanceof Object ? res : newObj;
}
9. 异步
1) 从事件循环的角度看异步代码如何运行:
在调用栈中:首先js引擎在执行上下文栈中Stack运行同步代码,遇到浏览器提供的相关API(WebAPI)就交给浏览器创建相应的进程运行(如计时器进程,网络请求进程),同时交给浏览器的还有回调函数callback。执行栈运行同步代码直到栈空,等在回调队列的回调函数压入栈中执行
- 在WebAPI中,浏览器创建相应的进程运行如计时器进程,网络请求进程;一旦这些任务执行完成,就将相应的回调函数放入回调队列中,等待放入调用栈中执行。
对于scroll,resize等这些事件;他们的回调会放在另一个任务队列中,优先级稍微高于普通的任务 列, 但又不是总是优先执行。这里我们可以得到下面的结论:
- 任务队列可能有一个或多个
- 鼠标,键盘事件
- 其他的任务
- 在调用栈为空时会执行微任务,而后优先判断是否需要渲染,渲染时机是由屏幕刷新率,页面性能等决定;一般每16.6ms重绘一次页面,保证页面60帧/s。当然也有可能两个宏任务之间跳过渲染,跳过渲染的条件:
- 浏览器判断更新渲染不会带来视觉上的变化
- 帧动画回调为空,也就是没有
requestAnimationFrame
的回调(map of animation frame callback
为空)
总结而言就是:
- 清空调用栈,即同步代码执行完毕
- 执行微任务
- 尝试渲染DOM:都是DOM重新渲染的机会
- 触发事件循环:从回调队列执行下一个回调函数,触发下一轮事件循环
2) 宏任务和微任务分类
宏任务:setTimeout,setInterval,DOM事件
微任务:Promise,async/await,MutationObserver
问题:为什么微任务在宏任务之前执行?
答:
- 微任务是由ES语法规定,微任务被压入微任务队列
- 宏任务是由浏览器规定,宏任务被压入宏任务队列
- 微任务在事件循环之前进行,宏任务在事件循环后进行
参考:深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调
10. Promise
1) Promise的三种状态:
- pending:进行中
- resolved,settled:已完成,一旦状态确定就无法改变
- then是状态改变的回调函数,包含可选的resolve,reject两个回调函数
promise.then(resolve, rejection);
; -
3) Promise.prototype.catch
catch是
promise.then(null, rejection)
,promise.then(undefined, rejection)
的另一种写法。catch可以捕获错误- catch也返回一个新的Promise实例,所以可以链式调用
- promise的错误可以一直向后传递知道被捕获
promise.then().then().catch()
。但是promise的错误并不会影响Promise外部的代码,会被吃掉(程序可以继续运行) ```javascript // 打印hi并报错,因为Error不会中止代码 const p1 = new Promise((resolve, reject) => { resolve(x + 1); });
p1
.then((result) => console.log(carry on
))
console.log(‘hi’);
4. promise的同步函数的Error是可以被rejection捕获到的
```javascript
// oh, error, ReferenceError: x is not defined
const p1 = new Promise((resolve, reject) => {
resolve(x + 1);
});
p1
.then((result) => console.log(`carry on`))
.catch((error) => console.log(`oh, error, ${error}`));
4) Promise.prototype.finally
- finally不接受任何参数,不关心前面的Promise的状态是resolve还是reject都会执行
- finally总会返回原来的值,但是是新的Promise对象 ```javascript const p1 = Promise.resolve(2) // 返回值是2,状态是resloved的Promise实例 const p2 = p1.finally(); console.log(p1 === p2) // false
const p3 = Promise.reject(4) // 返回值是4,状态是rejected的Promise实例 const p4 = p1.finally(); console.log(p3 === p4) // false
3. 相当于一个then的特例
```javascript
Promise.prototype.myFinally = function (callback) {
const pCtor = this.constructor;
return this.then(
(value) => pCtor.resolve(callback()).then(() => value),
(error) => pCtor.resolve(callback()).then(() => { throw error })
);
}
// 验证
const p2 = p1.myFinally(() => console.log('myFinally'));
console.log('p2 :>> ', p2);
const p3 = p1.finally(() => console.log('finally'));
console.log('p3 :>> ', p3);
// p2 :>> Promise {<pending>}[[Prototype]]: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: 1
// p3 :>> Promise {<pending>}[[Prototype]]: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: 1
// myFinally
// finally
5) Promise.all()
Promise.all
将多个Promise实例包装成一个Promise对象;
接受一个数组(或iterable对象),数组的成员必须是Promise实例
- 如果所有的Promise对象状态变成
fulfilled
,Promise.all的状态才变成fulfilled
,then
的返回值是所有promise返回值组成的数组 - 如果有一个Promise对象的状态变成
reject
。Promise.all的状态变成reject
,catch
的返回值是第一个变成reject的值// err :>> 2
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
const p2 = new Promise((resolve, reject) => {
reject(2);
});
const p3 = new Promise((resolve, reject) => {
resolve(3);
});
const p4 = new Promise((resolve, reject) => {
reject(4);
});
Promise.all([p1, p2, p3, p4])
.then(result => console.log('result :>> ', result))
.catch(err => console.log('err :>> ', err));
6) Promise.race()
Promise.race将多个Promise实例包装成一个Promise对象,并返回率先改变状态的promise对象,无论是fulfilled
还是rejected
// err :>> 2
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => reject(2), 500);
});
Promise.race([p1, p2])
.then(result => console.log('result :>> ', result))
.catch(err => console.log('err :>> ', err));
7) Promise.allSettled()
Promise.allSettled
将多个Promise实例包装成一个Promise对象,等到所有的实例改变状态后返回一个数组(无论是resolved
还是rejected
),数组包含所有改变状态后的对象({status: 'fulfilled', value: 1}
),用来确定一组异步操作是否都结束了/*
result :>> (2) [{…}, {…}]
0: {status: 'fulfilled', value: 1}
1: {status: 'rejected', reason: 2}
length: 2
[[Prototype]]: Array(0)
*/
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => reject(2), 500);
});
Promise.allSettled([p1, p2])
.then(result => console.log('result :>> ', result))
8) Promise.any()
Promise.any
的状态有两种可能
- 只要有一个Promise实例的状态变为
fulfilled
,则then
返回值是这个实例的值 如果所有的Promise实例状态都是
rejected
,则返回catch
返回值是AggregateError
/*
res1 :>> 1
res2 :>> AggregateError: All promises were rejected
*/
const p0 = Promise.resolve(0);
const p1 = Promise.resolve(1);
const p2 = Promise.reject(2);
const p3 = Promise.reject(3);
const p4 = Promise.reject(4);
Promise.any([p1, p2, p3, p4]).then((res1) => {
console.log('res1 :>> ', res1);
});
Promise.any([p2, p3]).catch((res2) => {
console.log('res2 :>> ', res2);
});
9) Promise.resolve()
Promise.resolve
将现有对象转换成Promise对象,状态为resolved
,现有对象有如下的情况:普通对象或空:转换为Promise对象,值为这个普通对象或undefined
// p0 p1同理
const p0 = Promise.resolve(0);
const p1 = new Promise((resolve, reject) => resolve(0));
Promise对象,原封不动的返回
const p0 = Promise.resolve();
const p1 = Promise.resolve(p0);
console.log('p0 === p1 :>> ', p0 === p1); // true
thenable对象(含then方法的对象),抓换为Promise对象并立即执行then
// res :>> 2
const p0 = {
then(resolve) {
resolve(2)
}
};
const p1 = Promise.resolve(p0);
p1.then((res) => console.log('res :>> ', res));
10) Promise.reject()
Promise.reject
同理,Promise状态为rejected
11) Promise.try() 浏览器还没实现
Promise.try
参数是一个函数fn,如果fn是同步函数就以同步的方式运行,如果是异步就以异步的方式运行11.Promise实现
在constructor中执行执行器executor
- executor传入resolve和reject方法,resolve和reject使用箭头函数
- 状态status和返回值value,reason在resolve和reject的改变
- then根据不同的状态执行不同的回调函数
- 考虑resolve不是立即执行的情况,在then里面状态为pending的时候缓存回调函数,在resolve里面如果有缓存函数则执行
- 考虑同一个Promise实例多次调用then;缓存回调函数使用数组
- 考虑链式调用,在then中返回新的Promise对象;
- 判断返回值类型;如果是promise对象立即调用它的then,使它的状态改变;否则直接调用resolve
- 判断返回值是否和then返回的promise对象是否相同;如果相同报错;这里需要使用
queueMicrotask
把回调函数放入微任务队列执行 - 考虑在执行器
executor
和then中捕获错误;在执行executor,onFulfilled和onReject的时候使用try catch
捕获 - 考虑使用then的两个回调函数都是可选的,并且是可传递的;如果不存在使用默认函数代替
- 考虑实现静态函数
Promise.resolve
和Promise.reject
- 考虑thenable对象的情况 ```javascript const PENDING = ‘pending’; const FULFILLED = ‘fulfilled’; const REJECTED = ‘rejected’;
class MyPromise { constructor(executor) { /**
* 执行器,创建Promise对象的时候马上执行
* 执行的时候传入resolve和reject函数
* reject可以捕获执行器中的错误
*/
try {
executor(this.resolve, this.reject);
} catch (e) {
this.reject(e);
}
}
value = null;
reason = null;
status = PENDING;
/** 为了支持多次调用,暂存的变量改成数组 */
onFulfilled = [];
onRejected = [];
/**
* 使用箭头函数是因为如果是普通函数,resolve、reject执行的时候,this指向全局
* 箭头函数保证了this指向promise对象的实例
* 只有在pending的状态下才能改变状态
*/
resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 如果有成功的回调就执行
while (this.onFulfilled.length) {
this.onFulfilled.shift()(value);
}
}
};
reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 如果有失败的回调就执行
while (this.onRejected.length) {
this.onRejected.shift()(reason);
}
}
};
/** 支持resolve,reject静态调用 */
static resolve(parameter) {
if (parameter instanceof MyPromise) {
return parameter;
}
return new MyPromise((resolve) => {
resolve(parameter)
});
}
static reject(parameter) {
if (parameter instanceof MyPromise) {
return parameter;
}
return new MyPromise((resolve, reject) => {
reject(parameter)
});
}
then(onFulfilled, onRejected) {
/** 改造两个回调函数为可选参数 */
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
onRejected = typeof onRejected === 'function' ? onRejected : (reason) => { throw reason };
/** 为了支持链式调用,返回一个新的Promise对象 */
const promise = new MyPromise((resolve, reject) => {
const fulfillMicroTask = () => {
/** 创建一个微任务等待返回的promise创建完成后判断是否和返回值x相同 */
queueMicrotask(() => {
/** 支持在then中捕获错误 */
try {
/** 获取成功回调的返回值 */
const x = onFulfilled(this.value);
/** 针对x不同的类型集中处理resolve */
this.resolvePromise(promise, x, resolve, reject);
} catch (e) {
reject(e);
}
})
}
const rejectedMicroTask = () => {
/**
* 改造成和fulfill相同的结构
* 1. 增加异步状态下的链式调用
* 2. 增加返回值类型的判断
* 3. 增加Promise返回自己的错误处理
* 4. 增加错误捕获
*/
queueMicrotask(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}
if (this.status === FULFILLED) {
fulfillMicroTask();
} else if (this.status === REJECTED) {
rejectedMicroTask();
} else if (this.status === PENDING) {
/**
* 成功和失败的回调,不一定里面执行。需要先暂存起来
* 完成和上面相同的改造
*/
this.onFulfilled.push(fulfillMicroTask);
this.onRejected.push(rejectedMicroTask);
}
});
return promise;
}
resolvePromise(promise, x, resolve, reject) {
if (x === promise) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
}
if (x instanceof MyPromise) {
/** 如果返回值是Promise对象,立即执行then,使Promise改变状态为fulfilled或rejected */
x.then(resolve, reject);
} else {
/** 否则直接将状态变为fulfilled状态 */
resolve(x);
}
}
}
参考:[从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节](https://juejin.cn/post/6945319439772434469#heading-28)
<a name="IrxQt"></a>
#### 12. Promise.all实现
```javascript
Promise.myAll = function (promises) {
return new Promise((resolve, reject) => {
if (typeof promises[Symbol.iterator] !== 'function') {
reject('Type Error');
} else if (promises.length === 0) {
resolve([]);
} else {
const res = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then((value) => {
res.push(value);
count++;
if (count === promises.length) {
resolve(res);
}
}).catch((reason) => {
reject(reason);
});
}
}
});
}
13. async/await 和 Promise的特点和区别
async/await
只能在函数中使用,返回Promise
对象Promise.all
所有异步操作同步执行;async/await
异步操作顺序执行async/await
使用try catch
捕获异常,Promise
使用catch
捕获异常async/await
有些情况下替代Promise
解决回调地狱14. 垃圾回收
标记清除:从root节点开始递归的清除所有的子对象,如果对象可达则将其标记。垃圾回收器会回收没有被标记的对象的内存
引用计数:如果对象被引用,则计数+1;如果该引用的变量被覆盖,则计数-1。如果计数为0则说明对象不可达,对象内存会被立即回收
参考:「硬核JS」你真的了解垃圾回收机制吗15. EventEmit实现
事件机制是订阅发布模式的实现;使用hashMap存储事件名和对应的回调函数;key是事件名,value是回调函数数组
emit(type, ...args)
触发事件回调on(type, fn)
注册事件once(type, fn)
单次注册事件;可以包装一个函数,触发时off
off(type, fn)
取消注册事件removeAllListeners
取消注册所有事件class MyEventEmitter {
constructor() {
this.events = {};
}
emit(type, ...args) {
if (type in this.events) {
const fns = this.events[type];
fns.forEach((fn) => {
fn.apply(this, args);
})
}
return this;
}
on(type, fn) {
if (!(type in this.events)) {
this.events[type] = [];
}
this.events[type].push(fn);
return this;
}
once(type, fn) {
const execFn = (...args) => {
fn.apply(this, args);
this.off(type, execFn);
}
this.on(type, execFn);
return this;
}
off(type, fn) {
if (type in this.events) {
const i = this.events[type].indexOf(fn);
if (i >= 0) {
this.events[type].splice(i, 1);
}
}
return this;
}
removeAllListeners(type) {
if (type in this.events) {
this.events[type] = null;
}
return this;
}
}
16.ArrayBuffer,TypedArray
ArrayBuffer:二进制对象,是对固定长度连续内存空间的引用。单位是字节
TypedArray:是ArrayBuffer类型化的视图,有Uint8Array, Int8Array, Float32Array, Float64Array
等
DataViwe:是未类型化的视图,可以在创建后使用方法类型化.getUi8nt(i)
TextDecoder, TextEncoder:二进制数据和字符串之间编解码
Blob:具有类型的二进制数组,blob = type + blobParts
; blobParts可以是blob, BufferSource, String
类型的数组
使用blob.arrayBuffer
转化为数组
File:继承自blob17. DOM,BOM
1) addEventListenerf的第三个参数
第三个参数是options可选参数,包含以下值:
capture:是否切换为捕获模式
- once:是否触发一次后立马移除
- passive:是否永远忽略回调函数中的
preventDefault
方法 signal:当传入的对象的
abort
方法被调用的时候移除监听18. Object的所有方法
Object.assign(target, source)
,将source中的可枚举属性复制覆盖到target中,不包含原型上的值。并返回target ```javascript function Parent() { this.pa = 1; }; Parent.prototype.pb = 2; const source = new Parent();
const target = { a: 1, b: 2 }; Object.assign(target, source); console.log(‘target :>> ‘, target); // 原型上没有pb
![image.png](https://cdn.nlark.com/yuque/0/2022/png/23168078/1649984093625-6c85ff30-cd80-4e52-91e2-b830e5b12dd5.png#clientId=u67a87f5e-129b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=143&id=ue996158f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=143&originWidth=280&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5157&status=done&style=none&taskId=u21b980ad-e719-496a-810a-e02ad3e53c0&title=&width=280)
2. `Object.create(obj, propertyOptions)`,返回一个对象,以obj为原型,可以在propertyOptions中像`defineProperty`一样添加属性
```javascript
const Parent = {
a: 1,
b: 2,
c() {
return this.a;
}
};
const p = Object.create(Parent, {
d: {
value: 4, // 默认undefined
writable: true, // 默认 false
configurable: true, // 默认false
enumerable: true // 默认false
// get 默认undefined
// set 默认undefined
}
});
console.log(p.__proto__ === Parent); // true
console.log(p.d); // 4
Object.defineProperty(obj, prop, options), Object.defineProperties(obj, propretyOptions)
前者针对单一属性, 后者批量更改属性 ```javascript const Parent = { a: 1, b: 2 }; const p = Object.defineProperties(Parent, { a: { value: 0 }, d: { value: 4, enumerable: true } }); console.log(Parent);
const p2 = Object.defineProperty(Parent, ‘c’, { value: 3, enumerable: true }) console.log(Parent);
3. `Object.entries`返回**自身对象**键值对的**二维数组**。
```javascript
const parent = {
a: 1
}
const child = Object.create(parent);
child.b = 2;
console.log(Object.entries(child)); // [ [ 'b', 2 ] ]
Object.freeze
冻结一个对象并返回它,不能增加,修改,删除对象自身,原型上的属性。也不能修改它的访问器属性。严格模式下会抛出错误 ```javascript ‘use strict’ const parent = { a: 1 }
const child = Object.create(parent); child.b = 2; Object.freeze(child); child.a = 2 // throw error
console.log(‘child.a :>> ‘, child.a);
5. `Object.fromEntries`将类Entreis的二维数组转化为对象,`entries`的逆向工程
```javascript
const child = Object.fromEntries([
['a', 1],
['b', 2],
]);
console.log('child :>> ', child); // child :>> { a: 1, b: 2 }
Object.getOwnPropertyDescriptor(obj, key)
返回对象自身某个属性的描述符
Object.getOwnPropertyDescriptors(obj)
返回对象自身所有属性的描述符
const Parent = {
a: 1
}
const child = Object.create(Parent);
child.b = 2
const descriptorA = Object.getOwnPropertyDescriptor(child, 'a');
const descriptorB = Object.getOwnPropertyDescriptor(child, 'b');
console.log(descriptorA); // undefined
console.log(descriptorB); // { value: 2, writable: true, enumerable: true, configurable: true }
console.log(Object.getOwnPropertyDescriptors(child));
//{
// b: { value: 2, writable: true, enumerable: true, configurable: true }
//}
Object.getOwnPropertyNames
返回对象自身普通属性的数组,包括不可枚举属性但是不包括Symbol属性 ```javascript const Parent = { a: 1 };
const child = Object.create(Parent, { b: { value: 2, enumerable: false } }); child.c = 3; child[Symbol(‘d’)] = 4;
console.log(Object.getOwnPropertyNames(child)); // [‘b’, ‘c’]
8. `Object.getOwnPropertySymbols`返回**对象自身Symbol属性**的数组,**包含不可枚举属性**但是不包括普通属性
```javascript
const Parent = {
[Symbol('a')]: 1
};
const child = Object.create(Parent, {
[Symbol('b')]: {
value: 2,
enumerable: false
}
});
child[Symbol('c')] = 3;
console.log(Object.getOwnPropertySymbols(child)); // [Symbol(b), Symbol(c)]
Object.getPrototypeOf
返回对象的原型 ```javascript const Parent = { a: 1 }
const child = Object.create(Parent);
console.log(Object.getPrototypeOf(child) === Parent); // true
10. `Object.prototype.hasOwnProperty`判断是否是**对象自身属性**的key
```javascript
const Parent = {
a: 1
}
const child = Object.create(Parent);
child.b = 2;
console.log(child.hasOwnProperty('a')); // false
console.log(child.hasOwnProperty('b')); // true
Object.is
判断两个值是否相同。其中NaN
相同,+0, -0
相同Object.isForzen
判断对象是否被冻结console.log(Object.isFrozen(Object.freeze({}))) // true
Object.prototype.isPrototypeOf
判断一个对象是否是另一个对象原型链上的原型const Parent = {
a: 1
};
const child = Object.create(Parent);
console.log(Parent.isPrototypeOf(child)); // true
console.log(child.isPrototypeOf(child)); // false
Object.seal
密封一个对象,可以改已有属性;该对象不可拓展新的属性,不可删除已有属性
Object.isSealed
判断一个对象是否是密封的对象
const Parent = {
a: 1
};
const child = Object.create(Parent);
child.b = 2;
Object.seal(child);
child.c = 3;
child.b = 4;
delete child.b;
console.log('child :>> ', child); // { b: 4 }
Object.preventExtensions
使一个对象不可拓展,可以改已有属性;但是可以删除已有属性 ```javascript const Parent = { a: 1 }; const child = Object.create(Parent); child.b = 2;
Object.preventExtensions(child); child.c = 3; delete child.b
console.log(‘child :>> ‘, child); // {}
16. `Object.keys`返回自身**可枚举属性**返回的数组
16. `Object.prototype.propertyIsEnumerable`判断对象**自身属性**是否是可枚举的
```javascript
const Parent = {
a: 1
};
const child = Object.create(Parent, {
d: {
value: 4,
enumerable: false
}
});
child.b = 2;
console.log(child.propertyIsEnumerable('a')); // false
console.log(child.propertyIsEnumerable('d')); // false
Object.setPrototypeOf(obj, prototype)
设定一个对象的原型,会有很大的性能问题Object.prototype.toLocaleString
返回一个对象在不同语言环境下的字符串表示 ```javascript const Parent = { a: 1 }; const child = Object.create(Parent, { d: { value: 4, enumerable: false } }); child.b = 2;
console.log(child.toLocaleString()); // 默认的[object Object]
<a name="vWX3k"></a>
### 二. Web存储
<a name="UcS5e"></a>
#### 1. cookie
**定义**:cookie是服务器发送给用户浏览器的一小片段数据;浏览器会自动将cookie存储在本地;浏览器会在下次发送请求到同一服务器的时候自动携带上它。一般用于确认两个请求是否来自于同一浏览器。<br />**使用场景**:
1. 会话状态管理(用户登录状态,购物车等)
1. 个性化设置(用户自定义设置,主题等)
1. 浏览器行为追踪(跟踪分析用户行为)
**特点**:
1. 大小限制为4kb
1. 只支持ASCII,其他字符要转码
**cookie参数**:服务器使用`Set-Cookie`设置响应头,响应头中`Set-Cookie`可以有多个<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/23168078/1647957611478-c798ba72-6209-46e1-b157-94f3f534b020.png#clientId=uf61c2c84-b568-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=327&id=u63c85fb0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=327&originWidth=501&originalType=binary&ratio=1&rotation=0&showTitle=false&size=25379&status=done&style=none&taskId=u6bc4a32b-95c7-4552-8fa0-0b5efc7587d&title=&width=501)<br />cookie当中可选参数的说明:
1. `httpOnly:true`是否只允许http访问,如果为true则js脚本的`document.cookie`无法访问该数据
![image.png](https://cdn.nlark.com/yuque/0/2022/png/23168078/1647958131113-ec7dc5e7-2239-41d8-bf78-fd6063f6e011.png#clientId=uf61c2c84-b568-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=45&id=u932eab65&margin=%5Bobject%20Object%5D&name=image.png&originHeight=45&originWidth=469&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5416&status=done&style=none&taskId=u291bdd39-a5cb-4cec-a926-479599fe3ec&title=&width=469)
```javascript
// age=12 无法访问到
const cookie = document.cookie;
console.log('cookie :>> ', cookie); // cookie :>> user=jay
secure
如果服务端设置了cookie为secure
则cookie只能通过加密传输(https)。浏览器端会报500错误并且无法获取cookie
服务端设置secure的效果:
ctx.cookies.set(
'user',
'jay',
{
maxAge: 5000000,
httpOnly: false,
secure: true // 无法通过http传输
}
);
客户端设置sucure的效果:
无法保存到本地且无法传输到服务端
document.cookie = 'user=chou;Secure';
axios.get(`${host}:3000/user`).then((res) => {
console.log(res.data);
const cookie = document.cookie;
console.log('cookie :>> ', cookie); // cookie :>>
});
由于浏览器做了特殊设置,localhost设置secure
效果和https相同;
参考:cookie设置secure属性不生效
SameSite
声明cookie在什么情况下可以携带,有效预防CSRF- strict:cookie只会在请求地址与当前域名相同时才会传输,完全禁止第三方发送请求携带cookie
- Lax:默认值;允许第三方get请求携带cookie
- None:没有限制,请求自动携带
Expires/Max-Age:Session
cookie过期时间,默认会话阶段;如果cookie过期会自动从本地删除(document.cookie
无法访问,下次请求也不会带上);Domain:origin
指定哪些主机可接受cookie,默认origin;如果指定了某个域,则其他域js脚本无法访问该cookie且请求同一主机时不会携带上它Path:/
指定主机下哪些路径可以接受cookie,效果和Domain
类似,是Domain
的补充// localhost可以正常访问修改;上传时会携带
// http://72.26.21.36:3000不可以访问修改,上传时不会携带
ctx.cookies.set(
'user',
'jay',
{
maxAge: 5000000,
httpOnly: false,
domain: 'localhost'
}
);
问题:跨域请求的时候如何携带上cookie?
答:客户端的xhr.withCredentials=true
,跨域的服务端(前提是允许跨域)的响应头需要设置Access-Control-Allow-Credential:true
。withCredentials
只对跨域的请求有效,同源的请求会被忽略2. localStorage和sessionStorage
localStorage
和sessionStorage
都是Web存储技术;使用的方法相同:getItem, setItem, removeItem, clear
localStorage['a']
像数组一样获取某个值value。没有key会返回null
localStorage.key(n)
获取第n位的键key,下标越界会返回null
- 总之storage找不到键值不会报错,而是会返回
null
共同点:
- 大小5M左右
- 操作是同步的
- 键值总是以字符的形式存储
区别:
localStorage的数据永久存储;sessionStorage只在会话期间存储,页面关闭会被删除
三. 跨域
1. 同源策略
同源策略限制不同源的资源之间的交互,如A网站不能请求B网站的资源;同源是指协议 + 域名 + 端口相同。
同源策略可以防止XSS,CSRF(中间人)攻击;
CSRF攻击:用户访问A网站,拿到A网站的cookie;然后访问恶意的B网站,如果没有同源策略B可以获取A网站的cookie,模拟用户携带A的cookie向A发送恶意请求。
同源策略限制访问非同源的:Cookie,localStorage,sessionStorage,indexDB等
- DOM
- ajax请求
不受同源策略限制的:
script
标签,可以使用jsonp跨域link
标签,外联cssimg``video
,audio
标签iframe
中嵌入的资源;总结就是有src、href的标签都可以跨域-
2. 跨域解决方案
1) CORS跨域资源共享
cors允许浏览器向跨源的服务器发送ajax请求;需要服务器添加响应头允许跨域
简单请求的CORS
简单请求意味着浏览器不需要发送预检请求OPTIONS
,只需要服务端响应头添加Access-Control-Allow-Origin
且值包含请求的域
Access-Control-Allow-Origin: * // 值包含origin(请求的域)就可以
简单请求满足以下条件:
- 请求方法只能是
GET
,POST
,HEAD
- 用户只能添加以下请求头:
- Content-Type:限制为
text/plain
,text/x-www-form-urlencoded
,multipart/form-data
- Content-Language:客户端希望采用的语言
- Accept-Language:客户端声明可以理解的自然语言
- Accept:告知服务器可以处理的类型
- Width:不推荐
- Viewport-Width:不推荐
- Save-Data:客户端对减少数据使用量的偏好
- DPR:客户端的像素比
- Downlink:连接服务器的大约带宽
- Content-Type:限制为
- XMLHttpRequestUpload对象没有注册任何事件,请求中没有使用
ReadableStream
对象- 复杂请求的CORS
复杂请求需要发送预检请求OPTIONS
,这就需要服务器支持处理OPTIONS
请求并添加3个必要的响应头
/**
简单请求和复杂请求都必须要有且包含请求的域;
如果设置允许携带Cookie(Access-Control-Allow-Credentials: true)就不允许设置为*
*/
ctx.set('Access-Control-Allow-Origin', ctx.headers.origin);
/**
允许跨域请求的方法,必须包含请求头中的Access-Control-Request-Method方法
*/
ctx.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
/**
允许跨域请求头自定义设置的header,必须包含Access-Control-Request-Headers中的字段
*/
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Accept');
/**
可选的是否允许携带Cookie,客户端请求需要设置xhr.withCredentials=true;
Access-Control-Allow-Origin不能为*
*/
ctx.set('Access-Control-Allow-Credentials', true);
/**
可选的OPTIONS缓存的生命周期,生命周期以内再次跨域请求不需要发送预检请求
默认位5s,-1代表禁止缓存OPTIONS
*/
ctx.set('Access-Control-Max-Age', 600);
2) JSONP
JSONP利用script
标签不受同源策略限制的特性;script
标签外链的脚本执行定义的回调函数,同时传递后端处理完的参数。
- 参数一般放在
query
里面,包含传递的参数,回调函数名称 - 仅支持GET请求
- 兼容性好
前端实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jsonp</title>
</head>
<body>
<script>
function jsonpCallback(who, res) {
console.log(who + res);
}
</script>
<script src="http://172.26.21.36:5000/sayhello?msg=jay&cb=jsonpCallback"></script>
</body>
</html>
后端实现
const Koa = require('koa');
const Router = require('@koa/router');
const app = new Koa();
const router = new Router();
router.get('/sayhello', (ctx, next) => {
const { cb, msg } = ctx.query;
ctx.body = `${cb}('teacher:' , '${msg}')`
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(5000, () => {
console.log('C server start at port 5000');
})
3) 代理
服务端通信没有同源策略的显示,可以在客户端和服务端之间添加一个和客户端同源的代理服务器;代理服务请请求到资源后转发给客户端。
前端开发过程中普遍采用http-proxy-middleware
库在开发过程中代理接口;如vue,webpack都有相关的快捷设置
还可以使用nginx等反向代理
- webpack中的proxy设置:
2, vue中的设置 ```javascript // vue2 config/index.js proxyTable: { ‘/api’: { target: ‘http://localhost:8080‘, } }// devServer.proxy
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
entry: {
index: "./index.js"
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
devServer: {
port: 8000,
proxy: {
"/api": {
target: "http://localhost:8080"
}
}
},
plugins: [new HtmlWebpackPlugin({
filename: "index.html",
template: "webpack.html"
})]
};
// vue3 vue.config.js module.exports = { devServer: { port: 8000, proxy: { “/api”: { target: “http://localhost:8080“ } } }, };
3. 自己实现一个
前端部分
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxy</title>
</head>
<body>
<button id="proxy">通过正向代理发送跨域请求</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const button = document.querySelector('#proxy');
button.addEventListener('click', function () {
// 可以用添加源,直接使用'/login'
axios.get('http://172.26.21.36:5000/login').then((res) => {
// res.data :>> {code: 0, message: '登陆成功'}
console.log('res.data :>> ', res.data);
})
});
</script>
</body>
</html>
后端部分
// proxy http-proxy-middleware koa兼容性不好
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use('/static', express.static('static'));
app.use('/login', createProxyMiddleware({
target: 'http://172.26.21.36:3000',
changeOrigin: true
}));
app.listen(5000, () => {
console.log('proxy server run at port 5000');
});
// login.js
const Koa = require('koa');
const Router = require('@koa/router');
const app = new Koa();
const router = new Router();
/**
* index.html 加载的时候会请求login接口
*/
router.get('/login', (ctx, next) => {
ctx.cookies.set(
'user',
'jay',
{
maxAge: 5000000,
httpOnly: false,
domain: '172.26.21.36',
path: '/static'
}
);
ctx.cookies.set('age', 12);
ctx.body = {
code: 0,
message: '登陆成功'
};
});
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000, () => {
console.log('A server start at port 3000');
})
4) WebSocket
WebSocket没有同源限制,这里刚好学习以下WebSocket
特点:
- WebSocket传输层使用TCP协议
- WebSocket默认端口是80,443;握手阶段采用http服务
- 可以双向发送数据,性能开销小
- 可以发送文本,二进制数据
- 没有同源限制
- 协议标识符ws、wss
ws.readyState有四种状态:
- 正在连接 0:WebSocket.CONNECTING
- 连接成功,可以通信 1:WebSocket.OPEN
- 正在关闭 2:WebSocket.CLOSING
- 已经关闭 3:WebSocket.CLOSED
四个事件:
- open
- message
- close
- error
两个的方法ws.send(data)``ws.close()
后端代码演示:
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 3010 });
server.on('connection', (socket) => {
console.log('有什么东西连进来啦');
socket.send('作为服务器得发点什么')
socket.on('message', (e) => {
console.log('收到了来自客户端的消息 :>> ', e);
})
})
前端代码演示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>websocket</title>
</head>
<body>
<script>
const ws = new WebSocket('ws://172.26.21.36:3010');
ws.onopen = (e) => {
console.log('连接成功');
}
ws.onmessage = (e) => {
console.log('接受到消息:', e);
}
ws.onclose = (e) => {
console.log('连接关闭');
}
ws.onerror = (e) => {
console.log('发生错误');
}
</script>
</body>
</html>
5) postMessage
使用window.postMessage
跨域,一般用于
- 当前页面和嵌套iframe通信
- 当前页面和新window.open的页面通信
- 多窗口页面的通信
发送方postmessage.html,需要接收方window实例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>postmessage</title>
</head>
<body>
<iframe src="http://172.26.21.36:5000/static/onmessage.html" frameborder="0" id="frame" onload="load()"
width="500px" height="500px"></iframe>
<script>
// 这个页面通过http://localhost:3000/static/postmessage.html打开
function load() {
const frame = document.querySelector('#frame');
frame.contentWindow.postMessage(
'通过postMessage发送',
'http://172.26.21.36:5000'
);
console.log('已经发送');
}
</script>
</body>
</html>
接收方onmessage.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>hi</p>
<p id="msg"></p>
<script>
window.addEventListener('message', (e) => {
console.log('onmessage接收到的消息 :>> ', e);
const msg = document.querySelector('#msg');
msg.innerHTML = e.data;
})
</script>
</body>
</html>
6) document.location,window.location.hash,window.name等
四. 手写
1. 防抖
定义:防抖就是让一个事件在触发n秒后才执行;如果事件在n秒内再次触发,就以最新触发的时间为准,n秒后执行;
防抖的应用:
- 防止resize,scroll事件频繁触发
- 防止input输入框动态搜索input事件频繁触发
- 按钮提交事件,只触发最后一次
实现的思路:
- 基础的:首先需要返回一个函数d,函数d内创建一个定时器timer,wait毫秒后执行;这个定时器可以用闭包的特性保存在返回函数d外;由于防抖需要在最后一次触发函数wait时间后执行;所以返回函数d在重新设置定时器timer前需要清除它
- 考虑this指向和传递参数;this的执行就是返回函数d的指向;参数就是返回函数d的arguments;可以用
func.apply
修改this指向和参数 - 考虑添加一个立即执行的immediate参数;是否在第一个触发的时候直接执行函数;添加一个first标志位,如果是第一个触发且立即执行则直接执行func函数;这里可以返回返回值
考虑添加取消防抖的方法;添加一个cancel的方法和canceled的标志位;cancel方法里设置canceled为false并且清除定时器
function debounce(func, wait, immediate) {
let timeout;
let first = true;
let canceled = false;
const d = function (...args) {
if ((first && immediate) || canceled) {
first = false;
return func.apply(this, args);
}
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
}
d.cancel = () => {
clearTimeout(timeout);
canceled = true;
}
return d;
}
2. 节流
定义:函数在触发n秒后执行;如果函数在n秒内再次触发则忽略它直到函数执行;即函数每个一段时间最多只执行一次
应用场景:搜索框input联想
- 鼠标不断点击、移动;规定一段时间内事件只触发一次
实现的思路:
setTimeout
版:如果计时器存在就不执行,如果不存在就设定一个计时器,函数n秒后执行;并把计时器设为null。考虑this的指向和传参问题。考虑增加立即执行immediate和第一次执行first的标志位timestamp
版:记住上一次执行的时间previous;如果触发函数的时候距离上一次执行时间大于wait;则执行函数并更新上一次执行时间previous ```javascript /*停止触发时执行最后一次 / function throttle(func, wait, immediate) { let timeout; let first = true; return function (…args) {
} }if (immediate && first) {
first = false;
return func.apply(this, args);
}
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(this, args);
}, wait);
}
/* 立即执行,停止触发后不执行最后一次 / function throttleTimestamp(func, wait) { let previous = +new Date(); return function (…args) { const now = +new Date(); if (now - previous >= wait) { func.apply(this, args); previous = now; } } }
<a name="ivyKF"></a>
#### 3. 快排
```javascript
function quickSort(array) {
sort(array, 0, array.length - 1);
}
function sort(array, left, right) {
if (left < right) {
const index = partion(array, left, right);
sort(array, left, index - 1);
sort(array, index + 1, right);
}
}
function partion(array, left, right) {
// 以左一为基准
const pivot = array[left];
let i = left;
let j = right;
while (i < j) {
while (i < j && array[j] >= pivot) {
j--;
}
// 不需要交换,直接覆盖最左边的
array[i] = array[j];
while (i < j && array[i] <= pivot) {
i++;
}
array[j] = array[i];
}
// 基准最后落在i位
array[i] = pivot;
return i;
}
4. instanceof
function myInstanceof(target, origin) {
if (typeof target !== 'object' || target === null) {
return false;
}
if (typeof origin !== 'function') {
throw new Error('Type Error');
}
let prototype = Object.getPrototypeOf(target);
if (prototype === origin.prototype) {
return true;
}
return myInstanceof(prototype, origin);
}
5. 数组扁平化
// reduce + 递归
function flatten(array) {
return array.reduce((previous, next) => {
return previous.concat(Array.isArray(next) ? flatten(next) : next);
}, []);
}
6. reduce
如果有初始值,从下标1开始遍历
Array.prototype.myReduce = function (callback, initial) {
const array = this;
if (array.length === 0) {
return initial;
}
const hasInitial = initial == null;
let previous = hasInitial ? array[0] : initial;
let k = hasInitial ? 1 : 0;
for (let i = k; i < array.length; i++) {
previous = callback(previous, array[i], i, array);
}
return previous;
}
7. 数组去重
function unique(array) {
return Array.from(new Set(array));
}
function unique2(array) {
return array.filter((item, index) => array.indexOf(item) === index);
}
8. 带并发限制的Promise异步调度器
问题:最多并发2个
class Scheduler {
add(promiseMaker) {}
}
const timeout = (time) =>
new Promise((resolve) => {
setTimeout(resolve, time);
});
const scheduler = new Scheduler();
const addTask = (time, order) => {
scheduler.add(() => timeout(time).then(() => console.log(order)));
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
// output:2 3 1 4
// 一开始,1,2两个任务进入队列。
// 500ms 时,2完成,输出2,任务3入队。
// 800ms 时,3完成,输出3,任务4入队。
// 1000ms 时,1完成,输出1。
思路:
- add返回一个函数
promiseCreator
;这个函数返回一个Promise对象; promiseCreator
并不是add之后立马执行;而是放在一个队列里面,并保存reslove,reject回调;尝试立马执行- 只有并行数量小于设定时才立马取出队头的
promiseCreator
执行;否则等一个任务执行完成后再尝试执行
参考:实现一个带并发限制的异步调度器,保证同时运行的任务最多有两个class Scheduler {
maxCount = 2;
runningCount = 0;
promises = [];
constructor(maxCount) {
if (typeof maxCount === 'number') {
this.maxCount = maxCount;
}
}
add(promiseCreator) {
return new Promise((resolve, reject) => {
promiseCreator.resolve = resolve;
promiseCreator.reject = reject;
this.promises.push(promiseCreator);
this.run();
})
}
run() {
if (this.runningCount < this.maxCount && this.promises.length) {
this.runningcount++;
const promise = this.promises.shift();
promise().then((value) => {
promise.resolve();
}).catch((error) => {
promise.reject(error);
}).finally(() => {
this.runningcount--;
this.run();
})
}
}
}