聊一聊自己对一道会被轻视的前端js面试题的理解

348 阅读5分钟

先来看看这道题目吧:

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

先谈一下我对这道题的理解吧,我认为这道题有关几个知识点:

  1. 构造函数
  2. 变量赋值
  3. 对象的属性赋值
  4. 原型
  5. 函数声明
  6. new操作符
  7. 预编译
  8. this指向

话不多说,直接上全过程解析:

在整体代码执行之前,全局会有一个预编译的阶段,首先会对这段代码进行预编译。

全局预编译的三个阶段:

  1. 首先会生成一个Global Object的对象。
  2. 找变量声明,将其变量名和函数名作为Global Object的属性名,并且给其赋值为undefined。
  3. 找函数声明,如果有和变量声明重名,则覆盖。其值为该函数体。

那我们就来进行预编译阶段看看吧:

1. 首先创建一个Global对象

Global:{


}
2. 找变量声明,将其变量名和函数名作为Global Object的属性名,并且给其赋值为undefined

Global: {
   getName: undefined
}

3. 找函数声明,如果有和变量声明重名,则覆盖。其值为该函数体。
Global: {
   getName: function() {
              alert(5);
            },
   Foo: function (){
        getName = function() {
            alert(1);
        };
        return this;
   }
}
重点强调: 这一步function getName(){}函数声明覆盖了原本的变量声明getName: undefined

到这里我们全局的预编译阶段就结束了,然后我们开始执行代码。

1. function Foo(){} 因为在预编译阶段已经帮我们做完这件事了,所以可以忽略这段代码
2. Foo.getName = function() { 
       alert(2) 
   }
   
俗话说,js一切皆对象哈哈哈哈哈哈哈哈哈。
Foo.prototype.__proto__ === Object.prototype
所以我们的Foo函数也是一个对象,自然也有给对象赋值操作同样的方法。和下述代码同理。

let obj = {} 
obj.getName = function (){}

这一步Global对象发生了变化
Global: {
   getName: function() {
              alert(5);
            },
   Foo: function (){
        getName = function() {
            alert(1);
        };
        getName: function () {
            alert(2)
        }
        return this;
   }
}

注意了这里面给getName进行赋值操作和定义一个是function的getName属性不是一回事!!!

3. Foo.prototype.getName = function() {
      alert(3);
   }
   
这一步给Foo这个构造函数的原型prototype赋值getName: function(){alert(3)}
这一步Global对象也发生了变化
Global: {
   getName: function() {
              alert(5);
            },
   Foo: function (){
        getName = function() {
            alert(1)
        };
        getName: function () {
            alert(2)
        }
        prototype: {
            getName: function () {
               alert(3)
            }
        }
        return this;
   }
}

4. var getName = function() {
       alert(4);
   }
   
同理,变量声明我们已经在预编译阶段帮我们做了。所以此代码片段可以视为变量赋值。
即给getName这个变量赋值为 function () { alert(4) }
在这里我们的Global对象也发生了变化

Global: {
   getName: function() {
       alert(4);
   }
   Foo: function (){
        getName = function() {
            alert(1)
        };
        getName: function () {
            alert(2)
        }
        prototype: {
            getName: function () {
               alert(3)
            }
        }
        return this;
   }
}

5. function getName() {
     alert(5);
   }

同理,函数声明我们在预编译阶段已经做过了。代码片段忽略。

走到了这里好像我们似乎可以开始做我们的题目了吧哈哈哈哈哈哈哈!!

Global: {
   getName: function() {
       alert(4);
   }
   Foo: function (){
        getName = function() {
            alert(1)
        };
        getName: function () {
            alert(2)
        }
        prototype: {
            getName: function () {
               alert(3)
            }
        }
        return this;
   }
}

//请写出以下输出结果:
1. Foo.getName(); 
调用Foo的getName方法 -> 弹出2

2.getName(); 
调用getName方法 -> 弹出4

3.Foo().getName(); 
这里分两步执行:
但是在函数执行前,也会有一个函数的预编译阶段,并且也会创建一个对象叫Activation Object(执行期上下文,也是函数内部的作用域)

函数预编译的几个步骤:
1. 创建一个Activation Object
2. 找形参和变量声明,形参名和变量名为Activation Object的属性名,其并且赋值为undefined
3. 将实参和形参统一(即把函数执行传入的实参赋值给形参)
4. 找函数声明,如果有和变量声明重名,则覆盖。其值为该函数体。

这里FooActivation Object:{}
          
然后开始执行函数
(1). Foo() -> 执行Foo函数 -> 给getName赋值 function (){ alert(1) }
但是在Foo的作用域中并没有gatName变量,也就是说给未经声明的变量赋值,那么相当于window.getName = function () { alert(1) }
在这里window(BOM) -> global(DOM)

然后我们的Global对象又发生了变化:
Global: {
   getName: function() {
       alert(1)
   }
   Foo: function (){
        getName = function() {
            alert(1)
        };
        getName: function () {
            alert(2)
        }
        prototype: {
            getName: function () {
               alert(3)
            }
        }
        return this;
   }
}

函数执行完 return this -> 这时候的this —> window

(2). Foo().getName() -> window.getName() -> 自然弹出1


4. getName() // 执行getName函数 -> 弹出1

5. new Foo.getName(); 

这里我们碰到了new操作符,我们应该知道胡new操作符接函数执行即是为该构造函数生成一个实例
// new 操作符
// 内部隐式创建一个空对象 {}
// 执行函数
// 返回这个对象
// 函数内部的this指向返回的这个对象 也就是我们的实例化对象

了解到这里,我们应该可以轻松理解new Foo.getName() -> 弹出2 且返回一个空对象

6. new Foo().getName(); 

同理遇到了new操作符,但是与上面不同的是,在这里我们先执行new Foo()
返回的对象为{
              __proto__: getName: function () { alert(3) }
           }
其实也应该是一个空对象,__proto__属性是Chrome浏览器为我们添加的一个[[prototype]]属性,称为该实例化对象的原型
意义是该实例化对象的__proto__ 指向其构造函数 Foo.prototype,也就是说 实例化对象.__proto__ === Foo.prototype

然后执行 实例化对象.getName(), 但是在该实例化对象的作用域中并没有getName函数,然后会随着原型链去寻找,然后可以在__proto__中也就是Foo.prototype中找到getName函数,然后执行 -> 弹出3

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

这里首先执行new Foo() 生成一个实例化对象 {
              __proto__: getName: function () { alert(3) }
           }
然后执行 new 实例化对象.getName() -> 同理参考上文 弹出3 返回一个空对象{}

以上为我个人所理解的知识量去解释的这道题目,如有侵权请与我联系。

感谢各位看到这里,如果能看到这里,麻烦给我点个赞吧哈哈哈哈哈哈。

然后说说看对于这些内容的一些看法,有些地方可能说的不对还需要大家帮忙指正一下。