编写javascript的开发者都知道,JS虽然没有类(ES6添加了class语法),但是可以模拟出OOP语言的类和面向对象的概念,比如我们都知道的一句话,Javascript中处处是对象,而面向对象语言的特性是继承,封装,多态,抽象,而本文讨论的是Javascript的继承,Javascript的继承方式有原型继承,组合继承,寄生继承等等,在日常开发中,哪种继承方式更好用在于开发者对于程序的结果以及性能的考虑。笔者在下面列举出原型继承中经常容易被忽略的错误。
常见错误一:
function Fa(name){ this.name=name; } Fa.prototype.myname=function(){ //设置Fa的原型方法 console.log(this.name); } function Son(name){ Fa.call(this,name); //将Fa的this显示绑定给Son,这里的this显示绑定机制后续笔者会更新 } Son.prototype=Fa.prototype; //将son的原型对象引用到fa的原型对象,其实就是c++里面的指针概念,直接指向fa的原型 //注意!!!!!:当你修改Son.prototype的constructor时
// Son.prototype.constructor=Son; // 这样会导致Fa创建出来的对象的constructor也指向了Son,会使对象的类型变的很混乱 Son.prototype.sayhello=function(){ //设置son的原型方法 console.log("this is son"); } //创建对象 var son=new Son("son"); son.sayhello(); //"this is son" console.log(son); var fa=new Fa("fa"); fa.sayhello(); //"this is son" console.log(fa); //这里发现在由Fa创建的对象中也存在sayhello方法,这是因为son.prototype直接引用了Fa.prototype // Son.prototype=Fa.prototype; 并不会创建一个关联到 Son.prototype 的新对象,它只 //是让 Son.prototype 直接引用 Fa.prototype 对象。因此当你执行类似 Son.prototype. //sayhello = ... 的赋值语句时也会直接修改 Fa.prototype 对象本身。显然这不是你想要的结 //果,否则你根本不需要 Son 对象,直接使用 Fa就可以了,这样一来代码也会更简单一些。
常见错误二:
function Fa(name){ this.name=name; } Fa.prototype.myname=function(){ console.log(this.name); } function Son(name){ Fa.call(this,name); } Son.prototype=new Fa("fa"); //调用Fa的构造函数new一个新的对象关联给Son.prototype Son.prototype.sayhello=function(){ console.log("this is son"); } var son=new Son("son"); console.log(son); son.myname(); //son var fa=new Fa("fa"); console.log(fa); fa.sayhello(); //这里会报错。Uncaught TypeError: fa.sayhello is not a function //我们发现在son的原型创建的方法并没有影响到Fa的原型。但是在Son.prototype = new Fa()后, // var son=new Son("son");我们输出son.name的值为son,在原型上有一个Fa实例对象,这个实例对象也有name属性 //而输出son是因为原型链上的隐式屏蔽,这一层的属性会屏蔽上一层相同属性的值。 // Son.prototype = new Fa() 的确会创建一个关联到 Son.prototype 的新对象。但是它使用 //了 Fa(..) 的“构造函数调用”,如果函数 Foo 有一些副作用(比如写日志、修改状态、注 //册到其他对象、给 this 添加数据属性,等等)的话,就会影响到 Son() 的“后代”,后果 //不堪设想 也会让原型变的臃肿
正确做法:
function Fa(name){ this.name=name; } Fa.prototype.myname=function(){ console.log(this.name); } function Son(name){ Fa.call(this,name); }
Son.prototype=Object.create(Fa.prototype); //注意这一句 //当修改son的原型时。son的constructor也会指向fa,这里需要手动修改constructor //ES6方法 属性描述符 //IE8以下不兼容 Object.defineProperty(Son.prototype,"constructor",{ writable:true, //读写属性 ,为false时为只读,外界无法修改 configurable:true, //配置属性,为false时,外界无法删除该属性,比如delete Son.prototype.constructor会失效 在严格模式下会报错
enumerable:false, //枚举属性,此时为false,在外界的for in访问方法不会列举出该属性,为true时反之; value:Son //将constructor关联到Son }); var son=new Son("son"); console.log(son); son.myname(); console.log(son instanceof Son); //true // 这段代码的核心部分就是语句 son.prototype = Object.create( fa.prototype ) 。调用 // Object.create(..) 会凭空创建一个“新”对象并把新对象内部的 Prototype 关联到你 // 指定的对象(本例中是 fa.prototype )。 // 换句话说,这条语句的意思是:“创建一个新的 son.prototype 对象并把它关联到 fa. // prototype ”。
这里我们对比一下三个方法:
和你想要的机制不一样!
Son.prototype = Fa.prototype;
基本上满足你的需求,但是可能会产生一些副作用 :(
Son.prototype = new Fa();
使用 Object.create(..) 而不是使用具有副
作用的 Fa(..) 。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能
直接修改已有的默认对象。
如果能有一个标准并且可靠的方法来修改对象的 Prototype关联就好了。在 ES6 之前,
我们只能通过设置 .__proto__ 属性来实现,但是这个方法并不是标准并且无法兼容所有浏
览器。ES6 添加了辅助函数 Object.setPrototypeOf(..) ,可以用标准并且可靠的方法来修
改关联。
ES6 之前需要抛弃默认的 Son.prototype
Son.ptototype = Object.create( Fa.prototype );
ES6 开始可以直接修改现有的 Son.prototype
Object.setPrototypeOf( Son.prototype,Fa.prototype );
如果忽略掉 Object.create(..) 方法带来的轻微性能损失(抛弃的对象需要进行垃圾回收)
它实际上比 ES6 及其之后的方法更短而且可读性更高。不过无论如何,这是两种完
全不同的语法。
以上是笔者对于原型继承中常常忽略的错误的总结以及更好的解决方法,至于寄生继承之类的方法并不是本文讨论的范围,后续笔者也会更新寄生继承的方法,如果忽视这些细节上的错误,对后续程序运行的结果也会产生一些不可预知的结果,细节决定成败。