JavaScript
TIP
一个支持IE9以上及webkit内核浏览器中用于记录页面加载和解析过程中关键时间点的机制。
一、缓存淘汰算法
1 FIFO
- FIFO (First Input First Output)可以说是最简单的一种缓存算法
- 通过设置缓存上限,当达到了缓存上限的时候,按照先进先出的策略进行淘汰,再增加进新的 key-value
// 使用了一个对象作为缓存,一个数组配合着记录添加进对象时的顺序
// 判断是否到达上限,若到达上限取数组中的第一个元素key,对应删除对象中的键值
class FifoCache {
constructor(limit) {
this.limit = limit || 10;
this.map = {}; // 保存键值信息
this.keys = []; // 记录key的个数
}
set(key, value) {
// 1 判断keys的长度是否达到限制
if (this.keys.length >= this.limit) {
let i = this.keys.indexOf(key);
if (i >= 0) {
// 如果达到限制判断key是否已经存在,存在将key删除
this.keys.splice(i, 1);
} else {
// 否则默认删除keys的第一个数据与对应map中的值
delete this.map[this.keys.shift()];
}
}
// 2 没达到限制直接追加(由于大于限制的时候会移除最前面的一个,新的都要添加进去)
this.keys.push(key);
this.map[key] = value;
}
get(key) {
return this.map[key];
}
}
// 测试
let fifo = new FifoCache(2);
fifo.set('a', 'a');
fifo.set('b', 'b');
fifo.set('c', 'c');
console.log(fifo.get('b')); // 'b'
console.log(fifo.get('c')); // 'c'
2 LRU
- LRU(Least recently used)算法算是最常见的缓存淘汰算法
- 根据数据的历史访问记录来进行淘汰数据,其核心思想是如果数据最近被访问过,那么将来被访问的几率也更高
// 按照首位淘汰来编码,超过容量就把首位删除,然后追加新的到最后面
var LRUCache = function(capacity) {
this.cache = new Map(); // 缓存
this.capacity = capacity || 10; // 容量
};
// 先判断是否存在,如果存在需要将当前取出的key和value更新到最后
LRUCache.prototype.get = function(key) {
let cache = this.cache;
if (cache.has(key)) {
let temp = cache.get(key);
cache.delete(key);
cache.set(key, temp); // 更新最新的值
return temp;
} else {
return -1;
}
};
// 存放的时候,先判断是否存在,已经存在删除重新加,如果不存在但是超过容量,需要删除首位,然后最近加到末位
LRUCache.prototype.put = function(key, value) {
let cache = this.cache;
if (cache.has(key)) {
// 如果已经存在对应的key,需要删除
cache.delete(key);
} else if (cache.size >= this.capacity) {
// 如果超过容量,需要将第一位的删除
cache.delete(cache.keys().next().value);
}
cache.set(key, value);
};
// 测试
let lru = new LRUCache(2);
lru.put('a', {a: 1});
lru.put('b', {b: 2});
lru.put('c', {c: 3});
console.log(lru);
console.log(lru.get('b'));
console.log(lru);
// LRUCache { cache: Map { 'b' => { b: 2 }, 'c' => { c: 3 } }, capacity: 2 }
// { b: 2 }
// LRUCache { cache: Map { 'c' => { c: 3 }, 'b' => { b: 2 } }, capacity: 2 }
3 LFU
- LFU(Least Frequently Used )也是一种常见的缓存算法
- 当空间满时,通过访问次数,淘汰问次数最小的数据,如果访问次数全部一样则默认淘汰最初添加的数据
var LFUCache = function (capacity) {
this.capacity = capacity;
this.cache = new Map(); // 存放值和频率{value: value,freq: 1}
this.useMap = new Map(); // 记录使用次数
}
LFUCache.prototype.get = function(key) {
if (!this.cache.has(key)) return -1;
// 获取值的时候,如果已经存在,需要删除原来的重新添加使用的次数也需要+1
let use = this.useMap.get(key);
let value = this.cache.get(key);
this.cache.delete(key);
this.useMap.set(key, use + 1);
this.cache.set(key, value);
return value;
}
LFUCache.prototype.put = function(key, value) {
// 边界条件
if (this.capacity === 0) return;
// 获取使用次数的最小值
let min = Math.min(...this.useMap.values());
if (this.cache.has(key)) {
// 如果已经存在,需要重新赋值,次数+1
this.cache.set(key, value);
let use = this.useMap.get(key);
this.useMap.set(key, use + 1);
} else {
this.cache.set(key, value);
this.useMap.set(key, 1);
}
// 如果容量超出了,需要删除使用次数最少的,如果最小次数一样,则按照时间,删除最早添加的
if (this.cache.size > this.capacity) {
let it = this.cache.keys();
let delKey = it.next().value;
while(this.useMap.get(delKey) != min) {
delKey = it.next().value;
}
this.useMap.delete(delKey);
this.cache.delete(delKey);
}
}
// 测试
let lfu = new LFUCache(1);
lfu.put(1, 1);
lfu.put(2, 2);
lfu.get(1); // 返回 1
lfu.put(3, 3); // 去除 key 2
lfu.get(2); // 返回 -1 (未找到key 2)
lfu.get(3); // 返回 3
lfu.put(4, 4); // 去除 key 1
lfu.get(1); // 返回 -1 (未找到 key 1)
lfu.get(3); // 返回 3
lfu.get(4); // 返回 4
二、函数式编程
- 函数式编程是一种编程范式,主要是利用函数把运算过程封装起来,通过组合各种函数来计算结果
1 高阶函数
- 一个函数返回一个函数
- 一个函数的参数可以接受一个函数
// 给核心代码的前面或者后面进行扩展方法,类似AOP编程
// 核心代码
function core(...args) {
// ...
console.log('core', args);
// ....
}
// 给core函数增加一些额外的逻辑 但是不能更改核心代码
Function.prototype.before = function (cb) {
return (...args) => {
cb(); // 执行扩展方法
this(...args); // this = core 执行原有方法
}
}
// 调用方式
let newCore = core.before(() => {
// TODO 处理扩展的逻辑
console.log('before');
});
newCore('a', 'b')
// before
// core [ 'a', 'b' ]
// 上述的核心思想就是 在两个函数执行的外面包一层函数
let newCore = () => {
before();
core();
}
2 纯函数
- 对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不用依赖外部环境的状态
- 例子: 数学公式 y = f(x)
// 纯函数,计算结果不需要依赖外部变量
function sum(a, b) {
return a + b
}
console.log(sum(1, 2)); // 3
let arr = [1, 2, 3, 4, 5];
// 纯函数
console.log(arr.slice(0, 3)); // [ 1, 2, 3 ]
console.log(arr.slice(0, 3)); // [ 1, 2, 3 ]
3 非纯函数
- 非纯函数中,函数的行为需要由外部的系统环境决定,这也输造成系统复杂性大大增加的主要原因
// 非纯函数,sum的计算结果依赖外部变量a,当a发生变化的时候,计算结果也发生了变化
let a = 10;
function sum(b) {
return a + b;
}
console.log(sum(1)); // 11
let arr = [1, 2, 3, 4, 5];
// 非纯函数
console.log(arr.splice(0, 3)); // [ 1, 2, 3 ]
console.log(arr.splice(0, 3)); // [ 4, 5 ]
4 函数科里化
定义:向函数传递一部分参数来调用它,让他返回一个函数去处理剩下的参数
通俗来讲:把多个参数的传入,转化为n个函数来处理,可以有暂存变量的功能
将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数
参数个数确定的函数科里化通用方法
// 记录每次调用时传入的参数,并且和函数的参数个数进行比较,
// 如果不满足总个数 就返回新函数,
// 如果传入的个数和参数一致 执行原来的函数
// 写法一,利用一个数组每次存储传入的参数
function curring(fn) {
// 存储每次调用时传入的参数
let inner = (args = []) => {
return args.length >= fn.length
? fn(...args) // 执行原函数
: (...userArgs) => inner([...args, ...userArgs]); // 递归返回函数,合并参数
}
return inner();
}
// 写法二,每次展开参数传递到下一层
function curring(fn) {
let inner = (...args) => {
return args.length >= fn.length
? fn(...args) // 执行原函数
: (...userArgs) => inner(...args.concat(...userArgs)); // 递归返回函数,合并参数
}
return inner;
}
- 使用案例
// 求和使用
function sum(a, b, c, d) {
return a + b + c + d
}
let sum1 = curring(sum) // 科里化
// 分布调用
let sum2 = sum1(1)
let sum3 = sum2(2,3)
let result = sum3(4);
console.log(result) // 10
// 一行调用
console.log(sum1(1)(2, 3)(4));
// 封装类型判断
function isType(typing, val){
return Object.prototype.toString.call(val) == `[object ${typing}]`;
}
let util = {};
['String','Number','Boolean','Null','Undefined'].forEach(type => {
util['is'+type] = curring(isType)(type)
});
console.log(util.isString('abc'))
- 一道不定参数的面试题
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function sum1 (...args) {
// 初始化参数需要带上
let argsAll = [...args];
let inner = (...args) => {
if (args.length === 0) {
// 判断最后一次传入的参数为0的时候,执行函数计算
return argsAll.reduce((a, b) => a + b);
} else {
argsAll.push(...args);
return inner; // 返回当前函数
}
}
return inner;
}
console.log(sum1(1)(2)(3)()); // 6
console.log(sum1(1, 2, 3)(4)()); // 10
console.log(sum1(1)(2)(3)(4)(5)()); // 15
5 偏函数
- 固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数
function ajax(url, data, callback) {
console.log(url, data, callback);
}
// 原始调用
function ajaxTest1(data, callback) {
ajax('http://www.test.com/test1', data, callback);
}
function ajaxTest2(data, callback) {
ajax('http://www.test.com/test2', data, callback);
}
// 偏函数调用
const partial = (fn, ...args) => {
return (...laterArgs) => {
let allArgs = args.concat(laterArgs);
return fn.apply(this, allArgs);
}
}
var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');
var ajaxTest2 = partial(ajax, 'http://www.test.com/test2');
6 反柯里化
- 扩大函数的适用范围,比如借用方法
// 通用函数
function unCurrying(fn) {
return (tar, ...args) => fn.apply(tar, args);
}
// 借用数组的push方法
const push = unCurrying(Array.prototype.push);
const obj = { a: 'a' }
push(obj, 'b', 'c', 'd')
console.log(obj)
// { '0': 'b', '1': 'c', '2': 'd', a: 'a', length: 3 }
7 函数收敛
- 把数组转化为一个结果集
// reduce 收敛函数的使用
// previousValue, currentValue, index, arr
// 初始值 当前值 索引 原数组
let r = ([1, 2, 3, 4, 5]).reduce(function(previousValue, currentValue, index, arr) {
console.log(previousValue, currentValue)
return previousValue + currentValue
}, 0);
// 第二个参数为初始值,如果没有则默认使用数组中的第一个元素作为初始值
console.log(r); // 15
- reduce实现原理
Array.prototype.reduce = function(callback, prev) {
for (let i = 0; i < this.length; i++) {
if (!prev) {
// 如果prev为空,则初始值为数组第一项
prev = callback(this[i], this[i + 1], i + 1, this);
i++; // 下次从3开始
} else {
prev = callback(prev, this[i], i, this);
}
}
return prev;
}
8 函数组合
- 把层层嵌套的函数调用,拆分为组合式函数调用
function sum(a, b) {
return a + b;
}
function len(str) {
return str.length;
}
function addPrefix(str) {
return '$' + str;
}
// 最开始的调用方式:包菜式代码
let r = addPrefix(len(sum('a','b')));
console.log(r); // $2
- 组合调用-reduceRight
// 从右往左依次执行,需要对第一次进行特殊处理
const compose = (...fns) => {
return function(...args) {
let lastFn = fns.pop(); // 取出最后一个
let lastRes = lastFn(...args); // 最后一个函数执行
// 计算函数
return fns.reduceRight((prev, current) => {
return current(prev);
}, lastRes);
}
}
let final = compose(addPrefix, len, sum);
console.log(final('a', 'b'));
// 简写
const compose = (...fns) => (...args) => {
let lastFn = fns.pop();
return fns.reduceRight((prev, current) => current(prev), lastFn(...args));
}
- 组合调用-reduce
// 第一次 a: addPrefix b:len
// 第二次 a: function(...args) {return addPrefix(len(...args))} b:sum
// 第三次
// function(...args) {
// return (function(...args) {return addPrefix(len(...args))})(sum(...args))
// }
// 最终结果
// function(...args) {
// return addPrefix(len(sum(...args)));
// }
const compose = (...fns) => {
return fns.reduce(function(a, b) {
return function(...args) {
return a(b(...args));
}
})
}
let final = compose(addPrefix, len, sum);
console.log(final('a', 'b'));
// 简写
const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)));
三、防抖节流
1 防抖
函数防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数 才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时
通俗来讲,就是多次连续调用函数,只有最后一次有效触发事件
适用场景:input输入 发送请求查询数据
只有最后一次生效的版本
// 定时器版本核心:每次都把之前的定时器清除了,重新创建一个新的定时器,保证最后一次有效
function debounce(fn, wait) {
let timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(()=>{
fn.apply(this, arguments);
timer = null;
}, wait);
}
}
// 每次输入间隔500ms执行一次,也就是暂停的最后一次生效
function handler(e) {
console.log(e.target.value);
}
document.getElementById("input").addEventListener('input', debounce(handler, 500));
- 首次点击有效的防抖版本
// 首次点击有效 immediate 为true表示第一次立刻执行
function debounce(fn, wait, immediate) {
let timer = null;
return function() {
clearTimeout(timer);
// 处理首次点击
if (immediate && !timer) {
fn.apply(this, arguments);
}
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, wait);
}
}
// 每次输入生效两次 第一次和 500ms到了之后的最后一次
function handler(e) {
console.log(e.target.value);
}
document.getElementById("input").addEventListener('input', debounce(handler, 500, true));
2节流
当持续触发事件时,保证一定时间段内只调用一次事件处理函数
节流通俗解释就比如我们水⻰头放水,阀⻔一打开,水哗哗的往下流,秉着勤俭节约的优良传统 美德,我们要把水⻰头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的 往下滴
适用场景:resize scroll
时间戳,第一次立刻执行
function throttle(fn, interval) {
let last = 0;
return function() {
let now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, arguments);
}
}
}
// 在滚动条滚动过程中,每隔1秒执行一次
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
- 定时器,延迟执行,即第一次不立刻执行,但是最后一次一定会延迟执行
function throttle(fn, delay) {
let timer = null;
return function() {
if (!timer) {
// 每次执行就创建一个定时器,让函数延迟执行
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null; // 清空定时器,方便创建下一个定时器
}, delay);
}
}
}
// 利用定时器延迟执行
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
- 两者结合(时间戳 + 定时器),第一次立刻执行,最后一次延迟执行
function throttle(fn, delay) {
let timer = null;
let startTime = Date.now();
return function() {
let curTime = Date.now();
// 计算剩余时间
let remaining = delay - (curTime - startTime);
// 先清除定时器
clearTimeout(timer);
if (remaining <= 0) {
// 保证第一次一定执行
fn.apply(this, arguments);
startTime = Date.now(); // 更新为当前时间,保证精准性
} else {
// 不是第一次就用定时器延迟执行
timer = setTimeout(fn, remaining);
}
}
}
// 滚动第一次立刻执行,然后最后一次延迟剩余时间执行
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
四、并发计数器
const fs = require('fs');
function after(times, callback) {
let arr = [];
return (data, index) => {
arr[index] = data; // 保证顺序 采用索引
if (--times === 0) { // 多个请求并发 需要靠计数器来实现
callback(arr);
}
}
}
let out = after(2, (arr) => {
console.log(arr)
})
fs.readFile('./a.txt', 'UTF8', function (err, data) {
out(data, 0)
})
fs.readFile('./b.txt', 'UTF8', function (err, data) {
out(data, 1)
})
五、发布订阅
- 发布订阅模式:目的是解耦合,核心就是把多个方法先暂存起来,最后一次执行
- 四个方法:订阅方法、发布方法、取消订阅、订阅一次
class EventEmitter {
constructor(maxListeners) {
this.maxListeners = maxListeners || Infinity;
this._events = {};
}
// 订阅
on(event, callback) {
// 如果没有赋值为数组存储多次订阅的事件
if (!this._events[event]) {
this._events[event] = [];
}
// 设置最大监听数,可以没有
if (this.maxListeners !== Infinity && this._events[event].length >= this.maxListeners) {
console.log(`当前事件${event}超过最大监听数`);
return this;
}
this._events[event].push(callback);
return this; // 返回this为了链式调用
}
// 发布
emit(event, ...args) {
const callbacks = this._events[event];
if (!callbacks) {
console.log('当前事件没有订阅过,请先订阅');
return this;
}
// 循环执行
callbacks.forEach(cb => cb(...args));
return this;
}
// 取消订阅
off(event, callback) {
if (!callback) {
this._events[event] = null; // 这里也可以不处理
} else {
// 过滤掉相同的回调部分
this._events[event] = this._events[event].filter(item => item !== callback && item.l !== callback);
}
return this;
}
// 订阅一次
once(event, callback) {
// 绑定执行完毕后移除
const one = (...args) => {
callback(...args); // AOP切片编程
this.off(event, one);
}
// 如果确定订阅依次的事件不能取消,需要单独加个标识
one.l = callback;
this.on(event, one);
return this;
}
}
// 使用
const add = (a, b) => console.log(a + b);
const minus = (a, b) => console.log(a - b);
const log = (...args) => console.log(...args);
const emitter = new EventEmitter();
emitter.on('add', add);
emitter.on('add', minus);
emitter.once('log', log);
setTimeout(() => {
emitter.off('add', add)
emitter.emit('add', 1, 2);
emitter.emit('log', 'a', 'b')
}, 1000);
// -1
// a b
六、观察者
- 观察者模式 需要有观察者和被观察者,而且被观察者需要收集观察者
// 被观察者的类
class Subject {
constructor(name) {
this.name = name;
this.state = '非常开心'
this.observers = []; // 收集观察者
}
// 收集方法
attach(o) {
this.observers.push(o);
}
// 改变状态触发更新
setState(newState) {
this.state = newState;
this.observers.forEach(o => o.update(this.name, newState))
}
}
// 观察者
class Observer {
constructor(name) {
this.name = name;
}
// 更新方法
update(s, state) {
console.log(this.name + ":" + s + '当前' + state);
}
}
// 使用
let s = new Subject('小宝宝');
let o1 = new Observer('爸爸');
let o2 = new Observer('妈妈');
s.attach(o1)
s.attach(o2)
s.setState('不开心了')
s.setState('开心了')
七、call | apply | bind
- JS中为了可以改变函数的this指向,内置了三个方法call | apply | bind
- 三个函数的第一个参数都是改变this指向的对象,如果非严格模式下,传递null/undefined指向也是window
- 三个的区别:call和apply立刻执行,call的参数是挨个传递,apply参数是数组传递,bind返回的是一个函数,需要执行再次执行函数
- 理论上call性能比apply要好些,因为内部少了一次参数解构,但是有了ES6的解构,基本差不多了,详情见call和apply的性能对比
1 call
- 首先判断this,防止直接调用,可以throw new Error,也可以返回undefined
- 其次判断context上下文,不传默认为window
- 再次创建一个唯一的属性,添加到上下文中
- 最后执行函数,并且删除临时属性,防止污染上下文,返回结果
// 调用方法:fn.call(obj, a, b)
Function.prototype.myCall = function (context, ...args) {
// 1 用于防止 Function.prototype.myCall() 直接调用
if (this === Function.prototype) {
return undefined;
}
// 2 判断上下文,如果context不传,默认为window
context = context || window;
// 3 使用symbol创一个唯一的属性,然后添加到上下文中
const fn = Symbol();
context[fn] = this;
// 4 执行函数,传入参数
const result = context[fn](...args);
// 5 删除临时属性,避免污染上下文,最后并返回结果
delete context[fn];
return result;
}
// 测试案例
var obj = { a: 10, b: 20 }
function test(key1, key2) {
console.log(this[key1] + this[key2]);
}
test.myCall(obj, 'a', 'b') // 30
2 apply
- 实现类似call,参数为数组
// 调用方法:fn.apply(obj, [a, b])
Function.prototype.myApply = function (context, args) {
// 0 容错处理,参数必须为数组
if (!Array.isArray(args)) {
throw new Error('CreateListFromArrayLike called on non-object');
}
// 1 用于防止 Function.prototype.myApply() 直接调用
if (this === Function.prototype) {
return undefined;
}
// 2 判断上下文,如果context不传,默认为window
context = context || window;
// 3 使用symbol创一个唯一的属性,然后添加到上下文中
const fn = Symbol();
context[fn] = this;
// 4 执行函数,传入参数
const result = context[fn](...args);
// 5 删除临时属性,避免污染上下文,最后并返回结果
delete context[fn];
return result;
}
// 测试案例
var obj = { a: 10, b: 20 }
function test(key1, key2) {
console.log(this[key1] + this[key2])
}
test.apply(obj, ['a', 'b']) // 30 注意这里是传入数组 ['a', 'b']
3 bind
- 首先判断this,防止直接调用,可以throw new Error,也可以返回undefined
- 其次返回一个函数,然后内部调用call执行
// 调用方法:let fn2 = fn.bind(obj, a, b) => fn()
Function.prototype.myBind = function (context, ...args) {
// 1 用于防止 Function.prototype.myApply() 直接调用
if (this === Function.prototype) {
throw new TypeError('Error');
}
return (...bindArgs) => this.call(context, ...args, ...bindArgs);
}
// 测试案例
var obj = { a: 10, b: 20 }
function test(key1, key2) {
console.log(this[key1] + this[key2])
}
var fn = test.myBind(obj)
fn('a', 'b') // 30
八、new原理
- 首先创建一个空对象,并且继承原型
- 其次执行函数
- 最后判断是函数还是对象,因为最终需要返回一个对象
function myNew(Fn, ...args) {
// 1 创建一个空对象,让对象的__proto__指向构造函数的prototype
// 等价于let target = {}; target.__proto__ = Fn.prototype;
const target = Object.create(Fn.prototype);
// 2 执行构造函数的代码
let result = Fn.apply(target , args);//Fn.myApply(target , args);
// 3 返回结果
return typeof result === 'object' || typeof result === 'function' ? result : target;
}
// 测试案例
function People(name, age) {
this.name = name
this.age = age
}
var tom = new People('tom', 20) // 原生
var mike = myNew(People, 'mike', 30) // 自定义
console.log(tom instanceof People, mike instanceof People) // true true
九 typeof | toString
1 typeof
// typeof 只能判断基本数据类型,对应引用类型都是object
// 基本类型
console.log(typeof 123); //number
console.log(typeof "abc"); //string
console.log(typeof true); //boolean
console.log(typeof undefined); //undefined
console.log(typeof null); //object
console.log(typeof Symbol());//symbol
// 引用类型
console.log(typeof [1, 2, 3]); //object
console.log(typeof {}); //object
console.log(typeof function () { }); //function
console.log(typeof Array); //function Array类型的构造函数
console.log(typeof Object); //function Object类型的构造函数
console.log(typeof Symbol); //function Symbol类型的构造函数
console.log(typeof Number); //function Number类型的构造函数
console.log(typeof String); //function String类型的构造函数
console.log(typeof Boolean); //function Boolean类型的构造函数
2 Object.prototype.toString.call
// 基本类型
console.log(Object.prototype.toString.call('An')); // "[object String]"
console.log(Object.prototype.toString.call(1)); // "[object Number]"
console.log(Object.prototype.toString.call(true)); // "[object Number]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(Symbol(1))); // "[object Symbol]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
// 引用类型
console.log(Object.prototype.toString.call(function () { })); // "[object Function]"
console.log(Object.prototype.toString.call({ name: 'An' })); // "[object Object]"
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"
console.log(Object.prototype.toString.call(/[a-z]/g)); // "[object RegExp]"
console.log(Object.prototype.toString.call(JSON)); // "[object JSON]"
console.log(Object.prototype.toString.call(Math)); // "[object Math]"
十、数组相关
1 判断数组的方法
let arr = [1, 2, 3];
// 1 ES5中新增了Array.isArray方法,IE8及以下不支持
console.log(Array.isArray(arr));
// 2 constructor判断 - Object的每个实例都有构造函数 constructor,用于保存着用于创建当前对象的函数
console.log(arr.constructor === Array);
// 3 instanceof判断 - instanceof 主要是用来判断某个实例是否属于某个对象
console.log(arr instanceof Array);
// 4 Object.prototype.toString.call
console.log(Object.prototype.toString.call(arr) === '[object Array]');
// 5 原型链上的isPrototypeOf判断 - isPrototypeOf()可以用于检测一个对象是否存在于另一个对象的原型链上
console.log(Array.prototype.isPrototypeOf(arr));
2 数组拍平
let arr = [
[1],
[2, 3],
[4, 5, 6, [7, 8, [9, 10, [11]]]],
12
];
// 1 ES6 - flat
console.log(arr.flat(Infinity));
// 2 toString
console.log(arr.toString().split(',').map(item => Number(item)));
// 3 stringify + 正则替换
console.log(JSON.stringify(arr).replace(/\[|\]/g, '').split(',').map(item => Number(item)));
// 4 while
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
console.log(arr);
// 5 prototype + 自定义falt方法
Array.prototype.flat = function () {
let result = [];
let _this = this;
function _flat(arr) {
for (let i = 0; i < arr.length; i++) {
let item = arr[i];
if (Array.isArray(item)) {
_flat(item);
} else {
result.push(item);
}
}
}
_flat(_this);
return result;
}
console.log(arr.flat());
// 6 reduce简化
Array.prototype.flat = function () {
return this.reduce((target, current) =>
Array.isArray(current) ? target.concat(flat(current)) : target.concat(current)
, [])
}
console.log(arr.flat());
十一、同源策略
- 1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策
- 定义:协议相同、域名相同、端口相同
- 同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据
- 三种限制:cookie 访问限制、DOM 访问限制、Ajax 请求限制
十一、跨域问题
参考跨域相关
- jsonp
- cors
- postMessage
- document.domain
- window.name
- location.hash
- http-proxy
- nginx
- websocket
1 jsonp
function jsonp({url, params, cb}) {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
window[cb] = function(data) {
resolve(data);
}
// 处理参数 wd=b&cb=show
params = {...params, cb}
let arr = [];
for(let key in params) {
arr.push(`${key}=${params[key]}`);
}
script.src = `${url}?${arr.join('&')}`;
document.body.appendChild(script);
})
}
// 只能发送get请求,不支持post、put、delete
// 不安全,容易出现xss攻击
jsonp({
url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
params: {wd: 'b'},
cb: 'show'
}).then(data => {
console.log(data);
})
2 cors
// 设置哪个源可以访问我
Access-Control-Allow-Origin
// 允许携带哪个头
Access-Control-Allow-Headers
// 允许哪个方法
Access-Control-Allow-Methods
// 允许携带cookie
Access-Control-Allow-Credentials
// 预检得存活时间
Access-Control-Max-Age
// 允许返回得头信息
Access-Control-Expose-Headers
十二、XSS 和 CSRF
1 xss
- 跨站脚本攻击 (Cross Site Scripting)
- 恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的
- 通常分为:反射型(非持久型)、存储型(持久型)、基于DOM
反射型
窃取网页浏览中的cookie值,诱导用户点击,获取cookie,是一次性的,需要服务端配合
<a href="attack.html?content=<img src='aaa.png' onerror='alert(1)'/>"></a>
<a href="attack.html?content=<img src='aaa.png' onerror='while(true)alert(/关不掉/)'/>">敏感词汇</a>
存储型
在有提交表单的地方,比如评论,用户提交一段恶意脚本,将恶意的脚本存储到了服务器上,所有人访问都会造成攻击,范围更大
<img src='' onerror='alert(/攻击脚本/)' />
DOM-Based型
通过恶意脚本修改页面的 DOM 结构,是纯粹发生在客户端的攻击,比如修改属性、插入内容、document.write
2 xss危害
- 窃取网页浏览中的cookie值
- 劫持流量实现恶意跳转
3 xss预防
- HttpOnly 防止劫取 Cookie
- 输入、输出检查
- csp (响应的http头中加入Content-Security-Policy),建立白名单,规定浏览器只能执行特定的来源代码
// 替换特殊标签
function escape(str) {
str = str.replace(/&/g, "&");
str = str.replace(/</g, "<");
str = str.replace(/>/g, ">");
str = str.replace(/"/g, "&quto;");
str = str.replace(/'/g, "&##39;");
str = str.replace(/`/g, "&##96;");
str = str.replace(/\//g, "&##x2F;");
return str
}
4 csrf
- 跨站请求伪造 (Cross Site Request Forgery)
- 一种劫持受信任用户向服务器发送非预期请求的攻击方式
5 csrf防御
- 验证码
- Referer Check
- 添加token令牌验证
十三、Promise
1 Promise的基础逻辑
- promise是一个类 ,无需考虑兼容性,因为每个框架都实现不一样,只需要遵守规则即可
- 当使用promise的时候 会传入一个执行器(executor),此执行器是立即执行,需要传入两个函数
- promise中有三个状态
- 等待态 Pending
- 成功态 Fulfilled
- 失败态 Rejected
- 状态只能由 pending --> fulfilled 或者 pending --> rejected,状态一旦改版不能再进行二次修改
- promise中使用resolve和reject两个函数来改变状态
- then方法内部就是状态判断
- 如果状态是成功的,就调用成功的回调函数
- 如果状态是失败的,就调用失败的回调函数
// 使用方式
const promise = new Promise((resolve, reject) => {
resolve('success');
// reject('error');
// throw new Error('失败了')
});
promise.then((value) => {
console.log('success', value);
}, (reason) => {
console.log('error', reason);
})
- 源代码
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
class Promise {
constructor(executor) {
this.status = PENDING; // 默认状态
this.value = undefined; // 成功的原因
this.reason = undefined; // 失败的原因
const resolve = (value) => { // 成功resolve函数,赋值并改变状态
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
}
}
const reject = (reason) => { // 失败reject函数
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
}
}
try {
// 执行器,立刻执行
executor(resolve, reject);
} catch (error) { // 异常捕获
// console.log(error);
reject(error);
}
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) { // 成功回调,并传入值
onFulfilled(this.value);
} else if (this.status === REJECTED) { // 失败回调,并传入原因
onRejected(this.reason);
}
}
}
2 Promise的异步逻辑和多次then处理
- 如果执行器中有异步函数,需要先存储回调函数,等状态改变了循环执行
- 为了满足多次then,需要用数组存储回调函数
/**
* 主线程代码立即执行,setTimeout 是异步代码,then 会马上执行,
* 这个时候需要判断 Promise 状态,如果状态是 Pending,需要等待状态改变了再执行then方法
*/
const promise = new Promise((resolve, reject) => {
// 异步调用
setTimeout(() => {
resolve('success');
}, 1000);
})
promise.then((value) => {
console.log('success1', value);
}, (reason) => {
console.log('error1', reason);
})
promise.then((value) => {
console.log('success2', value);
}, (reason) => {
console.log('error2', reason);
})
- 源代码
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
class Promise {
constructor(executor) {
this.status = PENDING; // 默认状态
this.value = undefined; // 成功的原因
this.reason = undefined; // 失败的原因
this.onResolvedCallbacks = []; // 存放then中成功的回调函数
this.onRejectedCallbacks = []; // 存放then中失败的回调函数
const resolve = (value) => { // 成功resolve函数,赋值并改变状态
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
// 执行then方法 =》 发布
this.onResolvedCallbacks.forEach(fn => fn());
}
}
const reject = (reason) => { // 失败reject函数
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
// 执行器
executor(resolve, reject);
} catch (error) {
// console.log(error);
reject(error);
}
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) { // 成功回调
onFulfilled(this.value);
}
if (this.status === REJECTED) { // 失败回调
onRejected(this.reason);
}
if (this.status === PENDING) { // 等待状态
// 包一层函数,AOP切片编程 =》 订阅
this.onResolvedCallbacks.push(() => {
// TODO... 可以加入额外的逻辑
onFulfilled(this.value);
});
this.onRejectedCallbacks.push(() => {
// TODO... 可以加入额外的逻辑
onRejected(this.reason);
})
}
}
}
3 Promise的链式调用
- promise的链式调用解决了什么问题?
- 链式调用解决嵌套回调的问题
- 同步并发问题
- 多个异步处理错误问题
- 当调用then方法后会返回一个新的promise
- then中方法返回的是一个(普通值 不是promise)的情况, 会作为外层下一次then的成功结果
- then中方法 执行出错 会走到外层下一次then的失败结果
- 如果then中方法返回的是一个promise对象, 需要判断状态,然后根据promise的结果来处理是走成功还是失败 (传入的是成功或者失败的内容)
- 无论上一次then走是成功还是失败,只要返回的是普通值 都会执行下一次then的成功
- 总结:
- 如果返回一个普通值 (除了promise) 就会传递给下一个then的成功,
- 如果返回一个失败的promise或者抛出异常,会走下一个then的失败
// 链式调用就是多次调用then方法
const promise = new Promise((resolve, reject) => {
// 异步调用
setTimeout(() => {
resolve('success');
}, 1000);
})
promise.then((value) => {
console.log('success1', value);
return value;
// return undefined;
// return 1;
// throw new Error('error');
}, (reason) => {
console.log('error1', reason);
}).then((value) => {
console.log('success2', value);
}, (reason) => {
console.log('error2', reason);
})
- 源代码
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
class Promise {
constructor(executor) {
this.status = PENDING; // 默认状态
this.value = undefined; // 成功的原因
this.reason = undefined; // 失败的原因
this.onResolvedCallbacks = []; // 存放then中成功的回调函数
this.onRejectedCallbacks = []; // 存放then中失败的回调函数
const resolve = (value) => { // 成功resolve函数,赋值并改变状态
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
// 执行then方法 =》 发布
this.onResolvedCallbacks.forEach(fn => fn());
}
}
const reject = (reason) => { // 失败reject函数
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
// 执行器
executor(resolve, reject);
} catch (error) {
// console.log(error);
reject(error);
}
}
then(onFulfilled, onRejected) {
/*********代码修改点*******/
// 用于实现链式调用
let promise2 = new Promise((resolve, reject) => {
if (this.status === FULFILLED) { // 成功回调
try {
let x = onFulfilled(this.value);
resolve(x); // 上一次返回的结果作为下一次then中函数的值
} catch (e) {
reject(e);
}
}
if (this.status === REJECTED) { // 失败回调
try {
let x = onRejected(this.reason);
resolve(x);
} catch (e) {
reject(e);
}
}
if (this.status === PENDING) { // 等待状态
// 包一层函数,AOP切片编程 =》 订阅
this.onResolvedCallbacks.push(() => {
try {
// TODO... 可以加入额外的逻辑
let x = onFulfilled(this.value);
resolve(x);
} catch (e) {
reject(e);
}
});
this.onRejectedCallbacks.push(() => {
try {
// TODO... 可以加入额外的逻辑
let x = onRejected(this.reason);
resolve(x);
} catch (e) {
reject(e);
}
})
}
})
return promise2;
}
}
4 Promise的x处理-核心逻辑
- x 可能是一个promise,如果是promise需要看一下这个promise是成功还是失败
- 如果成功则把成功的结果调用promise2的resolve传递进去,如果失败则同理
- 总结:x的值决定是调用promise2的resolve还是reject
- 如果是promise则取他的状态
- 如果是普通值则直接调用resolve
如果 then 方法返回的是自己的 Promise 对象,则会发生循环调用,这个时候程序会报错
// promise2 === x的情况
let promise2 = new Promise((resolve, reject) => {
resolve(1);
}).then(() => {
return promise2 // x
})
promise2.then((data) => {
console.log('x1 data', data);
}, (error) => {
console.log(('x1 error', error));
})
// 报错:TypeError: Chaining cycle detected for promise #<Promise>
兼容x.then是通过defineProperty来定义函数的,取值可能会发生异常,需要try catch处理
// x.then
let promise2 = new Promise((resolve, reject) => {
resolve(1);
}).then(() => {
let p = {}
Object.defineProperty(p, 'then', {
get() {
throw new Error('p.then is customer function');
}
})
return p.then;
})
为了then中的resolvePromise能拿到promise2,需要进行异步延时处理,官方推荐的(3 Notes)"This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick.",这里我们采用setTimeout
不能采用queueMicrotask,因为queueMicrotask是一个polyfill,底层也是promise,不能自己实现自己,详情见queueMicrotask
// then -> promise2
if (this.status === FULFILLED) { // 成功回调
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
resolve中返回还是一个promise的情况,需要在resolvePromise的成功中递归调用
let promise2 = new Promise((resolve)=>{
resolve(1);
}).then(data=>{
return new Promise((resolve,reject)=>{ // x 可能是一个promise
setTimeout(() => {
// resolve中还是一个promise
resolve(new Promise((resolve,reject)=>{
setTimeout(() => {
resolve('200');
}, 1000);
}))
}, 1000);
})
},(err)=>{
return 111
})
promise2.then(data=>{
console.log(data)
},err=>{
console.log('error',err)
});
resolvePromise函数的实现
// 利用x的值来判断是调用promise2的resolve还是reject
function resolvePromise(promise2, x, resolve, reject) {
// 如果 then 方法返回的是自己的 Promise 对象,则会发生循环调用,这个时候程序会报错
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
// 自己写的promise 要和别人的promise兼容,考虑不是自己写的promise情况
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
// 确保了别人promise符合规范:防止别人写的promise可能调用成功后 还能调用失败
let called = false;
// 兼容then方法可能是通过defineProperty来实现的,取值时可能会发生异常,需要try catch处理
try {
let then = x.then;
if (typeof then === 'function') {
// then 是函数的时候,表示为promise
then.call(x, y => { // x.then调用可能会触发getter可能会发生异常,所以采用call调用
if (called) return;
called = true;
// 如果then方法调用成功,需要继续递归解析,直到不是promise就停止
resolvePromise(promise2, y, resolve, reject);
}, r => {
if (called) return;
called = true;
reject(r);
})
} else {
// 如果then是一个对象 比如 {} {then: {}}
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 说明返回的是一个普通值 直接将他放到promise2.resolve中
resolve(x)
}
}
测试案例
const promise = new Promise((resolve, reject) => {
// 异步调用
setTimeout(() => {
resolve('success');
}, 1000);
})
promise.then((value) => {
console.log('success1', value);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ok');
}, 1000);
})
}, (reason) => {
console.log('error1', reason);
}).then((value) => {
console.log('success2', value);
}, (reason) => {
console.log('error2', reason);
})
// success1 success
// success2 ok
5 Promise的值穿透
- then中的参数是可选的,不传会默认处理
// 成功值穿透
new Promise((resolve) => {
resolve(200)
}).then().then().then((data) => {
console.log(data, 's')
}, err => {
console.log(err, 'e')
})
// 200 'e'
// 失败值穿透
new Promise((resolve, reject) => {
reject(200)
}).then(null).then((data) => {
console.log(data, 's')
}, err => {
console.log(err, 'e')
})
// 200 's'
判断then中的两个函数,如果不传默认就行传递,用于值穿透
then(onFulfilled, onRejected) {
// 处理then中的值穿透
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err;}
// 用于实现链式调用
let promise2 = new Promise((resolve, reject) => {
// ......
})
}
6 Promise 的延迟对象
// 延迟对象 帮我们减少一次套用 : 针对目前来说 应用不是很广泛
// 即将promise挂到对象上,使用的时候调用对象的方法
Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve= resolve;
dfd.reject = reject;
});
return dfd;
}
// promise延迟对象的使用
function readFile(filePath, encoding) {
let dfd = Promise.deferred();
// nodeApi 转化成promise
fs.readFile(filePath, encoding, (err, data) => {
if (err) return dfd.reject(err);
dfd.resolve(data);
})
return dfd.promise;
}
readFile('./a.txt', 'utf8').then((data => {
console.log(data)
}))
7 Promise A+测试
// 1 安装测试工具
npm install promises-aplus-tests -g
// 2 找到对应的目录
promises-aplus-tests promise4.js
8 Promise 中返回promise的特例
// 成功的嵌套
new Promise((resolve, reject) => {
resolve(new Promise((resolve, reject) => {
resolve(100)
}))
}).then(data=>{
console.log(data);
})
// 失败的嵌套
new Promise((resolve, reject) => {
reject(new Promise((resolve, reject) => {
setTimeout(() => {
resolve(100)
}, 1000);
}))
}).then(data=>{
console.log(data);
},err=>{
console.log(err,'err');
})
修改源代码的resolve方法
const resolve = (value) => { // 成功resolve函数,赋值并改变状态
if (value instanceof Promise) {
return value.then(resolve, reject)
}
// .......
}
9 Promise.resolve
// 使用
Promise.resolve(new Promise((resolve, reject) => {
setTimeout(() => {
resolve(200);
}, 1000);
})).then((data) => {
console.log(data) // 200
})
Promise.resolve() 会创造一个成功的promise
Promise.resolve = function(value) {
return new Promise((resolve, reject) => {
resolve(value);
})
}
10 Promise.reject
// 使用
Promise.reject(new Promise((resolve, reject) => {
setTimeout(() => {
resolve(200);
}, 1000);
})).then((data) => {
console.log(data)
}, err => {
console.log('err', err);
})
Promise.reject() 会创造一个失败的promise
Promise.reject = function(value) {
return new Promise((resolve, reject) => {
reject(value);
})
}
11 Promise.all
多个promise全部完成后获取结果,如果其中某一个失败了,这个promise就变为失败了
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功1')
}, 2000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功2');
// reject('失败2')
}, 1000);
});
Promise.all([p1, p2, 1, 2, 3]).then(data => {
console.log(data);
}, err => {
console.log('err', err)
})
实现原理:循环加计数器记录结果顺序
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let result = [];
let times = 0;
const processSuccess = function(index, data) {
result[index] = data;
// 判断传入的数组全部执行完成才能算成功,需要计数器,
// 不能用result.length的长度来判断,防止最后一个先执行了长度就变了
if (++times === promises.length) {
resolve(result);
}
}
for(let i = 0; i < promises.length; i++) {
// 判断是否为promise
let p = promises[i];
if (p && typeof p.then === 'function') {
p.then(data => {
// 成功就放入结果集
processSuccess(i, data)
}, reject); // 如果其中某一个promise失败了 直接执行失败即可
} else {
processSuccess(i, p);
}
}
});
}
11 Promise.race
竞赛,返回结果采用最先执行完的一个,其他也会执行,只是不采用结果
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功1')
}, 2000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功2');
// reject('失败2')
}, 1000);
});
Promise.race([p1, p2, 1, null]).then((data) => {
console.log(data);
}, err => {
console.log('err', err);
});
实现原理:循环一次,谁先完成用谁的结果
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
for(let i = 0; i < promises.length; i++) {
let p = promises[i];
if (p && typeof p.then === 'function') {
// 一旦成功就直接停止
p.then(resolve, reject);
} else {
resolve(p)
}
}
});
}
一次应用,请求超时的中断处理(请求中断,图片请求中断)
function wrap(p1) {
let abort;
// 自己构造的promise,暴露一个终端方法
let p = new Promise((resolve, reject) => {
abort = reject;
})
let p2 = Promise.race([p, p1]);
p2.abort = abort; // 如果用户调用abort方法 这个p就失败了 = p2 就失败了
return p2;
}
// 使用
let p1 = new Promise((resolve,reject)=>{
setTimeout(() => {
resolve('成功')
}, 3000);
});
let p2 = wrap(p1);
p2.then((data) => {
console.log(data)
}, (err) => {
console.log(err)
})
setTimeout(() => {
p2.abort('超过一秒了');
}, 1000);
12 Promise的异常
原型方法catch,捕获异常
Promise.prototype.catch = function (errorFn) {
// 透传即可
return this.then(null, errorFn)
}
// 使用
new Promise((resolve, reject) => {
reject(300);
}).then((data) => {
console.log(data)
}).catch(err => { // catch方法就是没有成功的失败
console.log('err', err)
});
原型方法finally,论如何都会执行,但是可以继续向下执行,不返回promise,但是有等待效果
Promise.prototype.finally = function(cb){
return this.then(data => {
// 等待cb执行完成之后,但是结果采用data
return Promise.resolve(cb()).then(() => data);
}, err => {
// 等待cb()后的promise完成,抛出错误
return Promise.resolve(cb()).then(() => {throw err})
})
}
// 使用
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功')
// reject('失败')
}, 3000);
}).finally(() => { // = then 无论状态如何都会执行
console.log('finally')
return new Promise((resolve, reject) => { // 不会使用promise的成功结果
setTimeout(() => {
resolve(1000);
}, 1000);
})
}).then((data) => {
console.log(data)
}).catch(e => {
console.log('catch', e)
})
13 Promise.allSettled
Promise.allSettled 表示全部成功之后返回一个结果数组
Promise.allSettled = function (promises) {
return new Promise((resolve, reject) => {
let result = [];
let times = 0;
const processSuccess = function (index, data) {
result[index] = data;
if (++times === promises.length) {
resolve(result);
}
}
for (let i = 0; i < promises.length; i++) {
let p = promises[i];
if (p && typeof p.then === 'function') {
// 执行p.then
p.then(data => {
processSuccess(i, { status: "fulfilled", value: data });
}, err => {
processSuccess(i, { status: "rejected", value: err });
})
} else {
processSuccess(i, { status: "fulfilled", value: p });
}
}
});
}
// 测试
const p1 = Promise.resolve(1)
const p2 = Promise.resolve(2)
const p3 = new Promise((resolve, reject) => {
setTimeout(reject, 1000, 'three');
});
Promise.allSettled([p1, p2, p3, 1, null]).then(values => {
console.log(values)
})
// [
// {status: "fulfilled", value: 1}
// {status: "fulfilled", value: 2}
// {status: "rejected", reason: "three"}
// {status: "fulfilled", value: 1}
// {status: "fulfilled", value: null}
// ]
14 Promise.any
Promise.any 表示其中只要有一个成功了,就取出第一个成功的值,否则全部失败了,才会走失败
// 一个成功的就取第一个成功的值
const p1 = Promise.resolve(1)
const p2 = Promise.resolve(2)
const p3 = new Promise((resolve, reject) => {
setTimeout(reject, 1000, 'three');
});
Promise.any([p1, p2, p3, 3]).then(data => {
console.log('data', data);
}).catch(err => {
console.log('err', err);
})
// data 1
// 全部失败才返回
const p1 = Promise.reject(1)
const p2 = Promise.reject(2)
const p3 = new Promise((resolve, reject) => {
setTimeout(reject, 1000, 'three');
});
Promise.any([p1, p2, p3]).then(data => {
console.log('data', data);
}).catch(err => {
console.log('err', err);
console.log(err.message)
console.log(err.name)
console.log(err.errors)
})
// err:AggregateError: All promises were rejected
// All promises were rejected
// AggregateError
// [1, 2, "three"]
编写源码
Promise.any = function (promises) {
return new Promise((resolve, reject) => {
let result = [];
let times = 0;
const processSuccess = function (index, data) {
result[index] = data;
if (++times === promises.length) {
// AggregateError 浏览器内置对象
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
reject(new AggregateError(result, 'All promises were rejected'));
}
}
for (let i = 0; i < promises.length; i++) {
let p = promises[i];
if (p && typeof p.then === 'function') {
p.then(resolve, err => {
// 只捕获异常
processSuccess(i, err);
})
} else {
resolve(p);
}
}
})
}
应用场景
- 从最快的服务器检索资源 : 来自世界各地的用户访问网站,如果你有多台服务器,则尽量使用响应速度最快的服务器,在这种情况下,可以使用 Promise.any() 方法从最快的服务器接收响应
function getUser(endpoint) {
return fetch(`https://superfire.${endpoint}.com/users`)
.then(response => response.json());
}
const promises = [getUser("jp"), getUser("uk"), getUser("us"), getUser("au"), getUser("in")]
Promise.any(promises).then(value => {
console.log(value)
}).catch(err => {
console.log(err);
})
- 显示第一张已加载的图片(来自MDN) : 有一个获取图片并返回 blob 的函数,我们使用 Promise.any() 来获取一些图片并显示第一张有效的图片(即最先 resolved 的那个 promise)
function fetchAndDecode(url) {
return fetch(url).then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
return response.blob();
}
})
}
let coffee = fetchAndDecode('coffee.jpg');
let tea = fetchAndDecode('tea.jpg');
Promise.any([coffee, tea]).then(value => {
let objectURL = URL.createObjectURL(value);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
}).catch(e => {
console.log(e.message);
});
15 Node中的promisify
promisify 主要的功能是将一个异步的方法转化成promise的形式 主要是给node来使用的
const fs = require('fs');
function promisify(fn) {
// 返回一个高阶函数
return function (...args) {
// 函数执行返回一个promise对象
return new Promise((resolve, reject) => {
// 执行对应的原函数
fn(...args, (err, data) => {
if (err) return reject(err);
resolve(data);
})
})
}
}
// 包装api方法
function promisifyAll(obj) {
let o = {};
for (let key in obj) {
if (typeof obj[key] === 'function') {
o[key + 'Promise'] = promisify(obj[key]);
}
}
return o;
}
let newFs = promisifyAll(fs); // bluebird库 http://bluebirdjs.com/docs/getting-started.html
newFs.readFilePromise('./.gitignore', 'utf8').then(data => {
console.log(data);
});
15 Promise的面试题
Promise.resolve().then(() => {
console.log(0);
return Promise.resolve(4);
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() => {
console.log(6);
})
// node环境 0 1 4 2 3 5 6
// 浏览器环境 0 1 2 3 4 5 6
为什么4被推迟了两个时序?
- 第一种解释:
- 浏览器会创建一个 PromiseResolveThenableJob 去处理这个 Promise 实例,这是一个微任务
- 等到下次循环到来这个微任务会执行,也就是PromiseResolveThenableJob 执行中的时候,因为这个Promise 实例是fulfilled状态,所以又会注册一个它的.then()回调
- 又等一次循环到这个Promise 实例它的.then()回调执行后,才会注册下面的这个.then(),于是就被推迟了两个时序
- 第二种解释:
- return Promise.resolve(4),JS引擎会安排一个job(job是 ECMA 中的概念,等同于微任务的概念),其回调目的是让其状态变为fulfilled,然后再继续两次then
实现promise的并发控制
function limitLoad(urls, handler, limit) {
const sequence = [].concat(urls);
let promises = [];
// 先取limit执行
promises = sequence.splice(0, limit).map((url, index) => {
return handler(url).then(() => {
return index;
})
});
let p = Promise.race(promises);
// 剩下的循环,把当前第一个返回的替换掉继续竞赛
for (let i = 0; i < sequence.length; i++) {
p = p.then(res => {
promises[res] = handler(sequence[i]).then(() => {
return res
})
return Promise.race(promises);
})
}
}
let urls = [
{
info: 'link1',
time: 3000
},
{
info: 'link2',
time: 2000
},
{
info: 'link3',
time: 5000
},
{
info: 'link4',
time: 1000
},
{
info: 'link5',
time: 1200
},
{
info: 'link6',
time: 800
},
{
info: 'link7',
time: 3000
}
];
// 需要执行的任务
function loadImg(url) {
return new Promise((resolve, reject) => {
console.log('------------' + url.info + ' start');
// Fetch请求
setTimeout(() => {
console.log('------------' + url.info + ' end');
resolve();
}, url.time);
})
}
// 图片数组 图片处理函数 并发个数
limitLoad(urls, loadImg, 3);
十四 Generator + Co
generator 采用的是 switch case + 有限状态机实现的
let regeneratorRuntime = {
mark(genFn) {
return genFn
},
wrap(iteratorFn) {
const context = {
next: 0,
done: false, // 表示迭代器没有完成
stop() {
this.done = true
}
}
let it = {};
it.next = function (v) { // 用户调用的next方法
context.sent = v
let value = iteratorFn(context);
return {
value,
done: context.done // 是否完成
}
}
return it;
}
}
"use strict";
var _marked = /*#__PURE__*/regeneratorRuntime.mark(read);
function read() {
var a, b, c;
return regeneratorRuntime.wrap(function read$(_context) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 1;
case 2:
a = _context.sent;
console.log('a', a);
_context.next = 6;
return 2;
case 6:
b = _context.sent;
console.log('b', b);
_context.next = 10;
return 3;
case 10:
c = _context.sent;
console.log('c', c);
case 12:
case "end":
return _context.stop();
}
}, _marked);
}
let it = read(); // 默认没有执行
let { value, done } = it.next('abc'); // 第一次传递参数是没有意义的
// 给next方法传递参数时 他的传参会给上一yield的返回值
{
let { value, done } = it.next('abc')
console.log(value, done)
}
{
let { value, done } = it.next('ddd')
console.log(value, done)
}
{
let { value, done } = it.next('eee')
console.log(value, done)
}
// a abc
// 2 false
// b ddd
// 3 false
// c eee
// undefined true
CO源码
const util = require('util');
const fs = require('fs')
let readFile = util.promisify(fs.readFile)
// TJ co
// const co = require('co');
function co(it) {
return new Promise((resolve, reject) => {
// 异步的迭代 只能用递归的方法
function next(data) {
let { value, done } = it.next(data);
if (done) { // 如果执行完毕则 完成
resolve(value);
} else {
// 原生的promise 有优化 如果是promise 内部会直接把promise返回
Promise.resolve(value).then(next, reject)
}
}
next();
})
}
function* read() {
let data = yield readFile('./a1.txt', 'utf8');
data = yield readFile(data, 'utf8');
return data;
}
co(read()).then(data => {
console.log(data)
}).catch(err => {
console.log(err)
})
四、事件相关
React和Vue的区别
1、react是一个UI组件库 vue是一个渐进式框架 2、监听数据变化的实现原理不同
react事件处理
性能优化
更快的网络通信
服务器通信层
- CDN 全局负载均衡 和 缓存系统(命中率和回源率)
- 资源合并 雪碧图
- 域名分片 - 多域名
数据传输层 - 缓存
- 强缓存 <- cache-control: max-age = 600 <- expires: Mon......
- 协商缓存 <- last-modified: -> if-modified-since: <- etag: -> if-none-match
数据传输层-压缩
- 数据压缩 gzip 与 新的br
- 代码文件压缩 HTML/CSS/JS中的注释、空格、长变量名等
- 静态资源 字体图标,去除元数据(图片的附加信息,拍摄日期等),缩小尺寸及分辨率,使用jpg或webp格式
- 头与报文 http1.1中减少不必要的头 减少cookie数据量
通信协议层面-http2
- 头部压缩 http2之前,请求头平均460字节的首部,http2 采用了头部压缩
- 专门的HPACK压缩算法 索引表 客户端和服务器同时维护一个61个首部键值对组成的静态表,索引表很小,不占内存 霍夫曼编码 压缩率90%
- 二进制帧 原因:文本字符分隔的数据流,解析慢且容易出错 二进制帧:帧长度、帧类型、帧标识 》 处理速度更快
- 链路复用 不用重复建立连接
http2中:资源合并和域名分片最好不要做
更高效的数据处理
ab -c客户端数 -n请求次数 -t持续时间 地址 node --prof .\http.js node --prof-process ./iso.... Chrome Devtool
框架代码 SSG方案
- Gatsby
- Gridsome
VueX
redux
- redux是状态管理,主要解决组件之间复杂的数据流转
- 核心思想是:将整个应用状态存储到到一个地方,称为store
- 里面存储了一棵状态树state tree,一个项目只有一个store
- store里面有三个核心方法,
- getState 获取最新的状态
- subscribe 其他组件订阅最新的状态,来刷新自己的视图
- dispatch 接受action,先调用reducer去更新state,然后执行subscribe中的订阅函数
- 通常在组件中通过派发dispatch行为action给store,而不是直接通知其它组件
react-redux
- 主要用于解决 react与redux的连接问题
- 核心思想: 通过最顶层React.createContext的Provide组件包裹store,像子组件传递 然后用connect高阶组件包裹子组件,在connetc中,如果是类组件就从store仓库中获取到getState最新状态,调用mapStateToProps将state映射到props,调用mapDispatchToProps将dispatch映射到props。 如果类组件通过订阅setState函数来触发组件更新,函数组件通过调用useLayoutEffect中订阅forceUpdate函数来触发组件更新。
redux的中间件
- 用于解决 action到reducer中间的状态变化
- 比如redux-logger 监控状态的日志
- 比如redux-thunk 处理函数action,默认只能派发普通对象
- 比如redux-promise 处理带promise异步的action
- 中间件的写法,必须是一个函数,里面两层返回函数,第一次参数为next,第二层为action,核心就是koa的洋葱圈模型,在applyMiddleware中利用compose组合函数依次执行中间件,每个中间件执行完毕就调用next函数向下传递,最后执行完毕
- applyMiddleware柯里化函数两端一个是 middewares 中间件数组,一个是store.dispatch,派发方法
redux-thunk
- thunk中间件可以让我们派发函数 funtion,函数中可以是异步调用
- 默认情况下,我们只能派发普通对象
function thunk({ getState, dispatch }) {
return function (next) {//为了实现中间件的级联,调用下一个中间件
return function (action) {//这才就是我们改造后的dispatch方法了
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
}
}
export default thunk;
// 使用
thunkAdd1() {
return function (dispatch, getState) {
setTimeout(function () {
dispatch({ type: types.ADD1 });
}, 2000);
}
},
// 源码
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
redux-promise
redux-saga
react-router
- hash
- history
- memory
connected-react-router
- //可以把路由的最新的信息存放到仓库里保存 //在ConnectedRouter里会监听路径变化,并路径变化的时候派发动作给仓库,通过reducer保存信息
//下面这二个文件是用来实现跳转路径的
浏览器缓存
webpack5新特性
- 持久化缓存:fileSystem memory 原理 编译会做一个快照,每次增量更新
- tree-shaking:原理类似rollup的深度作用域分析检测来实现
- 模块联邦:weback的模块联邦原理和import相似,也是做成了预留的promise坑位,通过webpackjsonp加载,获取变成发请求获取而已