JavaScript面试后的反思
2010-08-01
写此文目的是为了让更多的程序员理解javascript的一些概念,对,是理解,而不是了解。我们已经了解得够多了,该是向深入理解的方向靠拢的时候了。
为什么这么说,前些日子收到面试邀请,那就去试试呗,有几年没有面试过了吧。和面试官坐在沙发上,聊天式的他问我答,以下就是几个javascript方面的问题:
请创建一个对象,包括几个公有属性,接下来是为对象创建一个公有方法,然后为对象创建几个私有属性,一个私有方法。
说实话,这几个问题我默名其妙,要是他让我用jquery写个拖动插件什么的,我估计我能写挺好,原生的javascript,晕,虽然我看过jquery源码解读,但这些基本概念要命。
本文的例子输出使用如下方法,便于查看:
1 | function dwn(s) |
2 | { |
3 | document.write(s+ "<br />" ); |
4 | } |
function
从一开始接触到js就感觉好灵活,每个人的写法都不一样,比如一个function就有N种写法,如:
1 | function showMsg(){} |
2 | var showMsg = function (){} |
3 | showMsg = function (){} |
似乎没有什么区别,都是一样的嘛,真的是一样的吗,大家看看下面的例子:
01 | ///------------------------------------------------------------------------------------------------ |
02 | //函数定义:命名函数(声明式),匿名函数(引用式) |
03 | //声明式,定义代码先于函数执行代码被解析 |
04 | function t1() |
05 | { |
06 | dwn( "t1" ); |
07 | } |
08 | t1(); |
09 |
10 | function t1() |
11 | { |
12 | dwn( "new t1" ); |
13 | } |
14 | t1(); |
15 |
16 | //引用式,在函数运行中进行动态解析 |
17 | var t1 = function (){ |
18 | dwn( "new new t1" ); |
19 | } |
20 | t1(); |
21 |
22 | var t1 = function (){ |
23 | dwn( "new new new t1" ); |
24 | } |
25 | t1(); |
26 | //以上输出:new t1,new t1,new new t1,new new new t1 |
可能想着应该是输出t1,new t1,new newt1,new new new t1,结果却并不是这样,应该理解这句话:声明式,定义代码先于函数执行代码被解析。
如果深入一步,应该说是scope链问题,实际上前面两个方法等价于window.t1,可以理解为t1是window的一个公有属性,被赋了两次值,以最后一次赋值为最终值。
而后面两个方法,可以理解为是t1是个变量,第四个方法的var去掉之后的结果仍然不会改变。
然而,当第四个方法改成function t1(){}这样的声明式时,结果变成了new new new t1,new new new t1,new new t1,new new t1前面两个按照我的理解可以很好的理解为什么是这个答案,第三个也可以理解,但是最后一个输出让我比较纠结。
另外匿名函数还有(function(){...})()这样的写法,最后一个括号用于参数输入。
还有var t1=new function(){..}这样的声明,实际上t1已经是一个对象了。
1 | var t2 = new function () |
2 | { |
3 | var temp = 100; //私有成员 |
4 | this .temp = 200; //公有成员,这两个概念会在第三点以后展开说明 |
5 | return temp + this .temp; |
6 | } |
7 | alert( typeof (t2)); //object |
8 | alert(t2.constructor()); //300 |
除此之外,还有使用系统内置函数对象来构建一个函数,例:
1 | //这个位置加不加new结果都一样,WHY |
2 | var t3 = new Function( 'var temp = 100; this.temp = 200; return temp + this.temp;' ); |
3 | alert( typeof (t3)); //function |
4 | alert(t3()); //300 |
创建对象
首先我们理解一下面向对象编程(Object-Oriented Programming,OOP),使用OOP技术,常常要使用许多代码模块,每个模块都提供特定的功能,每个模块都是孤立的,甚至与其它模块完全独立。这种模块化编程方法提供了非常大的多样性,大大增加了代码的重用机会。可以举例进一步说明这个问题,假定计算机上的一个高性能应用程序是一辆一流赛车。如果使用传统的编程技巧,这辆赛车就是一个单元。如果要改进该车,就必须替换整个单元,把它送回厂商,让汽车专家升级它,或者购买一个新车。如果使用OOP技术,就只需从厂商处购买新的引擎,自己按照说明替换它,而不必用钢锯切割车体。
不过大部分的论点是,javascript并不是直接的面向对象的语言,但是通过模拟可以做到很多面向对象语言才能做到的事,如继承,多态,封装,javascript都能干(没有做不到,只是想不到):
01 | ///----------------------------------------------- |
02 | //以下三种构造对象的方法 |
03 | //new Object,实例化一个Object |
04 | var a = new Object(); |
05 | a.x=1, a.y=2; |
06 | //对象直接量 |
07 | var b = {x:1,y:2}; |
08 | //定义类型 |
09 | function Point(x,y) |
10 | { |
11 | //类似于C#中的类 |
12 | this .x=x; |
13 | this .y=y; |
14 | } |
15 | var p = new Point(1,2); //实例化类 |
第一种方法通过构造基本对象直接添加属性的方法来实现,第二种和第一种差不多,可以看成是第一种方法的快捷表示法。第三种方法中,可以以"类"为基础,创造多个类型相同的对象。
对象属性的封装(公有和私有)
以例子来说明:
01 | function List() |
02 | { |
03 | //私有成员,在对象外无法访问,如果此处无var声明,则m_elements将变成全局变量,这样外部是可以直接访问到的,如alert(m_elements[0]) |
04 | var m_elements=[]; |
05 | m_elements=Array.apply(m_elements,arguments); |
06 | //此处模拟getter,使用时alist.length; |
07 | //等价于getName()方式:this.length=function(){return m_elements.length;},使用时alist.length(); |
08 | //公有属性,可以通过"."运算符或下标来访问 |
09 | this .length = { |
10 | valueOf: function (){ |
11 | return m_elements.length; |
12 | }, |
13 | toString: function (){ |
14 | return m_elements.length; |
15 | } |
16 | } |
17 | //公有方法,此方法使用得alert(alist)相当于alert(alist.toString()) |
18 | this .toString= function (){ |
19 | return m_elements.toString(); |
20 | } |
21 |
22 | //公有方法 |
23 | this .add= function (){ |
24 | m_elements.push.apply(m_elements,arguments); |
25 | } |
26 |
27 | //私有方法如下形式,这里涉及到了闭包的概念,接下来继续说明 |
28 | //var add=function()或function add() |
29 | //{ |
30 | //m_elements.push.apply(m_elements,arguments); |
31 | //} |
32 | } |
33 |
34 | var alist= new List(1,2,3); |
35 | dwn(alist); //=alert(alist.toString()),输出1,2,3 |
36 | dwn(alist.length); //输出3 |
37 | alist.add(4,5,6); |
38 | dwn(alist); //输出1,2,3,4,5,6 |
39 | dwn(alist.length); //输出6 |
属性和方法的类型
javascript里,对象的属性和方法支持4种不同的类型:private property(私有属性),dynamic public property(动态公有属性),static public property/prototype property(静态公有属性或原型属性),static property(静态属性或类属性)。私有属性对外界完全不具备访问性,可以通过内部的getter和setter(都是模拟);动态公有属性外界可以访问,每个对象实例持有一个副本,不会相互影响;原型属性每个对象实例共享唯一副本;类属性不作为实例的属性,只作为类的属性。
以下是例子:
01 | ///------------------------------------------------------------------------------------------------ |
02 | //动态公有类型,静态公有类型(原型属性) |
03 | function myClass() |
04 | { |
05 | var p=100; //private property |
06 | this .x=10; //dynamic public property |
07 | } |
08 | myClass.prototype.y=20; |
09 | //static public property or prototype property,动态为myClass的原型添加了属性,将作用于所有实例化了的对象,注意这里用到了prototype,这是一个非常有用的东东 |
10 | //要想成为高级javascript阶段,prototype和闭包必须得理解和适当应用 |
11 | myClass.z=30; //static property |
12 | var a= new myClass(); |
13 | dwn(a.p) //undefined |
14 | dwn(a.x) //10 |
15 | dwn(a.y) //20 |
16 | a.x=20; |
17 | a.y=40; |
18 | dwn(a.x); //20 |
19 | dwn(a.y); //40 |
20 | delete (a.x); //删除对象a的属性x |
21 | delete (a.y); //删除对象a的属性y |
22 | dwn(a.x); //undefined |
23 | dwn(a.y); //20 静态公有属性y被删除后还原为原型属性y |
24 | dwn(a.z); //undefined 类属性无法通过对象访问 |
25 | dwn(myClass.z); |
原型(prototype)
这里只讲部分,prototype和闭包都不是几句话都能讲清楚的,如果这里可以给你一些启蒙,则万幸矣。习语"照猫画虎",这里的猫就是原型,虎是类型,可以表示成:虎.prototype=某只猫 or 虎.prototype=new 猫()。因为原型属性每个对象实例共享唯一副本,所以当实例中的一个调整了一个原型属性的值时,所有实例调用这个属性时都将发生变化,这点需要注意。
以下是原型关系的类型链:
01 | function ClassA(){ |
02 | } |
03 | ClassA.prototype= new Object(); |
04 | function ClassB(){ |
05 | } |
06 | ClassB.prototype= new ClassA(); |
07 | function ClassC(){ |
08 | } |
09 | ClassC.prototype= new ClassB(); |
10 | var obj= new ClassC(); |
11 | dwn(obj instanceof ClassC); //true |
12 | dwn(obj instanceof ClassB); //true |
13 | dwn(obj instanceof ClassA); //true |
14 | dwn(obj instanceof Object); //true |
15 | //带默认值的Point对象: |
16 | function Point2(x,y){ |
17 | if (x) this .x=x; |
18 | if (y) this .y=y; |
19 | } |
20 | //设定Point2对象的x,y默认值为0 |
21 | Point2.prototype.x=0; |
22 | Point2.prototype.y=0; |
23 | //p1是一个默认(0,0)的对象 |
24 | var p1= new Point2(); //可以写成var p1=new Point2也不会出错,WHY |
25 | //p2赋值 |
26 | var p2= new Point2(1,2); |
27 | dwn(p1.x+ "," +p1.y); //0,0 |
28 | dwn(p2.x+ "," +p2.y); //1,2 |
29 | delete 对象的属性后,原型属性将回到初始化的状态: |
30 | function ClassD(){ |
31 | this .a=100; |
32 | this .b=200; |
33 | this .c=300 |
34 | } |
35 | ClassD.prototype = new ClassD(); //将ClassD原有的属性设为原型,包括其值 |
36 | ClassD.prototype.reset = function (){ //将非原型属性删除 |
37 | for ( var each in this ) |
38 | { |
39 | delete this [each]; |
40 | } |
41 | } |
42 | var d = new ClassD(); |
43 | dwn(d.a); //100 |
44 | d.a*=2; |
45 | d.b*=2; |
46 | d.c*=2; |
47 | dwn(d.a); //200 |
48 | dwn(d.b); //400 |
49 | dwn(d.c); //600 |
50 | d.reset(); //删掉非原型属性,所有回来原型 |
51 | dwn(d.a); //100 |
52 | dwn(d.b); //200 |
53 | dwn(d.c); //300 |
继承
如果两个类都是同一个实例的类型,那么它们之间存在着某种关系,我们把同一个实例的类型之间的泛化关系称为继承。C#和JAVA中都有这个,具体的理解就不说了。
在javascript中,并不直接从方法上支持继承,但是就像前面说的,可以模拟。
方法可以归纳为四种:构造继承法,原型继承法,实例继承法和拷贝继承法。融会贯通之后,还有混合继续法,这是什么法,就是前面四种挑几种混着来~
以下例子涉及到了apply,call和一些Array的用法:
构造继续法例子
01 | //定义一个Collection类型 |
02 | function Collection(size) |
03 | { |
04 | this .size = function (){ return size}; //公有方法,可以被继承 |
05 | } |
06 | |
07 | Collection.prototype.isEmpty = function (){ //静态方法,不能被继承 |
08 | return this .size() == 0; |
09 | } |
10 | |
11 | //定义一个ArrayList类型,它"继承"Collection类型 |
12 | function ArrayList() |
13 | { |
14 | var m_elements = []; //私有成员,不能被继承 |
15 | m_elements = Array.apply(m_elements, arguments); |
16 | //ArrayList类型继承Collection |
17 | this .base = Collection; |
18 | this .base.call( this , m_elements.length); |
19 | |
20 | this .add = function () |
21 | { |
22 | return m_elements.push.apply(m_elements, arguments); |
23 | } |
24 | this .toArray = function () |
25 | { |
26 | return m_elements; |
27 | } |
28 | } |
29 |
30 | ArrayList.prototype.toString = function () |
31 | { |
32 | return this .toArray().toString(); |
33 | } |
34 | //定义一个SortedList类型,它继承ArrayList类型 |
35 | function SortedList() |
36 | { |
37 | //SortedList类型继承ArrayList |
38 | this .base = ArrayList; |
39 | this .base.apply( this , arguments); |
40 | this .sort = function () |
41 | { |
42 | var arr = this .toArray(); |
43 | arr.sort.apply(arr, arguments); |
44 | } |
45 | } |
46 | //构造一个ArrayList |
47 | var a = new ArrayList(1,2,3); |
48 | dwn(a); |
49 | dwn(a.size()); //a从Collection继承了size()方法 |
50 | dwn(a.isEmpty); //但是a没有继承到isEmpty()方法 |
51 | //构造一个SortedList |
52 | var b = new SortedList(3,1,2); |
53 | b.add(4,0); //b 从ArrayList继承了add()方法 |
54 | dwn(b.toArray()); //b 从ArrayList继承了toArray()方法 |
55 | b.sort(); //b 自己实现的sort()方法 |
56 | dwn(b.toArray()); |
57 | dwn(b); |
58 | dwn(b.size()); //b从Collection继承了size()方法 |
原型继承法例子
01 | //定义一个Point类型 |
02 | function Point(dimension) |
03 | { |
04 | this .dimension = dimension; |
05 | } |
06 | //定义一个Point2D类型,"继承"Point类型 |
07 | function Point2D(x, y) |
08 | { |
09 | this .x = x; |
10 | this .y = y; |
11 | } |
12 | Point2D.prototype.distance = function () |
13 | { |
14 | return Math.sqrt( this .x * this .x + this .y * this .y); |
15 | } |
16 | Point2D.prototype = new Point(2); //Point2D继承了Point |
17 | //定义一个Point3D类型,也继承Point类型 |
18 | function Point3D(x, y, z) |
19 | { |
20 | this .x = x; |
21 | this .y = y; |
22 | this .z = z; |
23 | } |
24 | Point3D.prototype = new Point(3); //Point3D也继承了Point |
25 | |
26 | //构造一个Point2D对象 |
27 | var p1 = new Point2D(0,0); |
28 | //构造一个Point3D对象 |
29 | var p2 = new Point3D(0,1,2); |
30 | dwn(p1.dimension); |
31 | dwn(p2.dimension); |
32 | dwn(p1 instanceof Point2D); //p1 是一个 Point2D |
33 | dwn(p1 instanceof Point); //p1 也是一个 Point |
34 | dwn(p2 instanceof Point); //p2 是一个Point |
以上两种方法是最常用的。
实例继承法例子
在说此法例子之前,说说构造继承法的局限,如下:
1 | function MyDate() |
2 | { |
3 | this .base = Date; |
4 | this .base.apply( this , arguments); |
5 | } |
6 | var date = new MyDate(); |
7 | //undefined,date并没有继承到Date类型,所以没有toGMTString方法 |
8 | alert(date.toGMTString); |
核心对象的某些方法不能被构造继承,原因是核心对象并不像我们自定义的一般对象那样在构造函数里进行赋值或初始化操作换成原型继承法呢?,如下:
1 | function MyDate(){} |
2 | MyDate.prototype= new Date(); |
3 | var date= new MyDate(); |
4 | //'[object]'不是日期对象,仍然没有继承到Date类型! |
5 | alert(date.toGMTString); |
现在,换成实例继承法:
01 | function MyDate() |
02 | { |
03 | //instance是一个新创建的日期对象 |
04 | var instance = new Date(); |
05 | instance.printDate = function (){ |
06 | document.write( "<p> " +instance.toLocaleString()+ "</p> " ); |
07 | } |
08 | //对instance扩展printDate()方法 |
09 | return instance; //将instance作为构造函数的返回值返回 |
10 | } |
11 | var myDate = new MyDate(); |
12 | //这回成功输出了正确的时间字符串,看来myDate已经是一个Date的实例了,继承成功 |
13 | dwn(myDate.toGMTString()); |
14 | //如果没有return instance,将不能以下标访问,因为是私有对象的方法 |
15 | myDate.printDate(); |
拷贝继承法例子
01 | Function.prototype.extends = function (obj) |
02 | { |
03 | for ( var each in obj) |
04 | { |
05 | this .prototype[each] = obj[each]; |
06 | //对对象的属性进行一对一的复制,但是它又慢又容易引起问题 |
07 | //所以这种"继承"方式一般不推荐使用 |
08 | } |
09 | } |
10 |
11 | var Point2D = function (){ |
12 | //…… |
13 | } |
14 |
15 | Point2D.extends( new Point()) |
16 | { |
17 | //…… |
18 | } |
这种继承法似乎是用得很少的。
混合继承例子
01 | function Point2D(x, y) |
02 | { |
03 | this .x = x; |
04 | this .y = y; |
05 | } |
06 |
07 | function ColorPoint2D(x, y, c) |
08 | { |
09 | Point2D.call( this , x, y); //这里是构造继承,调用了父类的构造函数 |
10 | //从前面的例子看过来,这里等价于 |
11 | //this.base=Point2D; |
12 | //this.base.call(this,x,y); |
13 | this .color = c; |
14 | } |
15 | ColorPoint2D.prototype = new Point2D(); //这里用了原型继承,让ColorPoint2D以Point2D对象为原型 |