题目
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) {
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 中构造函数可以有返回值也可以没有。
- 没有返回值则按照其他语言一样返回实例化对象。
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)//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();//
getName();//
Foo().getName();//
getName();//
new Foo.getName();//
new Foo().getName();//
new new Foo().getName();//
后续
后续我把这题的难度再稍微加大一点点(附上答案),在 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(); //
getName(); //
console.log(Foo())
Foo().getName(); //
getName(); //
new Foo.getName(); //
new Foo().getName(); //
//多了一问
new Foo().getName().getName(); //3 1
new new Foo().getName(); //
原文转发:https://www.v2ex.com/t/351261
个人读后感:
1、函数声明:
实例1:
function Foo(){ this.getName = function(){ console.log(1);
}
var getName = function(){ console.log(2);
}
return this;
}
1、内有this属性,为共有属性,则为构造函数,如果this没有new,则this指向window;
2、如果有,则为函数的私有属性,不能被调用;
实例2:
function getName(){
console.log(1);
} var getName = function(){
console.log(2)
} getName()
1、结果为2,这个考察了执行上下文的内容
2、函数表达式比函数声明优先赋值;上为函数声明,下为函数表达式
实例3:
var name = 'tom'; function getName(name){
name = 'jim';
console.log(name);
} console.log(name);
getName(name);
1、结果为:tom、jim,由于参数为name,所以就相当于声明var name,然后name = ‘jim’,赋值,所以getName(name)为jim,name已经声明了
2、如果没有参数,则name为全局变量
实例4:
优先级
new Foo.getName();
new Foo().getName();
new new Foo().getName();
1、new Foo.getName()
new Foo这是new没有参数列表
new Foo()这是有参数列表
new Foo.getName() 因为new Foo没有参数列表,所以点的优先级高于new Foo没有参数列表,先执行点,new (Foo.getName)()
当(Foo.getName)执行完以后 ,变为new O() 有参数列表的new,所以执行(new O)()
2、new Foo().getName()
由于new Foo()有参数列表和点属于同级,所以从左往右执行(new Foo()).getName(),括号执行完执行点,点执行完执行()
3、new new Foo().getName()
new ((new Foo()).getName) () ,先执行new 有参数列表的,因为和点同级,所以从左往右执行,执行完以后变为 new O (),先执行