当前位置: 首页 > Web前端 > HTML5

一个前端程序员经常忽略的JavaScript面试题

时间:2023-04-05 23:25:55 HTML5

}Foo.getName=function(){alert(2);};Foo.prototype.getName=function(){alert(3);};vargetName=function(){alert(4);};functiongetName(){alert(5);}//请编写以下输出:Foo.getName();getName();Foo().getName();getName();newFoo.getName();newFoo().getName();newnewFoo().getName();这几天在面试中多次遇到这个经典问题,特地从头到尾分析了答案。这道题的经典特点在于全面考察面试官的JavaScript综合能力,包括变量定义提升、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性和原型属性优先级等知识。有网上还有一些相关问题的解释,当然我觉得有些解释还是不恰当,不够清晰,所以我会从头分析到尾。当然我们会把最终答案放在最后,把这道题做的稍微难一点,改进版也会放在最后,让面试官在写题的时候有参考。第一个问题是看这个问题的前半部分做了什么。首先定义了一个名为Foo的函数,然后为Foo创建了一个名为getName的静态属性来存放匿名函数,然后新建了一个Foo的原型对象。名为getName的匿名函数。然后通过函数变量表达式创建一个getName函数,最后声明一个名为getName的函数。第一个问题的Foo.getName自然是为了访问存储在Foo函数上的静态属性。答案自然是2,这里不用过多解释。一般来说,对JS基础稍有了解的同学,第一道题应该没问题。这个问题,当然我们可以通过下面的代码来复习一下基础知识,首先加深理解functionUser(name){varname=name;//私有属性this.name=name;//publicattributefunctiongetName(){//私有方法返回名;}}User.prototype.getName=function(){//公共方法returnthis.name;}User.name='Wscats';//静态属性User.getName=function(){//静态方法returnthis.name;}varWscat=newUser('Wscats');//实例化注意以下几点:要调用公共方法和公共属性,首先要实例化对象,即使用new操作符来实现不能调用私有方法和静态方法。静态方法和静态属性意味着我们可以在不实例化的情况下调用对象的私有方法和属性。无法访问的第二个问题第二个问题是直接调用getName函数。既然是直接调用,就是访问上面当前作用域中叫getName的函数,所以这里应该直接关注4和5,和123没有关系。当然我问了几个我的同事和大部分都回答了5。这里其实有两个坑,一个是变量声明提升,一个是函数表达式和函数声明的区别。我们来看看为什么,参考(1)Javascript函数声明和函数表达式(2)JavaScript变量提升在Javascript中,有两种类型的函数functiondeclaration//functiondeclarationfunctionwscat(type){returntype==="wscat";}functionexpression//函数表达式varoaoafly=function(type){returntype==="oaoafly";}先看下面这个经典问题,在一个程序中同时使用函数声明和函数表达式定义一个名为getNamegetName()的函数//oaoaflyvargetName=function(){console.log('wscat')}getName()//wscatfunctiongetName(){console.log('oaoafly')}getName()//wscat上面的代码看起来很像,感觉也没有太大区别。但实际上,Javascript函数中的一个“陷阱”体现在Javascript中的两类函数定义上。JavaScript解释器中有一个变量声明提升机制,就是说函数声明会被提升到作用域的最前面,即使写代码的时候写在末尾,它仍然会被提升到最前面。用函数表达式创建的函数是在运行时赋值的,只有表达式赋值完成后才能调用。vargetName//变量被提升,此时是undefinedgetName()//oaoafly这里函数提升受函数声明的影响,虽然函数声明可以在最后提升到最前面vargetName=function(){console.log('wscat')}//函数表达式此时开始覆盖函数声明的定义getName()//wscatfunctiongetName(){console.log('oaoafly')}getName()//wscat在这里执行的是函数表达式的值,所以可以分解成这两个简单的问题,看看本质的区别vargetName;console.log(getName)//undefinedgetName()//UncaughtTypeError:getName不是函数vargetName=function(){console.log('wscat')}vargetName;console.log(getName)//functiongetName(){console.log('oaoafly')}getName()//oaoaflyfunctiongetName(){console.log('oaoafly')}这种差异看似微不足道,但在某些情况下确实是一个潜移默化的“致命”陷阱。造成这个陷阱的本质原因体现在两种类型在函数提升和运行时(analysistime/runtime)上的区别。当然,我们会给出一个总结:Javascript中的函数声明和函数表达式是有区别的。JS解析时提倡函数声明。因此,在同一个作用域内,无论在何处定义函数声明,都可以调用该函数。函数表达式的值在JS运行时就确定了,只有表达式赋值完成后才能调用函数。所以第二题的答案是4。5的函数声明被4的函数表达式覆盖了。第三题是Foo().getName();先执行Foo函数,然后调用Foo函数的返回值对象的getName属性函数。Foo的第一句functiongetName=function(){alert(1);};是一个函数赋值语句。注意它没有var声明,所以先在当前Foo函数的作用域中寻找getName变量,没有。然后到当前函数作用域的上层,也就是外层作用域,查找是否有getName变量,如果找到,就是第二题中的alert(4)函数,赋值这个变量的值到function(){alert(1)}。这里实际上修改了外作用域中的getName函数。注意:如果这里还是找不到,就一直找window对象。如果window对象中没有getName属性,则在window对象中创建一个getName变量。之后Foo函数的返回值就是this,JS的this问题很多文章都有介绍,这里不再多说。简单的说,this的指向是由函数的调用方式决定的。这里的直接调用方法中,this指向的是window对象。所以Foo函数返回的是window对象,相当于执行了window.getName(),而window中的getName已经修改为alert(1),所以最终会输出1。这里考察两个知识点,一是变量的作用域问题,一个是this指向的问题我们可以用下面的代码来复习一下这两个知识点varname="Wscats";//全局变量window.name="Wscats";//全局变量functiongetName(){name="Oaoafly";//去掉var,成为全局变量varprivateName="Stacsw";returnfunction(){console.log(this);//窗口返回privateName}}vargetPrivate=getName("Hello");//参数当然是局部变量,但是我在函数console.log(name)中没有接受这个参数//Oaoaflyconsole.log(getPrivate())//Stacsw没有块级作用域,因为JS,但是函数可以生成一个作用域,函数内部定义值的不同方法会直接或间接影响全局或局部变量。函数内部的私有变量可以通过闭包获得。函数真的是第一公民~而且关于这个,this的重点是定义函数的时候不能确定的。只有在函数执行的时候才能判断this指向了谁。事实上,这最终指向了调用它的对象。所以在第三个问题中,窗口实际上是在调用Foo()函数,所以重点是windowwindow.Foo().getName();//->window.getName();第四题直接调用了getName函数,相当于window.getName(),因为这个变量在执行Foo函数的时候被修改了,结果和第三题一样,都是1,也就是说,Foo执行后,全局的getName函数被重写了一次,所以结果就是Foo()执行的重写的getName函数。第五题第五题请教newFoo.getName();这里是JS操作符优先级的问题,我觉得这是这道题的灵魂,也是一道比较难的题下面是JS操作符的优先级表,从高到高再到低位。参考MDNoperatorpriority优先操作类型associativeoperator19parenthesesn/a(...)18个成员访问从左到右...。...成员访问从左到右计算...[...]新的(带参数列表)n/a新的...(...)17函数调用从左到右...(...)new(无参数列表)从右到左new...16后递增(之后的运算符)n/a...++后递减(之后的运算符)n/a...--15逻辑非右-向左!...按位NOT从右到左~...一元加法从右到左+...一元减法从右到左-...前面预递增从右到左++...pre-decrementright-to-left--...typeof从右到左typeof...void从右到左void...delete从右到左delete...14乘法从左到右...*...除法从左到右.../...模从左到右...%...13加法从左到右...+...减法从左到右...-。..12按位左移从左到右...<<...按位右移从左到右...>>...无符号右移从左到右...>>>...11小于左向右...<...小于或等于从左到右...<=...大于或等于从左到右...>...大于或等于从左到右...>=...inFromlefttoright...in...instanceofFromlefttoright...instanceof...10Equalsfromlefttoright...==...Notequalsfromlefttoright...!=...从左到右等于...===...非等号从左到右...!==...9按位与从左到右...&...8按位异或从左到右...^...7按位或从左到右...按位或...6逻辑与从左到右...&&...5逻辑或从左到右...逻辑或...4条件运算符从右到左...?...:...3从右到左赋值...=......+=......-=......*=。...../=......%=......<<=......>>=......>>>=......&=......^=......or=...2yieldfromRighttoleftyield...yield*righttoleftyield*...1expansionoperatorn/a......0逗号从左到右...,...本题先看new的第18和第17个优先级new(带参数列表)的优先级比new(不带参数列表)的优先级高高于函数调用,与成员访问同级newFoo.getName();优先级等同于:new(Foo.getName)();pointpriority(18)ishigherthannewwithoutparameterlist(17)具有高优先级。point操作完成后,因为有括号(),此时变成new了一个参数列表(18),所以直接执行new。当然,有些朋友可能会有疑问,为什么()不是函数调用new是因为函数调用(17)的优先级比new有参数列表(18)低。成员访问(18)->new有一个参数列表(18),所以这里其实是用getName函数作为构造函数来执行的,第六题和上一题唯一的区别就是多了一个括号呸。我们也可以看到,第五题有括号和没有括号的时候,在优先级上是有区别的。(newFoo()).getName()那么这里怎么判断呢?首先,new有一个参数列表(18)和点(18)的优先级是同级的。如果是同级,执行顺序是从左到右,所以先执行new带参数列表(18)再执行点(18)的优先级,最后函数调用(17)new有一个参数列表(18)->.成员访问(18)->()函数调用(17)这里又是一个小知识点,Foo作为构造函数是有返回值的,所以这里需要对构造函数的返回值进行说明在JS中。构造函数的返回值在传统语言中,构造函数不应该有返回值,真正执行的返回值是构造函数的实例化对象。在JS中,构造函数可以有返回值也可以没有。如果没有返回值,则与其他语言一样返回实例化对象。functionFoo(name){this.name=name}console.log(newFoo('wscats'))如果有返回值,检查返回值是否为引用类型。如果是非引用类型,比如基本类型(String、Number、Boolean、Null、Undefined),就和没有返回值一样,实际返回的是它的实例化对象。functionFoo(name){this.name=namereturn520}console.log(newFoo('wscats'))如果返回值是引用类型,那么实际返回值就是这个引用类型。functionFoo(name){this.name=namereturn{age:16}}console.log(newFoo('wscats'))原题中,由于返回了this,而构造函数中的this本来代表当前Instantiate对象,最后Foo函数返回实例化的对象。然后调用实例化对象的getName函数,因为在Foo的构造函数中没有给实例化对象添加任何属性,在当前对象的原型对象(prototype)中寻找getName函数。当然,这里还有一个题外话。如果构造函数和原型链都有相同的方法,比如下面的代码,那么默认会使用构造函数的public方法,而不是原型链。这个知识点在原题中没有展示。后来我添加了一个改进版本。functionFoo(name){this.name=namethis.getName=function(){returnthis.name}}Foo.prototype.name='Oaoafly';Foo.prototype.getName=function(){return'Oaoafly'}console.log((newFoo('Wscats')).name)//Wscatsconsole.log((newFoo('Wscats')).getName())//Wscats第七题newnewFoo().getName();也是运算符优先级的问题。做这道题,其实我觉得答案不是那么重要。关键是看面试官是否真的知道面试官在考察我们什么。最后实际执行是:new((newFoo()).getName)();newhasaparameterlist(18)->newhasaparameterlist(18)首先初始化Foo的实例化对象,然后在其原型上设置getName函数再次new为构造函数,所以最终结果是3个答案functionFoo(){getName=function(){alert(1);};返回这个;}Foo.getName=function(){alert(2);};Foo.prototype.getName=function(){alert(3);};vargetName=function(){alert(4);};functiongetName(){alert(5);}//答案:Foo.getName();//2getName();//4Foo().getName();//1getName();//1newFoo.getName();//2newFoo().getName();//3newnewFoo().getName();//3在后续的后续中,我会把这道题做的更难一些(附上答案),并为Foo函数添加一个公共方法getName。下面这道题,如果用在面试题中,通过率可能会更低,因为难度稍微高了一点,多了两个坑,但是理解了这道题的原理,就相当于理解了以上所有的知识点。functionFoo(){this.getName=function(){console.log(3);return{getName:getName//这是第六题涉及的构造函数的返回值}};//这是第六题中涉及到的JS构造函数公共方法和原型链方法的优先级getName=function(){console.log(1);};返回这个}Foo.getName=function(){console.log(2);};Foo.prototype.getName=function(){console.log(6);};vargetName=function(){console.log(4);};functiongetName(){console.log(5);}//answer:Foo.getName();//2getName();//4console.log(Foo())Foo().getName();//1获取名称();//1newFoo.getName();//2newFoo().getName();//3//还有一个问题newFoo().getName().getName();//31newnewFoo().getName();//3最后,其实我并不推荐这些问题作为面试官考试的唯一判断,但是作为一个合格的前端工程师,我们不应该因为浮躁而忽略了自己最基础的一些基础知识。当然,也祝愿各位面试官都能找到理想的工作,祝愿各位面试官都能找到心仪的马。千里马~交流如果文章和笔记能给你带来一点帮助或启发,请不要吝啬你的点赞和收藏。文章将同时持续更新。可以在微信搜索“前端之旅”,关注公众号,方便您稍后阅读。收录在https://github.com/Wscats/art...欢迎您的关注和交流,您的肯定是我前进的最大动力?