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();
这是一道比较经典的前端面试题,里面涵盖的内容主要包括了以下几点在js中比较容易被忽视的知识点。
接下来我来理一理这段代码中需要被注意的一些地方,首先是关于前面的第1点和第3点。
先解释一下上面提到的函数声明的概念,在js中我们声明一个函数有两种方式,一种是函数声明,一种是函数表达式,除了他们的表达方式有所不同以外,他们也存在着些许不同。
//函数声明
function funcName(){}
//函数表达式
var funcName = function(){}
在es5中存在着一种名为变量提升的机制,意思就是说js代码中关于函数声明和变量声明总是会被解释器悄悄地被“提升”到方法体的最顶部,而且函数声明的提升比变量提升更加靠前(需要注意的是变量提升不代表赋值提升),用一个例子来解释便是
a();//调用成功
console.log(q); //undefined
var q = '123';
function a(){
console.log('调用成功')
}
在这个例子中,之所以函数可以正常运行而不报错的原因就是函数声明提升与变量声明提示,上述代码其实等效于
function a(){
console.log('调用成功')
}
var q = undefined
a();//调用成功
console.log(q); //undefined
q = '123'; //之所以上面没有输出123只是因为赋值语句还没有被执行,q便console.log了
所以回到原来的例子
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)}
这段代码按照前面的思路可以变形为
function Foo(){
getName = function(){
alert(1);
};//这里需要注意,这里的getName只是一个赋值操作,只要Foo函数没有执行便没有意义
return this;
}
function getName(){alert(5)}
var getName = undefined;
Foo.getName = function(){alert(2);};
Foo.prototype.getName = function(){alert(3)};
getName = function(){alert(4);};
将代码变形后,前四个输出便显而易见
Foo.getName();//2
getName();//4
Foo().getName();//1
getName();//1
在讨论接下来的三个输出之前,我再举一个例子来说明什么是原型链。
从前有一个家族ABC,这个家族的所有成员都拥有一个神奇的特性——暂且称这个特性为非洲之力,并且只有继承这一家族的血脉才能拥有这一特性,
然而这一家族在后来分裂为了三个家族,分别名为A、B、C。
之后B的族长经过苦修,修炼了一种名为‘欧洲之力’的能力,该能力覆盖了原本的能力,B的族人到最后只能使用‘欧洲之力’,却再也使不出‘非洲之力’。
再后来随着历史的飞跃ABC慢慢忘记了三家都起源于同一个家族这一事情,随着资源越来越少,三家开始相互斗争,本着党同伐异的原则,AC两家开始联手侵蚀B的资源,寡不敌众B要求和解,但AC要求B证明自己拥有非洲之力的能力,否则就将B认作是异端讨伐,后来出现一个契机B族的子孙Bb通过 ‘proto’ 证明了自己与AC同源,于是三家同归于好,联起手来向外扩张,过上了幸福美满的生活。
然后让我们用代码来重现B证明自己与AC同源的这一经过
function ABC(){}
//只有ABC的后代才拥有这个能力
ABC.prototype.ability = '非洲之力';
//ABC分家了
var A = new ABC();
var B = new ABC();
var C = new ABC();
//B的族长经过苦修,修炼了一种名为‘欧洲之力’的能力,该能力覆盖了原本的能力
B.ability = '欧洲之力';
console.log(A.ability);//非洲之力
console.log(B.ability);//欧洲之力
console.log(C.ability);//非洲之力
while(B.__proto__.ability === A.ability){
console.log('ABC同族');
break;
}
这个例子其实就是一个典型的原型链,在原型链的概念中一般会出现三种角色,1.构造函数 2.构造函数的原型 3.由构造函数创建的实例 ,在这个例子中这三个角色分别对应了ABC函数,ABC函数的原型,以及所有通过ABC函数new出来的对象(A、B、C)
这三个角色之间的关系可以用下面一张图来解释
在我们创建ABC这一函数的同时,js的编译器同时也创建了一个ABC.prototype的对象,而这一对象的constructor同时也指向ABC函数,所有通过ABC这一构造出来的实例对象(如同A、B、C)中都有一个属性[[[prototype]]](在chrome暴露的接口为proto)都指向其构造函数的原型(即ABC.prototype)。
如果你看到了这里,那我想你应该会有一个疑问,为什么在C这一实例中并不存在ability这一属性却能输出‘非洲之力’,这里就牵扯到了原型链的一个作用了——所有实例都可以访问其构造函数原型内的属性。所以c.ability实际上就是在访问ABC.prototype.ability。而在B中由于实例内本身就有了ability这一属性,所以在访问B.ability时就是访问实例内自身的属性,所以这里又出现了一个原型链的特性,即优先访问本地的属性,如果本地的属性中没有想要访问的属性,则顺着原型链向上访问,直到找到相应的最近的属性才停止继续向上访问。
现在回到原来的题目上,下面的输出什么呢?
new Foo.getName();
new Foo().getName();
new new Foo().getName();
先来看第一句
new Foo.getName();
这一句很明显是创建一个以Foo.getName为构造函数的实例
所以这个操作会输出2
接下来看下一个
new Foo().getName();
代码中是先创建了一个以Foo为构造函数的实例,然后又调用了该函数中的getName属性,然而根据该构造函数本身
function Foo(){
getName = function(){
alert(1);
};
return this;
}
我们不难看出该构造函数创建的实例不会存在任何属性,所以在这一例子中调用的getName属性应该是来自Foo.prototype.getName
所以这一语句会输出3
new Foo().getName();//3
最后一句
new new Foo().getName();
这里连续写了两个new,这里会出现理解上的歧义,由于该代码的可读性十分低下,在开发中基本不会出现,除了面试拿来挖坑以外没啥别的用处。
该代码解读的顺序为
var a = new Foo();
new a.getName();
//这里a会被gc了
a = null;
而这里的a.getName其实就是Foo.prototype.getName,所以同样这里会输出3
new new Foo().getName();//3