高级js面试题及答案_第1页
高级js面试题及答案_第2页
高级js面试题及答案_第3页
高级js面试题及答案_第4页
高级js面试题及答案_第5页
已阅读5页,还剩23页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

高级js面试题及答案描述JavaScript原型链的完整查找机制,并解释为什么Ototype.__proto__为null。原型链的查找机制基于对象的[[Prototype]]内部属性(浏览器中通过__proto__访问)。当访问对象的属性或方法时,首先检查对象自身是否存在该属性;若不存在,则通过[[Prototype]]指针查找其原型对象(即构造函数的prototype属性指向的对象);若原型对象仍无该属性,继续向上查找原型对象的原型,直到找到Ototype。若Ototype也无该属性,则返回undefined。Ototype.__proto__为null是因为它是原型链的顶端。ECMAScript规范规定,所有对象的原型最终都指向Ototype,而Ototype本身没有原型,其[[Prototype]]被显式设置为null,以此终止原型链的查找,避免无限循环。例如:```javascriptconstobj={};console.log(obj.__proto__===Ototype);//trueconsole.log(Ototype.__proto__);//null```闭包在内存中如何存储?为什么循环中使用var声明的变量会导致事件处理函数获取不到正确的循环值?如何解决?闭包的本质是函数与其词法环境的组合。当函数在其定义的词法作用域外执行时,会保留对原作用域的引用,这些引用存储在闭包的作用域链中。具体来说,闭包的变量对象(包含所有局部变量)不会被垃圾回收机制回收,而是作为闭包的一部分持续存在,直到闭包不再被引用。循环中使用var声明变量时,由于var的函数作用域特性,变量会被提升到循环所在的函数作用域中。例如:```javascriptfor(vari=0;i<3;i++){setTimeout(()=>console.log(i),100);}//输出333(所有回调共享同一个i的引用)```每个setTimeout的回调函数都引用了同一个i变量,当循环结束时i的值为3,因此所有回调执行时i已变为3。解决方案有三种:1.使用IIFE(立即执行函数)为每个循环创建独立作用域:```javascriptfor(vari=0;i<3;i++){(function(j){setTimeout(()=>console.log(j),100);})(i);}```2.使用let声明变量(ES6块级作用域):```javascriptfor(leti=0;i<3;i++){setTimeout(()=>console.log(i),100);}//输出012(每个循环迭代创建独立的i绑定)```3.使用bind方法绑定当前i的值:```javascriptfor(vari=0;i<3;i++){setTimeout(console.log.bind(null,i),100);}```用Promise实现一个限制并发数的异步调度器,要求同时运行的任务数不超过2个。以下是基于Promise的并发调度器实现,核心逻辑是维护任务队列和当前运行数,任务完成后自动从队列中取出下一个执行:```javascriptclassScheduler{constructor(maxConcurrent){this.maxConcurrent=maxConcurrent;//最大并发数this.currentRunning=0;//当前运行任务数this.taskQueue=[];//任务队列}//添加任务到队列add(task){returnnewPromise((resolve,reject)=>{this.taskQueue.push({task,resolve,reject});this.run();});}//执行任务run(){while(this.currentRunning<this.maxConcurrent&&this.taskQueue.length>0){const{task,resolve,reject}=this.taskQueue.shift();this.currentRunning++;task().then(resolve).catch(reject).finally(()=>{this.currentRunning--;this.run();//任务完成后继续执行队列中的任务});}}}//使用示例constscheduler=newScheduler(2);constcreateTask=(id,time)=>()=>newPromise(resolve=>{setTimeout(()=>{console.log(`Task${id}completed`);resolve(id);},time);});scheduler.add(createTask(1,1000));scheduler.add(createTask(2,800));scheduler.add(createTask(3,500));scheduler.add(createTask(4,1200));//输出顺序://Task2completed(800ms)//Task1completed(1000ms)//Task3completed(500ms,此时前两个任务已完成,第三个开始执行)//Task4completed(1200ms)```解释JavaScript的垃圾回收机制,说明哪些情况会导致内存泄漏,如何检测和修复?JavaScript采用自动垃圾回收机制,主要算法有两种:1.标记清除(Mark-and-Sweep):最主流的算法。垃圾回收器从根对象(如全局window对象)开始遍历,标记所有可达对象;遍历完成后,未被标记的对象视为垃圾,其内存被回收。现代浏览器(如V8)在此基础上优化,采用分代回收(新生代和老生代),针对不同生命周期的对象使用不同的回收策略。2.引用计数(ReferenceCounting):较少使用。每个对象维护引用计数,当计数为0时回收。但无法处理循环引用(如对象A引用B,B引用A,无其他引用时计数不为0),因此逐渐被标记清除取代。常见内存泄漏场景:未清除的定时器:setInterval回调中引用了DOM元素,即使DOM被移除,定时器仍保留引用导致泄漏。闭包误用:闭包长期持有对外部变量的引用(如全局作用域中的闭包),导致变量无法被回收。全局变量:未声明的变量自动绑定到全局对象(如var遗漏导致的隐式全局变量),生命周期与应用一致。未移除的事件监听器:为DOM元素添加事件监听后,未在元素移除前调用removeEventListener,导致元素被引用无法回收(现代浏览器对DOM元素的事件监听有弱引用优化,但自定义对象仍需手动处理)。缓存未限制大小:如使用普通对象作为缓存,未设置过期策略或最大容量,导致内存持续增长。检测方法:ChromeDevTools的Memory面板:通过堆快照(HeapSnapshot)对比分析前后内存变化,定位未被回收的对象;使用时间线(AllocationTimeline)记录内存分配情况。Performance面板:记录一段时间内的垃圾回收次数和内存占用趋势,判断是否存在异常增长。修复策略:避免全局变量,使用块级作用域(let/const)或IIFE限制变量作用域。清除定时器(clearInterval/clearTimeout)和事件监听器(removeEventListener)。对闭包持有的变量,在不需要时显式置为null(如闭包外的变量obj=null)。使用WeakMap/WeakSet替代普通对象存储缓存,利用弱引用特性(键为对象时,若对象被回收,对应条目自动删除)。实现一个符合现代JavaScript规范的观察者模式,要求支持事件命名空间、事件移除和一次性事件监听。以下是支持命名空间、事件移除和一次性监听的观察者模式实现:```javascriptclassEventEmitter{constructor(){this.events=newMap();//存储事件,结构:{[eventName]:Set<listener>}}//注册事件(支持命名空间,如'click.ns1.ns2')on(eventName,listener){constnamespaces=eventName.split('.');constmainEvent=namespaces.shift();//主事件名(如'click')constkey=this.getEventKey(mainEvent,namespaces);if(!this.events.has(key)){this.events.set(key,newSet());}this.events.get(key).add(listener);}//注册一次性事件once(eventName,listener){constwrappedListener=(...args)=>{this.off(eventName,wrappedListener);//触发后自动移除listener(...args);};this.on(eventName,wrappedListener);}//移除事件(支持通配符,如'click.'或'.ns1')off(eventName,listener){constnamespaces=eventName.split('.');constmainEvent=namespaces[0]||'';//主事件名,若为空则匹配所有consttargetNamespaces=namespaces.slice(1);for(const[key,listeners]ofthis.events){const[currentMain,currentNamespaces]=this.parseEventKey(key);//匹配主事件(表示任意)if(mainEvent!==''&¤tMain!==mainEvent)continue;//匹配命名空间(targetNamespaces为空时移除所有,否则检查包含关系)if(targetNamespaces.length>0&&!this.matchNamespaces(currentNamespaces,targetNamespaces))continue;if(listener){listeners.delete(listener);if(listeners.size===0)this.events.delete(key);}else{this.events.delete(key);//无listener时移除整个事件}}}//触发事件(支持主事件名,如触发'click'会触发所有'click'相关命名空间的监听器)emit(eventName,...args){for(const[key]ofthis.events){const[currentMain]=this.parseEventKey(key);if(currentMain===eventName){constlisteners=this.events.get(key);listeners.forEach(listener=>listener(...args));}}}//提供事件键(主事件+命名空间排序后的字符串)getEventKey(mainEvent,namespaces){constsortedNs=[...namespaces].sort();//确保命名空间顺序不影响键return`${mainEvent}|${sortedNs.join('.')}`;}//解析事件键parseEventKey(key){const[mainEvent,nsStr]=key.split('|');return[mainEvent,nsStr?nsStr.split('.'):[]];}//检查命名空间是否匹配(支持通配符)matchNamespaces(currentNs,targetNs){returntargetNs.every(ns=>ns===''||currentNs.includes(ns));}}//使用示例constemitter=newEventEmitter();//注册带命名空间的事件emitter.on('click.ns1',()=>console.log('Clickns1'));emitter.on('click.ns2',()=>console.log('Clickns2'));emitter.on('scroll.ns1',()=>console.log('Scrollns1'));//触发主事件'click',所有'click'相关监听器执行emitter.emit('click');//输出Clickns1、Clickns2//移除命名空间ns1的所有事件emitter.off('.ns1');emitter.emit('click');//仅输出Clickns2emitter.emit('scroll');//无输出(scroll.ns1已被移除)//一次性事件emitter.once('message',msg=>console.log('Once:',msg));emitter.emit('message','Hello');//输出Once:Helloemitter.emit('message','World');//无输出```对比Reflect与Proxy的设计目标,实现一个通过Proxy拦截对象属性访问的日志系统,要求记录读取、设置、删除操作的详细信息。Reflect和Proxy均为ES6引入的元编程特性,但设计目标不同:Reflect是静态工具类,封装了对象操作的默认行为(如Reflect.get(target,prop)等价于target[prop]),主要用于替代部分Object方法(如Object.defineProperty),并为Proxy提供默认实现(避免手动调用target[prop]导致的this指向问题)。Proxy是元对象,用于创建对象的代理,允许拦截并自定义对象的基本操作(如属性访问、函数调用),实现对原对象的控制。以下是基于Proxy的日志系统实现,拦截get、set、deleteProperty操作并记录日志:```javascriptfunctioncreateLogProxy(target,logger=console.log){returnnewProxy(target,{get(target,prop,receiver){constvalue=Reflect.get(target,prop,receiver);logger(`[GET]Property'${String(prop)}'accessed.Value:${JSON.stringify(value)}`);returnvalue;},set(target,prop,value,receiver){constresult=Reflect.set(target,prop,value,receiver);logger(`[SET]Property'${String(prop)}'setto:${JSON.stringify(value)}.Success:${result}`);returnresult;},deleteProperty(target,prop){constresult=Reflect.deleteProperty(target,prop);logger(`[DELETE]Property'${String(prop)}'deleted.Success:${result}`);returnresult;},ownKeys(target){constkeys=Reflect.ownKeys(target);logger(`[OWN_KEYS]Objectownkeys:${JSON.stringify(keys)}`);returnkeys;}});}//使用示例constuser={name:'Alice',age:30};constloggedUser=createLogProxy(user);loggedU;//输出[GET]Property'name'accessed.Value:"Alice"loggedUser.age=31;//输出[SET]Property'age'setto:31.Success:truedeleteloggedUser.age;//输出[DELETE]Property'age'deleted.Success:trueObject.keys(loggedUser);//输出[OWN_KEYS]Objectownkeys:["name"]```解释JavaScript中事件循环的微任务与宏任务执行顺序,如何利用这一机制优化页面渲染性能?事件循环(EventLoop)是JavaScript处理异步任务的核心机制,其执行顺序遵循以下规则:1.执行栈中的同步代码优先执行。2.同步代码执行完毕后,检查微任务队列(MicrotaskQueue),依次执行所有微任务(直到队列为空)。3.微任务执行完毕后,进行一次UI渲染(仅当浏览器认为有必要时,如布局变化)。4.渲染完成后,检查宏任务队列(MacrotaskQueue),取出一个宏任务执行,重复步骤2-4。常见微任务包括:Promise.then/catch/finally、MutationObserver、process.nextTick(Node.js)。常见宏任务包括:setTimeout、setInterval、setImmediate(Node.js)、I/O操作、UI事件。利用事件循环优化渲染性能的关键是:避免长时间占用微任务队列:微任务在渲染前执行,若微任务过多或执行时间过长(如大量同步计算),会阻塞渲染,导致页面卡顿。应将大任务拆分为多个小任务,或使用setTimeout(宏任务)将部分操作延迟到下一循环。优先在微任务中执行数据更新:若多个状态变更需要同步到UI,将其放入同一个微任务中(如Vue3的批量更新),避免多次渲染。例如:```javascript//错误示例:多次setTimeout(宏任务)导致多次渲染setTimeout(()=>{state.a=1;},0);setTimeout(()=>{state.b=2;},0);//正确示例:使用Promise(微任务)合并更新Promise.resolve().then(()=>{state.a=1;}).then(()=>{state.b=2;});//两个更新在同一个微任务周期内完成,仅触发一次渲染```使用requestAnimationFrame:将UI更新操作放入requestAnimationFrame回调中,该回调在渲染前执行(属于宏任务的一种),确保与浏览器渲染节奏同步,避免不必要的重排。Vue3的响应式系统如何利用Proxy和Reflect实现?对比Vue2的Object.defineProperty有哪些优势?Vue3的响应式系统核心是通过Proxy拦截对象的所有操作(如属性读取、设置、删除),并利用Reflect调用默认行为,同时跟踪依赖(Track)和触发更新(Trigger)。具体流程如下:1.当访问响应式对象的属性时(get拦截),触发Track,将当前活跃的副作用函数(如组件渲染函数)记录到该属性的依赖集合中。2.当修改响应式对象的属性时(set拦截),触发Trigger,遍历该属性的依赖集合,重新执行副作用函数以更新视图。关键代码逻辑(简化版):```javascriptfunctionreactive(target){returnnewProxy(target,{get(target,prop,receiver){track(target,prop);//跟踪依赖returnReflect.get(target,prop,receiver);},set(target,prop,value,receiver){constresult=Reflect.set(target,prop,value,receiver);trigger(target,prop);//触发更新returnresult;}});}consttargetMap=newWeakMap();//存储对象-属性-依赖的映射letactiveEffect=null;functiontrack(target,prop){if(!activeEffect)return;letdepsMap=targetMap.get(target);if(!depsMap){depsMap=newMap();targetMap.set(target,depsMap);}letdep=depsMap.get(prop);if(!dep){dep=newSet();depsMap.set(prop,dep);}dep.add(activeEffect);}functiontrigger(target,prop){constdepsMap=targetMap.get(targe

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

最新文档

评论

0/150

提交评论