一. JS基础
1. 数据类型
总共8种数据类型,7种基础的数据类型String,Number,Boolean,Null,Undefined,Symbol,BigInt,一个对象类型Object
Symbol可以定义对象的唯一属性名BigInt可以表示很大的数
1) typeof判断数据类型
typeof可以判断基本的数据类型,可以判断function,无法判断Null,Object和Array,都为object类型
typeof 'a' // stringtypeof 1 // numbertypeof true // booleantypeof undefined // undefinedtypeof Symbol() // symboltypeof 12n // biginttypeof function(){} // function/** 无法判断 */typeof null // objecttypeof {} // objecttypeof [] // object
2) instanceof判断对象类型
instanceof可以判断对象类型,原理是判断对象的原型链是否有该类型的原型
class People {}class Jay extends People {}const jayChou = new Jay();/** 因为实例jayChou顺着原型链可以找到Jay.prototype和People.prototype */jayChou instanceof People // truejayChou 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 NullObject.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 // trueObject.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 />缺点:无法继承父类的原型```javascriptfunction 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. 声明在不可达的区域不能提升```javascriptif (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对象```javascriptAO = {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// 执行到foofooContext = {AO: ...,Scope: [fooContext.AO, foo.[[scope]]]}// 执行到barbarContext = {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. 提供私有变量的公共访问方法```javascriptfunction 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,可以使用一个中继函数tempFnFunction.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 definedconst 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的特例```javascriptPromise.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 :>> 2const 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 :>> 2const 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 :>> 1res2 :>> 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 :>> 2const p0 = {then(resolve) {resolve(2)}};const p1 = Promise.resolve(p0);p1.then((res) => console.log('res :>> ', res));
10) Promise.reject()
Promise.reject同理,Promise状态为rejected11) 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实现```javascriptPromise.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)单次注册事件;可以包装一个函数,触发时offoff(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
2. `Object.create(obj, propertyOptions)`,返回一个对象,以obj为原型,可以在propertyOptions中像`defineProperty`一样添加属性```javascriptconst Parent = {a: 1,b: 2,c() {return this.a;}};const p = Object.create(Parent, {d: {value: 4, // 默认undefinedwritable: true, // 默认 falseconfigurable: true, // 默认falseenumerable: true // 默认false// get 默认undefined// set 默认undefined}});console.log(p.__proto__ === Parent); // trueconsole.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`返回**自身对象**键值对的**二维数组**。```javascriptconst 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`的逆向工程```javascriptconst 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 = 2const descriptorA = Object.getOwnPropertyDescriptor(child, 'a');const descriptorB = Object.getOwnPropertyDescriptor(child, 'b');console.log(descriptorA); // undefinedconsole.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属性**的数组,**包含不可枚举属性**但是不包括普通属性```javascriptconst 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```javascriptconst Parent = {a: 1}const child = Object.create(Parent);child.b = 2;console.log(child.hasOwnProperty('a')); // falseconsole.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)); // trueconsole.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`判断对象**自身属性**是否是可枚举的```javascriptconst Parent = {a: 1};const child = Object.create(Parent, {d: {value: 4,enumerable: false}});child.b = 2;console.log(child.propertyIsEnumerable('a')); // falseconsole.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. 大小限制为4kb1. 只支持ASCII,其他字符要转码**cookie参数**:服务器使用`Set-Cookie`设置响应头,响应头中`Set-Cookie`可以有多个<br /><br />cookie当中可选参数的说明:1. `httpOnly:true`是否只允许http访问,如果为true则js脚本的`document.cookie`无法访问该数据```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:Sessioncookie过期时间,默认会话阶段;如果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会返回nulllocalStorage.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.proxyconst 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.jsconst 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. 快排```javascriptfunction 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();})}}}
