壹 ❀ 引

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(); //
getName(); //
Foo().getName(); //
getName(); //
new Foo.getName(); //
new Foo().getName(); //
new new Foo().getName(); //

如果大家搜这个题,那说明肯定是对于某一部分执行是有疑虑,那么现在就跟着我的思路重新理一遍,本文开始:

 贰 ❀ 分析

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]; //

这个例子中,我们为函数添加了一个用于存储执行结果的对象cache,将每次调用函数的参数作为对象的key,执行结果作为value,对于执行特别复杂的操作,这样只用执行一次之后就可以直接通过参数访问到最终结果。

如果对于静态属性有兴趣,想了解更多可以阅读博主这篇文章 精读JavaScript模式(七),命名空间模式,私有成员与静态成员

2.getName()

为什么输出4而不是5,这里考的是变量提升与函数声明提升。我们知道使用var声明变量会存在变量提升的情况,比如下面的例子中,即使在声明前使用变量a也不会报错

console.log(a)// undefined
var a = 1;
console.log(a)//

这是因为声明提前会让声明提升到代码的最上层,而赋值操作停留在原地,所以上面代码等同于:

var a
console.log(a)// undefined
a = 1;
console.log(a)//

而函数声明(注意是函数声明,不是函数表达式或者构造函数创建函数)也会存在声明提前的情况,即我们可以在函数声明前调用函数:

fn() //
function fn() {
console.log(1);
};
fn() // //因为函数声明提前,导致函数声明也会被提到代码顶端,所以等同于
function fn() {
console.log(1);
};
fn() //
fn() //

那这样就存在一个问题了,变量声明会提升,函数声明也会提升,谁提升的更高呢?在你不知道的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; // function acfun() {
console.log(1);
};
// window上可以找到这个方法
window.acfun(); //

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

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

JS Foo.getName笔试题解析,杂谈静态属性与实例属性,变量提升,this指向,new一个函数的过程的更多相关文章

  1. Java中有关构造函数的一道笔试题解析

    Java中有关构造函数的一道笔试题解析 1.详细题目例如以下 下列说法正确的有() A. class中的constructor不可省略 B. constructor必须与class同名,但方法不能与c ...

  2. js静态属性,实例属性,封装性,prototype,__proto__综合解析

    原创作品,转载请注明来源,sogeisetsu,我的csdn上也有这篇文章csdn js静态属性,实例属性,封装性,prototype,__proto__综合解析 下面是我在写博客的源代码,您可以先不 ...

  3. 17 webpack中babel的配置——静态属性与实例属性

    // class关键字,是ES6中提供的新语法,是用来实现ES6中面向对象编程的方式 class Person{ // 使用static关键字,可以定义静态属性 // 所谓的静态属性,就是可以直接通过 ...

  4. 由阿里巴巴一道笔试题看Java静态代码块、静态函数、动态代码块、构造函数等的执行顺序

    一.阿里巴巴笔试题: public class Test { public static int k = 0; public static Test t1 = new Test("t1&qu ...

  5. .NET面试题解析(05)-常量、字段、属性、特性与委托

      系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 弱小和无知不是生存的障碍,傲慢才是!——<三体> 常见面试题目: 1. const和reado ...

  6. Java笔试题解析(二)——2015届唯品会校招

    曾经总是看别人写的笔经面经.今天自己最终能够写自己亲身经历的一篇了 T-T. 前阵子去了唯品会的秋招宣讲会,华工场(如今才知道原来找家互联网公司工作的人好多).副总裁介绍了VIP的商业模式是逛街式的购 ...

  7. Java类的连接与初始化 (及2013阿里初始化笔试题解析)

    Java虚拟机通过装载.连接.初始化来使得一个Java类型可以被Java程序所使用,如下图所示,其中连接过程又分为验证.准备.解析三个部分.其中部分类的解析过程可以推迟到程序真正使用其某个符号引用时再 ...

  8. js 面向对象中,定义一个函数的过程

    定义一个函数做的两件事:1: 实例化一个Function对象:2: 实例化一个Object对象,并给该函数扩展prototype属性指向这个构造函数 大致过程如图所示: 每一种引用类型(函数,对象,数 ...

  9. Python中的静态属性、实例属性、静态方法、实例方法之间的区别

随机推荐

  1. 人工智能技术导论——逻辑程序设计语言PROLOG

    最近在复习人工智能导论,里面介绍了一种逻辑关系语言PROLOG,但这本书里面用到的编译器是Turbo PROLOG,这个编译器早就被淘汰了,我后来找的了它的升级版Visual PROLOG,但一些语法 ...

  2. forEach()和for/in循环的缺点与for-of循环

    以数组为例,JavaScript 提供多种遍历语法.最原始的写法就是for循环. for (var index = 0; index < myArray.length; index++) { c ...

  3. 【Git】远程分支

    [Git]远程分支 转载:https://www.cnblogs.com/yangchongxing/p/10239270.html 目录 ============================ 1 ...

  4. django----csrf跨站请求伪造 auth组件 settings源码 importlib模块

    目录 importlib模块 csrf跨站请求伪造 form表单发送 ajax发送 csrf装饰器 auth模块 如何创建超级用户(root) 创建用户 校验用户名和密码是否正确 保存用户登录状态 判 ...

  5. Microsemi Libero使用技巧——FPGA全局网络的设置

    前言 刚开始做Microsemi FPGA+SoC开发时,会用到几个ARM专用的IP Core,功能一复杂起来,就会遇到某些信号如rst_n不能分配到指定的引脚上的情况,IO类型为CLKBUF,并不是 ...

  6. Wiki语法大全

    原文链接:wiki语法大全  编辑一个维客页面十分容易.只要点击页面上方的“编辑本页”或右侧的[编辑]链接即可修改该页,或点击“讨论本页”然后再点击“编辑页面”来讨论该页面.点击后您就会看到一个包含那 ...

  7. Python—脚本程序生成exe可执行程序(pyinstaller)

    一.pyinstaller的简介 Python是一个脚本语言,被解释器解释执行.它的发布方式: .py文件:对于开源项目或者源码没那么重要的,直接提供源码,需要使用者自行安装Python并且安装依赖的 ...

  8. c++--语言本身

    c++ 面向对象概念(cout cin 类.对象 面向对象和面向过程求解问题) 易犯错误模型(引入成员函数的必要性) C语言和C++语言的关系 namespace 定义(嵌套).使用.标准命名空间st ...

  9. SAP Business One对象清单

    中文描述 对象号 表名 主键 英文描述 总账科目 1 OACT AcctCode G/L Accounts 业务伙伴 2 OCRD CardCode Business Partner 银行代码 3 O ...

  10. Matplotlib的使用

    目录 1.pyplot基础语法 2.散点图与折线图 3.3D图与等高线图 1.pyplot基础语法 (1)创建画布 figure()创建一个空白画布,可以指定画布的大小figsize和设置分辨率dpi ...