ES6 javascript中Class类继承用法实例详解
本文实例讲述了ES6javascript中Class类继承用法。分享给大家供大家参考,具体如下:
1.基本用法
Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。
classColorPointextendsPoint{}
上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。下面,我们在ColorPoint内部加上代码。
classColorPointextendsPoint{ constructor(x,y,color){ super(x,y);//调用父类的constructor(x,y) this.color=color; } toString(){ returnthis.color+''+super.toString();//调用父类的toString() } }
上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
classPoint{/*...*/} classColorPointextendsPoint{ constructor(){} } letcp=newColorPoint();//ReferenceError
上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
constructor(...args){ super(...args); }
另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。
classPoint{ constructor(x,y){ this.x=x; this.y=y; } } classColorPointextendsPoint{ constructor(x,y,color){ this.color=color;//ReferenceError super(x,y); this.color=color;//正确 } }
上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。
下面是生成子类实例的代码。
letcp=newColorPoint(25,8,'green'); cpinstanceofColorPoint//true cpinstanceofPoint//true
上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与ES5的行为完全一致。
2.类的prototype属性和__proto__属性
大多数浏览器的ES5实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
classA{} classBextendsA{} B.__proto__===A//true B.prototype.__proto__===A.prototype//true
上面代码中,子类B的__proto__属性指向父类A,子类B的prototype属性的__proto__属性指向父类A的prototype属性。
这样的结果是因为,类的继承是按照下面的模式实现的。
classA{} classB{} //B的实例继承A的实例 Object.setPrototypeOf(B.prototype,A.prototype); //B继承A的静态属性 Object.setPrototypeOf(B,A);
《对象的扩展》一章给出过Object.setPrototypeOf方法的实现。
Object.setPrototypeOf=function(obj,proto){ obj.__proto__=proto; returnobj; }
因此,就得到了上面的结果。
Object.setPrototypeOf(B.prototype,A.prototype); //等同于 B.prototype.__proto__=A.prototype; Object.setPrototypeOf(B,A); //等同于 B.__proto__=A;
这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。
Object.create(A.prototype); //等同于 B.prototype.__proto__=A.prototype;
3.Extends的继承目标
extends关键字后面可以跟多种类型的值。
classBextendsA{}
上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。
下面,讨论三种特殊情况。
第一种特殊情况,子类继承Object类。
classAextendsObject{} A.__proto__===Object//true A.prototype.__proto__===Object.prototype//true
这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。
第二种特殊情况,不存在任何继承。
classA{} A.__proto__===Function.prototype//true A.prototype.__proto__===Object.prototype//true
这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Funciton.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。
第三种特殊情况,子类继承null。
classAextendsnull{} A.__proto__===Function.prototype//true A.prototype.__proto__===undefined//true
这种情况与第二种情况非常像。A也是一个普通函数,所以直接继承Funciton.prototype。但是,A调用后返回的对象不继承任何方法,所以它的__proto__指向Function.prototype,即实质上执行了下面的代码。
classCextendsnull{ constructor(){ returnObject.create(null); } }
4.Object.getPrototypeOf()
Object.getPrototypeOf方法可以用来从子类上获取父类。
Object.getPrototypeOf(ColorPoint)===Point //true
因此,可以使用这个方法判断,一个类是否继承了另一个类。
5.super关键字
super这个关键字,有两种用法,含义不同。
(1)作为函数调用时(即super(...args)),super代表父类的构造函数。
(2)作为对象调用时(即super.prop或super.method()),super代表父类。注意,此时super即可以引用父类实例的属性和方法,也可以引用父类的静态方法。
classBextendsA{ getm(){ returnthis._p*super._p; } setm(){ thrownewError('该属性只读'); } }
上面代码中,子类通过super关键字,调用父类实例的_p属性。
由于,对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。
varobj={ toString(){ return"MyObject:"+super.toString(); } }; obj.toString();//MyObject:[objectObject]
6.实例的__proto__属性
子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。
varp1=newPoint(2,3); varp2=newColorPoint(2,3,'red'); p2.__proto__===p1.__proto__//false p2.__proto__.__proto__===p1.__proto__//true
上面代码中,ColorPoint继承了Point,导致前者原型的原型是后者的原型。
因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。
p2.__proto__.__proto__.printName=function(){ console.log('Ha'); }; p1.printName()//"Ha"
上面代码在ColorPoint的实例p2上向Point类添加方法,结果影响到了Point的实例p1。
原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些。
Boolean() Number() String() Array() Date() Function() RegExp() Error() Object()
以前,这些原生构造函数是无法继承的,比如,不能自己定义一个Array的子类。
functionMyArray(){ Array.apply(this,arguments); } MyArray.prototype=Object.create(Array.prototype,{ constructor:{ value:MyArray, writable:true, configurable:true, enumerable:true } });
上面代码定义了一个继承Array的MyArray类。但是,这个类的行为与Array完全不一致。
varcolors=newMyArray(); colors[0]="red"; colors.length//0 colors.length=0; colors[0]//"red"
之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。
ES5是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。
下面的例子中,我们想让一个普通对象继承Error对象。
vare={}; Object.getOwnPropertyNames(Error.call(e)) //['stack'] Object.getOwnPropertyNames(e) //[]
上面代码中,我们想通过Error.call(e)这种写法,让普通对象e具有Error对象的实例属性。但是,Error.call()完全忽略传入的第一个参数,而是返回一个新对象,e本身没有任何变化。这证明了Error.call(e)这种写法,无法继承原生构造函数。
ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。
classMyArrayextendsArray{ constructor(...args){ super(...args); } } vararr=newMyArray(); arr[0]=12; arr.length//1 arr.length=0; arr[0]//undefined
上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。这意味着,ES6可以自定义原生数据结构(比如Array、String等)的子类,这是ES5无法做到的。
上面这个例子也说明,extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。下面就是定义了一个带版本功能的数组。
classVersionedArrayextendsArray{ constructor(){ super(); this.history=[ [] ]; } commit(){ this.history.push(this.slice()); } revert(){ this.splice(0,this.length,...this.history[this.history.length-1]); } } varx=newVersionedArray(); x.push(1); x.push(2); x//[1,2] x.history//[[]] x.commit(); x.history//[[],[1,2]] x.push(3); x//[1,2,3] x.revert(); x//[1,2]
上面代码中,VersionedArray结构会通过commit方法,将自己的当前状态存入history属性,然后通过revert方法,可以撤销当前版本,回到上一个版本。除此之外,VersionedArray依然是一个数组,所有原生的数组方法都可以在它上面调用。
下面是一个自定义Error子类的例子。
classExtendableErrorextendsError{ constructor(message){ super(); this.message=message; this.stack=(newError()).stack; this.name=this.constructor.name; } } classMyErrorextendsExtendableError{ constructor(m){ super(m); } } varmyerror=newMyError('ll'); myerror.message//"ll" myerrorinstanceofError//true myerror.name//"MyError" myerror.stack //Error //atMyError.ExtendableError //...
注意,继承Object的子类,有一个行为差异。
classNewObjextendsObject{ constructor(){ super(...arguments); } } varo=newNewObj({ attr:true }); console.log(o.attr===true);//false
上面代码中,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为ES6改变了Object构造函数的行为,一旦发现Object方法不是通过newObject()这种形式调用,ES6规定Object构造函数会忽略参数。
更多相关内容可查看本站专题:《ECMAScript6(ES6)入门教程》、《JavaScript数组操作技巧总结》、《JavaScript字符与字符串操作技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript错误与调试技巧总结》及《javascript面向对象入门教程》
希望本文所述对大家基于ECMAScript的程序设计有所帮助。