由于工作需要,需要大量使用javascript,于是对其进行了一下学习。
学习任何一个语言,最重要的是掌握其和其他语言不同的关键特性。对javascript来说,我总结就是prototype。就像metatable之于lua的地位,如果理解了prototype,再加上其他语言的使用经验,javascript将很容易掌握。
首先,明确一个概念,javascript中所有东西皆是对象,包括函数。这是后面讨论的基础。
关于prototype,相关的概念有两个:
1 一切函数对象,都有一个默认的属性:prototype。
2 一切对象,都有一个默认的属性:__proto__。
比如我写一个函数:
function Speak() { alert("speak!"); }
这个函数将会有一个叫做prototype的属性。
prototype更多的只是一个普通的对象(只有在使用new关键字的时候,它才会显出语法层面的特殊性);而__proto__则有特殊的语法意义:
当访问一个对象的属性,而这个属性不存在的时候,将在这个对象的__proto__中寻找这个属性,如果再找不到,就继续找__proto__的__proto__,直到最顶层(Object)。设置时则在对象上增加这个属性。
这个功能,类似于lua中的metatable中的index,如果metatable的index指定了一个表,那么找不到字段时会从那个表里找。
明白了__proto__的意义之后,再说prototype,事实上prototype就是为了设置__proto__而存在的:当使用new关键字调用函数,将把该函数对象的prototype设置到新实例的__proto__上。
例如:
1 function Animal(hp) 2 { 3 this.hp = hp; 4 } 5 6 var a = new Animal(20);
此时,a的__proto__将是Animal的prototype。
prototype预置了一个属性叫constructor,constructor如果不主动修改,默认指向自己(本函数对象)。
因此,如果需要添加类成员,只需要在prototype上面加,prototype作为原型,只有一份,所有new出来的对象都使用它的属性。
1 function Animal(hp) 2 { 3 this.hp = hp; 4 } 5 6 Animal.prototype.getHP = function() { return this.hp; } 7 8 var a = new Animal(20);
此时,a.getHP()将调用到Animal.prototype.getHP。
关于类与实例,到这里就没问题了。可以认为prototype就是类模板,Animal(hp)就是构造函数。当调用new的时候,javascript做了下面的事情:
1 创建一个新的对象(Object)。
2 将该对象的__proto__赋值为new后所调用函数的prototype。
3 将该对象赋值给this参数(隐含在参数表中),调用该函数。
4 函数返回后,将该对象作为new表达式的返回值返回。
由于所有对象共享一个prototype,因此,此时要注意一点:如果要定义的类中有属性引用到可能被修改的对象,如:
1 function Animal(hp) 2 { 3 } 4 5 Animal.prototype.family = {father:"a", mother:"b" }; 6 7 var a = new Animal(); 8 var b = new Animal();
此时,如果修改了a的father,b的也一起被修改了。因此,这个family字段必须在Animal构造函数中设置,不能放在prototype中。如果该对象不太可能被修改,如纯粹的数值或字符串,就可以放到prototype中。
如果要实现继承,就要利用__proto__的链条形式往上查找属性的特性。将prototype指定为父类的实例。
1 function Animal(hp) 2 { 3 this.hp = hp; 4 } 5 6 Animal.prototype.getHP = function() { return this.hp; } 7 8 function Person(name) 9 { 10 this.name = name; 11 } 12 13 Person.prototype = new Animal(); 14 Person.prototype.constructor = Person; 15 Person.prototype.getName = function() { return this.name; } 16 17 var personInstance = new Person("Eagle");
此时,当我们访问personInstance.getHP,首先personInstance找不到,到personInstance.__proto__中寻找,即Person.prototype,因为Person.prototype是在Animal实例的基础上修改的,其__proto__是Animal.prototype,因此找到了Animal.prototype中的getHP。
解决了继承的问题,但是依然存在一个问题:如何调用父类的构造函数。上面说的getHP实际上调用会出错,因为Animal函数没有被调用过,因此,this.hp = hp这一句没有执行,其实是没有hp属性的。
此时,我们需要在构造函数中嵌套调用父类构造函数,如下:
1 function Animal(hp) 2 { 3 this.hp = hp; 4 } 5 6 function Person(name, hp) 7 { 8 Animal.call(this, hp); //注意这句话:函数对象的call方法可以显式的修改this指针。如果直接调用Animal(hp),this指向的是global。 9 this.name = name; 10 } 11 12 var a = new Person("Eagle", 100);
到此为止,通过prototype的一套体系,实现了javascript的类,实例和继承。