题目
浏览往期更多优质文章可移步我的 GitHub 查看哦
function Foo() { getName = function () { alert (1); }; return this;}Foo.getName = function () { alert (2);};Foo.prototype.getName = function () { alert (3);};var getName = function () { alert (4);};function getName() { alert (5);} //请写出以下输入后果:Foo.getName();getName();Foo().getName();getName();new Foo.getName();new Foo().getName();new new Foo().getName();
这几天面试上几次碰上这道经典的题目,顺便从头到尾来剖析一次答案,这道题的经典之处在于它综合考查了面试者的JavaScript的综合能力,蕴含了变量定义晋升、this指针指向、运算符优先级、原型、继承、全局变量净化、对象属性及原型属性优先级等常识,此题在网上也有局部相干的解释,当然我感觉有局部解释还欠妥,不够清晰,顺便重头到尾来剖析一次,当然咱们会把最终答案放在前面,并把此题再改高一点点难度,改进版也放在最初,不便面试官在出题的时候有个参考。
第一问
先看此题的上半局部做了什么,首先定义了一个叫Foo的函数,之后为Foo创立了一个叫getName的动态属性存储了一个匿名函数,之后为Foo的原型对象新创建了一个叫getName的匿名函数。之后又通过函数变量表达式创立了一个getName的函数,最初再申明一个叫getName函数。
第一问的Foo.getName天然是拜访Foo函数上存储的动态属性,答案天然是2,这里就不须要解释太多的,一般来说第一问对于略微懂JS根底的同学来说应该是没问题的,当然咱们能够用上面的代码来回顾一下根底,先加深一下理解
function User(name) { var name = name; //公有属性 this.name = name; //私有属性 function getName() { //公有办法 return name; }}User.prototype.getName = function() { //私有办法 return this.name;}User.name = 'Wscats'; //动态属性User.getName = function() { //静态方法 return this.name;}var Wscat = new User('Wscats'); //实例化
留神上面这几点:
- 调用私有办法,私有属性,咱们必须先实例化对象,也就是用new操作符实化对象,就可构造函数实例化对象的办法和属性,并且私有办法是不能调用公有办法和静态方法的
- 静态方法和动态属性就是咱们无需实例化就能够调用
- 而对象的公有办法和属性,内部是不能够拜访的
第二问
第二问,间接调用getName函数。既然是间接调用那么就是拜访以后上文作用域内的叫getName的函数,所以这里应该间接把关注点放在4和5上,跟1 2 3都没什么关系。当然起初我问了我的几个共事他们大多数答复了5。此处其实有两个坑,一是变量申明晋升,二是函数表达式和函数申明的区别。
咱们来看看为什么,可参考(1)对于Javascript的函数申明和函数表达式 (2)对于JavaScript的变量晋升
在Javascript中,定义函数有两种类型
函数申明
// 函数申明function wscat(type) { return type === "wscat";}
函数表达式
// 函数表达式var oaoafly = function(type) { return type === "oaoafly";}
先看上面这个经典问题,在一个程序外面同时用函数申明和函数表达式定义一个名为getName的函数
getName() //oaoaflyvar getName = function() { console.log('wscat')}getName() //wscatfunction getName() { console.log('oaoafly')}getName() //wscat
下面的代码看起来很相似,感觉也没什么太大差异。但实际上,Javascript函数上的一个“陷阱”就体现在Javascript两种类型的函数定义上。
- JavaScript 解释器中存在一种变量申明被晋升的机制,也就是说函数申明会被晋升到作用域的最后面,即便写代码的时候是写在最初面,也还是会被晋升至最后面。
- 而用函数表达式创立的函数是在运行时进行赋值,且要等到表达式赋值实现后能力调用
var getName //变量被晋升,此时为undefinedgetName() //oaoafly 函数被晋升 这里受函数申明的影响,尽管函数申明在最初能够被晋升到最后面了var getName = function() { console.log('wscat')} //函数表达式此时才开始笼罩函数申明的定义getName() //wscatfunction getName() { console.log('oaoafly')}getName() //wscat 这里就执行了函数表达式的值
所以能够合成为这两个简略的问题来看分明区别的实质
var getName;console.log(getName) //undefinedgetName() //Uncaught TypeError: getName is not a functionvar getName = function() { console.log('wscat')}
var getName;console.log(getName) //function getName() {console.log('oaoafly')}getName() //oaoaflyfunction getName() { console.log('oaoafly')}
这个区别看似微不足道,但在某些状况下的确是一个难以觉察并且“致命“的陷阱。呈现这个陷阱的实质起因体现在这两种类型在函数晋升和运行机会(解析时/运行时)上的差别。
当然咱们给一个总结:Javascript中函数申明和函数表达式是存在区别的,函数申明在JS解析时进行函数晋升,因而在同一个作用域内,不论函数申明在哪里定义,该函数都能够进行调用。而函数表达式的值是在JS运行时确定,并且在表达式赋值实现后,该函数能力调用。
所以第二问的答案就是4,5的函数申明被4的函数表达式笼罩了
第三问
Foo().getName();
先执行了Foo函数,而后调用Foo函数的返回值对象的getName属性函数。
Foo函数的第一句getName = 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指向问题
咱们能够利用上面代码来回顾下这两个知识点
var name = "Wscats"; //全局变量window.name = "Wscats"; //全局变量function getName() { name = "Oaoafly"; //去掉var变成了全局变量 var privateName = "Stacsw"; return function() { console.log(this); //window return privateName }}var getPrivate = getName("Hello"); //当然传参是局部变量,但函数外面我没有承受这个参数console.log(name) //Oaoaflyconsole.log(getPrivate()) //Stacsw
因为JS没有块级作用域,然而函数是能产生一个作用域的,函数外部不同定义值的办法会间接或者间接影响到全局或者局部变量,函数外部的公有变量能够用闭包获取,函数还真的是第一公民呀~
而对于this,this的指向在函数定义的时候是确定不了的,只有函数执行的时候能力确定this到底指向谁,实际上this的最终指向的是那个调用它的对象
所以第三问中实际上就是window在调用Foo()函数,所以this的指向是window
window.Foo().getName();//->window.getName();
第四问
间接调用getName函数,相当于window.getName()
,因为这个变量曾经被Foo函数执行时批改了,遂后果与第三问雷同,为1,也就是说Foo执行后把全局的getName函数给重写了一次,所以后果就是Foo()执行重写的那个getName函数
第五问
第五问new Foo.getName();
此处考查的是JS的运算符优先级问题,我感觉这是这题灵魂的所在,也是难度比拟大的一题
上面是JS运算符的优先级表格,从高到低排列。可参考MDN运算符优先级
优先级 | 运算类型 | 关联性 | 运算符 |
---|---|---|---|
19 | 圆括号 | n/a | ( … ) |
18 | 成员拜访 | 从左到右 | … . … |
需计算的成员拜访 | 从左到右 | … [ … ] | |
new (带参数列表) | n/a new | … ( … ) | |
17 | 函数调用 | 从左到右 | … ( … ) |
new (无参数列表) | 从右到左 | new … | |
16 | 后置递增(运算符在后) | n/a | … ++ |
后置递加(运算符在后) | n/a | … -- | |
15 | 逻辑非 | 从右到左 | ! … |
按位非 | 从右到左 | ~ … | |
一元加法 | 从右到左 | + … | |
一元减法 | 从右到左 | - … | |
前置递增 | 从右到左 | ++ … | |
前置递加 | 从右到左 | -- … | |
typeof | 从右到左 | typeof … | |
void | 从右到左 | void … | |
delete | 从右到左 | delete … | |
14 | 乘法 | 从左到右 | … * … |
除法 | 从左到右 | … / … | |
取模 | 从左到右 | … % … | |
13 | 加法 | 从左到右 | … + … |
减法 | 从左到右 | … - … | |
12 | 按位左移 | 从左到右 | … << … |
按位右移 | 从左到右 | … >> … | |
无符号右移 | 从左到右 | … >>> … | |
11 | 小于 | 从左到右 | … < … |
小于等于 | 从左到右 | … <= … | |
大于 | 从左到右 | … > … | |
大于等于 | 从左到右 | … >= … | |
in | 从左到右 | … in … | |
instanceof | 从左到右 | … instanceof … | |
10 | 等号 | 从左到右 | … == … |
非等号 | 从左到右 | … != … | |
全等号 | 从左到右 | … === … | |
非全等号 | 从左到右 | … !== … | |
9 | 按位与 | 从左到右 | … & … |
8 | 按位异或 | 从左到右 | … ^ … |
7 | 按位或 | 从左到右 | … 按位或 … |
6 | 逻辑与 | 从左到右 | … && … |
5 | 逻辑或 | 从左到右 | … 逻辑或 … |
4 | 条件运算符 | 从右到左 | … ? … : … |
3 | 赋值 | 从右到左 | … = … |
… += … | |||
… -= … | |||
… *= … | |||
… /= … | |||
… %= … | |||
… <<= … | |||
… >>= … | |||
… >>>= … | |||
… &= … | |||
… ^= … | |||
… 或= … | |||
2 | yield | 从右到左 | yield … |
yield* | 从右到左 | yield* … | |
1 | 开展运算符 | n/a | ... … |
0 | 逗号 | 从左到右 | … , … |
这题首先看优先级的第18和第17都呈现对于new的优先级,new (带参数列表)比new (无参数列表)高比函数调用高,跟成员拜访同级
new Foo.getName();
的优先级是这样的
相当于是:
new (Foo.getName)();
- 点的优先级(18)比new无参数列表(17)优先级高
- 当点运算完后又因为有个括号
()
,此时就是变成new有参数列表(18),所以间接执行new,当然也可能有敌人会有疑难为什么遇到()不函数调用再new呢,那是因为函数调用(17)比new有参数列表(18)优先级低
.成员拜访(18)->new有参数列表(18)
所以这里实际上将getName函数作为了构造函数来执行,遂弹出2。
第六问
这一题比上一题的惟一区别就是在Foo那里多出了一个括号,这个有括号跟没括号咱们在第五问的时候也看进去优先级是有区别的
(new Foo()).getName()
那这里又是怎么判断的呢?首先new有参数列表(18)跟点的优先级(18)是同级,同级的话依照从左向右的执行程序,所以先执行new有参数列表(18)再执行点的优先级(18),最初再函数调用(17)
new有参数列表(18)->.成员拜访(18)->()函数调用(17)
这里还有一个小知识点,Foo作为构造函数有返回值,所以这里须要阐明下JS中的构造函数返回值问题。
构造函数的返回值
在传统语言中,构造函数不应该有返回值,理论执行的返回值就是此构造函数的实例化对象。
而在JS中构造函数能够有返回值也能够没有。
没有返回值则依照其余语言一样返回实例化对象。
function Foo(name) { this.name = name}console.log(new Foo('wscats'))
若有返回值则查看其返回值是否为援用类型。如果是非援用类型,如根本类型(String,Number,Boolean,Null,Undefined)则与无返回值雷同,理论返回其实例化对象。
function Foo(name) { this.name = name return 520}console.log(new Foo('wscats'))
- 若返回值是援用类型,则理论返回值为这个援用类型。
function Foo(name) { this.name = name return { age: 16 }}console.log(new Foo('wscats'))
原题中,因为返回的是this,而this在构造函数中原本就代表以后实例化对象,最终Foo函数返回实例化对象。
之后调用实例化对象的getName函数,因为在Foo构造函数中没有为实例化对象增加任何属性,以后对象的原型对象(prototype)中寻找getName函数。
当然这里再拓展个题外话,如果构造函数和原型链都有雷同的办法,如上面的代码,那么默认会拿构造函数的私有办法而不是原型链,这个知识点在原题中没有体现进去,前面改进版我曾经加上。
function Foo(name) { this.name = name this.getName = function() { return this.name }}Foo.prototype.name = 'Oaoafly';Foo.prototype.getName = function() { return 'Oaoafly'}console.log((new Foo('Wscats')).name) //Wscatsconsole.log((new Foo('Wscats')).getName()) //Wscats
第七问
new new Foo().getName();
同样是运算符优先级问题。做到这一题其实我曾经感觉答案没那么重要了,要害只是考查面试者是否真的晓得面试官在考查咱们什么。
最终理论执行为:
new ((new Foo()).getName)();
new有参数列表(18)->new有参数列表(18)
先初始化Foo的实例化对象,而后将其原型上的getName函数作为构造函数再次new,所以最终后果为3
答案
function Foo() { getName = function () { alert (1); }; return this;}Foo.getName = function () { alert (2);};Foo.prototype.getName = function () { alert (3);};var getName = function () { alert (4);};function getName() { alert (5);}//答案:Foo.getName();//2getName();//4Foo().getName();//1getName();//1new Foo.getName();//2new Foo().getName();//3new new Foo().getName();//3
后续
后续我把这题的难度再略微加大一点点(附上答案),在Foo函数外面加多一个私有办法getName,对于上面这题如果用在面试题上那通过率可能就更低了,因为难度又大了一点,又多了两个坑,然而明确了这题的原理就等同于明确了下面所有的知识点了
function Foo() { this.getName = function() { console.log(3); return { getName: getName //这个就是第六问中波及的构造函数的返回值问题 } }; //这个就是第六问中波及到的,JS构造函数私有办法和原型链办法的优先级 getName = function() { console.log(1); }; return this}Foo.getName = function() { console.log(2);};Foo.prototype.getName = function() { console.log(6);};var getName = function() { console.log(4);};function getName() { console.log(5);} //答案:Foo.getName(); //2getName(); //4console.log(Foo())Foo().getName(); //1getName(); //1new Foo.getName(); //2new Foo().getName(); //3//多了一问new Foo().getName().getName(); //3 1new new Foo().getName(); //3
最初,其实我是不倡议把这些题作为考查面试者的惟一评判,然而作为一名合格的前端工程师咱们不应该因为塌实疏忽了咱们的一些最根本的基础知识,当然我也祝福所有面试者找到一份现实的工作,祝福所有面试官找到心中那匹千里马~
交换
如果文章和笔记能带您一丝帮忙或者启发,请不要悭吝你的赞和珍藏,文章同步继续更新,能够微信搜寻「 前端漫游 」关注公众号不便你往后浏览,往期文章也收录在 https://github.com/Wscats/art...
欢迎您的关注和交换,你的必定是我后退的最大能源?