关于javascript中原型方法访问私有变量的探索
今天闲来无事,突然想到一个关于javascript中原型方法访问私有变量的问题,代码示例如下:
1 function Person() { 2 var name ; 3 var age ; 4 5 this.setName = function(newName) { 6 name = newName ; 7 }; 8 9 this.getName = function() { 10 return name ; 11 }; 12 13 this.setAge = function(newAge) { 14 age = newAge ; 15 }; 16 17 this.getAge = function() { 18 return age ; 19 }; 20 }
上面的代码声明了一个叫做Person的构造函数,在构造函数中会为每个实例化的对象提供四个公有方法,分别是setName、getName、setAge和getAge。这四个方法通过js的闭包机制来访问和修改局部变量name和age,同时由于函数作用域的问题,name和age这两个局部变量在构造函数外是不可见的,也就是说只有这四个函数能够访问到这两个局部变量,我们将这两个变量称为私有变量。
为了进一步弄清楚这四个实例方法的作用,我写了如下的测试代码:
1 var zhaojian = new Person() ; 2 zhaojian.setName("zhaojian") ; 3 console.log("The name of zhaojian is " + zhaojian.getName()) ; //这一步输出zhaojian 4 5 var epson= new Person() ; 6 epson.setName("epson") ; 7 console.log("The name of epson is " + epson.getName()) ; //这一步输出epson 8 console.log("The name of zhaojian is " + zhaojian.getName()) ; //这一步输出zhaojian 9 10 11 zhaojian.setAge(18) ; 12 console.log("The age of zhaojian is " + zhaojian.getAge()) ; //这一步输出18 13 epson.setAge(28) ; 14 console.log("The age of epson is " + epson.getAge()) ; //这一步输出28 15 console.log("The age of zhaojian is " + zhaojian.getAge()) ; //这一步输出18
由此可见,每一次调用构造函数实例化对象的时候都会生成一个闭包,每个闭包中都含有两个局部的私有变量name和age,因此每个对象实例的私有变量name和age都是不相同的。
但是问题来了,如果我把其中的两个实例方法改成原型方法,那会出现什么情况呢?如下所示:
1 function Person() { 2 var name ; 3 var age ; 4 5 Person.prototype.setName = function(newName) { 6 name = newName ; 7 }; 8 9 Person.prototype.getName = function() { 10 return name ; 11 }; 12 13 this.setAge = function(newAge) { 14 age = newAge ; 15 }; 16 17 this.getAge = function() { 18 return age ; 19 }; 20 }
继续对上述代码调用之前的测试代码,得到结果如下:
1 var zhaojian = new Person() ; 2 zhaojian.setName("zhaojian") ; 3 console.log("The name of zhaojian is " + zhaojian.getName()) ; //这一步输出zhaojian 4 5 var epson= new Person() ; 6 epson.setName("epson") ; 7 console.log("The name of epson is " + epson.getName()) ; //这一步输出epson 8 console.log("The name of zhaojian is " + zhaojian.getName()) ; //注意,这一步现在输出的是epson 9 10 11 zhaojian.setAge(18) ; 12 console.log("The age of zhaojian is " + zhaojian.getAge()) ; //这一步输出18 13 epson.setAge(28) ; 14 console.log("The age of epson is " + epson.getAge()) ; //这一步输出28 15 console.log("The age of zhaojian is " + zhaojian.getAge()) ; //这一步还是输出18
问题来了,如果去掉上述代码中的epson.setName("epson");一句,结果会发生什么改变呢?
1 var zhaojian = new Person() ; 2 zhaojian.setName("zhaojian") ; 3 console.log("The name of zhaojian is " + zhaojian.getName()) ; //这一步输出zhaojian 4 5 var epson= new Person() ; 6 //epson.setName("epson") ; 7 console.log("The name of epson is " + epson.getName()) ; //这一步输出undefined 8 console.log("The name of zhaojian is " + zhaojian.getName()) ; //这一步输出undefined 9 10 11 zhaojian.setAge(18) ; 12 console.log("The age of zhaojian is " + zhaojian.getAge()) ; //这一步输出18 13 epson.setAge(28) ; 14 console.log("The age of epson is " + epson.getAge()) ; //这一步输出28 15 console.log("The age of zhaojian is " + zhaojian.getAge()) ; //这一步还是输出18
对象zhaojian和epson的getName函数返回的都是undefined,可是zhaojian之前确实是有调用过setName方法设置过name变量的值的啊,怎么就变成了undefined?为了弄清疑问,再加上一句测试代码看看:
1 var zhaojian = new Person() ; 2 zhaojian.setName("zhaojian") ; 3 console.log("The name of zhaojian is " + zhaojian.getName()) ; //这一步输出zhaojian 4 5 var epson= new Person() ; 6 //epson.setName("epson") ; 7 zhaojian.setName("aaa") ; 8 console.log("The name of epson is " + epson.getName()) ; //这一步输出aaa 9 console.log("The name of zhaojian is " + zhaojian.getName()) ; //这一步输出aaa 10 11 12 zhaojian.setAge(18) ; 13 console.log("The age of zhaojian is " + zhaojian.getAge()) ; //这一步输出18 14 epson.setAge(28) ; 15 console.log("The age of epson is " + epson.getAge()) ; //这一步输出28 16 console.log("The age of zhaojian is " + zhaojian.getAge()) ; //这一步还是输出18
诡异的现象出现了,调用epson对象的setName方法能够影响到zhaojian对象的name变量,调用zhaojian对象的setName方法同样也能够影响到zhaojian对象的name变量,但是两个对象的age变量之间却是井水不犯河水,相处得非常和谐,这是为什么呢?
这里涉及到实例方法、原型方法和函数作用域的问题,我们知道实例方法会被每一个实例化的对象复制一份,即每个实例化后的对象都拥有一份独立的实例方法的代码体。而原型方法则是在对象的原型对象(prototype)上,原型方法的代码体只有一份,且为所有的对象实例所共享。因此在调用方法的时候,每个对象引用的都是不同的实例方法,而所有对象引用的都是同一个原型方法。至于函数作用域,在javascript中的函数作用域指的是函数的声明域而不是调用域,因此原型方法setName和getName引用的都是在其被声明的作用域内的name变量。那么如果有实例化多个对象的话,setName引用的是哪一个闭包中的name变量呢?对构造函数Person进行分析,我们可以进行猜测,每一次调用构造函数都会声明一次setName方法,也就是说每调用一次构造函数setName方法的声明域都会发生改变,且setName方法的声明域始终是在最后一次调用构造函数所形成的闭包之中,因此所有的对象实例在调用setName方法时引用的都是最后被实例化的对象的name变量。为了验证以上的猜测,我们给Person类增加一个getSelfName实例方法,如下所示:
1 this.getSelfName = function() { 2 return name ; 3 };
该实例方法返回的是当前对象的name变量,于是对测试代码再做一点修改,来验证我们的想法:
1 var zhaojian = new Person() ; 2 zhaojian.setName("zhaojian") ; 3 console.log("The name of zhaojian is " + zhaojian.getName()) ; //这一步输出zhaojian 4 5 var epson= new Person() ; 6 //epson.setName("epson") ; 7 zhaojian.setName("aaa") ; 8 console.log("The name of zhaojian itself is " + zhaojian.getSelfName() ) ;//输出zhaojian 9 console.log("The name of epson is " + epson.getName()) ; //输出aaa 10 console.log("The name of zhaojian is " + zhaojian.getName()) ; //输出aaa 11 console.log("The name of epson itself is " + epson.getSelfName()) ;//输出aaa 12 13 zhaojian.setAge(18) ; 14 console.log("The age of zhaojian is " + zhaojian.getAge()) ; //输出18 15 epson.setAge(28) ; 16 console.log("The age of epson is " + epson.getAge()) ; //输出28 17 console.log("The age of zhaojian is " + zhaojian.getAge()) ; //输出18
由此可见,在zhaojian对象调用了setName方法之后,epson对象的name变量被改变成了aaa,但是zhaojian对象的name变量的值还是zhaojian。因此原型方法setName所引用的是epson对象的name变量,而zhaojian对象的name变量在epson对象被实例化之后就被setName方法弃之不顾了,原型方法setName和getName所引用的始终都是最后一个被构造函数Person实例化的对象的name变量。上述的猜测是正确的,大功告成。