注意上面的继承方法是错误的,这样只是简单的将UString的原型指向了MyString的原型,即UString和MyString使用了相同的原型,子类UString增删改原型的方法,MyString也会相应地变化,另外一个继承MyString如AsciiString的类也会相应地变化。依照上文分析,应该是让UString的原型里的的__proto__属性指向MyString的原型,而不是让UString的原型指向MyString。也就是说,得让UString有自己的独立的原型,在它的原型上添加一个指针指向父类的原型:
UString.prototype.__proto__ = MyString.prototype; //不是正确的实现
因为__proto__不是一个标准的语法,在有些浏览器上是不可见的,如果在Firefox上运行上面这段代码,Firefox会给出警告:
mutating the [[Prototype]] of an object will cause your code to run very slowly; instead create the object with the correct initial [[Prototype]] value using Object.create
合理的做法应该是让prototype等于一个object,这个object的__proto__指向父类的原型,因此这个object须要是一个function的实例,而这个function的prototype指向父类的原型,所以得出以下实现:
Object.create = function(o){
var F = function(){};
F.prototype = o;
return new F();
};
UString.prototype = Object.create(MyString.prototype);
代码第2行,定义一个临时的function,第3行让这个function的原型指向父类的原型,第4行返回一个实例,这个实例的__proto__就指向了父类的prototype,第7行再把这个实例赋值给子类的prototype。继承的实现到这里基本上就完成了。
但是还有一个小问题。正常的prototype里面会有一个constructor指向构造函数function本身,例如上面的MyString:
这个constructor的作用就在于,可在原型里面调用构造函数,例如给MyString类增加一个copy拷贝函数:
MyString.prototype.copy = function(){
// return MyString(this.content);
//这样实现有问题,下面再作分析
return new this.constructor(this.content); //正确实现
};
var anotherName = name.copy();
console.log(anotherName.toString());
//输出hanMeimei
console.log(anotherName instanceof MyString); //输出true
问题就于:Object.create的那段代码里第7行,完全覆盖掉了UString的prototype,取代的是一个新的object,这个object的__proto__指向父类即MyString的原型,因此UString.prototype.constructor在查找的时候,UString.prototype没有constructor这个属性,于是向它指向的__proto__查找,找到了MyString的constructor,因此UString的constructor实际上是MyString的constuctor,如下所示,ustr2实际上是MyString的实例,而不是期望的UString。而不用constructor,直接使用名字进行调用(上面代码第2行)也会有这个问题。
var ustr = new UString();
var ustr2 = ustr.copy();
console.log(ustr instanceof UString); //输出true
console.log(ustr2 instanceof UString); //输出false
console.log(ustr2 instanceof Mystring); //输出true
所以实现继承后需要加多一步操作,将子类UString原型里的constructor指回它自己:
UString.prototype.constructor = UString;
在执行copy函数里的this.constructor()时,实际上就是UString()。这时候再做instanseof判断就正常了:
console.log(ustr2 instanceof Ustring); //输出true
可以把相关操作封装成一个函数,方便复用。基本的继承核心的地方到这里就结束了,接下来还有几个问题需要考虑。
第一个是子类构造函数里如何调用父类的构造函数,直接把父类的构造函数当作一个普通的函数用,同时传一个子类的this指针:
var UString = function(str){
// MyString(str); //不正确的实现
MyString.call(this, str);
};
var ustr = new UString("hanMeimei");
console.log(ustr + ""); //输出hanMeimei
注意第3行传了一个this指针,在调用MyString的时候,这个this就指向了新产生的UString对象,如果直接使用第2行,那么执行的上下文是window,this将会指向window,this.content = str等价于window.content = str。
第二个问题是私有属性的实现,在最开始的构造函数里定义的变量,其实例是公有的,可以直接访问,如下:
var MyString = function(str){
this.content = str;
};
var str = new MyString("hello");
console.log(str.content);
//直接访问,输出hello
但是典型的面向对象编程里,属性应该是私有的,操作属性应该通过类提供的方法/接口进行访问,这样才能达到封装的目的。在JS里面要实现私有,得借助function的作用域:
var MyString = function(str){
this.sayHi = function(){
return "hi " + str;
}
};
var str = new MyString("hanMeimei");
console.log(str.sayHi());
//输出hi, hanMeimei
但是这样的一个问题是,必须将函数的定义放在构造函数里,而不是之前讨论的原型,导致每生成一个实例,就会给这个实例添加一个一模一样的函数,造成内存空间的浪费。所以这样的实现是内存为代价的。如果产生很多实例,内存空间会大幅增加,这个问题是不可忽略的,因此在JS里面实现属性私有不太现实,即使在ES6的class语法也没有实现。但是可以给类添加静态的私有成员变量,这个私有的变量为类的所有实例所共享,如下面的案例:
var Worker;
(function(){
var id = 1000;
Worker = function(){
id++;
};
Worker.prototype.getId = function(){
return id;
};
})();
var worker1 = new Worker();
console.log(worker1.getId()); //输出1001
var worker2 = new Worker();
console.log(worker2.getId()); //输出1002
上面的例子使用了类的静态变量,给每个worker产生唯一的id。同时这个id是不允许worker实例直接修改的。