手写 Promise.then
then
办法返回一个新的 promise
实例,为了在 promise
状态发生变化时(resolve
/ reject
被调用时)再执行 then
里的函数,咱们应用一个 callbacks
数组先把传给 then 的函数暂存起来,等状态扭转时再调用。
那么,怎么保障后一个 **then**
里的办法在前一个 **then**
(可能是异步)完结之后再执行呢? 咱们能够将传给 then
的函数和新 promise
的 resolve
一起 push
到前一个 promise
的 callbacks
数组中,达到承前启后的成果:
- 承前:以后一个
promise
实现后,调用其resolve
变更状态,在这个resolve
里会顺次调用callbacks
里的回调,这样就执行了then
里的办法了 - 启后:上一步中,当
then
里的办法执行实现后,返回一个后果,如果这个后果是个简略的值,就间接调用新promise
的resolve
,让其状态变更,这又会顺次调用新promise
的callbacks
数组里的办法,周而复始。。如果返回的后果是个promise
,则须要等它实现之后再触发新promise
的resolve
,所以能够在其后果的then
里调用新promise
的resolve
then(onFulfilled, onReject){
// 保留前一个 promise 的 this
const self = this;
return new MyPromise((resolve, reject) => {
// 封装前一个 promise 胜利时执行的函数
let fulfilled = () => {
try{const result = onFulfilled(self.value); // 承前
return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); // 启后
}catch(err){reject(err)
}
}
// 封装前一个 promise 失败时执行的函数
let rejected = () => {
try{const result = onReject(self.reason);
return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
}catch(err){reject(err)
}
}
switch(self.status){
case PENDING:
self.onFulfilledCallbacks.push(fulfilled);
self.onRejectedCallbacks.push(rejected);
break;
case FULFILLED:
fulfilled();
break;
case REJECT:
rejected();
break;
}
})
}
留神:
- 间断多个
then
里的回调办法是同步注册的,但注册到了不同的callbacks
数组中,因为每次then
都返回新的promise
实例(参考下面的例子和图) - 注册实现后开始执行构造函数中的异步事件,异步实现之后顺次调用
callbacks
数组中提前注册的回调
实现 apply 办法
思路: 利用
this
的上下文个性。apply
其实就是改一下参数的问题
Function.prototype.myApply = function(context = window, args) {
// 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); // 这里和 call 传参不一样
// 革除定义的 this 不删除会导致 context 属性越来越多
delete context[key];
// 返回后果
return result;
}
// 应用
function f(a,b){console.log(a,b)
console.log(this.name)
}
let obj={name:'张三'}
f.myApply(obj,[1,2]) //arguments[1]
AJAX
const getJSON = function(url) {return new Promise((resolve, reject) => {const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
xhr.open('GET', url, false);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {if (xhr.readyState !== 4) return;
if (xhr.status === 200 || xhr.status === 304) {resolve(xhr.responseText);
} else {reject(new Error(xhr.responseText));
}
}
xhr.send();})
}
手写 Object.create
思路:将传入的对象作为原型
function create(obj) {function F() {}
F.prototype = obj
return new F()}
模仿 Object.create
Object.create()办法创立一个新对象,应用现有的对象来提供新创建的对象的__proto__。
// 模仿 Object.create
function create(proto) {function F() {}
F.prototype = proto;
return new F();}
实现 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
参考 前端进阶面试题具体解答
手写 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(构造函数, 初始化参数);
实现 apply 办法
apply 原理与 call 很类似,不多赘述
// 模仿 apply
Function.prototype.myapply = function(context, arr) {var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {result = context.fn();
} else {var args = [];
for (var i = 0, len = arr.length; i < len; i++) {args.push("arr[" + i + "]");
}
result = eval("context.fn(" + args + ")");
}
delete context.fn;
return result;
};
实现 jsonp
// 动静的加载 js 文件
function addScript(src) {const script = document.createElement('script');
script.src = src;
script.type = "text/javascript";
document.body.appendChild(script);
}
addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");
// 设置一个全局的 callback 函数来接管回调后果
function handleRes(res) {console.log(res);
}
// 接口返回的数据格式
handleRes({a: 1, b: 2});
手写 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)
})
}
})
}
// test
let 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 秒后再执行回调,如果在这 n 秒内事件又被触发,则从新计时。这能够应用在一些点击申请的事件上,防止因为用户的屡次点击向后端发送屡次申请。
// 函数防抖的实现
function debounce(fn, wait) {
let timer = null;
return function() {
let context = this,
args = arguments;
// 如果此时存在定时器的话,则勾销之前的定时器从新记时
if (timer) {clearTimeout(timer);
timer = null;
}
// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {fn.apply(context, args);
}, wait);
};
}
实现 JSONP 办法
利用
<script>
标签不受跨域限度的特点,毛病是只能反对get
申请
- 创立
script
标签 - 设置
script
标签的src
属性,以问号传递参数,设置好回调函数callback
名称 - 插入到
html
文本中 - 调用回调函数,
res
参数就是获取的数据
function jsonp({url,params,callback}) {return new Promise((resolve,reject)=>{let script = document.createElement('script')
window[callback] = function (data) {resolve(data)
document.body.removeChild(script)
}
var arr = []
for(var key in params) {arr.push(`${key}=${params[key]}`)
}
script.type = 'text/javascript'
script.src = `${url}?callback=${callback}&${arr.join('&')}`
document.body.appendChild(script)
})
}
// 测试用例
jsonp({
url: 'http://suggest.taobao.com/sug',
callback: 'getData',
params: {
q: 'iphone 手机',
code: 'utf-8'
},
}).then(data=>{console.log(data)})
- 设置
CORS: Access-Control-Allow-Origin:*
postMessage
解析 URL Params 为对象
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 后果
{ user: 'anonymous',
id: [123, 456], // 反复呈现的 key 要组装成数组,能被转成数字的就转成数字类型
city: '北京', // 中文需解码
enabled: true, // 未指定值得 key 约定为 true
}
*/
function parseParam(url) {const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 前面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 宰割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {if (/=/.test(param)) { // 解决有 value 的参数
let [key, val] = param.split('='); // 宰割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则增加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创立 key 并设置值
paramsObj[key] = val;
}
} else { // 解决没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
实现事件总线联合 Vue 利用
Event Bus
(Vue、Flutter 等前端框架中有出镜)和Event Emitter
(Node 中有出镜)出场的“剧组”不同,然而它们都对应一个独特的角色—— 全局事件总线。
全局事件总线,严格来说不能说是观察者模式,而是公布 - 订阅模式。它在咱们日常的业务开发中利用十分广。
如果只能选一道题,那这道题肯定是
Event Bus/Event Emitter
的代码实现——我都说这么分明了,这个知识点到底要不要把握、须要把握到什么水平,就看各位本人的了。
在 Vue 中应用 Event Bus 来实现组件间的通信
Event Bus/Event Emitter
作为全局事件总线,它起到的是一个 沟通桥梁 的作用。咱们能够把它了解为一个事件核心,咱们所有事件的订阅 / 公布都不能由订阅方和公布方“私下沟通”,必须要委托这个事件核心帮咱们实现。
在 Vue 中,有时候 A 组件和 B 组件中距离了很远,看似没什么关系,但咱们心愿它们之间可能通信。这种状况下除了求助于 Vuex
之外,咱们还能够通过 Event Bus
来实现咱们的需要。
创立一个 Event Bus
(实质上也是 Vue 实例)并导出:
const EventBus = new Vue()
export default EventBus
在主文件里引入EventBus
,并挂载到全局:
import bus from 'EventBus 的文件门路'
Vue.prototype.bus = bus
订阅事件:
// 这里 func 指 someEvent 这个事件的监听函数
this.bus.$on('someEvent', func)
公布(触发)事件:
// 这里 params 指 someEvent 这个事件被触发时回调函数接管的入参
this.bus.$emit('someEvent', params)
大家会发现,整个调用过程中,没有呈现具体的发布者和订阅者(比方下面的
PrdPublisher
和DeveloperObserver
),全程只有bus
这个货色一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的公布 / 订阅操作,必须经由事件核心,禁止所有“私下交易”!
上面,咱们就一起来实现一个Event Bus
(留神看正文里的解析):
class EventEmitter {constructor() {
// handlers 是一个 map,用于存储事件与回调之间的对应关系
this.handlers = {}}
// on 办法用于装置事件监听器,它承受指标事件名和回调函数作为参数
on(eventName, cb) {
// 先检查一下指标事件名有没有对应的监听函数队列
if (!this.handlers[eventName]) {
// 如果没有,那么首先初始化一个监听函数队列
this.handlers[eventName] = []}
// 把回调函数推入指标事件的监听函数队列里去
this.handlers[eventName].push(cb)
}
// emit 办法用于触发指标事件,它承受事件名和监听函数入参作为参数
emit(eventName, ...args) {
// 查看指标事件是否有监听函数队列
if (this.handlers[eventName]) {
// 如果有,则一一调用队列里的回调函数
this.handlers[eventName].forEach((callback) => {callback(...args)
})
}
}
// 移除某个事件回调队列里的指定回调函数
off(eventName, cb) {const callbacks = this.handlers[eventName]
const index = callbacks.indexOf(cb)
if (index !== -1) {callbacks.splice(index, 1)
}
}
// 为事件注册单次监听器
once(eventName, cb) {
// 对回调函数进行包装,使其执行结束主动被移除
const wrapper = (...args) => {cb.apply(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}
在日常的开发中,大家用到
EventBus/EventEmitter
往往提供比这五个办法多的多的多的办法。但在面试过程中,如果大家可能残缺地实现出这五个办法,曾经十分能够阐明问题了,因而楼上这个EventBus
心愿大家能够熟练掌握。学有余力的同学
实现数组的 flat 办法
function _flat(arr, depth) {if(!Array.isArray(arr) || depth <= 0) {return arr;}
return arr.reduce((prev, cur) => {if (Array.isArray(cur)) {return prev.concat(_flat(cur, depth - 1))
} else {return prev.concat(cur);
}
}, []);
}
实现 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);
循环打印红黄绿
上面来看一道比拟典型的问题,通过这个问题来比照几种异步编程办法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯一直交替反复亮灯?
三个亮灯函数:
function red() {console.log('red');
}
function green() {console.log('green');
}
function yellow() {console.log('yellow');
}
这道题简单的中央在于 须要“交替反复”亮灯,而不是“亮完一次”就完结了。
(1)用 callback 实现
const task = (timer, light, callback) => {setTimeout(() => {if (light === 'red') {red()
}
else if (light === 'green') {green()
}
else if (light === 'yellow') {yellow()
}
callback()}, timer)
}
task(3000, 'red', () => {task(2000, 'green', () => {task(1000, 'yellow', Function.prototype)
})
})
这里存在一个 bug:代码只是实现了一次流程,执行后红黄绿灯别离只亮一次。该如何让它交替反复进行呢?
下面提到过递归,能够递归亮灯的一个周期:
const step = () => {task(3000, 'red', () => {task(2000, 'green', () => {task(1000, 'yellow', step)
})
})
}
step()
留神看黄灯亮的回调里又再次调用了 step 办法 以实现循环亮灯。
(2)用 promise 实现
const task = (timer, light) =>
new Promise((resolve, reject) => {setTimeout(() => {if (light === 'red') {red()
}
else if (light === 'green') {green()
}
else if (light === 'yellow') {yellow()
}
resolve()}, timer)
})
const step = () => {task(3000, 'red')
.then(() => task(2000, 'green'))
.then(() => task(2100, 'yellow'))
.then(step)
}
step()
这里将回调移除,在一次亮灯完结后,resolve 以后 promise,并仍然应用递归进行。
(3)用 async/await 实现
const taskRunner = async () => {await task(3000, 'red')
await task(2000, 'green')
await task(2100, 'yellow')
taskRunner()}
taskRunner()
手写 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);
}
}
函数柯里化的实现
函数柯里化指的是一种将应用多个参数的一个函数转换成一系列应用一个参数的函数的技术。
function curry(fn, args) {
// 获取函数须要的参数长度
let length = fn.length;
args = args || [];
return function() {let subArgs = args.slice(0);
// 拼接失去现有的所有参数
for (let i = 0; i < arguments.length; i++) {subArgs.push(arguments[i]);
}
// 判断参数的长度是否曾经满足函数所需参数的长度
if (subArgs.length >= length) {
// 如果满足,执行函数
return fn.apply(this, subArgs);
} else {
// 如果不满足,递归返回科里化的函数,期待参数的传入
return curry.call(this, fn, subArgs);
}
};
}
// es6 实现
function curry(fn, ...args) {return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}
用正则写一个依据 name 获取 cookie 中的值的办法
function getCookie(name) {var match = document.cookie.match(new RegExp('(^|)' + name + '=([^;]*)'));
if (match) return unescape(match[2]);
}
- 获取页面上的
cookie
能够应用document.cookie
这里获取到的是相似于这样的字符串:
'username=poetry; user-id=12345; user-roles=home, me, setting'
能够看到这么几个信息:
- 每一个 cookie 都是由
name=value
这样的模式存储的 - 每一项的结尾可能是一个空串
''
(比方username
的结尾其实就是), 也可能是一个空字符串' '
(比方user-id
的结尾就是) - 每一项用
";"
来辨别 - 如果某项中有多个值的时候,是用
","
来连贯的 (比方user-roles
的值) - 每一项的结尾可能是有
";"
的(比方username
的结尾),也可能是没有的 (比方user-roles
的结尾) - 所以咱们将这里的正则拆分一下:
'(^|)'
示意的就是获取每一项的结尾,因为咱们晓得如果^
不是放在[]
里的话就是示意结尾匹配。所以这里(^|)
的意思其实就被拆分为(^)
示意的匹配username
这种状况,它后面什么都没有是一个空串 (你能够把(^)
了解为^
它前面还有一个暗藏的''
);而|
示意的就是或者是一个" "
(为了匹配user-id
结尾的这种状况)+name+
这没什么好说的=([^;]*)
这里匹配的就是=
前面的值了,比方poetry
;刚刚说了^
要是放在[]
里的话就示意"除了 ^ 前面的内容都能匹配"
,也就是非的意思。所以这里([^;]*)
示意的是除了";"
这个字符串别的都匹配 (*
应该都晓得什么意思吧,匹配 0 次或屡次)- 有的大佬等号前面是这样写的
'=([^;]*)(;|$)'
,而最初为什么能够把'(;|$)'
给省略呢?因为其实最初一个cookie
项是没有';'
的,所以它能够合并到=([^;]*)
这一步。 - 最初获取到的
match
其实是一个长度为 4 的数组。比方:
[
"username=poetry;",
"","poetry",";"
]
- 第 0 项:全量
- 第 1 项:结尾
- 第 2 项:两头的值
- 第 3 项:结尾
所以咱们是要拿第 2 项 match[2]
的值。
- 为了避免获取到的值是
%xxx
这样的字符序列,须要用unescape()
办法解码。