关于javascript:前端程序员经常忽视的一个JavaScript面试题

22次阅读

共计 7528 个字符,预计需要花费 19 分钟才能阅读完成。

题目

浏览往期更多优质文章可移步我的 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() //oaoafly
var getName = function() {console.log('wscat')
}
getName() //wscat
function getName() {console.log('oaoafly')
}
getName() //wscat

下面的代码看起来很相似,感觉也没什么太大差异。但实际上,Javascript 函数上的一个“陷阱”就体现在 Javascript 两种类型的函数定义上。

  • JavaScript 解释器中存在一种变量申明被晋升的机制,也就是说函数申明会被晋升到作用域的最后面,即便写代码的时候是写在最初面,也还是会被晋升至最后面。
  • 而用函数表达式创立的函数是在运行时进行赋值,且要等到表达式赋值实现后能力调用
var getName // 变量被晋升,此时为 undefined

getName() //oaoafly 函数被晋升 这里受函数申明的影响,尽管函数申明在最初能够被晋升到最后面了
var getName = function() {console.log('wscat')
} // 函数表达式此时才开始笼罩函数申明的定义
getName() //wscat
function getName() {console.log('oaoafly')
}
getName() //wscat 这里就执行了函数表达式的值

所以能够合成为这两个简略的问题来看分明区别的实质

var getName;
console.log(getName) //undefined
getName() //Uncaught TypeError: getName is not a function
var getName = function() {console.log('wscat')
}            
var getName;
console.log(getName) //function getName() {console.log('oaoafly')}
getName() //oaoafly
function 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) //Oaoafly
console.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 中构造函数能够有返回值也能够没有。

  1. 没有返回值则依照其余语言一样返回实例化对象。

    function Foo(name) {this.name = name}
    console.log(new Foo('wscats'))

  2. 若有返回值则查看其返回值是否为援用类型。如果是非援用类型,如根本类型(String,Number,Boolean,Null,Undefined)则与无返回值雷同,理论返回其实例化对象。

    function Foo(name) {
     this.name = name
     return 520
    }
    console.log(new Foo('wscats'))

  3. 若返回值是援用类型,则理论返回值为这个援用类型。
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) //Wscats
console.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();//2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new 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(); //2
getName(); //4
console.log(Foo())
Foo().getName(); //1
getName(); //1
new Foo.getName(); //2
new Foo().getName(); //3
// 多了一问
new Foo().getName().getName(); //3 1
new new Foo().getName(); //3             

最初,其实我是不倡议把这些题作为考查面试者的惟一评判,然而作为一名合格的前端工程师咱们不应该因为塌实疏忽了咱们的一些最根本的基础知识,当然我也祝福所有面试者找到一份现实的工作,祝福所有面试官找到心中那匹千里马~

交换

如果文章和笔记能带您一丝帮忙或者启发,请不要悭吝你的赞和珍藏,文章同步继续更新,能够微信搜寻「前端漫游」关注公众号不便你往后浏览,往期文章也收录在 https://github.com/Wscats/art…
欢迎您的关注和交换,你的必定是我后退的最大能源?

正文完
 0