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)));

三、防抖节流

debouncethrottle

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的性能对比open in new window

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 请求限制

十一、跨域问题

参考跨域相关open in new window

  • 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, "&amp;");
    str = str.replace(/</g, "&lt;");
    str = str.replace(/>/g, "&gt;");
    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令牌验证

XSS和CSRFopen in new window

十三、Promise

Promise A+open in new window

1 Promise的基础逻辑

  • promise是一个类 ,无需考虑兼容性,因为每个框架都实现不一样,只需要遵守规则即可
  • 当使用promise的时候 会传入一个执行器(executor),此执行器是立即执行,需要传入两个函数
  • promise中有三个状态
    1. 等待态 Pending
    2. 成功态 Fulfilled
    3. 失败态 Rejected
  • 状态只能由 pending --> fulfilled 或者 pending --> rejected,状态一旦改版不能再进行二次修改
  • promise中使用resolve和reject两个函数来改变状态
  • then方法内部就是状态判断
    1. 如果状态是成功的,就调用成功的回调函数
    2. 如果状态是失败的,就调用失败的回调函数
// 使用方式
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的链式调用解决了什么问题?
    1. 链式调用解决嵌套回调的问题
    2. 同步并发问题
    3. 多个异步处理错误问题
  • 当调用then方法后会返回一个新的promise
    1. then中方法返回的是一个(普通值 不是promise)的情况, 会作为外层下一次then的成功结果
    2. then中方法 执行出错 会走到外层下一次then的失败结果
    3. 如果then中方法返回的是一个promise对象, 需要判断状态,然后根据promise的结果来处理是走成功还是失败 (传入的是成功或者失败的内容)
    4. 无论上一次then走是成功还是失败,只要返回的是普通值 都会执行下一次then的成功
  • 总结:
    1. 如果返回一个普通值 (除了promise) 就会传递给下一个then的成功,
    2. 如果返回一个失败的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是成功还是失败
    1. 如果成功则把成功的结果调用promise2的resolve传递进去,如果失败则同理
  • 总结:x的值决定是调用promise2的resolve还是reject
    1. 如果是promise则取他的状态
    2. 如果是普通值则直接调用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,不能自己实现自己,详情见queueMicrotaskopen in new window

// 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);
			}	
		}
	})
}

应用场景

  1. 从最快的服务器检索资源 : 来自世界各地的用户访问网站,如果你有多台服务器,则尽量使用响应速度最快的服务器,在这种情况下,可以使用 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);
})
  1. 显示第一张已加载的图片(来自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)
})

四、事件相关

事件相关open in new window

React和Vue的区别

1、react是一个UI组件库 vue是一个渐进式框架 2、监听数据变化的实现原理不同

区别open in new window

react事件处理

react合成事件open in new window

性能优化

更快的网络通信

服务器通信层

  • 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保存信息

//下面这二个文件是用来实现跳转路径的

浏览器缓存

强缓存和协商缓存open in new window

webpack5新特性

  • 持久化缓存:fileSystem memory 原理 编译会做一个快照,每次增量更新
  • tree-shaking:原理类似rollup的深度作用域分析检测来实现
  • 模块联邦:weback的模块联邦原理和import相似,也是做成了预留的promise坑位,通过webpackjsonp加载,获取变成发请求获取而已