壹 ❀ 引
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.name与this.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年还有公司愿意使用它的原因吧,不过看过本文的你应该无所畏惧了。
那么到这里本文结束,如有看不懂的地方欢迎留言,我会第一时间回复。
那么到这里本文结束。