JavaScript的ES3,ES5,ES6三种实现继承方式的教程
es3 继承
在javascript中,所谓的类就是函数,函数就是类。一般情况下,我们在函数的prototype上面定义方法,因为这样所有类的实例都可以公用这些方法;在函数内部(构造函数)中初始化属性,这样所有类的实例的属性都是相互隔离的。 我们定义classa和classb两个类,想让classb继承自classa。 classa代码如下所示:
function classa(name, age) { this.name = name; this.age = age; } classa.prototype.sayname = function () { console.log(this.name); }; classa.prototype.sayage = function () { console.log(this.age); };
classa构造函数内部定义了name和age两个属性,并且在其原型上定义了sayname和sayaage两个方法。 classb如下所示:
function classb(name, age, job) { classa.apply(this, [name, age]); this.job = job; }
classb新增了job属性,我们在其构造函数中执行classa.apply(this, [name, age]);,相当于在java类的构造函数中通过super()调用父类的构造函数以初始化相关属性。
此时我们可以通过var b = new classb(“sunqun”, 28, “developer”);进行实例化,并可以访问b.name、b.age、b.job三个属性,但此时b还不能访问classa中定义的sayname和sayaage两个方法。
然后我们新增代码classb.prototype = classa.prototype;,此时classb的代码如下所示:
function classb(name, age, job) { classa.apply(this, [name, age]); this.job = job; } //新增 classb.prototype = classa.prototype;
当执行var b = new classb(“sunqun”, 28, “developer”);时,b.__proto__指向的是classb.prototype,由于通过新增的代码已经将classb.prototype指向了classa.prototype,所以此时b.__proto__指向了classa.prototype。这样当执行b.sayname()时,会执行b.__proto__.sayname(),即最终执行了classa.prototype.sayname(),这样classb的实例就能调用classa中方法了。
此时我们想为classb新增加一个sayjob方法用于输出job属性的值,如下所示:
function classb(name, age, job) { classa.apply(this, [name, age]); this.job = job; } classb.prototype = classa.prototype; //新增 classb.prototype.sayjob = function(){ console.log(this.job); };
此时问题出现了,我们为classb.prototype添加sayjob方法时,其实修改了classa.prototype,这样会导致classa所有的实例也都有了sayjob方法,这显然不是我们期望的。 为了解决这个问题,我们再次修改classb的代码,如下所示:
function classb(name, age, job) { classa.apply(this, [name, age]); this.job = job; } // classb.prototype = classa.prototype; //修改 classb.prototype = new classa(); classb.prototype.constructor = classb; classb.prototype.sayjob = function(){ console.log(this.job); };
我们通过执行classb.prototype = new classa();将classa实例化的对象作为classb的prototype,这样classb仍然能够使用classa中定义的方法,但是classb.prototype已经和classa.prototype完全隔离了。
我们的目的达到了,我们可以随意向classb.prototype添加我们想要的方法了。有个细节需要注意,classb.prototype = new classa();会导致classb.prototype.constructor指向classa的实例化对象,为此我们通过classb.prototype.constructor = classb;解决这个问题。
关于此处为什么要设置.constructor可参见这篇博客
https://blog.csdn.net/c_kite/article/details/78484191
一切貌似完美的解决了,但是这种实现还是存在隐患。我们在执行classb.prototype = new classa();的时候,给classa传递的是空参数,但是classa的构造函数默认参数是有值的,可能会在构造函数中对传入的参数进行各种处理,传递空参数很有可能导致报错(当然本示例中的classa不会)。于是我们再次修改classb的代码如下所示:
function classb(name, age, job) { classa.apply(this, [name, age]); this.job = job; } //修改 function classmiddle() { } classmiddle.prototype = classa.prototype; classb.prototype = new classmiddle(); classb.prototype.constructor = classb; classb.prototype.sayjob = function () { console.log(this.job); };
这次我们引入了一个不需要形参的函数classmiddle作为classb和classa之间的中间桥梁。
1. classmiddle.prototype = classa.prototype;: 将classmiddle.prototype指向classa.prototype,这样classmiddle可以访问classa中定义的方法。
2. classb.prototype = new classmiddle();: 将classmiddle的实例化对象赋值给classb.prototype,这样就相当于执行了classb.prototype.__proto__ = classmiddle.prototype;,所以classb就能使用classmiddle中定义的方法,又因为classmiddle.prototype指向了classa.prototype,所以classb.prototype.__proto__也指向了classa.prototype,这样classb能使用classa中定义的方法。
以上思路的精妙之处在于classmiddle是无参的,它起到了classb和classa之间的中间桥梁的作用。 现在我们为classa添加一些静态属性和方法,classa新增如下代码:
... //为classa添加静态属性 classa.staticvalue = "static value"; //为classa添加静态方法 classa.getstaticvalue = function() { return classa.staticvalue; }; classa.setstaticvalue = function(value) { classa.staticvalue = value; };
静态属性和方法不属于某一个实例,而是属于类本身。classa.prototype上面定义的方法是实例方法,不是静态的。静态属性和方法是直接添加在classa上的。 为了使classb也能继承classa的静态属性和方法,我们需要为classb添加如下代码:
... //classb继承classa的静态属性和方法 for (var p in classa) { if (classa.hasownproperty(p)) { classb[p] = classa[p]; } }
我们最终可以将上述继承代码的公共部分抽离成一个extendsclass方法,如下所示:
function extendsclass(child, father) { //继承父类prototype中定义的实例属性和方法 function classmiddle() { } classmiddle.prototype = father.prototype; child.prototype = new classmiddle(); child.prototype.constructor = child; //继承父类的静态属性和方法 for (var p in father) { if (father.hasownproperty(p)) { child[p] = father[p]; } } }
我们只需要执行extendsclass(classb, classa);就可以完成大部分继承的逻辑。 最终classa的完整代码如下所示:
function classa(name, age) { this.name = name; this.age = age; } classa.prototype.sayname = function() { console.log(this.name); }; classa.prototype.sayage = function() { console.log(this.age); }; classa.staticvalue = "static value"; classa.getstaticvalue = function() { return classa.staticvalue; }; classa.setstaticvalue = function(value) { classa.staticvalue = value; };
classb的完整代码如下所示:
function classb(name, age, job) { classa.apply(this, [name, age]); this.job = job; } extendsclass(classb, classa); classb.prototype.sayjob = function() { console.log(this.job); };
es5继承
es5.1规范中新增了object.create()方法,该方法会传入一个对象,然后会返回一个对象,返回的对象的原型指向传入的对象,比如执行代码var output = object.create(input),相当于执行代码output.__proto__ = input;,output的原型是input。我们可以简化之前的代码,不再需要classmiddle,只需要执行classb.prototype = object.create(classa.prototype);即可,相当于执行代码classb.prototype.__proto__ = classa.prototype;。
而且es5.1中新增了object.keys()方法用以获取对象自身的属性数组,我们可以用该方法简化继承父类静态属性和方法的过程。
根据以上两点,我们修改extendsclass方法如下所示:
function extendsclass(child, father) { //继承父类prototype中定义的实例属性和方法 child.prototype = object.create(father.prototype); child.prototype.constructor = child; //继承父类的静态属性和方法 object.keys(father).foreach(function(key) { child[key] = father[key]; }); }
es6 继承
我们之前提到,es6规范定义了object.prototype.proto属性,该属性既可读又可写,通过__proto__属性我们可以直接指定对象的原型。于是在es6中我们将extendsclass修改为如下所示:
function extendsclass(child, father) { //继承父类prototype中定义的实例属性和方法 child.prototype.__proto__ = father.prototype;//暴力直接,利用__proto__属性设置对象的原型 //继承父类的静态属性和方法 child.__proto__ = father; }
直接修改对象的__proto__属性值不是最佳选择,es6规范中还定义了object.setprototypeof()方法,通过执行object.setprototypeof(b, a)会将a对象作为b对象的原型,即相当于执行了b.__proto__ = a;。为此我们利用该方法再次精简我们的extendsclass方法,如下所示:
function extendsclass(child, father) { //继承父类prototype中定义的实例属性和方法 object.setprototypeof(child.prototype, father.prototype); //继承父类的静态属性和方法 object.setprototypeof(child, father); }
object.setprototypeof(child.prototype, father.prototype);相当于执行代码child.prototype.__proto__ = father.prototype;,使得child能够继承father中的实例属性和方法。
object.setprototypeof(child, father);相当于执行代码child.__proto__ = father;,使得child能够继承father中的静态属性和方法。
由于现代 javascript 引擎优化属性访问所带来的特性的关系,更改对象的 [[prototype]]在各个和 javascript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.__proto__ = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [[prototype]]。相反,你应该使用 object.create()来创建带有你想要的[[prototype]]的新对象。
es6中引入了class关键字,可以用class直接定义类,通过extends关键字实现类的继承,还可以通过static关键字定义类的静态方法。
我们用class等关键字重新实现classa和classb的代码,如下所示:
class classa{ constructor(name, age){ this.name = name; this.age = age; } sayname(){ console.log(this.name); } sayage(){ console.log(this.age); } static getstaticvalue(){ return classa.staticvalue; } static setstaticvalue(value){ classa.staticvalue = value; } } classa.staticvalue = "static value"; class classb extends classa{ constructor(name, age, job){ super(name, age); this.job = job; } sayjob(){ console.log(this.job); } }
es6中不能通过static定义类的静态属性,我们可以直接通过classa.staticvalue = "static value";定义类的静态属性。
需要注意的是,class关键字只是原型的语法糖,javascript继承仍然是基于原型实现的。
并不是所有的浏览器都支持class关键字,在生产环境中,我们可以编写es6的代码,然后用babel或typescript将其编译为es5等主流浏览器支持的语法格式。
es6 class和之前几种继承区别
一个很重要的区别在于, es5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面, 例如上面的说的例子
function classb(name, age, job) { classa.apply(this, [name, age]); this.job = job; }
es6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
总结
执行var x = new x();时,浏览器会执行x.__proto__ = x.prototype,会将实例化对象的原型设置为对应的类的prototype对象。
实现类继承的关键是child.prototype.__proto__ = father.prototype;,这样会将father.prototype作为child.prototype的原型。object.prototype.__proto__属性是在es6规范中所引入的,为了在es3和es5中需要通过各种方式模拟实现对object.prototype.__proto__进行赋值。
通过执行child.__proto__ = father;可以实现继承父类的静态属性和方法。