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

 壹 ? 引

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年还有公司愿意使用它的原因吧,不过看过本文的你应该无所畏惧了。

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

那么到这里本文结束。

原文地址:https://www.cnblogs.com/echolun/p/11741328.html

时间: 2024-10-13 03:31:36

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

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

Java中有关构造函数的一道笔试题解析 1.具体题目如下 下列说法正确的有() A. class中的constructor不可省略 B. constructor必须与class同名,但方法不能与class同名 C. constructor在一个对象被new时执行 D.一个class只能定义一个constructor 2.解析说明 (1)class中的构造函数是可以省略的 /** * @Title:User.java * @Package:com.you.user.model * @Descrip

java笔试题解析

1.数组乱序 天天搞排序,今天遇到一道乱序的问题居然无从下手,知道random,然后想了很复杂的if条件判断. 其实,只要在数组里面依次拿出一个数,然后产生数组长度范围内的一个数作为下标,然后互换即可! public class RandomNumber { public static void main(String[] args) { int change = 6; int[] sequence = new int[change]; for (int i = 0; i < change; i

js简直了,连大小都不能直接比,还变量提升,这个是挖了多少坑啊,故意的把,,,,写起来又不简单,运行起来又不是很稳,很迷啊

如题,var eq = tr.eq(i);            var kucun_amt = eq.children('td#inputline').find('input').val();            var L7_amt = eq.children('td#L7').text();            var check1 = kucun_amt - L7_amt <=0;这样写是可以的 但是如果俩变量比大小就得parseInt($.trim($("#max"

ES6---class的静态方法、静态属性和实例属性

前言: 类相当于实例的原型, 所有在类中定义的方法, 都会被实例继承. 主体: 如果在一个方法前, 加上static关键字, 就表示该方法不会被实例继承, 而是直接通过类来调用, 这就称为“ 静态方法”. 但是注意父类的静态方法,也会被子类继承 注意:当用实例调用时会报错 TypeError: dad.habit is not a function (2)静态方法也可以从super对象上调用 (3)静态属性 静态属性指的是 Class 本身的属性, 即Class.propname, 而不是定义在

es6中类中的静态属性、实例属性、静态方法、实例方法的个人理解

静态的就是不会被实例继承的,是属于类自身的,实例继承不了,也调用不了,跟作用域一样. 静态属性.静态方法:就是类自身的属性和方法,只能在类自身调用,实例对象是无法调用到静态属性和方法的,只能类自身调用,当然子类也可以调用父类的静态属性和方法: 实例属性.实例方法:就是实例可以调用的属性和方法,记住实例是无法调用类的静态属性和方法的,但是类可以调用实例属性和实例方法: 声明静态属性: 就和普通的Object添加属性一样,object.a = a;(目前唯一一种方法):有人提议在类内部加static

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

曾经总是看别人写的笔经面经.今天自己最终能够写自己亲身经历的一篇了 T-T. 前阵子去了唯品会的秋招宣讲会,华工场(如今才知道原来找家互联网公司工作的人好多).副总裁介绍了VIP的商业模式是逛街式的购物,与京东和淘宝不同. 宣讲会之后还没有笔试.网上找了一些曾经的题目,当练练手. 2014校招的. 1.下列不可作为java语言修饰符的是(D) A. a1 B. $1 C. _1 D. 11 这题目有问题,修饰符是public这些,不能作为变量名才对. 2.整形数据类型中.须要内存空间最少的是(D

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

以前总是看别人写的笔经面经,今天自己终于可以写自己亲身经历的一篇了 T-T. 前阵子去了唯品会的秋招宣讲会,华工场(现在才知道原来找家互联网公司工作的人好多),副总裁介绍了VIP的商业模式是逛街式的购物,与京东和淘宝不同.宣讲会之后还没有笔试.网上找了一些以前的题目,当练练手. 2014校招的. 1.下列不可作为java语言修饰符的是(D) A. a1 B. $1 C. _1 D. 11 这题目有问题,修饰符是public这些,不能作为变量名才对. 2.整形数据类型中,需要内存空间最少的是(D)

2014年腾讯实习生笔试题解析

本答案是我自己搜索资料解答出来,假设不正确敬请指出 1. 使用深度优先算法遍历下图.遍历的顺序为(C) A ABCDEFG B ABDCFEG C ABDECFG D ABCDFEG 解析: 深度优先遍历相似于树的前序遍历,其基本思想为: (1).訪问顶点v; (2).从v的未被訪问的邻接点中选取一个顶点w,从w出发进行深度优先遍历. (3).反复以上两步: 选C 2. 输入序列ABCABC经过栈操作变成ABCCBA,以下哪些是可能的栈操作( AD) A. push pop push pop p

贝壳网2020笔试题解析

2019年8月10号我参加贝壳笔试,没想到是四道编程题,这个着实让我措手不及.下面我就来带大家看看这四道题目.首先我要吐槽下赛码网的系统,为啥非得写输入输出,能不能学学Leetcode ! 第一题:计算绝对值 题目描述:给出n个整数,要找出相邻两个数字中差的绝对值最小的一对数字,如果差的绝对值相同的,则输出最前面的一对数.2<=n<=10,正整数都在10^16次方范围内. 输入:输入包含两行,第一行是n,第二行是n个用空格间隔的正整数. 输出:输出包含一行两个正整数,要求按照原来的顺序输出.