event 模块
实现 node 中回调函数的机制,node 中回调函数其实是内部使用了观察者模式。
观察者模式:定义了对象间一种一对多的依赖关系,当目标对象 Subject 发生改变时,所有依赖它的对象 Observer 都会得到通知。
function EventEmitter() { this.events = new Map();}
// 需要实现的一些方法:// addListener、removeListener、once、removeAllListeners、emit
// 模拟实现addlistener方法const wrapCallback = (fn, once = false) => ({ callback: fn, once });EventEmitter.prototype.addListener = function(type, fn, once = false) { const hanlder = this.events.get(type); if (!hanlder) { // 没有type绑定事件 this.events.set(type, wrapCallback(fn, once)); } else if (hanlder && typeof hanlder.callback === 'function') { // 目前type事件只有一个回调 this.events.set(type, [hanlder, wrapCallback(fn, once)]); } else { // 目前type事件数>=2 hanlder.push(wrapCallback(fn, once)); }}// 模拟实现removeListenerEventEmitter.prototype.removeListener = function(type, listener) { const hanlder = this.events.get(type); if (!hanlder) return; if (!Array.isArray(this.events)) { if (hanlder.callback === listener.callback) this.events.delete(type); else return; } for (let i = 0; i < hanlder.length; i++) { const item = hanlder[i]; if (item.callback === listener.callback) { hanlder.splice(i, 1); i--; if (hanlder.length === 1) { this.events.set(type, hanlder[0]); } } }}// 模拟实现once方法EventEmitter.prototype.once = function(type, listener) { this.addListener(type, listener, true);}// 模拟实现emit方法EventEmitter.prototype.emit = function(type, ...args) { const hanlder = this.events.get(type); if (!hanlder) return; if (Array.isArray(hanlder)) { hanlder.forEach(item => { item.callback.apply(this, args); if (item.once) { this.removeListener(type, item); } }) } else { hanlder.callback.apply(this, args); if (hanlder.once) { this.events.delete(type); } } return true;}EventEmitter.prototype.removeAllListeners = function(type) { const hanlder = this.events.get(type); if (!hanlder) return; this.events.delete(type);}
复制代码
实现 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
模板引擎实现
let template = '我是{{name}},年龄{{age}},性别{{sex}}';let data = { name: '姓名', age: 18}render(template, data); // 我是姓名,年龄18,性别undefined
复制代码
function render(template, data) { const reg = /\{\{(\w+)\}\}/; // 模板字符串正则 if (reg.test(template)) { // 判断模板里是否有模板字符串 const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段 template = template.replace(reg, data[name]); // 将第一个模板字符串渲染 return render(template, data); // 递归的渲染并返回渲染后的结构 } return template; // 如果模板没有模板字符串直接返回}
复制代码
实现深拷贝
浅拷贝: 浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用 Object.assign 和展开运算符来实现。
深拷贝: 深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败
(1)JSON.stringify()
JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringify 将js对象序列化(JSON 字符串),再使用JSON.parse来反序列化(还原)js对象。
这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。
let obj1 = { a: 0, b: { c: 0 } };let obj2 = JSON.parse(JSON.stringify(obj1));obj1.a = 1;obj1.b.c = 1;console.log(obj1); // {a: 1, b: {c: 1}}console.log(obj2); // {a: 0, b: {c: 0}}
复制代码
(2)函数库 lodash 的_.cloneDeep 方法
该函数库也有提供_.cloneDeep 用来做 Deep Copy
var _ = require('lodash');var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3]};var obj2 = _.cloneDeep(obj1);console.log(obj1.b.f === obj2.b.f);// false
复制代码
(3)手写实现深拷贝函数
// 深拷贝的实现function deepCopy(object) { if (!object || typeof object !== "object") return;
let newObject = Array.isArray(object) ? [] : {};
for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = typeof object[key] === "object" ? deepCopy(object[key]) : object[key]; } }
return newObject;}
复制代码
参考:前端手写面试题详细解答
实现 Ajax
步骤
了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。
实现有并行限制的 Promise 调度器
题目描述:JS 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个
addTask(1000,"1"); addTask(500,"2"); addTask(300,"3"); addTask(400,"4"); 的输出顺序是:2 3 1 4
整个的完整执行流程:
一开始1、2两个任务开始执行500ms时,2任务执行完毕,输出2,任务3开始执行800ms时,3任务执行完毕,输出3,任务4开始执行1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行1200ms时,4任务执行完毕,输出4
复制代码
实现代码如下:
class Scheduler { constructor(limit) { this.queue = []; this.maxCount = limit; this.runCounts = 0; } add(time, order) { const promiseCreator = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(order); resolve(); }, time); }); }; this.queue.push(promiseCreator); } taskStart() { for (let i = 0; i < this.maxCount; i++) { this.request(); } } request() { if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) { return; } this.runCounts++; this.queue .shift()() .then(() => { this.runCounts--; this.request(); }); }}const scheduler = new Scheduler(2);const addTask = (time, order) => { scheduler.add(time, order);};addTask(1000, "1");addTask(500, "2");addTask(300, "3");addTask(400, "4");scheduler.taskStart();
复制代码
手写 instanceof 方法
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
实现步骤:
首先获取类型的原型
然后获得对象的原型
然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
具体实现:
function myInstanceof(left, right) { let proto = Object.getPrototypeOf(left), // 获取对象的原型 prototype = right.prototype; // 获取构造函数的 prototype 对象
// 判断构造函数的 prototype 对象是否在对象的原型链上 while (true) { if (!proto) return false; if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto); }}
复制代码
实现 JSON.parse
var json = '{"name":"cxk", "age":25}';var obj = eval("(" + json + ")");
复制代码
此方法属于黑魔法,极易容易被 xss 攻击,还有一种new Function大同小异。
手写 call 函数
call 函数的实现步骤:
判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
判断传入上下文对象是否存在,如果不存在,则设置为 window 。
处理传入的参数,截取第一个参数后的所有参数。
将函数作为上下文对象的一个属性。
使用上下文对象来调用这个方法,并保存返回结果。
删除刚才新增的属性。
返回结果。
// call函数实现Function.prototype.myCall = function(context) { // 判断调用对象 if (typeof this !== "function") { console.error("type error"); } // 获取参数 let args = [...arguments].slice(1), result = null; // 判断 context 是否传入,如果未传入则设置为 window context = context || window; // 将调用函数设为对象的方法 context.fn = this; // 调用函数 result = context.fn(...args); // 将属性删除 delete context.fn; return result;};
复制代码
二分查找
function search(arr, target, start, end) { let targetIndex = -1;
let mid = Math.floor((start + end) / 2);
if (arr[mid] === target) { targetIndex = mid; return targetIndex; }
if (start >= end) { return targetIndex; }
if (arr[mid] < target) { return search(arr, target, mid + 1, end); } else { return search(arr, target, start, mid - 1); }}
// const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];// const position = search(dataArr, 6, 0, dataArr.length - 1);// if (position !== -1) {// console.log(`目标元素在数组中的位置:${position}`);// } else {// console.log("目标元素不在数组中");// }
复制代码
实现 new 的过程
new 操作符做了这些事:
创建一个全新的对象
这个对象的__proto__要指向构造函数的原型 prototype
执行构造函数,使用 call/apply 改变 this 的指向
返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象
function myNew(fn, ...args) { // 基于原型链 创建一个新对象 let newObj = Object.create(fn.prototype); // 添加属性到新对象上 并获取obj函数的结果 let res = fn.apply(newObj, args); // 改变this指向
// 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象 return typeof res === 'object' ? res: newObj;}
复制代码
// 用法function Person(name, age) { this.name = name; this.age = age;}Person.prototype.say = function() { console.log(this.age);};let p1 = myNew(Person, "poety", 18);console.log(p1.name);console.log(p1);p1.say();
复制代码
实现 instanceOf
思路:
步骤 1:先取得当前类的原型,当前实例对象的原型链
步骤 2:一直循环(执行原型链的查找机制)
取得当前实例对象原型链的原型链(proto = proto.__proto__,沿着原型链一直向上查找)
如果 当前实例的原型链__proto__上找到了当前类的原型prototype,则返回 true
如果 一直找到Object.prototype.__proto__ == null,Object的基类(null)上面都没找到,则返回 false
// 实例.__ptoto__ === 类.prototypefunction _instanceof(example, classFunc) { // 由于instance要检测的是某对象,需要有一个前置判断条件 //基本数据类型直接返回false if(typeof example !== 'object' || example === null) return false;
let proto = Object.getPrototypeOf(example); while(true) { if(proto == null) return false;
// 在当前实例对象的原型链上,找到了当前类 if(proto == classFunc.prototype) return true; // 沿着原型链__ptoto__一层一层向上查 proto = Object.getPrototypeof(proto); // 等于proto.__ptoto__ }}
console.log('test', _instanceof(null, Array)) // falseconsole.log('test', _instanceof([], Array)) // trueconsole.log('test', _instanceof('', Array)) // falseconsole.log('test', _instanceof({}, Object)) // true
复制代码
实现数组元素求和
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
复制代码
交换 a,b 的值,不能用临时变量
巧妙的利用两个数的和、差:
a = a + bb = a - ba = a - b
复制代码
深克隆(deepclone)
简单版:
const newObj = JSON.parse(JSON.stringify(oldObj));
复制代码
局限性:
他无法实现对函数 、RegExp 等特殊对象的克隆
会抛弃对象的 constructor,所有的构造函数会指向 Object
对象有循环引用,会报错
面试版:
/** * deep clone * @param {[type]} parent object 需要进行克隆的对象 * @return {[type]} 深克隆后的对象 */const clone = parent => { // 判断类型 const isType = (obj, type) => { if (typeof obj !== "object") return false; const typeString = Object.prototype.toString.call(obj); let flag; switch (type) { case "Array": flag = typeString === "[object Array]"; break; case "Date": flag = typeString === "[object Date]"; break; case "RegExp": flag = typeString === "[object RegExp]"; break; default: flag = false; } return flag; };
// 处理正则 const getRegExp = re => { var flags = ""; if (re.global) flags += "g"; if (re.ignoreCase) flags += "i"; if (re.multiline) flags += "m"; return flags; }; // 维护两个储存循环引用的数组 const parents = []; const children = [];
const _clone = parent => { if (parent === null) return null; if (typeof parent !== "object") return parent;
let child, proto;
if (isType(parent, "Array")) { // 对数组做特殊处理 child = []; } else if (isType(parent, "RegExp")) { // 对正则对象做特殊处理 child = new RegExp(parent.source, getRegExp(parent)); if (parent.lastIndex) child.lastIndex = parent.lastIndex; } else if (isType(parent, "Date")) { // 对Date对象做特殊处理 child = new Date(parent.getTime()); } else { // 处理对象原型 proto = Object.getPrototypeOf(parent); // 利用Object.create切断原型链 child = Object.create(proto); }
// 处理循环引用 const index = parents.indexOf(parent);
if (index != -1) { // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象 return children[index]; } parents.push(parent); children.push(child);
for (let i in parent) { // 递归 child[i] = _clone(parent[i]); }
return child; }; return _clone(parent);};
复制代码
局限性:
一些特殊情况没有处理: 例如 Buffer 对象、Promise、Set、Map
另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间
原理详解实现深克隆
手写 Promise.all
1) 核心思路
接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数
这个方法返回一个新的 promise 对象,
遍历传入的参数,用 Promise.resolve()将参数"包一层",使其变成一个 promise 对象
参数所有回调成功才是成功,返回值数组与参数顺序一致
参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。
2)实现代码
一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了
function promiseAll(promises) { return new Promise(function(resolve, reject) { if(!Array.isArray(promises)){ throw new TypeError(`argument must be a array`) } var resolvedCounter = 0; var promiseNum = promises.length; var resolvedResult = []; for (let i = 0; i < promiseNum; i++) { Promise.resolve(promises[i]).then(value=>{ resolvedCounter++; resolvedResult[i] = value; if (resolvedCounter == promiseNum) { return resolve(resolvedResult) } },error=>{ return reject(error) }) } })}// testlet p1 = new Promise(function (resolve, reject) { setTimeout(function () { resolve(1) }, 1000)})let p2 = new Promise(function (resolve, reject) { setTimeout(function () { resolve(2) }, 2000)})let p3 = new Promise(function (resolve, reject) { setTimeout(function () { resolve(3) }, 3000)})promiseAll([p3, p1, p2]).then(res => { console.log(res) // [3, 1, 2]})
复制代码
实现类的继承
类的继承在几年前是重点内容,有 n 种继承方式各有优劣,es6 普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。
function Parent(name) { this.parent = name}Parent.prototype.say = function() { console.log(`${this.parent}: 你打篮球的样子像kunkun`)}function Child(name, parent) { // 将父类的构造函数绑定在子类上 Parent.call(this, parent) this.child = 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.parent}好,我是练习时长两年半的${this.child}`);}
// 注意记得把子类的构造指向子类本身Child.prototype.constructor = Child;
var parent = new Parent('father');parent.say() // father: 你打篮球的样子像kunkun
var child = new Child('cxk', 'father');child.say() // father好,我是练习时长两年半的cxk
复制代码
手写 new 操作符
在调用 new 的过程中会发生以上四件事情:
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
function objectFactory() { let newObject = null; let constructor = Array.prototype.shift.call(arguments); let result = null; // 判断参数是否是一个函数 if (typeof constructor !== "function") { console.error("type error"); return; } // 新建一个空对象,对象的原型为构造函数的 prototype 对象 newObject = Object.create(constructor.prototype); // 将 this 指向新建对象,并执行函数 result = constructor.apply(newObject, arguments); // 判断返回对象 let flag = result && (typeof result === "object" || typeof result === "function"); // 判断返回结果 return flag ? result : newObject;}// 使用方法objectFactory(构造函数, 初始化参数);
复制代码
手写 Promise
const PENDING = "pending";const RESOLVED = "resolved";const REJECTED = "rejected";
function MyPromise(fn) { // 保存初始化状态 var self = this;
// 初始化状态 this.state = PENDING;
// 用于保存 resolve 或者 rejected 传入的值 this.value = null;
// 用于保存 resolve 的回调函数 this.resolvedCallbacks = [];
// 用于保存 reject 的回调函数 this.rejectedCallbacks = [];
// 状态转变为 resolved 方法 function resolve(value) { // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变 if (value instanceof MyPromise) { return value.then(resolve, reject); }
// 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变, if (self.state === PENDING) { // 修改状态 self.state = RESOLVED;
// 设置传入的值 self.value = value;
// 执行回调函数 self.resolvedCallbacks.forEach(callback => { callback(value); }); } }, 0); }
// 状态转变为 rejected 方法 function reject(value) { // 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变 if (self.state === PENDING) { // 修改状态 self.state = REJECTED;
// 设置传入的值 self.value = value;
// 执行回调函数 self.rejectedCallbacks.forEach(callback => { callback(value); }); } }, 0); }
// 将两个方法传入函数执行 try { fn(resolve, reject); } catch (e) { // 遇到错误时,捕获错误,执行 reject 函数 reject(e); }}
MyPromise.prototype.then = function(onResolved, onRejected) { // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数 onResolved = typeof onResolved === "function" ? onResolved : function(value) { return value; };
onRejected = typeof onRejected === "function" ? onRejected : function(error) { throw error; };
// 如果是等待状态,则将函数加入对应列表中 if (this.state === PENDING) { this.resolvedCallbacks.push(onResolved); this.rejectedCallbacks.push(onRejected); }
// 如果状态已经凝固,则直接执行对应状态的函数
if (this.state === RESOLVED) { onResolved(this.value); }
if (this.state === REJECTED) { onRejected(this.value); }};
复制代码
请实现一个 add 函数,满足以下功能
add(1); // 1add(1)(2); // 3add(1)(2)(3);// 6add(1)(2, 3); // 6add(1, 2)(3); // 6add(1, 2, 3); // 6
复制代码
function add(...args) { // 在内部声明一个函数,利用闭包的特性保存并收集所有的参数值 let fn = function(...newArgs) { return add.apply(null, args.concat(newArgs)) }
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 fn.toString = function() { return args.reduce((total,curr)=> total + curr) }
return fn}
复制代码
考点:
// 测试,调用toString方法触发求值
add(1).toString(); // 1add(1)(2).toString(); // 3add(1)(2)(3).toString();// 6add(1)(2, 3).toString(); // 6add(1, 2)(3).toString(); // 6add(1, 2, 3).toString(); // 6
复制代码
评论