JS Foo.getName笔试题解析,杂谈静态属性与实例属性,变量提升,this指向,new一个函数的过程

 壹 ❀ 引

Foo.getName算是一道比较老的面试题了,大致百度了一下在17年就有相关文章在介绍它,遗憾的是我在19年才遇到它,比较奇妙的是现在仍有公司会使用这道题。相关解析网上是有的,这里我站在自己的理解做个记录,也算是相关知识的一次复习,题目如下,输出过程也直接标出来了:

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(); //2
getName(); //4
Foo().getName(); //1
getName(); //1
new Foo.getName(); //2
new Foo().getName(); //3
new new Foo().getName(); //3

如果大家搜这个题,那说明肯定是对于某一部分执行是有疑虑,那么现在就跟着我的思路重新理一遍,本文开始:

 贰 ❀ 分析

1.Foo.getName()

为什么输出2,不是3?这就得说说构造函数的静态属性与实例属性

我们都知道函数属于对象,而对象拥有可以自由添加属性的特性,函数也不例外,构造函数也是函数:

function Fn() {};
Fn.person = '听风是风';
Fn.sayName = function () {
    console.log(this.person);
};
Fn.sayName(); // 听风是风

比如这个例子中,我为构造函数Fn添加了静态属性person静态方法sayName,我们可以通过构造函数Fn直接访问。在JS中,我们将绑定在构造函数自身上的属性方法称为静态成员,静态成员可通过构造函数自身访问,而实例无法访问。

let people = new Fn();
people.sayName();// 报错,实例无法访问构造函数的静态属性/方法

那有什么属性是实例可以访问而构造函数自身无法访问的呢,当然有,比如实例属性。这里我将实例属性细分为构造器属性与原型属性两种,看下面的例子:

function Fn() {
    // 构造器属性
    this.name = '听风是风';
    this.age = 26;
};
// 原型属性
Fn.prototype.sayName = function () {
    console.log(this.name);
};
let people = new Fn();
people.sayName(); // 听风是风

在这个例子中,我们在构造函数Fn中添加了两条构造器属性this.namethis.age,此外还在函数外面通过原型添加了一个原型方法sayName

当我们new一个实例后,实例可以直接访问这些构造器属性原型属性,所以这里将两种属性统称为实例属性实例属性只有实例才能访问,构造函数自身无法访问:

Fn.sayName()// 报错,找不到此方法

说到这大家有没有觉得静态属性与实例属性像一对欢喜冤家,静态属性只有构造函数自身可以使用,而实例属性呢只有实例可以使用,两者看似划清界限,但都由构造函数产生。

那么大家可能又有疑问了,你说实例属性好歹可以用在继承上,这静态属性取了个高大上的名字也感觉有什么大作用啊,其实是有的,比如JS的Memoization(记忆化)模式:

//Memoization模式
const myFunc = function (param) {
    //do something
    if (!myFunc.cache[param]) {
        myFunc.cache[param] = param * 100;
    };
};
//在函数上添加了一个用于储存的对象
myFunc.cache = {};
//调用函数
myFunc(1);
//访问存储
myFunc.cache[1]; //100

这个例子中,我们为函数添加了一个用于存储执行结果的对象cache,将每次调用函数的参数作为对象的key,执行结果作为value,对于执行特别复杂的操作,这样只用执行一次之后就可以直接通过参数访问到最终结果。

如果对于静态属性有兴趣,想了解更多可以阅读博主这篇文章 精读JavaScript模式(七),命名空间模式,私有成员与静态成员

2.getName()

为什么输出4而不是5,这里考的是变量提升与函数声明提升。我们知道使用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而不是5了。想更详细了解变量提升,函数提升规则,可以阅读博主这篇文章 【JS点滴】声明提前,变量声明提前,函数声明提前,声明提前的先后顺序

3.Foo().getName()

这里全局变量与window的关系以及this指向的问题。

我们知道使用var声明的全局变量等同于给window添加属性,以及函数声明的函数也会成为window属性

var a = 1;
// window上可以找到这条属性
window.a; //1

function acfun() {
    console.log(1);
};
// window上可以找到这个方法
window.acfun(); //1

JS Foo.getName笔试题解析,杂谈静态属性与实例属性,变量提升,this指向,new一个函数的过程_第1张图片

了解了这一点后,我们再来看函数执行过程,第一步执行Foo(),在分析第二个执行时我们知道了getName是全局变量,所以在函数Foo内也能直接访问,于是getName被修改成了输出1的函数,之后返回了一个this。

由于Foo().getName()等同于window.Foo().getName(),所以this指向window,这里返回的this其实就是window。

现在执行第二步window.getName(),前面已经说了全局变量等同于给window添加属性,而且全局变量getName的值在执行Foo()时被修改,所以这里输出1。

4.getName()

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

5.new Foo.getName()

在分析一中我们已经知道了Foo.getName是Foo的静态方法,这里的getName虽然是Foo的静态方法,但是既没有继承Foo的原型,自身内部也没提供任何构造器属性(this.name这样的),所以new这个静态方法只能得到一个空属性的实例。

因此这里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。

我们可以将Foo函数改写成下面这样,其它不变,猜猜new Foo().getName()输出什么:

function Foo() {
    getName = function () {
        console.log(1);
    };
    return {
        getName: function () {
            console.log(6);
        }
    };
};

如果你对于new一个函数过程以及new函数返回值规则不太了解,我在上面的分析应该是会读的不太理解。如果你存在疑问,可以阅读博主这两篇文章:

精读JavaScript模式(三),new一个构造函数究竟发生了什么?  这篇文章直接看第四、五节的知识。

js new一个对象的过程,实现一个简单的new方法 这篇文章关于new的过程介绍更为精确。

7.new new Foo().getName()

老实说这个执行给出来真的就是满满的恶意,先不说new不new什么的,怎么执行都把人难住,第一眼也是看的我很懵,我们知道new一个函数都是new fn(),函数带括号的。所以这里其实可以拆分成这样:

var a = new Foo();
new a.getName();

那这样就好说了,第一步执行上面已经有分析过了,由于构造函数Foo自身啥构造器属性都没有,只有原型上有一个输出3的原型方法,所以实例a是一个原型上有输出3的函数getName,除此之外的光杆司令。

那么第二步,由于原型上的getName方法也没提供构造器属性,自身原型上也没属性,所以第二步也算是单纯执行a.getName()输出3,然后得到了一个什么自定义属性都没有实例。

我们可以尝试输出这两步得到的实例:

 叁 ❀ 总

那么到这里这道面试题就分析完了,通过本文,我们知道了构造函数静态属性与实例属性的概念,其中静态属性只有构造函数自身可以访问,实例无权访问;实例属性由构造器属性与原型属性组成,实例可以继承访问,而构造函数却无权访问。

其次,我们知道了变量提升与函数声明提升,而且函数声明提升比变量提升更高。

还有呢,通过var声明的全局变量或者函数声明的函数,都等同于给window添加属性,我们可以通过window访问这些属性,这也是为什么说调用一个Foo()等同于window.Foo()的原因。

最后,我们简单了解了new一个构造函数的过程,原来new中间发生了这么多有趣的事情。

一道看似普通的面试题,居然涵盖了不少知识点,我想这也是为何19年还有公司愿意使用它的原因吧,不过看过本文的你应该无所畏惧了。

那么到这里本文结束,如有看不懂的地方欢迎留言,我会第一时间回复。

那么到这里本文结束。

你可能感兴趣的:(JS Foo.getName笔试题解析,杂谈静态属性与实例属性,变量提升,this指向,new一个函数的过程)