js超经典面试题Foo.getName()的故事

4,211 阅读4分钟

下面是一道超经典的JS面试题。
蕴含了静态属性与实例属性,变量提升,this指向,new一个函数的过程

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);
};

Foo.getName(); 
getName(); 
Foo().getName(); 
getName(); 
new Foo.getName();
new Foo().getName();
new new Foo().getName(); 

输出一下结果

Foo.getName(); //2
getName(); //4
Foo().getName(); //1
getName(); //1
new Foo.getName(); //2
new Foo().getName(); //3
new new Foo().getName(); //3

一、解析:
1.Foo.getName()

我们先看此题的上半部分做了什么,首先定义了一个叫Foo的函数,之后为Foo创建了一个叫getName的静态属性存储了一个匿名函数,之后为Foo的原型对象新创建了一个叫getName的匿名函数。之后又通过函数变量表达式创建了一个getName的函数,最后再声明一个叫getName函数。

第一问的 Foo.getName 自然是访问Foo函数上存储的静态属性,自然是2

二、解析:
2.getName()

为何输出是4,这里考的是变量提升与函数声明提升。我们知道使用var声明变量会存在变量提升的情况,比如下面的例子中,即使在声明前使用变量a也不会报错,举例:

console.log(a)// undefined
var a = 1;
console.log(a)// 1

因为声明提前会让声明提升到代码的最上层,而赋值操作停留在原地,所以上面代码等同于:

var a
console.log(a)// undefined
a = 1;
console.log(a)// 1

而函数声明(注意是函数声明,不是函数表达式或者构造函数创建函数)也会存在声明提前的情况,即我们可以在函数声明前调用函数:

fn() // 1
function fn() {
    console.log(1);
};
fn() // 1

//因为函数声明提前,导致函数声明也会被提到代码顶端,所以等同于
function fn() {
    console.log(1);
};
fn() // 1
fn() // 1

那这样就存在一个问题了,变量声明会提升,函数声明也会提升,谁提升的更高呢?在你不知道的JavaScript中明确指出,函数声明会被优先提升,也就是说都是提升,但是函数比变量提升更高,所以题目中的两个函数顺序可以改写成:

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

var getName;

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

这样就解释了为什么是输出4。

三、解析:
3.Foo().getName()

其实可以看出来,我们在执行Foo()函数的时候getName这个变量提升到外部的全局作用域中了,因为在js中,如果对于一个变量没用用var 或者 let等声明的话,他就默认是全局属性,就是window对象的一个属性。所以在这里我们的全局的getName又被改了
因为我们Foo()执行的时候返回了this而这里的this就是window对象 我们需要知道的是在浏览器中所有全局的声明都是window对象的属性和方法,所以这里我们调用this.getName()就会返回1了。

四、解析:
4.getName()

这里输出1已经毫无悬念,上一分析中,getName的值在Foo执行时被修改了,所以再调用getName一样等同于window.getName(),同样是输出1。

五、解析:
5.new Foo.getName()

首先还是先看运算符优先级吧,我自个看完的结果是【new Foo() > Foo() > new Foo】,先运算方式2的Foo.getName() 结果为“2”,再new一个Foo实例对象,因此这里new的过程就相当于单纯把Foo.getName执行了一遍输出2。

六、解析:
6.new Foo().getName()

这里考了new基本概念,首先这个调用分为两步,第一步new Foo()得到一个实例,第二步调用实例的getName方法。

我们知道new一个构造函数的过程大致为,以构造函数原型创建一个对象(继承原型链),调用构造函数并将this指向这个新建的对象,好让对象继承构造函数中的构造器属性,如果构造函数没有手动返回一个对象,则返回这个新建的对象。

所以在执行new Foo()时,先以Foo原型创建了一个对象,由于Foo.prototype上事先设置了一个getName方法(输出3的那个),所以这个对象可通过原型访问到这个方法,其次由于Foo内部也没提供什么构造器属性,最终返回了一个this(这个this指向实例),因此这里的this还是等同于我们前面概念提到的以Foo原型创建的对象,可以尝试输出这个实例,除了原型上有一个getName方法就没有其它任何属性,因此这里输出3。

七、解析:
7.new new Foo().getName()

相当于new(new Foo().getName()) 先执行new Foo().getName()由6部知道了输出3,再创建Foo.prototype.getName()的实例返回。结果为3