看到这次的征文,笔者很兴奋,一是因为笔者最近也在准备面试,根据各位前辈的征文内容,可以收获满满的干货;二是可以把自己梳理过的面试题拿来与大家一起分享,略尽绵薄之力,
今天笔者梳理到函数的三种角色,那我们就从一道阿里的经典面试题,剖析一下函数的三种角色:
原题如下:
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
的原型;
到这里,我们的原型链基本就完善了;我们来简单总结一下:
- 每一个函数也都是普通对象(辅助角色),也都会自带一个
__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
- 让
var getName
function getName(){...} ;
- 同上;
第二步:代码执行:
- 1、
function Foo() { getName = function () {console.log(1);};return this;}
- 1、
- 上步已经变量提升过;所以这步直接跳过
- 2、
Foo.getName = function () {console.log(2);};
- 2、
- 把
Foo
函数当作对象,存储方法getName
方法
- 把
- 3、
Foo.prototype.getName = function () {console.log(3);};
- 3、
- 给
Foo
的原型增加getName
方法
- 给
- 4、
var getName = function () {console.log(4);};
- 4、
- 先创建一个函数
CCCFFF000
- 先创建一个函数
- 把代码当作字符串存储起来
- 让
prototype
指向原型 - 让原型的
constructor
指向getName
- 让
getName.__proto__ : Function.prototype
- 让
getName.prototype.__proto__ : Object.prototype
- 创建变量(之前变量提升阶段已经完成),这里直接跳过
- 变量与新地址
CCCFFF000
关联,原来关联的BBBFFF000
解除关联并销毁
- 5、
function getName() {console.log(5);}
- 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
- 1、创建一个堆内存
window.getName();
- 看图可知,此时
window
下的getName
输出的是 - => 1
- 看图可知,此时
第四步:
getName();
- 全局下的
getName
执行; - => 1
- 全局下的
第五步:
new Foo.getName();
- 此时涉及到了优先级问题 ,我们根据MDN运算符优先级
- 成员访问 : 19
- 带参数new : 19
- 无参数new : 18
- 可知这一步的顺序用应该是
- 1、
Foo.getName
- 1、
- 把
Foo
函数当作对象,查找里边属性名为getName
的 - 在图中找到是:
function(){console.log(2);}
;(我们暂且把他命名为A
)
- 把
- 2、
new A();
- 2、
- 让函数
A
执行 - => 2
- 同时创建一个
A
函数的实例;(由于函数A
里面没有带this
的,所以实例中没有键值对) (在此题中没有用到,我们就先不画图了)
- 让函数
第六步:
new Foo().getName();
- 还是优先级问题:
- 1、
new Foo();
- 让函数
Foo
执行
- 让函数
- 重新给全局设置
getName
属性(步骤同第三步一致) - 同时创建一个
Foo
函数的实例;(由于函数Foo
里面return this
) - 返回这个实例(没有键值对的空对象)
- 重新给全局设置
- 2、
实例.getName();
- 2、
- 由于实例中没有
getName
这个属性,所以通过作用域链,向上级查找是 - 由图可以看出输出的是
Foo.prototype
上的getName
- => 3
- 由于实例中没有
第七步:
new new Foo().getName();
- 优先级问题
- 1、
new Foo();
- 让函数
Foo
执行
- 让函数
- 重新给全局设置
getName
属性(步骤同第三步一致) - 同时创建一个
Foo
函数的实例;(由于函数Foo
里面return this
) - 返回这个实例(没有键值对的空对象)
- 重新给全局设置
- 2、
new 新实例.getName();
- 2、
- 优先级问题
- 1、
新实例.getName
- 找到新实例中的
getName
方法,新实例中没有这个方法所以向原型查找;找到的是function (){console.log(3);};
我们假设为B
;
- 找到新实例中的
- 3、
new B();
- 3、
- 让函数
B
执行; - => 3
- 同时创建一个
B
函数的实例; (在此题中没有用到,我们就先不画图了)
- 让函数
到了这一步我们的题算是解完了;
这道题虽然算不上很难,但绝对算得上经典,涉及到了,函数的三种角色问题和运算符优先级的问题;而且需要我们细心一些,并有扎实的原生基础;
笔者在梳理面试题时,对这题就很感兴趣,所以在此处梳理一下,希望能帮助到刚好需要的您;
最后希望大家都能收到心仪的“offer”,高调上岸;