高频js的一道面试题及答案_第1页
高频js的一道面试题及答案_第2页
高频js的一道面试题及答案_第3页
高频js的一道面试题及答案_第4页
高频js的一道面试题及答案_第5页
已阅读5页,还剩28页未读 继续免费阅读

下载本文档

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

文档简介

高频js的一道面试题及答案描述JavaScript中原型链的作用,并分析以下代码的输出结果:```javascriptfunctionParent(){this.parentVal='parent';}Ptotype.getParentVal=function(){returnthis.parentVal;};functionChild(){Parent.call(this);this.childVal='child';}Ctotype=Object.create(Ptotype);Ctotype.getChildVal=function(){returnthis.childVal;};constchild=newChild();console.log(child.getParentVal());console.log(child.getChildVal());console.log(child.__proto__===Ctotype);console.log(Ctotype.__proto__===Ptotype);console.log(childinstanceofChild);console.log(childinstanceofParent);```原型链是JavaScript实现继承的核心机制,其本质是通过对象的`__proto__`属性(隐式原型)链接到其构造函数的`prototype`属性(显式原型),形成一条用于属性和方法查找的链式结构。当访问对象的属性或方法时,若对象自身不存在该属性,JS引擎会沿着`__proto__`向上查找,直到找到`Ototype`(其`__proto__`为`null`),形成原型链。代码分析:1.`Parent`构造函数定义实例属性`parentVal`,并在其原型上添加方法`getParentVal`。2.`Child`构造函数通过`Parent.call(this)`调用父类构造函数,实现实例属性的继承(避免原型共享问题)。3.`Ctotype=Object.create(Ptotype)`将子类原型指向通过父类原型创建的新对象,此时`Ctotype`的`__proto__`指向`Ptotype`,形成原型链。这种方式避免了直接赋值`Ctotype=Ptotype`导致的子类修改影响父类的问题。4.为`Ctotype`添加`getChildVal`方法,该方法仅存在于子类原型中。输出结果及原因:`child.getParentVal()`:`child`自身无`getParentVal`方法,沿原型链查找至`Ctotype`(无该方法),再查找其`__proto__`(即`Ptotype`),找到方法并执行,返回`this.parentVal`(`this`指向`child`实例,因`Parent.call(this)`已为`child`添加`parentVal`属性),输出`"parent"`。`child.getChildVal()`:`child`原型(`Ctotype`)存在该方法,直接调用返回`child.childVal`,输出`"child"`。`child.__proto__===Ctotype`:`newChild()`创建的实例的`__proto__`指向构造函数的`prototype`,输出`true`。`Ctotype.__proto__===Ptotype`:因`Ctotype`通过`Object.create(Ptotype)`创建,其`__proto__`即为`Ptotype`,输出`true`。`childinstanceofChild`:`instanceof`检查对象原型链是否包含构造函数的`prototype`,`child.__proto__`是`Ctotype`,输出`true`。`childinstanceofParent`:`child`原型链包含`Ctotype`→`Ptotype`→`Ototype`,因此`Ptotype`在链中,输出`true`。解释闭包的定义,说明其形成条件,并实现一个基于闭包的计数器函数,要求每次调用返回递增的数值,同时分析闭包可能带来的性能问题及解决方法。闭包是指函数与其词法环境(定义时所在的作用域)的组合,使得函数即使在其定义作用域之外执行,仍能访问原作用域中的变量。闭包的本质是JS引擎对函数定义时作用域的持久化存储。形成条件:1.函数嵌套:内部函数定义在外部函数内部。2.内部函数引用外部函数的变量:内部函数需访问或修改外部函数作用域中的变量。3.外部函数执行并返回内部函数:外部函数执行后,其作用域未被垃圾回收(因内部函数仍引用该作用域),形成闭包。计数器函数实现:```javascriptfunctioncreateCounter(initial=0){letcount=initial;returnfunction(){count++;returncount;};}constcounter=createCounter(5);console.log(counter());//6console.log(counter());//7console.log(counter());//8```上述代码中,`createCounter`执行后返回内部函数,该函数引用了外部的`count`变量。由于内部函数被外部变量`counter`引用,`createCounter`的作用域不会被回收,`count`变量持续存在,每次调用`counter()`都会修改并返回递增后的值。闭包的性能问题及解决:内存泄漏风险:闭包会使外部函数的作用域长期驻留内存,若大量创建闭包(如循环中定义函数),可能导致内存占用过高。例如,在循环中为DOM元素绑定事件处理函数时,若函数引用了循环变量,可能导致每个函数都保留独立的作用域,造成内存浪费。性能优化方法:1.避免不必要的闭包:若无需持久化变量,优先使用块级作用域(`let`/`const`)。2.及时释放引用:当闭包不再需要时,将其引用置为`null`(如`counter=null`),触发垃圾回收。3.限制闭包作用域大小:尽量减少闭包中引用的变量数量,避免引用大对象(如DOM节点)。4.使用弱引用数据结构:如`WeakMap`/`WeakSet`,其键为对象时不阻止垃圾回收,可用于缓存需要关联对象但不影响其回收的场景。分析以下代码的输出顺序,并详细说明事件循环的执行流程:```javascriptconsole.log('start');setTimeout(()=>{console.log('setTimeout1');Promise.resolve().then(()=>{console.log('promiseInSetTimeout1');});},0);Promise.resolve().then(()=>{console.log('promise1');setTimeout(()=>{console.log('setTimeout2');},0);});asyncfunctionasyncFunc(){console.log('asyncFuncStart');constres=await'result';console.log('asyncFuncAfterAwait',res);return'asyncResult';}asyncFunc().then((val)=>{console.log('asyncThen',val);});console.log('end');```事件循环(EventLoop)是JS处理异步任务的核心机制,其核心是协调同步任务、微任务(Microtask)和宏任务(Macrotask)的执行顺序。执行流程大致为:1.执行调用栈中的同步代码(主线程任务)。2.同步代码执行完毕后,依次执行微任务队列中的所有任务(微任务包括`Promise.then`、`async/await`的`await`后代码、`process.nextTick`(Node.js)等)。3.微任务队列清空后,从宏任务队列中取出一个任务执行(宏任务包括`setTimeout`、`setInterval`、`setImmediate`(Node.js)、I/O、UI渲染等)。4.重复步骤2和3,形成循环。代码执行步骤拆解:1.执行同步代码:`console.log('start')`→输出`start`。遇到`setTimeout`,将回调函数(`()=>{...}`)加入宏任务队列(延迟0ms实际约4ms,不影响顺序)。`Promise.resolve().then(...)`:`then`的回调加入微任务队列。执行`asyncFunc()`:`console.log('asyncFuncStart')`→输出`asyncFuncStart`。`await'result'`等价于`Promise.resolve('result').then()`,将后续代码(`console.log('asyncFuncAfterAwait',res)`和返回值处理)加入微任务队列,当前函数返回`Promise`(状态为`pending`)。`console.log('end')`→输出`end`。此时同步代码执行完毕,调用栈为空,开始处理微任务队列(按入队顺序执行)。2.执行微任务:第一个微任务是`Promise.resolve().then`的回调:`console.log('promise1')`→输出`promise1`。遇到`setTimeout`,将回调加入宏任务队列(此时宏任务队列有`setTimeout1`和`setTimeout2`两个任务)。第二个微任务是`asyncFunc`中`await`后的代码:`console.log('asyncFuncAfterAwait',res)`→输出`asyncFuncAfterAwaitresult`。函数返回`'asyncResult'`,触发`asyncFunc().then`的回调(加入微任务队列末尾)。第三个微任务是`asyncFunc().then`的回调:`console.log('asyncThen',val)`→输出`asyncThenasyncResult`。微任务队列清空,开始处理宏任务队列。3.执行宏任务(按入队顺序,先处理`setTimeout1`的回调):`setTimeout1`的回调执行:`console.log('setTimeout1')`→输出`setTimeout1`。`Promise.resolve().then(...)`的回调加入微任务队列。宏任务执行完毕,再次处理微任务队列(此时有`promiseInSetTimeout1`的回调)。4.执行新的微任务:`promiseInSetTimeout1`的回调执行:`console.log('promiseInSetTimeout1')`→输出`promiseInSetTimeout1`。5.继续处理宏任务队列中的下一个任务(`setTimeout2`的回调):`console.log('setTimeout2')`→输出`setTimeout2`。最终输出顺序:```startasyncFuncStartendpromise1asyncFuncAfterAwaitresultasyncThenasyncResultsetTimeout1promiseInSetTimeout1setTimeout2```实现一个通用的深拷贝函数,要求处理对象、数组、函数、Date、RegExp、Map、Set等数据类型,并解决循环引用问题。深拷贝是指创建一个与原对象完全独立的新对象,新对象的所有属性(包括嵌套对象)均为原对象的副本,修改新对象不会影响原对象。浅拷贝仅复制对象的第一层属性,嵌套对象仍共享引用。常见误区:`JSON.parse(JSON.stringify(obj))`无法处理函数、`Symbol`、`Date`(会转为字符串)、`RegExp`(会丢失`lastIndex`等属性)、循环引用(会报错),因此需手动实现。深拷贝函数实现思路:1.类型判断:使用`Ototype.toString.call`准确判断数据类型。2.特殊对象处理:针对`Date`、`RegExp`、`Map`、`Set`等内置对象,通过构造函数创建实例并复制属性。3.函数处理:普通函数通过`function`关键字定义的,可直接复制(因函数是引用类型,但深拷贝通常保留函数引用;若需完全隔离,可使用`eval`或`newFunction`重新构建,但可能存在安全风险,一般保留原函数)。4.循环引用检测:使用`WeakMap`存储已拷贝的对象,避免递归时重复拷贝导致栈溢出。实现代码:```javascriptfunctiondeepClone(obj,hash=newWeakMap()){//处理原始类型(null、undefined、number、string、boolean、symbol、bigint)if(obj===null||typeofobj!=='object'){returnobj;}//处理特殊对象类型(优先判断,避免被普通对象处理覆盖)consttypeStr=Ototype.toString.call(obj);switch(typeStr){case'[objectDate]':returnnewDate(obj);case'[objectRegExp]':constregex=newRegExp(obj.source,obj.flags);regex.lastIndex=obj.lastIndex;//恢复lastIndex属性returnregex;case'[objectMap]':constmap=newMap();obj.forEach((val,key)=>map.set(deepClone(key,hash),deepClone(val,hash)));returnmap;case'[objectSet]':constset=newSet();obj.forEach(val=>set.add(deepClone(val,hash)));returnset;case'[objectFunction]'://普通函数直接返回(或根据需求使用eval重建,此处保留原函数)returnobj;case'[objectSymbol]'://Symbol需通过Symbol.for或重新创建(注意描述相同但不是同一Symbol)returnSymbol(obj.description);}//处理普通对象或数组(已排除特殊对象)if(hash.has(obj)){returnhash.get(obj);//已拷贝过,返回缓存的副本}//创建新对象(数组或普通对象)constclone=Array.isArray(obj)?[]:{};hash.set(obj,clone);//记录原对象与副本的映射//遍历所有属性(包括Symbol属性)constkeys=[...Object.keys(obj),...Object.getOwnPropertySymbols(obj)];for(constkeyofkeys){clone[key]=deepClone(obj[key],hash);}returnclone;}```关键点说明:循环引用处理:使用`WeakMap`存储已拷贝的对象,键为原对象,值为副本。递归前检查`hash`中是否存在原对象,存在则直接返回副本,避免无限递归。Symbol属性处理:通过`Object.getOwnPropertySymbols`获取对象的`Symbol`属性,确保所有属性被复制。特殊对象处理:`Date`、`RegExp`等内置对象需通过构造函数创建实例,并复制特有属性(如`RegExp`的`lastIndex`)。函数处理:通常认为函数是行为而非数据,深拷贝时保留引用。若需完全隔离(如沙箱环境),可通过`newFunction(obj.toString())`重新构建,但可能丢失闭包变量和`prototype`信息,需谨慎使用。分析以下代码中`this`的指向,并总结`this`绑定的四种规则:```javascriptconstobj={name:'obj',getName:function(){console.log();}};constgetName=obj.getName;obj.getName();getName();functionPerson(name){=name;this.getName=function(){console.log();};}constp1=newPerson('p1');p1.getName();constp2={name:'p2'};p1.getName.call(p2);constarrowFunc=()=>{console.log(this);};arrowFunc();````this`的指向在函数调用时动态确定,取决于调用方式,而非定义位置。其绑定规则可总结为以下四种:1.默认绑定(独立调用)当函数作为独立函数调用(无明确上下文)时,`this`指向全局对象(非严格模式)或`undefined`(严格模式)。示例:`getName()`中,`getName`是独立函数调用,无显式绑定,`this`为全局对象(浏览器中为`window`,Node.js中为`global`)。若代码在严格模式下(`'usestrict'`),`this`为`undefined`,访问``会报错。2.隐式绑定(对象方法调用)当函数作为对象的方法调用时(调用位置在对象属性上),`this`指向该对象。示例:`obj.getName()`中,`getName`是`obj`的方法,调用时`this`指向`obj`,输出`"obj"`。3.显式绑定(`call`/`apply`/`bind`)通过`call`、`apply`或`bind`方法显式指定`this`的指向。`call`和`apply`立即执行函数,参数分别为`(thisArg,arg1,arg2...)`和`(thisArg,[argsArray])`。`bind`返回一个新函数,其`this`永久绑定为指定值(硬绑定)。示例:`p1.getName.call(p2)`中,`call`显式将`this`绑定为`p2`,输出`"p2"`。4.`new`绑定(构造函数调用)使用`new`调用函数时,函数作为构造函数执行,`this`指向新创建的实例对象。示例:`constp1=newPerson('p1')`中,`Person`作为构造函数,`new`会创建空对象并将`this`指向该对象,最终`p1.getName()`调用时,`this`指向`p1`,输出`"p1"`。箭头函数的`this`特性箭头函数不使用上述四种绑定规则,其`this`在定义时继承外层作用域的`this`(词法绑定)。若外层作用域无明确`this`(如全局作用域),则指向全局对象(非严格模式)。示例:`arrowFunc()`中,箭头函数定义在全局作用域,外层`this`为全局对象(浏览器中为`window`),因此输出`window`对象。代码输出结果及原因:`obj.getName()`:隐式绑定,`this`指向`obj`,输出`"obj"`。`getName()`:默认绑定(非严格模式),`this`指向全局对象(假设全局无`name`属性),输出`undefined`(若全局有`varname='global'`则输出`"global"`)。`p1.getName()`:`new`绑定,`this`指向`p1`,输出`"p1"`。`p1.getName.call(p2)`:显式绑定,`this`指向`p2`,输出`"p2"`。`arrowFunc()`:箭头函数继承全局作用域的`this`,输出全局对象(如`window`)。对比ES5构造函数继承与ES6class继承的区别,编写一个子类继承父类的示例,并说明`super`在子类构造函数中的作用。ES5和ES6的继承本质都是基于原型链,但ES6的`class`语法是ES5构造函数和原型链的语法糖,提供了更简洁、更符合面向对象范式的写法。ES5继承的常见实现(组合继承)ES5通过构造函数和原型链实现继承,常见方式为“组合继承”(原型链继承+借用构造函数继承):原型链继承:通过`Ctotype=newParent()`使子类原型指向父类实例,继承父类原型方法。借用构造函数继承:在子类构造函数中调用`Parent.call(this)`,继承父类实例属性。缺点:父类构造函数会被调用两次(`newParent()`和`Parent.call(this)`),导致父类实例属性在子类原型和实例中重复存在(虽不影响使用,但浪费内存)。ES6class继承的改进ES6使用`class`和`extends`关键字,底层仍基于原型链,但优化了继承逻辑:`classChildextendsParent`自动设置`Ctotype.__proto__=Ptotype`,形成原型链。子类构造函数中必须先调用`super()`(即父类构造函数),否则无法使用`this`(因`this`由父类构造函数初始化)。示例代码(ES6class继承)```javascriptclassParent{constructor(name){=name;this.hobbies=['reading'];}sayHello(){console.log(`Hello,I'm${}`);}staticstaticMethod(){console.log('Parentstaticmethod');}}classChildextendsParent{constructor(name,age){super(name);//调用父类构造函数,初始化父类属性this.age=age;}//重写父类方法sayHello(){super.sayHello();//调用父类的sayHello方法console.log(`I'm${this.age}yearsold`);}//子类新增方法getHobbies(){returnthis.hobbies;}}constchild=newChild('Alice',12);child.sayHello();child.hobbies.push('swimming');console.log(child.getHobbies());Child.staticMethod();````super`的作用`super`是一个关键字,用于访问父类的属性和方法,其行为在构造函数和普通方法中不同:1.构造函数中:`super()`作为函数调用,等价于`Ptotype.constructor.call(this,...args)`,即调用父类构造函数并将`this`绑定到子类实例。子类构造函数必须在使用`this`之前调用`super()`,否则会报错(因`this`尚未初始化)。2.普通方法中:`super.method()`访问父类原型上的方法,`this`指向当前子类实例。例如`super.sayHello()`调用父类的`sayHello`方法,此时`this`是`child`实例,因此``为`"Alice"`。3.静态方法中:`super`指向父类的静态方法。若子类定义了静态方法,`super`可访问父类的静态属性和方法。代码输出分析:`child.sayHello()`:调用子类的`sayHello`方法,先执行`super.sayHello()`(输出`"Hello,I'mAlice"`),再输出`"I'm12yearsold"`。`child.hobbies.push('swimming')`:`hobbies`属性继承自父类构造函数(`this.hobbies=['reading']`),子类实例修改后,`child.getHobbies()`返回`['reading','swimming']`。`Child.staticMethod()`:子类继承父类的静态方法(静态方法存在于构造函数自身,`Child`的`[[Prototype]]`指向`Parent`,因此可通过`Child.staticMethod()`调用,输出`"Parentstaticmethod"`)。实现数组的`flat`方法,要求支持指定展开深度,默认深度为1;并实现数组的`reduce`方法,要求处理空数组时抛出错误。数组的`flat`方法实现`flat`方法用于将嵌套数组展开为一维数组,展开深度由参数`depth`指定(默认1),`Infinity`表示展开所有层级。实现思路:递归遍历数组元素,若元素是数组且当前深度未达上限,则递归展开;否则直接加入结果。处理边界情况:非数组元素直接返回,`depth`为0时返回原数组。```javascriptfunctionflat(arr,depth=1){if(!Array.isArray(arr)){thrownewTypeError('Expectedanarray');}returnarr.reduce((acc,val)=>{if(Array.isArray(val)&&depth>0){//递归展开,深度减1returnacc.concat(flat(val,depth1));}else{returnacc.concat(val);}},[]);}//测试console.log(flat([1,[2,[3,[4]]],5],2));//[1,2,3,[4],5]console.log(flat([1,[2,[3]]],Infinity

温馨提示

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

评论

0/150

提交评论