从一道阿里经典面试题剖析函数三种角色|掘金技术征文

1,196 阅读10分钟

看到这次的征文,笔者很兴奋,一是因为笔者最近也在准备面试,根据各位前辈的征文内容,可以收获满满的干货;二是可以把自己梳理过的面试题拿来与大家一起分享,略尽绵薄之力,

今天笔者梳理到函数的三种角色,那我们就从一道阿里的经典面试题,剖析一下函数的三种角色:

原题如下:

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

题先放在这里,我们先来复习一下函数的三种角色相关的知识;

函数的三种角色

类(函数)即是函数数据类型(主类型),也是对象数据类型(辅助类型)

  • [函数类型]
    • 普通函数(EC/AO/SCOPE/SCOPE-CHAIN...)
    • 构造函数(类/实例/原型/原型链...)
  • [对象类型]
    • 普通对象(和一个OBJ没啥区别,就是用来存储键值对的)
    • __proto__:所有的函数都是Function内置类的实例,所以函数的__proto__指向Function.prototype

前面的文章中我们已经讲过;普通函数:JS中function的基础知识构造函数创建自定义类JS中的原型和原型链 ;每篇文章都通过每步一图的方式详细的进行讲解了,并且配上了思维导图供大家更好的梳理(PS:是不是很贴心,哇哈哈😝);这样一看我们今天的内容就已经完成一大半了;接下来我们就主要围绕函数的第三种角色详细分析下吧;

一、函数的第三种角色——对象

函数也是一个普通对象,这只是函数的一个辅助角色,为啥说他是辅助角色呢,因为当函数作为对象时没啥特殊的作用(就和普通对象一样),就是用来储存键值对的;

我们控制台输出一下,当函数作为对象时是都有什么默认的属性呢

作为普通对象时:函数的默认属性

  • length: 代表形参的个数
  • name: 代表当前函数的名字
  • arguments: 我们都知道的
  • caller: 现在很少用到了就不多说了
  • __proto__: 我们熟悉的原型链;

在前一篇原型链的时候我们并没有把函数也话进去,所以是还不完整的,今天我们就把他也补充进去,画一个完整的,真真正正的原型链查找图;

废话不多说,老套路,我们还是以一道小题为例逐步画图分析:

function Fn() {
	this.x = 10;
	this.y = 20;
}
Fn.n = 1000;
Fn.say = function () {
	console.log('hello world!');
};
Fn.prototype.sum = function () {
	return this.x + this.y;
};
let f1 = new Fn;
Fn.say();

这里就直接省略了一些基础的步骤,我们直接从代码执行说起

第一步:function Fn() { this.x = 10; this.y = 20; }

  • 开辟一个堆内存,把函数体以字符串的形式存储进去;
  • 每一个函数都有一个prototype原型属性;
  • 每个原型都有一个constructor属性指向当前所属类(同时每个原型又是一个对象);
  • 每个对象都有一个__proto__属性指向当前所属类的原型;
  • 所有对象数据类型都是Object的实例
  • Object__proto__指向null

二、原型链补充完善

第二步:Fn.n = 1000;

  • Fn.n这种形式我们回想一下他肯定不能是函数Fn,只有对象才可以写成这种形式,所以这一步是把函数当作对象,并且给这个对象存了个 n:1000的键值对 ;

第三步:Fn.say = function () { console.log('hello world!'); };

  • say方法存储到Fn中;

函数作为对象时,他的__proto__原型链指向谁呢?

  • 我们上面一直说原型线是指向当前所属类的原型,那函数所属的类是谁呢?
    • 换句话就是函数是谁的实例?找到他的最大类就是Function

根据图我们我们可以看出Function类的__proto__还没有指向,同时Function.prototype原型中的__proto__还没有指向,那他们都指向谁呢?

  • 先解决第一个问题:Function类的__proto__指向其所属类的原型,

    • Function内置类,是所有类的基类
    • 所以:Function类的__proto__指向其自己的原型;
  • 第二个问题:Function.prototype原型中的__proto__指向所属类的原型

    这里有一点比较特殊:需要我们单独记一下:Function.prototype是一个匿名空函数""anonymous/empty"(正常所属类的原型都是对象,都是Object的实例,__proto__都指向Object.prototype)但是和其他对象类型原型操作一模一样

    • 所以Function.prototype原型中的__proto__指向Object的原型

有细心的小伙伴会有这样的一个疑问🤔️:Object内置类是咋出来的呢?

  • 答:这里我们在记住一句话

所有内置类都是一个函数,都是Function的实例

Object作为对象类型,的__proto__原型链指向其所属类的原型

  • 所有内置类都是一个函数,都是Function的实例
  • 所以:Object.__proto__指向Function的原型;

到这里,我们的原型链基本就完善了;我们来简单总结一下:

    1. 每一个函数也都是普通对象(辅助角色),也都会自带一个__proto__的属性(当前也会存贮一些自己的键值对)
    1. 主角色还是函数类型,所以每一个函数(不论是自定义类还是内置类再或者普通函数)都是Function这个内置类的实例,所以函数.__proto__===Function.prototype (特别恶心的是:Function本身也是函数,他是自己类的一个实例)
    • “Function instanceof Function => true”

FunctionObject到底谁大呢?

  • 1、如果认为Function最大:
    • Function原型链查找顺序为: 自己私有的 => __proto__找到Function.prototye => __proto__找到 Object.prototype;

Function instanceof Object => true 说明Function也是Object的一个实例(所有的函数都是对象)

  • 2、如果认为Object最大
    • Object instanceof Function => true 所有类都是函数
    • Function instanceof Function => true所有的类都是函数
    • Object instanceof Object => true 所有函数都是对象

Object.__proto__.__proto__ === Object.prototype

有没有觉得像一个先有鸡🐔还是先有蛋🥚的问题😂;

放下这个千古难题,我们继续研究这个题

三、继续回到这个例题

第四步:Fn.prototype.sum = function () { return this.x + this.y; };

  • Fn 的原型上 添加一个 sum 方法

第五步:let f1 = new Fn;

  • 创建一个Fn的实例 赋给f1

第六步:Fn.say();

  • 执行函数,那我们可以根据上图,直接找让Fn中的say执行
  • 输出:hello world!

好了,完成了,我们在梳理下这个题;

function Fn() {
	this.x = 10;
	this.y = 20;
}
// 当做普通对象设置的私有属性方法,只能 Fn.xxx 调用
Fn.n = 1000;
Fn.say = function () {
	console.log('hello world!');
};
// 当做类,在原型上设置的属性方法,供实例调取的:实例.xxx 或者 Fn.prototype.xxx
Fn.prototype.sum = function () {
	return this.x + this.y;
};
let f1 = new Fn;
// f1.say(); //Uncaught TypeError: f1.say is not a function   say是Fn当做普通对象私有的属性方法,实例f1找的是Fn.prototype上的属性方法 (函数的角色之间是没有啥必然联系的)
Fn.say(); //=> hello world!
// Fn.sum(); //Uncaught TypeError: Fn.sum is not a function sum是它原型上的方法,实例可以调用,或者Fn.prototype.sum这样调用,但是Fn这个对象本身无法调用 

知识点全都梳理完了,我们回到这个在回到阿里这道经典的面试题;

面试题详解

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

我们还是画图分析: (依然忽略一些的细节)

  • 第一步:变量提升:
    • function Foo(){...} ;
      • 1、创建堆内存(存储代码字符串)
        • prototype指向原型
        • 让原型的constructor指向Foo
        • Foo.__proto__ : Function.prototype
        • Foo.prototype.__proto__ : Object.prototype
    • var getName
    • function getName(){...} ;
      • 同上;
  • 第二步:代码执行:
    • 1、function Foo() { getName = function () {console.log(1);};return this;}
      • 上步已经变量提升过;所以这步直接跳过
    • 2、Foo.getName = function () {console.log(2);};
      • Foo函数当作对象,存储方法getName方法
    • 3、Foo.prototype.getName = function () {console.log(3);};
      • Foo的原型增加 getName方法
    • 4、var getName = function () {console.log(4);};
      • 先创建一个函数CCCFFF000
        • 把代码当作字符串存储起来
        • prototype指向原型
        • 让原型的constructor指向getName
        • getName.__proto__ : Function.prototype
        • getName.prototype.__proto__ : Object.prototype
      • 创建变量(之前变量提升阶段已经完成),这里直接跳过
      • 变量与新地址CCCFFF000关联,原来关联的BBBFFF000解除关联并销毁
    • 5、function getName() {console.log(5);}
      • 变量提升阶段已经完成直接跳过;

好了图现在画完了,我们开始输出结果

  • 第一步:Foo.getName();
    • 直接在图中找到执行即可
    • 输出结果 => 2
  • 第二步:getName();
    • 让全局下的getName执行
    • 输出结果 => 4
  • 第三步:Foo().getName();
    • 先让Foo执行:getName = function () {console.log(1);};return this;
      • 1、创建一个堆内存 DDDFFF000
      • 2、在全局上下文中常见变量getName(找到全局发现这个变量已经有了,就跳过此步);
      • 3、让变量getName与堆DDDFFF000关联(原来关联的堆CCCFFF000取消关联,销毁);
      • 4、返回return this
      • 那我们就得看Foo的执行主体是谁,是window
      • 所以Foo执行的返回结果是window
    • window.getName();
      • 看图可知,此时window下的getName输出的是
      • => 1

  • 第四步:getName();

    • 全局下的getName执行;
    • => 1
  • 第五步:new Foo.getName();

    • 此时涉及到了优先级问题 ,我们根据MDN运算符优先级
      • 成员访问 : 19
      • 带参数new : 19
      • 无参数new : 18
    • 可知这一步的顺序用应该是
      • 1、Foo.getName
        • Foo函数当作对象,查找里边属性名为getName
        • 在图中找到是:function(){console.log(2);};(我们暂且把他命名为A)
      • 2、new A();
        • 让函数A执行
        • => 2
        • 同时创建一个A函数的实例;(由于函数A里面没有带this的,所以实例中没有键值对) (在此题中没有用到,我们就先不画图了)
  • 第六步:new Foo().getName();

    • 还是优先级问题:
    • 1、new Foo();
      • 让函数Foo执行
        • 重新给全局设置getName属性(步骤同第三步一致)
        • 同时创建一个Foo函数的实例;(由于函数Foo里面return this
        • 返回这个实例(没有键值对的空对象)
    • 2、实例.getName();
      • 由于实例中没有getName这个属性,所以通过作用域链,向上级查找是
      • 由图可以看出输出的是Foo.prototype上的getName
      • => 3
  • 第七步:new new Foo().getName();

    • 优先级问题
    • 1、new Foo();
      • 让函数Foo执行
        • 重新给全局设置getName属性(步骤同第三步一致)
        • 同时创建一个Foo函数的实例;(由于函数Foo里面return this
        • 返回这个实例(没有键值对的空对象)

    • 2、new 新实例.getName();
      • 优先级问题
      • 1、新实例.getName
        • 找到新实例中的getName方法,新实例中没有这个方法所以向原型查找;找到的是function (){console.log(3);};我们假设为B;
    • 3、new B();
      • 让函数B执行;
      • => 3
      • 同时创建一个B函数的实例; (在此题中没有用到,我们就先不画图了)

到了这一步我们的题算是解完了;

这道题虽然算不上很难,但绝对算得上经典,涉及到了,函数的三种角色问题和运算符优先级的问题;而且需要我们细心一些,并有扎实的原生基础;

笔者在梳理面试题时,对这题就很感兴趣,所以在此处梳理一下,希望能帮助到刚好需要的您;

最后希望大家都能收到心仪的“offer”,高调上岸;