看到这次的征文,笔者很兴奋,一是因为笔者最近也在准备面试,根据各位前辈的征文内容,可以收获满满的干货;二是可以把自己梳理过的面试题拿来与大家一起分享,略尽绵薄之力,
今天笔者梳理到函数的三种角色,那我们就从一道阿里的经典面试题,剖析一下函数的三种角色:
原题如下:
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的基础知识 (opens new window) ;构造函数创建自定义类 (opens new window) ;JS中的原型和原型链 (opens new window) ;每篇文章都通过每步一图的方式详细的进行讲解了,并且配上了思维导图供大家更好的梳理(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的原型;
到这里,我们的原型链基本就完善了;我们来简单总结一下:
- 每一个函数也都是普通对象(辅助角色),也都会自带一个
__proto__的属性(当前也会存贮一些自己的键值对)
- 每一个函数也都是普通对象(辅助角色),也都会自带一个
- 主角色还是函数类型,所以每一个函数(不论是自定义类还是内置类再或者普通函数)都是
Function这个内置类的实例,所以函数.__proto__===Function.prototype(特别恶心的是:Function本身也是函数,他是自己类的一个实例)
“Function instanceof Function => true”
- 主角色还是函数类型,所以每一个函数(不论是自定义类还是内置类再或者普通函数)都是
那
Function和Object到底谁大呢?
- 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
- 让
- 1、创建堆内存(存储代码字符串)
var getNamefunction 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);}- 变量提升阶段已经完成直接跳过;
- 1、
好了图现在画完了,我们开始输出结果
- 第一步:
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
- 1、创建一个堆内存
window.getName();- 看图可知,此时
window下的getName输出的是 - => 1
- 看图可知,此时
- 先让
第四步:
getName();- 全局下的
getName执行; - => 1
- 全局下的
第五步:
new Foo.getName();- 此时涉及到了优先级问题 ,我们根据MDN运算符优先级 (opens new window)
- 成员访问 : 19
- 带参数new : 19
- 无参数new : 18
- 可知这一步的顺序用应该是
- 1、
Foo.getName- 把
Foo函数当作对象,查找里边属性名为getName的 - 在图中找到是:
function(){console.log(2);};(我们暂且把他命名为A)
- 把
- 2、
new A();- 让函数
A执行 - => 2
- 同时创建一个
A函数的实例;(由于函数A里面没有带this的,所以实例中没有键值对) (在此题中没有用到,我们就先不画图了)
- 让函数
- 1、
- 此时涉及到了优先级问题 ,我们根据MDN运算符优先级 (opens new window)
第六步:
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”,高调上岸;