JavaScript的this关键字
和一些面向对象的语言一样,JavaScript也支持this关键字,顾名思义,this关键字指代本对象,在传统的面向对象语言中,this代表该方法所属的对象,或者该对象的指针。
在JavaScript中没有类的概念,this方法的指代含义也有些许的变化,指代的是调用该方法的对象。
还是以上次的程序作为例子:
var User = function() { this.username = 'username'; this.password = 'password'; } User.prototype.toString = function() { return this.username + ':' + this.password; };
toString方法中就使用了this关键字来指代调用者本身,一般情况下,调用者和该方法的所属对象是一致的,这种情况下,就与其他语言没什么不同。不过,JavaScript中调用者和方法所属的对象并不一定是一致的,许多情况下与闭包特性结合,也能产生许多特殊的效果。下面是一个稍微复杂的例子。
var x = function() { this.show = function(msg, func, scope) { document.write(msg + " : " + this.value + '<br />'); if (scope === undefined) scope = this; func.apply(scope, []); func.call(scope); } }; var y = new x(); x.prototype.value = 4; y.value = 5; var z = new x(); z.value = 6; var w = new x(); x.prototype.value = 7; y.show('value is', function() { document.write('value in scope : ' + this.value + '<br />'); }, z); y.show('value is', function() { document.write('value in scope : ' + this.value + '<br />'); }, x.prototype); y.show('value is', function() { document.write('value in scope : ' + this.value + '<br />'); }, w);
x构造函数定义了一个show方法,首先输出调用者的value值,然后如果scope没有传入,则定义为方法的调用者本身。
随后出现了两个方法,分别为apply和call,这两个方法是所有function都具有的,作用是强制指定某对象作为该方法的调用者,两者除了调用格式略有不同,作用是相同的。这两句话以scope的身份调用了func。
后面定义了y、z、w三个变量,都分别是x的实例,接着以y的身份调用了3次show方法,只有scope参数传入了不同的值。
第一次传入了对象z,此时y作为show的调用者,而show中的参数匿名函数func的调用者却被设定为z,因此输出结果为:
value is : 5 value in scope : 6 value in scope : 6
第二次传入x.prototype,原理如上面相同,show的func参数是由x.prototype调用的,因此输入结果为:
value is : 5 value in scope : 7 value in scope : 7
第三次传入了对象w,原理也和上面相同,func的调用者是w,那么此时w的值是多少呢?我们在实例化w以后,我们修改了x.prototype.value的值,那么此时w的value应该是x.prototype.value修改前的值还是修改后的值呢?
答案是修改后的值,结果是:
value is : 5 value in scope : 7 value in scope : 7
因为w并没有显式定义value属性,因此w的value属性是位于其__proto__属性中,也就是x.prototype的引用,因此当x.prototype发生了改变,对于w也是会同时产生影响,具体关于prototype的原理和行为,请参考之前的其他文章。
JavaScript继承特性的一种实现
上次讲到了JavaScript实现封装的特性,那么JavaScript能否模拟继承的特性呢?
var User = function() { this.username = 'username'; this.password = 'password'; } User.prototype.toString = function() { return this.username + ':' + this.password; };
这个是上次所写的User的定义。现在我们要新建一个Student,使其具有User的所有属性,和就类似与继承特性。
var extend = function(derived, base) { baseObj = new base(); for (property in baseObj) { derived.prototype[property] = baseObj[property]; } } var Student = function() { this.studentNumber = '11111'; } extend(Student, User); var student = new Student(); document.write(student);
extend方法是为了模拟继承特性,函数体很简单,首先新建一个“基类”的对象,然后将“基类”对象中的所有属性,放到“派生类”的prototype中,就使得derived的对象可以访问到所有base对象具有的属性。
实际操作时,我们首先创建一个Student,并且定义其相对与User不同的部分。随后调用extend函数进行“继承”,使其具有User构造的对象的所有属性。
然后尝试使用Student来构造一个student对象。
JavaScript中prototype关键字的作用
JavaScript并不是一种面向对象的程序设计语言,严格来讲,它并没有什么类的概念,顶多只是JSON对象而已,然而在JavaScript中有prototype这个关键字,利用它,我们可以间接地实现一些面向对象的特性。
var User = function() { this.username = 'username'; this.password = 'password'; this.toString = function() { return this.username + ':' + this.password; } } var user = new User(); document.write(user + '<br />');
这是一个很简单的封装实现,此时的User可以被视为一个“类”,这里还有一个方法叫做toString,实际上,在需要字符串的地方,对象的toString方法会自动被调用。
如果在上面这个例子中,toString没有被定义在User里面,也可以通过prototype关键字来定义在外部,效果是等价的。
User.prototype.toString = function() { return this.username + ':' + this.password; };
如果在这个例子中没有写prototype关键字,那么user对象实际上是没有toString这个函数可以调用的,因为没有prototype代表这个toString函数仅被定义在User本身之上。
这是什么原理呢?
首先,使用new关键字进行新对象user的构造时,是调用了上面赋值给User的匿名函数,这个函数赋予了新对象user一些属性,就像在函数体中所写的那样。如果toString没有被定义在里面,那么此时user只有username和password两个属性。
如果在外部定义的toString没有加上prototype关键字,那么会发生什么情况呢?User对象具有了toString方法,然而User本身却没有username和password。因此,此时User的username与password均为undefined,有没有觉得比较像面向对象语言中的静态方法呢?是的,相同点在于:我们可以通过“类名”直接调用该方法,该方法不能使用“非静态”成员。
如果在toString被定义在User.prototype之下,user对象就会具有toString方法,这是为什么呢?因为每个对象都隐含了一个名为__proto__的属性,相当于在User构造函数的最后增加了:
this.__proto__ = User.prototype; //注意,这个__proto__是内部名称,不同浏览器实现不同名称也可能不相同。
prototype这个特殊的属性在构造函数定义时被初始化,初始化时包含了User的构造函数本身和自己的__proto__属性,当用户通过User.prototype的方式定义新属性或方法的时候,这些属性或方法就被增加到User.prototype这个JSON对象之中。由于this.__proto__ = User.prototype;的缘故,调用这个构造函数构造的所有对象的__proto__属性中,都包含有这个新属性或方法。因此,实际上toString并没有成为user下的一个方法,而是user.__proto__下的一个方法。
然后,实际执行user.toString()时,浏览器将检查user是否具有toString方法可以调用,如果没有,浏览器将继续检查其__proto__属性中是否有toString方法,如果仍然没有,则继续递归向上寻找这个__proto__属性的__proto__属性。如此,就可以解释为什么prototype下定义的方法,都可以被该“类”的所有实例调用的原因了。