zoukankan      html  css  js  c++  java
  • 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

    了解了这一点后,我们再来看函数执行过程,第一步执行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年还有公司愿意使用它的原因吧,不过看过本文的你应该无所畏惧了。

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

  • 相关阅读:
    【转载】理解本真的REST架构风格
    Linux常用命令
    使用MongoDB存储集合的一些问题
    AutoMapper快速上手
    JavaScript instanceof 运算符深入剖析
    使用c#对MongoDB进行查询(1)
    centos7安装rabbitmq3.7.9
    nginx1.14.0版本高可用——keepalived双机热备
    nginx1.14.0版本https加密配置
    nginx1.14.0版本负载均衡upstream配置
  • 原文地址:https://www.cnblogs.com/echolun/p/11741328.html
Copyright © 2011-2022 走看看