一道关于面向对象程序设计的笔试题(非常经典)

话不多说,直接上题,代码如下:

function Foo (){
    getName = function (){
	alert(1);
    };
    return this;
}

Foo.getName = function (){
    alert(2);
};

Foo.prototype.getName = function () {
    alert(3);
};

var getName = function (){
    alert(4);
};

function getName (){
    alert(5);
};

Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

看看这题的各部分输出是多少?

只要能把这道题完全做对,基本上在笔试题上就能拿下不低的分了。当然了,这题不仅仅考的是面向对象程序设计,它还考察了其他的基础知识点,而这些知识点也许刚刚好是大家遗漏的,不信?大家可以先试试能做对多少?

下面来看对于这道题需要的知识点:

1)首先大家要明白公有属性、公有方法、私有属性、私有方法、静态属性和静态方法的区别与使用

2)然后要明白一些运算符的运算顺序和js程序执行的顺序

3)最后要明白和理解原型链、继承和this的指向问题

知识点1:那么这里先举一个例子说明公有属性、公有方法、私有属性、私有方法、静态属性和静态方法的区别,代码如下:

function User(name){
    var name = name;//私有属性
    this.name = name;//公有属性
    function getName (){//私有方法
	return name;
    };
    this.getName = function (){//公有方法
	return this.name;
    }
}
User.prototype.getName = function (){//公有方法
    return this.name;
}
User.name = 'Charles';//静态属性
User.getName = function (){//静态方法
    return this.name;
}
var userCharles = new User('Charles');//实例化

那它们的使用是怎么样的呢?

1.调用公有方法,公有属性必须先实例化对象,即用new操作符实例化对象;
2.公有方法不能调用私有方法和静态方法;
3.静态方法和静态属性无需实例化即可调用;
4.对象的私有方法和属性,外部是不可以访问的。

知识点2:再来看一些运算符的运算顺序和js程序执行的顺序

js执行顺序是按照编写的顺序执行的,但是一定要知道js预解析的时候,变量声明和函数声明是要提前的,且函数声明更优先(可参考链接:js变量名与函数名重名的问题)。另外js异步回调也是其中之一的知识点,大家可以参考链接:关于js异步回调

知识点3:要明白和理解原型链、继承(可参考链接:js原型、原型链与继承的理解)和this的指向问题(可参考链接:js中this的作用和使用方法

好,复习完上述三个知识点之后,大家再做一做这道题,看能做对几问呢?

该题的上半部分定义了一个叫Foo的函数对象,之后为Foo创建了一个名为getName的静态方法,随后给Foo的原型对象创建了一个名为getName的公有方法,然后又通过函数表达式创建了一个getName的匿名函数,最后声明一个名为getName的函数。

下面来看第一题:Foo.getName();

由于是直接调用Foo的getName方法,但由于此时并没有实例对象,故这里是访问不到Foo函数对象里的getName方法的,而且Foo函数对象里的getName方法是私有方法,根本访问不到。

进一步分析,我们明白js在预解析的时候会将函数声明置顶,所以执行时代码应如下:

//函数声明置顶
function getName (){
    alert(5);
};

function Foo (){
    getName = function (){
	alert(1);
    };
    return this;
}

Foo.getName = function (){
    alert(2);
};

Foo.prototype.getName = function () {
    alert(3);
};

var getName = function (){
    alert(4);
};

好,函数声明置顶之后,跟Foo调用getName方法没关系,所以代码执行下来,Foo函数访问的自然是存在其自身上的静态方法getName方法,所以该题答案是2。有的同学可能会问,为什么不是4?其实可以把最下面的函数表达式分割成这样:

var getName;
getName = function (){
    alert(4);
};

就是js在解析函数表示式时,先将变量getName声明置顶,然后将一个匿名函数赋值给getName。而变量的调用就不用我多说了吧,跟方法的调用形式是不一样的。

第二题:getName();

这里很明显的是直接调用getName函数,按道理来说,是可以直接输出5的,但是这题并不是,大家发现没,这里出现了变量名和函数名重名的问题了!这里我再讲解一次吧,下面来看函数声明和函数表达式的区别:

//函数声明
function funName(){
    alert(123);
}
//函数表达式
var funName = function (){
    alert(123);
}

理解区别之后,再来额外看一道问题:在一个程序里同时用函数声明和函数表达式定义一个名为getName的函数

getName();//woman
var getName = function (){
    console.log('man');
}
getName();//man
function getName (){
    console.log('woman');
}
getName();//man

这里涉及js预解析的知识之函数声明和变量声明提升,函数声明提升更优先,所以上述代码等于:

function getName (){//函数声明提升
    console.log('woman');
}
var getName;//变量声明提升,此时为undefined
getName();//woman,还未执行到函数表达式且还未赋值,所以执行的是函数声明的值
getName = function (){//这里因为赋值,覆盖了函数声明的定义
    console.log('man');
}
getName();//man,这里就执行了函数表达式的值
getName();//man,同上

这里说明下,也算是总结:js中函数声明和函数表达式是存在区别的,函数声明在js解析时会进行函数提升,因此在同一个作用域内,不管函数声明在哪定义,该函数都可以调用。而函数表达式的值是在js运行时确定,且要等到表达式赋值完成后该函数才能调用,跟变量调用类似。

所以第二问的答案为4,执行的是函数表达式里的值。

第三题:Foo().getName();

解这题的关键在于理解this的指向问题。首先计算Foo函数,而Foo返回的是this(很明显指向的是window,因为是直接调用),然后计算this.getName()(相当于window.getName()),而我们知道Foo函数内部有一个函数赋值语句,且名为getName,所以按照作用域来讲,肯定是先查找Foo内部作用域里是否有声明getName这个变量或者函数,所以查找结果是没有的,因为没有var声明;随后向外(函数外)查找,发现有一个var getName = function (){alert(4);}声明了getName变量,并且赋值了,但是在进行计算getName()时,是按照作用域的顺序计算的,即先查找Foo函数内部有没有对变量getName进行赋值,有则优先计算,否则再查找外部是否有对getName进行赋值然后再计算。在代码执行时发现在Foo函数内部有一个对getName变量赋值的语句,相当于把外部的赋值语句给覆盖了,于是结果输出1。(如果在外部没查找到,就会在全局环境下创建一个名为getName的变量)

第四题:getName();

直接调用getName(),相当于执行window.getName(),因为这个getName变量在执行Foo().getName();的时候被改变了,输出为1,因此结果应该与第三题相同,输出也为1.

第五题:new Foo.getName();

这题需要知道运算符号的优先顺序,点的优先级比new无参数列表优先级高,当点运算完后又因为有个()就变成new有参数列表,所以直接运行new。

new(有参数列表)>点运算符>函数调用>new(无参数列表)

故该题的执行顺序应该是这样子:new (Foo.getName)();

这里实际上是将Foo.getName作为构造函数来执行的,因此输出2.

第六题:new Foo().getName();

跟第五题类似,执行顺序(若是同级,则按从左到右的顺序执行)应该是这样的:new有参数列表>点运算符>函数调用>new无参数列表,即(new Foo()).getName();

这里还涉及到了一个小的知识点:构造函数的返回值

在传统语言中,构造函数不应该有返回值,实际执行的返回值就是此构造函数的实例化对象,而在js中构造函数可以有返回值也可以没有。

1)若没有则返回实例化对象

2)若有则检查返回值是否为引用类型。如果是非引用类型,如String,Number,Boolean,Null,Undefined则与无返回值相同;如果是引用类型,则实际返回值为这个引用类型

原题中,由于返回的是this,而this在构造函数中本来就代表当前实例化对象,最终Foo函数返回的就是实例化对象。之后调用的是实例化对象的getName方法,学过原型链的孩子应该知道,在查找方法的时候是按照原型链的方式查找的,首先要查找原型链的开始端,也就是Foo函数内部是否有公有方法getName,发现没有,往原型链上面查找,在Foo函数的原型对象(Foo.prototype)上查找到了getName方法,于是就输出3。

第七题:new new Foo().getName();

同理首先要判断其执行顺序,最终执行顺序为:new ((new Foo()).getName)();即先舒适化Foo的实例化对象,然后将其原型上的getName方法作为构造函数再次new(实例化),然后计算,结果为3


好的,这个答题的分析就结束了,总的来说,做完这道题可以将我们很多的重要知识点重新拾起,同时也帮我们进一步巩固了这些js常考的知识点,与君共勉。

猜你喜欢

转载自blog.csdn.net/charles_tian/article/details/79885668