异步并发数限制
/** * 关键点 * 1. new promise 一经创建,立即执行 * 2. 使用 Promise.resolve().then 可以把任务加到微任务队列,防止立即执行迭代方法 * 3. 微任务处理过程中,产生的新的微任务,会在同一事件循环内,追加到微任务队列里 * 4. 使用 race 在某个任务完成时,继续添加任务,保持任务按照最大并发数进行执行 * 5. 任务完成后,需要从 doingTasks 中移出 */function limit(count, array, iterateFunc) { const tasks = [] const doingTasks = [] let i = 0 const enqueue = () => { if (i === array.length) { return Promise.resolve() } const task = Promise.resolve().then(() => iterateFunc(array[i++])) tasks.push(task) const doing = task.then(() => doingTasks.splice(doingTasks.indexOf(doing), 1)) doingTasks.push(doing) const res = doingTasks.length >= count ? Promise.race(doingTasks) : Promise.resolve() return res.then(enqueue) }; return enqueue().then(() => Promise.all(tasks))}
// testconst timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i))limit(2, [1000, 1000, 1000, 1000], timeout).then((res) => { console.log(res)})
复制代码
实现 some 方法
Array.prototype.mySome=function(callback, context = window){ var len = this.length, flag=false, i = 0;
for(;i < len; i++){ if(callback.apply(context, [this[i], i , this])){ flag=true; break; } } return flag; }
// var flag=arr.mySome((v,index,arr)=>v.num>=10,obj) // console.log(flag);
复制代码
实现 ES6 的 extends
function B(name){ this.name = name;};function A(name,age){ //1.将A的原型指向B Object.setPrototypeOf(A,B); //2.用A的实例作为this调用B,得到继承B之后的实例,这一步相当于调用super Object.getPrototypeOf(A).call(this, name) //3.将A原有的属性添加到新实例上 this.age = age; //4.返回新实例对象 return this;};var a = new A('poetry',22);console.log(a);
复制代码
实现 call 方法
call 做了什么:
将函数设为对象的属性
执行和删除这个函数
指定this到函数并传入给定参数执行函数
如果不传入参数,默认指向为 window
// 模拟 call bar.mycall(null);//实现一个call方法:// 原理:利用 context.xxx = self obj.xx = func-->obj.xx()Function.prototype.myCall = function(context = window, ...args) { if (typeof this !== "function") { throw new Error('type error') } // this-->func context--> obj args--> 传递过来的参数
// 在context上加一个唯一值不影响context上的属性 let key = Symbol('key') context[key] = this; // context为调用的上下文,this此处为函数,将这个函数作为context的方法 // let args = [...arguments].slice(1) //第一个参数为obj所以删除,伪数组转为数组
// 绑定参数 并执行函数 let result = context[key](...args); // 清除定义的this 不删除会导致context属性越来越多 delete context[key];
// 返回结果 return result;};
复制代码
//用法:f.call(obj,arg1)function f(a,b){ console.log(a+b) console.log(this.name)}let obj={ name:1}f.myCall(obj,1,2) //否则this指向window
复制代码
实现 map 方法
回调函数的参数有哪些,返回值如何处理
不修改原来的数组
Array.prototype.myMap = function(callback, context){ // 转换类数组 var arr = Array.prototype.slice.call(this),//由于是ES5所以就不用...展开符了 mappedArr = [], i = 0;
for (; i < arr.length; i++ ){ // 把当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].map((curr,index,arr)) mappedArr.push(callback.call(context, arr[i], i, this)); } return mappedArr;}
复制代码
实现 reduce 方法
初始值不传怎么处理
回调函数的参数有哪些,返回值如何处理。
Array.prototype.myReduce = function(fn, initialValue) { var arr = Array.prototype.slice.call(this); var res, startIndex;
res = initialValue ? initialValue : arr[0]; // 不传默认取数组第一项 startIndex = initialValue ? 0 : 1;
for(var i = startIndex; i < arr.length; i++) { // 把初始值、当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].reduce((initVal,curr,index,arr)) res = fn.call(null, res, arr[i], i, this); } return res;}
复制代码
参考 前端进阶面试题详细解答
实现 Array.isArray 方法
Array.myIsArray = function(o) { return Object.prototype.toString.call(Object(o)) === '[object Array]';};
console.log(Array.myIsArray([])); // true
复制代码
实现节流函数(throttle)
节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是: 事件,按照一段时间的间隔来进行触发 。
像 dom 的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
手写简版
使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行
时间戳方式:
// func是用户传入需要防抖的函数// wait是等待时间const throttle = (func, wait = 50) => { // 上一次执行该函数的时间 let lastTime = 0 return function(...args) { // 当前时间 let now = +new Date() // 将当前时间和上一次执行函数时间对比 // 如果差值大于设置的等待时间就执行函数 if (now - lastTime > wait) { lastTime = now func.apply(this, args) } }}
setInterval( throttle(() => { console.log(1) }, 500), 1)
复制代码
定时器方式:
使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数
function throttle(func, delay){ var timer = null; returnfunction(){ var context = this; var args = arguments; if(!timer){ timer = setTimeout(function(){ func.apply(context, args); timer = null; },delay); } }}
复制代码
适用场景:
总结
实现迭代器生成函数
我们说迭代器对象全凭迭代器生成函数帮我们生成。在ES6中,实现一个迭代器生成函数并不是什么难事儿,因为 ES6 早帮我们考虑好了全套的解决方案,内置了贴心的 生成器 (Generator)供我们使用:
// 编写一个迭代器生成函数function *iteratorGenerator() { yield '1号选手' yield '2号选手' yield '3号选手'}
const iterator = iteratorGenerator()
iterator.next()iterator.next()iterator.next()
复制代码
丢进控制台,不负众望:
写一个生成器函数并没有什么难度,但在面试的过程中,面试官往往对生成器这种语法糖背后的实现逻辑更感兴趣。下面我们要做的,不仅仅是写一个迭代器对象,而是用ES5去写一个能够生成迭代器对象的迭代器生成函数(解析在注释里):
// 定义生成器函数,入参是任意集合function iteratorGenerator(list) { // idx记录当前访问的索引 var idx = 0 // len记录传入集合的长度 var len = list.length return { // 自定义next方法 next: function() { // 如果索引还没有超出集合长度,done为false var done = idx >= len // 如果done为false,则可以继续取值 var value = !done ? list[idx++] : undefined
// 将当前值与遍历是否完毕(done)返回 return { done: done, value: value } } }}
var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手'])iterator.next()iterator.next()iterator.next()
复制代码
此处为了记录每次遍历的位置,我们实现了一个闭包,借助自由变量来做我们的迭代过程中的“游标”。
运行一下我们自定义的迭代器,结果符合预期:
实现数组的 map 方法
Array.prototype._map = function(fn) { if (typeof fn !== "function") { throw Error('参数必须是一个函数'); } const res = []; for (let i = 0, len = this.length; i < len; i++) { res.push(fn(this[i])); } return res;}
复制代码
实现 Object.is
Object.is不会转换被比较的两个值的类型,这点和===更为相似,他们之间也存在一些区别
Object.is = function (x, y) { if (x === y) { // 当前情况下,只有一种情况是特殊的,即 +0 -0 // 如果 x !== 0,则返回true // 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断 return x !== 0 || 1 / x === 1 / y; }
// x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样 // x和y同时为NaN时,返回true return x !== x && y !== y;};
复制代码
手写节流函数
函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。
// 函数节流的实现;function throttle(fn, delay) { let curTime = Date.now();
return function() { let context = this, args = arguments, nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - curTime >= delay) { curTime = Date.now(); return fn.apply(context, args); } };}
复制代码
实现 Event(event bus)
event bus 既是 node 中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础。
简单版:
class EventEmeitter { constructor() { this._events = this._events || new Map(); // 储存事件/回调键值对 this._maxListeners = this._maxListeners || 10; // 设立监听上限 }}
// 触发名为type的事件EventEmeitter.prototype.emit = function(type, ...args) { let handler; // 从储存事件键值对的this._events中获取对应事件回调函数 handler = this._events.get(type); if (args.length > 0) { handler.apply(this, args); } else { handler.call(this); } return true;};
// 监听名为type的事件EventEmeitter.prototype.addListener = function(type, fn) { // 将type事件以及对应的fn函数放入this._events中储存 if (!this._events.get(type)) { this._events.set(type, fn); }};
复制代码
面试版:
class EventEmeitter { constructor() { this._events = this._events || new Map(); // 储存事件/回调键值对 this._maxListeners = this._maxListeners || 10; // 设立监听上限 }}
// 触发名为type的事件EventEmeitter.prototype.emit = function(type, ...args) { let handler; // 从储存事件键值对的this._events中获取对应事件回调函数 handler = this._events.get(type); if (args.length > 0) { handler.apply(this, args); } else { handler.call(this); } return true;};
// 监听名为type的事件EventEmeitter.prototype.addListener = function(type, fn) { // 将type事件以及对应的fn函数放入this._events中储存 if (!this._events.get(type)) { this._events.set(type, fn); }};
// 触发名为type的事件EventEmeitter.prototype.emit = function(type, ...args) { let handler; handler = this._events.get(type); if (Array.isArray(handler)) { // 如果是一个数组说明有多个监听者,需要依次此触发里面的函数 for (let i = 0; i < handler.length; i++) { if (args.length > 0) { handler[i].apply(this, args); } else { handler[i].call(this); } } } else { // 单个函数的情况我们直接触发即可 if (args.length > 0) { handler.apply(this, args); } else { handler.call(this); } }
return true;};
// 监听名为type的事件EventEmeitter.prototype.addListener = function(type, fn) { const handler = this._events.get(type); // 获取对应事件名称的函数清单 if (!handler) { this._events.set(type, fn); } else if (handler && typeof handler === "function") { // 如果handler是函数说明只有一个监听者 this._events.set(type, [handler, fn]); // 多个监听者我们需要用数组储存 } else { handler.push(fn); // 已经有多个监听者,那么直接往数组里push函数即可 }};
EventEmeitter.prototype.removeListener = function(type, fn) { const handler = this._events.get(type); // 获取对应事件名称的函数清单
// 如果是函数,说明只被监听了一次 if (handler && typeof handler === "function") { this._events.delete(type, fn); } else { let postion; // 如果handler是数组,说明被监听多次要找到对应的函数 for (let i = 0; i < handler.length; i++) { if (handler[i] === fn) { postion = i; } else { postion = -1; } } // 如果找到匹配的函数,从数组中清除 if (postion !== -1) { // 找到数组对应的位置,直接清除此回调 handler.splice(postion, 1); // 如果清除后只有一个函数,那么取消数组,以函数形式保存 if (handler.length === 1) { this._events.set(type, handler[0]); } } else { return this; } }};
复制代码
实现具体过程和思路见实现event
实现数组元素求和
arr=[1,2,3,4,5,6,7,8,9,10],求和
let arr=[1,2,3,4,5,6,7,8,9,10]let sum = arr.reduce( (total,i) => total += i,0);console.log(sum);
复制代码
arr=[1,2,3,[[4,5],6],7,8,9],求和
var = arr=[1,2,3,[[4,5],6],7,8,9]let arr= arr.toString().split(',').reduce( (total,i) => total += Number(i),0);console.log(arr);
复制代码
递归实现:
let arr = [1, 2, 3, 4, 5, 6]
function add(arr) { if (arr.length == 1) return arr[0] return arr[0] + add(arr.slice(1)) }console.log(add(arr)) // 21
复制代码
实现类的继承
实现类的继承-简版
类的继承在几年前是重点内容,有 n 种继承方式各有优劣,es6 普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。
// 寄生组合继承function Parent(name) { this.name = name}Parent.prototype.say = function() { console.log(this.name + ` say`);}Parent.prototype.play = function() { console.log(this.name + ` play`);}
function Child(name, parent) { // 将父类的构造函数绑定在子类上 Parent.call(this, parent) this.name = name}
/** 1. 这一步不用Child.prototype = Parent.prototype的原因是怕共享内存,修改父类原型对象就会影响子类 2. 不用Child.prototype = new Parent()的原因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性3. Object.create是创建了父类原型的副本,与父类原型完全隔离*/Child.prototype = Object.create(Parent.prototype);Child.prototype.say = function() { console.log(this.name + ` say`);}
// 注意记得把子类的构造指向子类本身Child.prototype.constructor = Child;
复制代码
// 测试var parent = new Parent('parent');parent.say()
var child = new Child('child');child.say() child.play(); // 继承父类的方法
复制代码
ES5 实现继承-详细
第一种方式是借助 call 实现继承
function Parent1(){ this.name = 'parent1';}function Child1(){ Parent1.call(this); this.type = 'child1' }console.log(new Child1);
复制代码
这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类中一旦存在方法那么子类无法继承。那么引出下面的方法
第二种方式借助原型链实现继承:
function Parent2() { this.name = 'parent2'; this.play = [1, 2, 3] } function Child2() { this.type = 'child2'; } Child2.prototype = new Parent2();
console.log(new Child2());
复制代码
看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:
var s1 = new Child2(); var s2 = new Child2(); s1.play.push(4); console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4]
复制代码
明明我只改变了 s1 的 play 属性,为什么 s2 也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象
第三种方式:将前两种组合:
function Parent3 () { this.name = 'parent3'; this.play = [1, 2, 3]; } function Child3() { Parent3.call(this); this.type = 'child3'; } Child3.prototype = new Parent3(); var s3 = new Child3(); var s4 = new Child3(); s3.play.push(4); console.log(s3.play, s4.play); // [1,2,3,4] [1,2,3]
复制代码
之前的问题都得以解决。但是这里又徒增了一个新问题,那就是 Parent3 的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是我们不愿看到的。那么如何解决这个问题?
第四种方式: 组合继承的优化 1
function Parent4 () { this.name = 'parent4'; this.play = [1, 2, 3]; } function Child4() { Parent4.call(this); this.type = 'child4'; } Child4.prototype = Parent4.prototype;
复制代码
这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下
var s3 = new Child4(); var s4 = new Child4(); console.log(s3)
复制代码
子类实例的构造函数是 Parent4,显然这是不对的,应该是 Child4。
第五种方式(最推荐使用):优化 2
function Parent5 () { this.name = 'parent5'; this.play = [1, 2, 3]; } function Child5() { Parent5.call(this); this.type = 'child5'; } Child5.prototype = Object.create(Parent5.prototype); Child5.prototype.constructor = Child5;
复制代码
这是最推荐的一种方式,接近完美的继承。
实现 Array.of 方法
Array.of()方法用于将一组值,转换为数组
Array.of(3, 11, 8) // [3,11,8]Array.of(3) // [3]Array.of(3).length // 1
复制代码
实现
function ArrayOf(){ return [].slice.call(arguments);}
复制代码
实现深拷贝
简洁版本
简单版:
const newObj = JSON.parse(JSON.stringify(oldObj));
复制代码
局限性:
面试简版
function deepClone(obj) { // 如果是 值类型 或 null,则直接return if(typeof obj !== 'object' || obj === null) { return obj }
// 定义结果对象 let copy = {}
// 如果对象是数组,则定义结果数组 if(obj.constructor === Array) { copy = [] }
// 遍历对象的key for(let key in obj) { // 如果key是对象的自有属性 if(obj.hasOwnProperty(key)) { // 递归调用深拷贝方法 copy[key] = deepClone(obj[key]) } }
return copy}
复制代码
调用深拷贝方法,若属性为值类型,则直接返回;若属性为引用类型,则递归遍历。这就是我们在解这一类题时的核心的方法。
进阶版
// 递归拷贝 (类型判断)function deepClone(value,hash = new WeakMap){ // 弱引用,不用map,weakMap更合适一点 // null 和 undefiend 是不需要拷贝的 if(value == null){ return value;} if(value instanceof RegExp) { return new RegExp(value) } if(value instanceof Date) { return new Date(value) } // 函数是不需要拷贝 if(typeof value != 'object') return value; let obj = new value.constructor(); // [] {} // 说明是一个对象类型 if(hash.get(value)){ return hash.get(value) } hash.set(value,obj); for(let key in value){ // in 会遍历当前对象上的属性 和 __proto__指代的属性 // 补拷贝 对象的__proto__上的属性 if(value.hasOwnProperty(key)){ // 如果值还有可能是对象 就继续拷贝 obj[key] = deepClone(value[key],hash); } } return obj // 区分对象和数组 Object.prototype.toString.call}
复制代码
// test
var o = {};o.x = o;var o1 = deepClone(o); // 如果这个对象拷贝过了 就返回那个拷贝的结果就可以了console.log(o1);
复制代码
实现完整的深拷贝
1. 简易版及问题
JSON.parse(JSON.stringify());
复制代码
估计这个 api 能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:
无法解决循环引用的问题。举个例子:
const a = {val:2};a.target = a;
复制代码
拷贝a会出现系统栈溢出,因为出现了无限递归的情况。
无法拷贝一些特殊的对象,诸如 RegExp, Date, Set, Map等
无法拷贝函数(划重点)。
因此这个 api 先 pass 掉,我们重新写一个深拷贝,简易版如下:
const deepClone = (target) => { if (typeof target === 'object' && target !== null) { const cloneTarget = Array.isArray(target) ? []: {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop]); } } return cloneTarget; } else { return target; }}
复制代码
现在,我们以刚刚发现的三个问题为导向,一步步来完善、优化我们的深拷贝代码。
2. 解决循环引用
现在问题如下:
let obj = {val : 100};obj.target = obj;
deepClone(obj);//报错: RangeError: Maximum call stack size exceeded
复制代码
这就是循环引用。我们怎么来解决这个问题呢?
创建一个 Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
const deepClone = (target, map = new Map()) => { if(map.get(target)) return target;
if (isObject(target)) { map.set(target, true); const cloneTarget = Array.isArray(target) ? []: {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop],map); } } return cloneTarget; } else { return target; } }
复制代码
现在来试一试:
const a = {val:2};a.target = a;let newA = deepClone(a);console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } }
复制代码
好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是 map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了
在计算机程序设计中,弱引用与强引用相对,
被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a 一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放。
怎么解决这个问题?
很简单,让 map 的 key 和 map 构成弱引用即可。ES6 给我们提供了这样的数据结构,它的名字叫 WeakMap,它是一种特殊的 Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的
稍微改造一下即可:
const deepClone = (target, map = new WeakMap()) => { //...}
复制代码
3. 拷贝特殊对象
可继续遍历
对于特殊的对象,我们使用以下方式来鉴别:
Object.prototype.toString.call(obj);
复制代码
梳理一下对于可遍历对象会有什么结果:
["object Map"]["object Set"]["object Array"]["object Object"]["object Arguments"]
复制代码
以这些不同的字符串为依据,我们就可以成功地鉴别这些对象。
const getType = Object.prototype.toString.call(obj);
const canTraverse = { '[object Map]': true, '[object Set]': true, '[object Array]': true, '[object Object]': true, '[object Arguments]': true,};
const deepClone = (target, map = new Map()) => { if(!isObject(target)) return target; let type = getType(target); let cloneTarget; if(!canTraverse[type]) { // 处理不能遍历的对象 return; }else { // 这波操作相当关键,可以保证对象的原型不丢失! let ctor = target.prototype; cloneTarget = new ctor(); }
if(map.get(target)) return target; map.put(target, true);
if(type === mapTag) { //处理Map target.forEach((item, key) => { cloneTarget.set(deepClone(key), deepClone(item)); }) }
if(type === setTag) { //处理Set target.forEach(item => { target.add(deepClone(item)); }) }
// 处理数组和对象 for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop]); } } return cloneTarget;}
复制代码
不可遍历的对象
const boolTag = '[object Boolean]';const numberTag = '[object Number]';const stringTag = '[object String]';const dateTag = '[object Date]';const errorTag = '[object Error]';const regexpTag = '[object RegExp]';const funcTag = '[object Function]';
复制代码
对于不可遍历的对象,不同的对象有不同的处理。
const handleRegExp = (target) => { const { source, flags } = target; return new target.constructor(source, flags);}
const handleFunc = (target) => { // 待会的重点部分}
const handleNotTraverse = (target, tag) => { const Ctor = targe.constructor; switch(tag) { case boolTag: case numberTag: case stringTag: case errorTag: case dateTag: return new Ctor(target); case regexpTag: return handleRegExp(target); case funcTag: return handleFunc(target); default: return new Ctor(target); }}
复制代码
4. 拷贝函数
虽然函数也是对象,但是它过于特殊,我们单独把它拿出来拆解。
提到函数,在 JS 种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是
Function 的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那我们只需要
处理普通函数的情况,箭头函数直接返回它本身就好了。
那么如何来区分两者呢?
答案是: 利用原型。箭头函数是不存在原型的。
const handleFunc = (func) => { // 箭头函数直接返回自身 if(!func.prototype) return func; const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); // 分别匹配 函数参数 和 函数体 const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if(!body) return null; if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); }}
复制代码
5. 完整代码展示
const getType = obj => Object.prototype.toString.call(obj);
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
const canTraverse = { '[object Map]': true, '[object Set]': true, '[object Array]': true, '[object Object]': true, '[object Arguments]': true,};const mapTag = '[object Map]';const setTag = '[object Set]';const boolTag = '[object Boolean]';const numberTag = '[object Number]';const stringTag = '[object String]';const symbolTag = '[object Symbol]';const dateTag = '[object Date]';const errorTag = '[object Error]';const regexpTag = '[object RegExp]';const funcTag = '[object Function]';
const handleRegExp = (target) => { const { source, flags } = target; return new target.constructor(source, flags);}
const handleFunc = (func) => { // 箭头函数直接返回自身 if(!func.prototype) return func; const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); // 分别匹配 函数参数 和 函数体 const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if(!body) return null; if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); }}
const handleNotTraverse = (target, tag) => { const Ctor = target.constructor; switch(tag) { case boolTag: return new Object(Boolean.prototype.valueOf.call(target)); case numberTag: return new Object(Number.prototype.valueOf.call(target)); case stringTag: return new Object(String.prototype.valueOf.call(target)); case symbolTag: return new Object(Symbol.prototype.valueOf.call(target)); case errorTag: case dateTag: return new Ctor(target); case regexpTag: return handleRegExp(target); case funcTag: return handleFunc(target); default: return new Ctor(target); }}
const deepClone = (target, map = new WeakMap()) => { if(!isObject(target)) return target; let type = getType(target); let cloneTarget; if(!canTraverse[type]) { // 处理不能遍历的对象 return handleNotTraverse(target, type); }else { // 这波操作相当关键,可以保证对象的原型不丢失! let ctor = target.constructor; cloneTarget = new ctor(); }
if(map.get(target)) return target; map.set(target, true);
if(type === mapTag) { //处理Map target.forEach((item, key) => { cloneTarget.set(deepClone(key, map), deepClone(item, map)); }) }
if(type === setTag) { //处理Set target.forEach(item => { cloneTarget.add(deepClone(item, map)); }) }
// 处理数组和对象 for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop], map); } } return cloneTarget;}
复制代码
实现 prototype 继承
所谓的原型链继承就是让新实例的原型等于父类的实例:
//父方法function SupperFunction(flag1){ this.flag1 = flag1;}
//子方法function SubFunction(flag2){ this.flag2 = flag2;}
//父实例var superInstance = new SupperFunction(true);
//子继承父SubFunction.prototype = superInstance;
//子实例var subInstance = new SubFunction(false);//子调用自己和父的属性subInstance.flag1; // truesubInstance.flag2; // false
复制代码
实现 AJAX 请求
AJAX 是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
创建 AJAX 请求的步骤:
创建一个 XMLHttpRequest 对象。
在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发 onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
const SERVER_URL = "/server";let xhr = new XMLHttpRequest();// 创建 Http 请求xhr.open("GET", SERVER_URL, true);// 设置状态监听函数xhr.onreadystatechange = function() { if (this.readyState !== 4) return; // 当请求成功时 if (this.status === 200) { handle(this.response); } else { console.error(this.statusText); }};// 设置请求失败时的监听函数xhr.onerror = function() { console.error(this.statusText);};// 设置请求头信息xhr.responseType = "json";xhr.setRequestHeader("Accept", "application/json");// 发送 Http 请求xhr.send(null);
复制代码
实现 async/await
分析
// generator生成器 生成迭代器iterator
// 默认这样写的类数组是不能被迭代的,缺少迭代方法let likeArray = {'0': 1, '1': 2, '2': 3, '3': 4, length: 4}
// // 使用迭代器使得可以展开数组// // Symbol有很多元编程方法,可以改js本身功能// likeArray[Symbol.iterator] = function () {// // 迭代器是一个对象 对象中有next方法 每次调用next 都需要返回一个对象 {value,done}// let index = 0// return {// next: ()=>{// // 会自动调用这个方法// console.log('index',index)// return {// // this 指向likeArray// value: this[index],// done: index++ === this.length// }// }// }// }// let arr = [...likeArray]
// console.log('arr', arr)
// 使用生成器返回迭代器// likeArray[Symbol.iterator] = function *() {// let index = 0// while (index != this.length) {// yield this[index++]// }// }// let arr = [...likeArray]
// console.log('arr', arr)
// 生成器 碰到yield就会暂停// function *read(params) {// yield 1;// yield 2;// }// 生成器返回的是迭代器// let it = read()// console.log(it.next())// console.log(it.next())// console.log(it.next())
// 通过generator来优化promise(promise的缺点是不停的链式调用)const fs = require('fs')const path = require('path')// const co = require('co') // 帮我们执行generator
const promisify = fn=>{ return (...args)=>{ return new Promise((resolve,reject)=>{ fn(...args, (err,data)=>{ if(err) { reject(err) } resolve(data) }) }) }}
// promise化let asyncReadFile = promisify(fs.readFile)
function * read() { let content1 = yield asyncReadFile(path.join(__dirname,'./data/name.txt'),'utf8') let content2 = yield asyncReadFile(path.join(__dirname,'./data/' + content1),'utf8') return content2}
// 这样写太繁琐 需要借助co来实现// let re = read()// let {value,done} = re.next()// value.then(data=>{// // 除了第一次传参没有意义外 剩下的传参都赋予了上一次的返回值 // let {value,done} = re.next(data) // value.then(d=>{// let {value,done} = re.next(d)// console.log(value,done)// })// }).catch(err=>{// re.throw(err) // 手动抛出错误 可以被try catch捕获// })
// 实现co原理function co(it) {// it 迭代器 return new Promise((resolve,reject)=>{ // 异步迭代 需要根据函数来实现 function next(data) { // 递归得有中止条件 let {value,done} = it.next(data) if(done) { resolve(value) // 直接让promise变成成功 用当前返回的结果 } else { // Promise.resolve(value).then(data=>{ // next(data) // }).catch(err=>{ // reject(err) // }) // 简写 Promise.resolve(value).then(next,reject) } } // 首次调用 next() })}
co(read()).then(d=>{ console.log(d)}).catch(err=>{ console.log(err,'--')})
复制代码
整体看一下结构
function asyncToGenerator(generatorFunc) { return function() { const gen = generatorFunc.apply(this, arguments) return new Promise((resolve, reject) => { function step(key, arg) { let generatorResult try { generatorResult = gen[key](arg) } catch (error) { return reject(error) } const { value, done } = generatorResult if (done) { return resolve(value) } else { return Promise.resolve(value).then(val => step('next', val), err => step('throw', err)) } } step("next") }) }}
复制代码
分析
function asyncToGenerator(generatorFunc) { // 返回的是一个新的函数 return function() {
// 先调用generator函数 生成迭代器 // 对应 var gen = testG() const gen = generatorFunc.apply(this, arguments)
// 返回一个promise 因为外部是用.then的方式 或者await的方式去使用这个函数的返回值的 // var test = asyncToGenerator(testG) // test().then(res => console.log(res)) return new Promise((resolve, reject) => {
// 内部定义一个step函数 用来一步一步的跨过yield的阻碍 // key有next和throw两种取值,分别对应了gen的next和throw方法 // arg参数则是用来把promise resolve出来的值交给下一个yield function step(key, arg) { let generatorResult
// 这个方法需要包裹在try catch中 // 如果报错了 就把promise给reject掉 外部通过.catch可以获取到错误 try { generatorResult = gen[key](arg) } catch (error) { return reject(error) }
// gen.next() 得到的结果是一个 { value, done } 的结构 const { value, done } = generatorResult
if (done) { // 如果已经完成了 就直接resolve这个promise // 这个done是在最后一次调用next后才会为true // 以本文的例子来说 此时的结果是 { done: true, value: 'success' } // 这个value也就是generator函数最后的返回值 return resolve(value) } else { // 除了最后结束的时候外,每次调用gen.next() // 其实是返回 { value: Promise, done: false } 的结构, // 这里要注意的是Promise.resolve可以接受一个promise为参数 // 并且这个promise参数被resolve的时候,这个then才会被调用 return Promise.resolve( // 这个value对应的是yield后面的promise value ).then( // value这个promise被resove的时候,就会执行next // 并且只要done不是true的时候 就会递归的往下解开promise // 对应gen.next().value.then(value => { // gen.next(value).value.then(value2 => { // gen.next() // // // 此时done为true了 整个promise被resolve了 // // 最外部的test().then(res => console.log(res))的then就开始执行了 // }) // }) function onResolve(val) { step("next", val) }, // 如果promise被reject了 就再次进入step函数 // 不同的是,这次的try catch中调用的是gen.throw(err) // 那么自然就被catch到 然后把promise给reject掉啦 function onReject(err) { step("throw", err) }, ) } } step("next") }) }}
复制代码
评论