循环打印红黄绿
下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?
三个亮灯函数:
functionred(){
console.log('red');}functiongreen(){
console.log('green');}functionyellow(){
console.log('yellow');}
这道题复杂的地方在于需要“交替重复”亮灯,而不是“亮完一次”就结束了。
(1)用 callback 实现
consttask=(timer, light, callback)=>{setTimeout(()=>{if(light ==='red'){red()}elseif(light ==='green'){green()}elseif(light ==='yellow'){yellow()}callback()}, timer)}task(3000,'red',()=>{task(2000,'green',()=>{task(1000,'yellow',Function.prototype)})})
这里存在一个 bug:代码只是完成了一次流程,执行后红黄绿灯分别只亮一次。该如何让它交替重复进行呢?
上面提到过递归,可以递归亮灯的一个周期:
conststep=()=>{task(3000,'red',()=>{task(2000,'green',()=>{task(1000,'yellow', step)})})}step()
注意看黄灯亮的回调里又再次调用了 step 方法 以完成循环亮灯。
(2)用 promise 实现
consttask=(timer, light)=>newPromise((resolve, reject)=>{setTimeout(()=>{if(light ==='red'){red()}elseif(light ==='green'){green()}elseif(light ==='yellow'){yellow()}resolve()}, timer)})conststep=()=>{task(3000,'red').then(()=>task(2000,'green')).then(()=>task(2100,'yellow')).then(step)}step()
这里将回调移除,在一次亮灯结束后,resolve 当前 promise,并依然使用递归进行。
(3)用 async/await 实现
consttaskRunner=async()=>{awaittask(3000,'red')awaittask(2000,'green')awaittask(2100,'yellow')taskRunner()}taskRunner()
手写 Promise
constPENDING="pending";constRESOLVED="resolved";constREJECTED="rejected";functionMyPromise(fn){// 保存初始化状态var self =this;// 初始化状态this.state =PENDING;// 用于保存 resolve 或者 rejected 传入的值this.value =null;// 用于保存 resolve 的回调函数this.resolvedCallbacks =[];// 用于保存 reject 的回调函数this.rejectedCallbacks =[];// 状态转变为 resolved 方法functionresolve(value){// 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变if(value instanceofMyPromise){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 方法functionreject(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);}};
实现双向数据绑定
let obj ={}let input = document.getElementById('input')let span = document.getElementById('span')// 数据劫持
Object.defineProperty(obj,'text',{configurable:true,enumerable:true,get(){
console.log('获取数据了')},set(newVal){
console.log('数据更新了')
input.value = newVal
span.innerHTML = newVal
}})// 输入监听
input.addEventListener('keyup',function(e){
obj.text = e.target.value
})
Array.prototype.filter()
Array.prototype.filter=function(callback, thisArg){if(this==undefined){thrownewTypeError('this is null or not undefined');}if(typeof callback !=='function'){thrownewTypeError(callback +'is not a function');}const res =[];// 让O成为回调函数的对象传递(强制转换对象)constO=Object(this);// >>>0 保证len为number,且为正整数const len =O.length >>>0;for(let i =0; i < len; i++){// 检查i是否在O的属性(会检查原型链)if(i inO){// 回调函数调用传参if(callback.call(thisArg,O[i], i,O)){
res.push(O[i]);}}}return res;}
实现类数组转化为数组
类数组转换为数组的方法有这样几种:
- 通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
- 通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike,0);
- 通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
- 通过 Array.from 方法来实现转换
Array.from(arrayLike);
手写 call 函数
call 函数的实现步骤:
- 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 处理传入的参数,截取第一个参数后的所有参数。
- 将函数作为上下文对象的一个属性。
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性。
- 返回结果。
// call函数实现Function.prototype.myCall=function(context){// 判断调用对象if(typeofthis!=="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;};
参考 前端进阶面试题详细解答
实现一个迷你版的vue
入口
// js/vue.jsclassVue{constructor(options){// 1. 通过属性保存选项的数据this.$options = options ||{}this.$data = options.data ||{}this.$el =typeof options.el ==='string'? document.querySelector(options.el): options.el
// 2. 把data中的成员转换成getter和setter,注入到vue实例中this._proxyData(this.$data)// 3. 调用observer对象,监听数据的变化newObserver(this.$data)// 4. 调用compiler对象,解析指令和差值表达式newCompiler(this)}_proxyData(data){// 遍历data中的所有属性
Object.keys(data).forEach(key=>{// 把data的属性注入到vue实例中
Object.defineProperty(this, key,{enumerable:true,configurable:true,get(){return data[key]},set(newValue){if(newValue === data[key]){return}
data[key]= newValue
}})})}}
实现Dep
classDep{constructor(){// 存储所有的观察者this.subs =[]}// 添加观察者addSub(sub){if(sub && sub.update){this.subs.push(sub)}}// 发送通知notify(){this.subs.forEach(sub=>{
sub.update()})}}
实现watcher
classWatcher{constructor(vm, key, cb){this.vm = vm
// data中的属性名称this.key = key
// 回调函数负责更新视图this.cb = cb
// 把watcher对象记录到Dep类的静态属性target
Dep.target =this// 触发get方法,在get方法中会调用addSubthis.oldValue = vm[key]
Dep.target =null}// 当数据发生变化的时候更新视图update(){let newValue =this.vm[this.key]if(this.oldValue === newValue){return}this.cb(newValue)}}
实现compiler
classCompiler{constructor(vm){this.el = vm.$el
this.vm = vm
this.compile(this.el)}// 编译模板,处理文本节点和元素节点compile(el){let childNodes = el.childNodes
Array.from(childNodes).forEach(node=>{// 处理文本节点if(this.isTextNode(node)){this.compileText(node)}elseif(this.isElementNode(node)){// 处理元素节点this.compileElement(node)}// 判断node节点,是否有子节点,如果有子节点,要递归调用compileif(node.childNodes && node.childNodes.length){this.compile(node)}})}// 编译元素节点,处理指令compileElement(node){// console.log(node.attributes)// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr=>{// 判断是否是指令let attrName = attr.name
if(this.isDirective(attrName)){// v-text --> text
attrName = attrName.substr(2)let key = attr.value
this.update(node, key, attrName)}})}update(node, key, attrName){let updateFn =this[attrName +'Updater']
updateFn &&updateFn.call(this, node,this.vm[key], key)}// 处理 v-text 指令textUpdater(node, value, key){
node.textContent = value
newWatcher(this.vm, key,(newValue)=>{
node.textContent = newValue
})}// v-modelmodelUpdater(node, value, key){
node.value = value
newWatcher(this.vm, key,(newValue)=>{
node.value = newValue
})// 双向绑定
node.addEventListener('input',()=>{this.vm[key]= node.value
})}// 编译文本节点,处理差值表达式compileText(node){// console.dir(node)// {{ msg }}let reg =/\{\{(.+?)\}\}/let value = node.textContent
if(reg.test(value)){let key = RegExp.$1.trim()
node.textContent = value.replace(reg,this.vm[key])// 创建watcher对象,当数据改变更新视图newWatcher(this.vm, key,(newValue)=>{
node.textContent = newValue
})}}// 判断元素属性是否是指令isDirective(attrName){return attrName.startsWith('v-')}// 判断节点是否是文本节点isTextNode(node){return node.nodeType ===3}// 判断节点是否是元素节点isElementNode(node){return node.nodeType ===1}}
实现Observer
classObserver{constructor(data){this.walk(data)}walk(data){// 1. 判断data是否是对象if(!data ||typeof data !=='object'){return}// 2. 遍历data对象的所有属性
Object.keys(data).forEach(key=>{this.defineReactive(data, key, data[key])})}defineReactive(obj, key, val){let that =this// 负责收集依赖,并发送通知let dep =newDep()// 如果val是对象,把val内部的属性转换成响应式数据this.walk(val)
Object.defineProperty(obj, key,{enumerable:true,configurable:true,get(){// 收集依赖
Dep.target && dep.addSub(Dep.target)return val
},set(newValue){if(newValue === val){return}
val = newValue
that.walk(newValue)// 发送通知
dep.notify()}})}}
使用
<!DOCTYPEhtml><htmllang="cn"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge"><title>Mini Vue</title></head><body><divid="app"><h1>差值表达式</h1><h3>{{ msg }}</h3><h3>{{ count }}</h3><h1>v-text</h1><divv-text="msg"></div><h1>v-model</h1><inputtype="text"v-model="msg"><inputtype="text"v-model="count"></div><scriptsrc="./js/dep.js"></script><scriptsrc="./js/watcher.js"></script><scriptsrc="./js/compiler.js"></script><scriptsrc="./js/observer.js"></script><scriptsrc="./js/vue.js"></script><script>let vm =newVue({el:'#app',data:{msg:'Hello Vue',count:100,person:{name:'zs'}}})
console.log(vm.msg)// vm.msg = { test: 'Hello' }
vm.test ='abc'</script></body></html>
数组扁平化
数组扁平化是指将一个多维数组变为一个一维数组
const arr = [1, [2, [3, [4, 5]]], 6];
// => [1, 2, 3, 4, 5, 6]
方法一:使用flat()
const res1 = arr.flat(Infinity);
方法二:利用正则
const res2 =JSON.stringify(arr).replace(/\[|\]/g,'').split(',');
但数据类型都会变为字符串
方法三:正则改良版本
const res3 =JSON.parse('['+JSON.stringify(arr).replace(/\[|\]/g,'')+']');
方法四:使用reduce
constflatten=arr=>{return arr.reduce((pre, cur)=>{return pre.concat(Array.isArray(cur)?flatten(cur): cur);},[])}const res4 =flatten(arr);
方法五:函数递归
const res5 =[];constfn=arr=>{for(let i =0; i < arr.length; i++){if(Array.isArray(arr[i])){fn(arr[i]);}else{
res5.push(arr[i]);}}}fn(arr);
Promise并行限制
就是实现有并行限制的Promise调度器问题
classScheduler{constructor(){this.queue =[];this.maxCount =2;this.runCounts =0;}add(promiseCreator){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();});}}consttimeout=time=>newPromise(resolve=>{setTimeout(resolve, time);})const scheduler =newScheduler();constaddTask=(time,order)=>{
scheduler.add(()=>timeout(time).then(()=>console.log(order)))}addTask(1000,'1');addTask(500,'2');addTask(300,'3');addTask(400,'4');
scheduler.taskStart()// 2// 3// 1// 4
实现 jsonp
// 动态的加载js文件functionaddScript(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函数来接收回调结果functionhandleRes(res){
console.log(res);}// 接口返回的数据格式handleRes({a:1,b:2});
实现发布-订阅模式
classEventCenter{// 1. 定义事件容器,用来装事件数组let handlers ={}// 2. 添加事件方法,参数:事件名 事件方法addEventListener(type, handler){// 创建新数组容器if(!this.handlers[type]){this.handlers[type]=[]}// 存入事件this.handlers[type].push(handler)}// 3. 触发事件,参数:事件名 事件参数dispatchEvent(type, params){// 若没有注册该事件则抛出错误if(!this.handlers[type]){returnnewError('该事件未注册')}// 触发事件this.handlers[type].forEach(handler=>{handler(...params)})}// 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和发布removeEventListener(type, handler){if(!this.handlers[type]){returnnewError('事件无效')}if(!handler){// 移除事件deletethis.handlers[type]}else{const index =this.handlers[type].findIndex(el=> el === handler)if(index ===-1){returnnewError('无该绑定事件')}// 移除事件this.handlers[type].splice(index,1)if(this.handlers[type].length ===0){deletethis.handlers[type]}}}}
JSONP
script标签不遵循同源协议,可以用来进行跨域请求,优点就是兼容性好但仅限于GET请求
constjsonp=({ url, params, callbackName })=>{constgenerateUrl=()=>{let dataSrc ='';for(let key in params){if(Object.prototype.hasOwnProperty.call(params, key)){
dataSrc +=`${key}=${params[key]}&`;}}
dataSrc +=`callback=${callbackName}`;return`${url}?${dataSrc}`;}returnnewPromise((resolve, reject)=>{const scriptEle = document.createElement('script');
scriptEle.src =generateUrl();
document.body.appendChild(scriptEle);
window[callbackName]=data=>{resolve(data);
document.removeChild(scriptEle);}})}
实现千位分隔符
// 保留三位小数parseToMoney(1234.56);// return '1,234.56'parseToMoney(123456789);// return '123,456,789'parseToMoney(1087654.321);// return '1,087,654.321'
functionparseToMoney(num){
num =parseFloat(num.toFixed(3));let[integer, decimal]=String.prototype.split.call(num,'.');
integer = integer.replace(/\d(?=(\d{3})+$)/g,'$&,');return integer +'.'+(decimal ? decimal :'');}
手写类型判断函数
functiongetType(value){// 判断数据是 null 的情况if(value ===null){return value +"";}// 判断数据是引用类型的情况if(typeof value ==="object"){let valueClass =Object.prototype.toString.call(value),
type = valueClass.split(" ")[1].split("");
type.pop();return type.join("").toLowerCase();}else{// 判断数据是基本数据类型的情况和函数的情况returntypeof value;}}
用正则写一个根据name获取cookie中的值的方法
functiongetCookie(name){var match = document.cookie.match(newRegExp('(^| )'+ name +'=([^;]*)'));if(match)returnunescape(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()
方法解码。
手写 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的thisconst self =this;returnnewMyPromise((resolve, reject)=>{// 封装前一个promise成功时执行的函数letfulfilled=()=>{try{const result =onFulfilled(self.value);// 承前return result instanceofMyPromise? result.then(resolve, reject):resolve(result);//启后}catch(err){reject(err)}}// 封装前一个promise失败时执行的函数letrejected=()=>{try{const result =onReject(self.reason);return result instanceofMyPromise? result.then(resolve, reject):reject(result);}catch(err){reject(err)}}switch(self.status){casePENDING:
self.onFulfilledCallbacks.push(fulfilled);
self.onRejectedCallbacks.push(rejected);break;caseFULFILLED:fulfilled();break;caseREJECT:rejected();break;}})}
注意:
- 连续多个
then
里的回调方法是同步注册的,但注册到了不同的callbacks
数组中,因为每次then
都返回新的promise
实例(参考上面的例子和图) - 注册完成后开始执行构造函数中的异步事件,异步完成之后依次调用
callbacks
数组中提前注册的回调
字符串最长的不重复子串
题目描述
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:输入: s ="abcabcbb"输出:3解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:输入: s ="bbbbb"输出:1解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:输入: s ="pwwkew"输出:3解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:输入: s =""输出:0
答案
constlengthOfLongestSubstring=function(s){if(s.length ===0){return0;}let left =0;let right =1;let max =0;while(right <= s.length){let lr = s.slice(left, right);const index = lr.indexOf(s[right]);if(index >-1){
left = index + left +1;}else{
lr = s.slice(left, right +1);
max = Math.max(max, lr.length);}
right++;}return max;};
Promise.all
Promise.all
是支持链式调用的,本质上就是返回了一个Promise实例,通过
resolve
和
reject
来改变实例状态。
Promise.myAll=function(promiseArr){returnnewPromise((resolve, reject)=>{const ans =[];let index =0;for(let i =0; i < promiseArr.length; i++){
promiseArr[i].then(res=>{
ans[i]= res;
index++;if(index === promiseArr.length){resolve(ans);}}).catch(err=>reject(err));}})}
Object.is
Object.is
解决的主要是这两个问题:
+0===-0// trueNaN===NaN// false
constis=(x, y)=>{if(x === y){// +0和-0应该不相等return x !==0|| y !==0||1/x ===1/y;}else{return x !== x && y !== y;}}
手写 apply 函数
apply 函数的实现步骤:
- 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 将函数作为上下文对象的一个属性。
- 判断参数值是否传入
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性
- 返回结果
// apply 函数实现Function.prototype.myApply=function(context){// 判断调用对象是否为函数if(typeofthis!=="function"){thrownewTypeError("Error");}let result =null;// 判断 context 是否存在,如果未传入则为 window
context = context || window;// 将函数设为对象的方法
context.fn =this;// 调用方法if(arguments[1]){
result = context.fn(...arguments[1]);}else{
result = context.fn();}// 将属性删除delete context.fn;return result;};
版权归原作者 helloworld1024fd 所有, 如有侵权,请联系我们删除。