V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
wsscats
V2EX  ›  JavaScript

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

  wsscats · 2017-03-29 17:57:29 +08:00 · 9428 次点击
这是一个创建于 2789 天前的主题,其中的信息可能已经有所发展或是发生改变。

题目

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 指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级等知识,此题在网上也有部分相关的解释,当然我觉得有部分解释还欠妥,不够清晰,特地重头到尾来分析一次,当然我们会把最终答案放在后面,并把此题再改高一点点难度,改进版也放在最后,方便面试官在出题的时候有个参考 顺便附上原文链接 image

第一问

先看此题的上半部分做了什么,首先定义了一个叫 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) {
			console.log(name); //Hello
			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 运算符优先级

这题首先看优先级的第 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'))

image

  1. 若有返回值则检查其返回值是否为引用类型。如果是非引用类型,如基本类型( String,Number,Boolean,Null,Undefined )则与无返回值相同,实际返回其实例化对象。
                function Foo(name){
			this.name = name
			return 520
		}
		console.log(new Foo('wscats'))

image

  1. 若返回值是引用类型,则实际返回值为这个引用类型。
                function Foo(name){
			this.name = name
			return {
				age:16
			}
		}
		console.log(new Foo('wscats'))

image 原题中,由于返回的是 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

@Wscats 原题最初版来源: 码农网 cnblogs Github

第 1 条附言  ·  2017-03-30 10:38:29 +08:00
其实我是不建议把这些题作为考察面试者的唯一评判,但是作为一名合格的前端工程师我们不应该因为浮躁忽略了我们的一些最基本的基础知识,当然我也祝愿所有面试者找到一份理想的工作,祝愿所有面试官找到心中那匹千里马~
34 条回复    2017-03-31 07:27:32 +08:00
guokeke
    1
guokeke  
   2017-03-29 18:53:05 +08:00
赞。
aitaii
    2
aitaii  
   2017-03-29 19:31:10 +08:00 via Android
以前好像见过这题
mcfog
    3
mcfog  
   2017-03-29 19:33:15 +08:00 via Android   ❤️ 6
建议谨慎考虑问这类问题的公司。类似的还有问 i++ + ++i 的。我觉得就算我因为某种原因面试用了这样的题目,也是期待面试者吐槽“不要 var x; function x(),好好取名字“,而不是告诉我到底谁覆盖了谁
mcfog
    4
mcfog  
   2017-03-29 19:38:12 +08:00 via Android
哦,能顺便从基础的 iife 开始到前端模块化聊聊前端命名空间管理的更赞
lwbjing
    5
lwbjing  
   2017-03-29 19:38:58 +08:00
这些我都了解,能拿 6K 吗。
shenqi
    6
shenqi  
   2017-03-29 20:49:52 +08:00
答对了能有 3.8k 吗?

要是我的同事写这个代码, review 不通过重写。
jarlyyn
    7
jarlyyn  
   2017-03-29 20:52:18 +08:00   ❤️ 1
这 sb 题目……
murmur
    8
murmur  
   2017-03-29 20:52:43 +08:00
喜欢出这个题的公司 编码风格是啥样啊
wdlth
    9
wdlth  
   2017-03-29 21:02:56 +08:00   ❤️ 1
反问出这个题的人
http://davidshariff.com/js-quiz/
上面的题目能答满分么?
lijsh
    10
lijsh  
   2017-03-29 21:07:22 +08:00
招聘者应该知道出笔试题的目的是什么吧,这种题不具备任何现实意义。
peneazy
    11
peneazy  
   2017-03-29 21:26:19 +08:00
挺好的,学到了新东西
yhxx
    12
yhxx  
   2017-03-29 21:38:16 +08:00
直接拉到最后,发现居然不是培训班的广告
不科学
liuxin5959
    13
liuxin5959  
   2017-03-29 21:40:58 +08:00
还好全对路过。✌️
rashawn
    14
rashawn  
   2017-03-29 22:40:52 +08:00 via iPhone
还不如问 一个项目 npm install 报错 给出不同错误 问你咋办
CodingPuppy
    15
CodingPuppy  
   2017-03-30 00:07:36 +08:00
题是好题,用来招人不太合适。
issuz
    16
issuz  
   2017-03-30 01:36:21 +08:00
第三问的举例代码好像有处错误?我运行了下, console.log(name)打印出的是 Wscats ,而不是 Oaoafly
WildCat
    17
WildCat  
   2017-03-30 01:58:34 +08:00
虽然楼主很厉害,但是, ES6 完美解决
Cbdy
    18
Cbdy  
   2017-03-30 07:29:21 +08:00 via Android   ❤️ 5
拿糟粕当特性的典型
q397064399
    19
q397064399  
   2017-03-30 07:57:21 +08:00 via iPhone
万恶的反人类原型继承
seki
    20
seki  
   2017-03-30 10:33:20 +08:00
2017 年了还在搞 prototype 么,业务代码哪里会接触到这么多 prototype
crashX
    21
crashX  
   2017-03-30 11:13:01 +08:00
好多 js 里的糟粕,新的 es6 都在尽力规避,不知道问这题的人什么心态。
zhuangzhuang1988
    22
zhuangzhuang1988  
   2017-03-30 11:25:50 +08:00
渣语言 + 渣题目 + 八股
coderluan
    23
coderluan  
   2017-03-30 11:29:53 +08:00
就我想吐槽 [经常忽视的面试题] 吗?
heww
    24
heww  
   2017-03-30 11:38:34 +08:00   ❤️ 1
看到楼上的这些回复我就放心了!
vghdjgh
    25
vghdjgh  
   2017-03-30 12:03:43 +08:00
这代码通不过类型检查、 lint 检查的,也通不过 code review 的。
nino
    26
nino  
   2017-03-30 13:38:28 +08:00
我真的很讨厌这种题啊
palmers
    27
palmers  
   2017-03-30 13:48:04 +08:00
第二问 node 环境下值为啥不是不一样
```js
function Foo() {
getName = function () { console.log(1); };
return this;
}

Foo.getName = function () { console.log(2);};

Foo.prototype.getName = function () { console.log(3);};

var getName = function () { console.log(4);};


function getName() { console.log(5);}
```

```node
> getName();
5
```

node --version v7.7.3
yoa1q7y
    28
yoa1q7y  
   2017-03-30 13:52:17 +08:00
老生常谈
WindsonYan
    29
WindsonYan  
   2017-03-30 14:34:02 +08:00
别程序员经常忽视了。自己把代码复制到 chrome console ,或者把 alert 改成 console.log 放到 node 底下试试吧。
我觉得我还是告诉楼主吧,“茴”字一共有四种写法。
liuxu
    30
liuxu  
   2017-03-30 14:45:19 +08:00
学习了
wangxiaoer
    31
wangxiaoer  
   2017-03-30 15:59:56 +08:00
第五、第七说没啥意义还可以接受,其它为毛没有意义?变量提升、作用域、原型 这些东西不是很基础的?
MouCai
    32
MouCai  
   2017-03-30 17:10:30 +08:00 via iPhone
战略性 mark ,这道题可以加入我写的 js 教程中。但不会用来做面试题,实际开发中程序员会自行测试出哪种写法符合预期。同时团队项目也会直接通过 eslint 直接干掉类似这种调用构造函数不加 new ,混合函数表达式和函数声明,随意修改全局变量,各种重名变量等陋习。
peneazy
    33
peneazy  
   2017-03-30 21:57:14 +08:00
我个人非常喜欢楼主的这个帖子,尤其是第 5 问到第 7 问的对 new 和属性访问的解析。最近看一本 JS 设计模式的教材,里面有
VehicleFactory.prototype.createVehicle = function ( options ) {

if( options.vehicleType === "car" ){
this.vehicleClass = Car;
}else{
this.vehicleClass = Truck;
}

return new this.vehicleClass( options );

};
看了楼主的分析,才对这段代码最后的 return 有了正确理解。
支持楼主
weixiangzhe
    34
weixiangzhe  
   2017-03-31 07:27:32 +08:00 via iPhone
厉害了,晚点在看 mark
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2674 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 27ms · UTC 03:35 · PVG 11:35 · LAX 19:35 · JFK 22:35
Developed with CodeLauncher
♥ Do have faith in what you're doing.