0


高级前端必会手写面试题及答案

循环打印红黄绿

下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 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 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  3. 处理传入的参数,截取第一个参数后的所有参数。
  4. 将函数作为上下文对象的一个属性。
  5. 使用上下文对象来调用这个方法,并保存返回结果。
  6. 删除刚才新增的属性。
  7. 返回结果。
// 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]);}
  1. 获取页面上的cookie可以使用 document.cookie

这里获取到的是类似于这样的字符串:

'username=poetry; user-id=12345; user-roles=home, me, setting'

可以看到这么几个信息:

  • 每一个cookie都是由 name=value 这样的形式存储的
  • 每一项的开头可能是一个空串''(比如username的开头其实就是), 也可能是一个空字符串' '(比如user-id的开头就是)
  • 每一项用";"来区分
  • 如果某项中有多个值的时候,是用","来连接的(比如user-roles的值)
  • 每一项的结尾可能是有";"的(比如username的结尾),也可能是没有的(比如user-roles的结尾)
  1. 所以我们将这里的正则拆分一下:
  • '(^| )'表示的就是获取每一项的开头,因为我们知道如果^不是放在[]里的话就是表示开头匹配。所以这里(^| )的意思其实就被拆分为(^)表示的匹配username这种情况,它前面什么都没有是一个空串(你可以把(^)理解为^它后面还有一个隐藏的'');而|表示的就是或者是一个" "(为了匹配user-id开头的这种情况)
  • +name+这没什么好说的
  • =([^;]*)这里匹配的就是=后面的值了,比如poetry;刚刚说了^要是放在[]里的话就表示"除了^后面的内容都能匹配",也就是非的意思。所以这里([^;]*)表示的是除了";"这个字符串别的都匹配(*应该都知道什么意思吧,匹配0次或多次)
  • 有的大佬等号后面是这样写的'=([^;]*)(;|$)',而最后为什么可以把'(;|$)'给省略呢?因为其实最后一个cookie项是没有';'的,所以它可以合并到=([^;]*)这一步。
  1. 最后获取到的match其实是一个长度为4的数组。比如:
["username=poetry;","","poetry",";"]
  • 第0项:全量
  • 第1项:开头
  • 第2项:中间的值
  • 第3项:结尾

所以我们是要拿第2项

match[2]

的值。

  1. 为了防止获取到的值是%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 里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 promiseresolve,让其状态变更,这又会依次调用新 promisecallbacks 数组里的方法,循环往复。。如果返回的结果是个 promise,则需要等它完成之后再触发新 promiseresolve,所以可以在其结果的 then 里调用新 promiseresolve
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 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  3. 将函数作为上下文对象的一个属性。
  4. 判断参数值是否传入
  5. 使用上下文对象来调用这个方法,并保存返回结果。
  6. 删除刚才新增的属性
  7. 返回结果
// 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;};
标签: javascript

本文转载自: https://blog.csdn.net/helloworld1024fd/article/details/128518891
版权归原作者 helloworld1024fd 所有, 如有侵权,请联系我们删除。

“高级前端必会手写面试题及答案”的评论:

还没有评论