ES6 類(Class)的繼承(extends)和自定義存(setter)取值(getter)詳解
ES6的Class之間可以通過(guò)extends關(guān)鍵字實(shí)現(xiàn)繼承,這比ES5的通過(guò)修改原型鏈實(shí)現(xiàn)繼承,要簡(jiǎn)單很多,這也是平常大多數(shù)面向?qū)ο笳Z(yǔ)言的方式。
1.類的super方法
子類必須在constructor方法中調(diào)用super方法,否則新建實(shí)例時(shí)會(huì)報(bào)錯(cuò)。如果子類在constructor方法中使用了this初始化實(shí)例屬性,調(diào)用super方法必須放到this初始化實(shí)例屬性的前面。這是因?yàn)樽宇悰](méi)有自己的this對(duì)象,而是繼承父類的this對(duì)象,然后對(duì)其進(jìn)行加工。如果不調(diào)用super方法,子類就得不到this對(duì)象。
- //ExtendStu.js
- //正確 構(gòu)造
- constructor(name, age, height) {
- super(name,age);
- this.height = height;
- }
-
- //錯(cuò)誤 構(gòu)造
- constructor(name, age, height) {
- this.height = height;
- super(name,age);
- //SyntaxError: 'this' is not allowed before super()
- }
ES5的繼承,實(shí)質(zhì)是先創(chuàng)造子類的實(shí)例對(duì)象this,然后再將父類的方法添加到this上面(Parent.apply(this))。ES6的繼承機(jī)制完全不同,實(shí)質(zhì)是先創(chuàng)造父類的實(shí)例對(duì)象this(所以必須先調(diào)用super方法),然后再用子類的構(gòu)造函數(shù)修改this。如果子類沒(méi)有定義constructor方法,這個(gè)方法會(huì)被默認(rèn)添加。
(1)super作為函數(shù)時(shí),指向父類的構(gòu)造函數(shù)。super()只能用在子類的構(gòu)造函數(shù)之中,用在其他地方就會(huì)報(bào)錯(cuò)。
注意:super雖然代表了父類ExtendStuParent的構(gòu)造函數(shù),但是返回的是子類ExtendStu的實(shí)例,即super內(nèi)部的this指的是ExtendStu,因此super()在這里相當(dāng)于ExtendStuParent.prototype.constructor.call(this)。
(2)super作為對(duì)象時(shí),指向父類的原型對(duì)象。
- toString(){
- return "name:"+ super.getName() + " age:"+this.age + " height:"+this.height;
- }
上面代碼中,子類ExtendStu當(dāng)中的super.getName(),就是將super當(dāng)作一個(gè)對(duì)象使用。這時(shí),super指向ExtendStuParent.prototype,所以super.getName()就相當(dāng)于ExtendStuParent.prototype.getName()。
由于super指向父類的原型對(duì)象,所以定義在父類實(shí)例上的屬性,是無(wú)法通過(guò)super調(diào)用的。
- toString(){
- console.log(super.age);//undefined
- return "name:"+ super.getName() + " age:"+this.age + " height:"+this.height;
- }
age是父類的實(shí)例屬性,子類通過(guò)是super不能調(diào)用
- //ExtendStuParent.js
- ExtendStuParent.prototype.width = '30cm';
- //ExtendStu.js
- toString(){
- console.log(super.age);//undefined
- console.log(super.width);//30cm
- return "name:"+ super.getName() + " age:"+this.age + " height:"+this.height;
- }
width定義在父類的原型對(duì)象上,所以width可以通過(guò)super取到。
ES6 規(guī)定,通過(guò)super調(diào)用父類的方法時(shí),super會(huì)綁定子類的this。由于綁定子類的this,所以如果通過(guò)super對(duì)某個(gè)屬性賦值,這時(shí)super就是this,賦值的屬性會(huì)變成子類實(shí)例的屬性。
- // 構(gòu)造
- constructor(name, age, height) {
- super(name,age);
- this.height = height;
- //父類和子類都有實(shí)例屬性name,但是在toString方法調(diào)用getName的時(shí)候,
- //返回的是子類的的數(shù)據(jù)
- this.name = 'LiSi 子類';
-
- //
- this.color = 'black';
- super.color = 'white';
- console.log(super.color);//undefined
- console.log(this.color);//black
- }
最后,由于對(duì)象總是繼承其他對(duì)象的,所以可以在任意一個(gè)對(duì)象中,使用super關(guān)鍵字。
- import ExtendStuParent from './ExtendStuParent';//父類
- import ExtendStu from './ExtendStu';//子類
- let extendStu = new ExtendStu('LiSi',12,'170cm');
- console.log(extendStu.toString());
- console.log(extendStu instanceof ExtendStuParent);//true
- console.log(extendStu instanceof ExtendStu);//true
說(shuō)明;ExtendStu繼承ExtendStuParent之后,ExtendStu創(chuàng)建的對(duì)象同時(shí)是ExtendStu和ExtendStuParent兩個(gè)類的實(shí)例。
2.類的prototype屬性和__proto__屬性
Class作為構(gòu)造函數(shù)的語(yǔ)法糖,同時(shí)有prototype屬性和__proto__屬性,因此同時(shí)存在兩條繼承鏈。
(1)子類的__proto__屬性,表示構(gòu)造函數(shù)的繼承,總是指向父類。
(2)子類prototype屬性的__proto__屬性,表示方法的繼承,總是指向父類的prototype屬性。
- console.log(ExtendStu.__proto__ === ExtendStuParent);//true
- console.log(ExtendStu.prototype.__proto__ === ExtendStuParent.prototype);//true
- //繼承的原理
- // ExtendStu的實(shí)例繼承ExtendStuParent的實(shí)例
- Object.setPrototypeOf(ExtendStu.prototype, ExtendStuParent.prototype);
- // ExtendStu的實(shí)例繼承ExtendStuParent的靜態(tài)屬性
- Object.setPrototypeOf(ExtendStu, ExtendStuParent);
作為一個(gè)對(duì)象,子類(ExtendStu)的原型(__proto__屬性)是父類(ExtendStuParent);作為一個(gè)構(gòu)造函數(shù),子類(ExtendStu)的原型(prototype屬性)是父類的實(shí)例。
- console.log(ExtendStuParent.__proto__ === Function.prototype);//true
- console.log(ExtendStuParent.prototype.__proto__ === Object.prototype);//true
ExtendStuParent作為一個(gè)基類(即不存在任何繼承),就是一個(gè)普通函數(shù),所以直接繼承Funciton.prototype。但是,ExtendStuParent調(diào)用后返回一個(gè)空對(duì)象(即Object實(shí)例),所以ExtendStuParent.prototype.__proto__指向構(gòu)造函數(shù)(Object)的prototype屬性。
- console.log(Object.getPrototypeOf(ExtendStu) === ExtendStuParent );//true
Object.getPrototypeOf方法可以用來(lái)從子類上獲取父類。因此,可以使用這個(gè)方法判斷,一個(gè)類是否繼承了另一個(gè)類。
3.實(shí)例的__proto__屬性
子類實(shí)例的__proto__屬性的__proto__屬性,指向父類實(shí)例的__proto__屬性。也就是說(shuō),子類的原型的原型,是父類的原型。
- class A{}
- class B extends A{}
- let a = new A();
- let b = new B();
- console.log(b.__proto__ === a.__proto__);//false
- console.log(b.__proto__.__proto__ === a.__proto__);//true
因此,通過(guò)子類實(shí)例的__proto__.__proto__屬性,可以修改父類實(shí)例的行為。
- b.__proto__.__proto__.getName = ()=>{
- console.log('給父類添加一個(gè)函數(shù)');
- };
- b.getName();//給父類添加一個(gè)函數(shù)
- a.getName();//給父類添加一個(gè)函數(shù)
說(shuō)明:使用子類的實(shí)例給父類添加getName方法,由于B繼承了A,所以B和A中都添加了getName方法。
4.原生構(gòu)造函數(shù)的繼承
原生構(gòu)造函數(shù)是指語(yǔ)言內(nèi)置的構(gòu)造函數(shù),通常用來(lái)生成數(shù)據(jù)結(jié)構(gòu)。ECMAScript的原生構(gòu)造函數(shù)大致有下面這些。
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
以前,這些原生構(gòu)造函數(shù)是無(wú)法繼承的,比如,不能自己定義一個(gè)Array的子類。
- function MyArray() {
- Array.apply(this, arguments);
- }
-
- MyArray.prototype = Object.create(Array.prototype, {
- constructor: {
- value: MyArray,
- writable: true,
- configurable: true,
- enumerable: true
- }
- });
上面代碼定義了一個(gè)繼承Array的MyArray類。但是,這個(gè)類的行為與Array完全不一致。
- var colors = new MyArray();
- colors[0] = "red";
- colors.length // 0
-
- colors.length = 0;
- colors[0] // "red"
之所以會(huì)發(fā)生這種情況,是因?yàn)樽宇悷o(wú)法獲得原生構(gòu)造函數(shù)的內(nèi)部屬性,通過(guò)Array.apply()或者分配給原型對(duì)象都不行。原生構(gòu)造函數(shù)會(huì)忽略apply方法傳入的this,也就是說(shuō),原生構(gòu)造函數(shù)的this無(wú)法綁定,導(dǎo)致拿不到內(nèi)部屬性。ES5是先新建子類的實(shí)例對(duì)象this,再將父類的屬性添加到子類上,由于父類的內(nèi)部屬性無(wú)法獲取,導(dǎo)致無(wú)法繼承原生的構(gòu)造函數(shù)。比如,Array構(gòu)造函數(shù)有一個(gè)內(nèi)部屬性[[DefineOwnProperty]],用來(lái)定義新屬性時(shí),更新length屬性,這個(gè)內(nèi)部屬性無(wú)法在子類獲取,導(dǎo)致子類的length屬性行為不正常。
ES6允許繼承原生構(gòu)造函數(shù)定義子類,因?yàn)镋S6是先新建父類的實(shí)例對(duì)象this,然后再用子類的構(gòu)造函數(shù)修飾this,使得父類的所有行為都可以繼承。下面是一個(gè)繼承Array的例子。
- //ExtendsArray.js
- class ExtendsArray extends Array{}
-
- let extendsArray = new ExtendsArray();
- console.log("=====ExtendsArray=====");
- extendsArray[0] = '數(shù)據(jù)1';
- console.log(extendsArray.length);
上面代碼定義了一個(gè)ExtendsArray類,繼承了Array構(gòu)造函數(shù),因此就可以從ExtendsArray生成數(shù)組的實(shí)例。這意味著,ES6可以自定義原生數(shù)據(jù)結(jié)構(gòu)(比如Array、String等)的子類,這是ES5無(wú)法做到的。上面這個(gè)例子也說(shuō)明,extends關(guān)鍵字不僅可以用來(lái)繼承類,還可以用來(lái)繼承原生的構(gòu)造函數(shù)。因此可以在原生數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ)上,定義自己的數(shù)據(jù)結(jié)構(gòu)。
5.Class的取值函數(shù)(getter)和存值函數(shù)(setter)
取值函數(shù)(getter)和存值函數(shù)(setter)可以自定義賦值和取值行為。當(dāng)一個(gè)屬性只有g(shù)etter沒(méi)有setter的時(shí)候,我們是無(wú)法進(jìn)行賦值操作的,第一次初始化也不行。如果把變量定義為私有的(定義在類的外面),就可以只使用getter不使用setter。
- //GetSet.js
- let data = {};
-
- class GetSet{
-
- // 構(gòu)造
- constructor() {
- this.width = 10;
- this.height = 20;
- }
-
- get width(){
- console.log('獲取寬度');
- return this._width;
- }
-
- set width(width){
- console.log('設(shè)置寬度');
- this._width = width;
- }
- //當(dāng)一個(gè)屬性只有g(shù)etter沒(méi)有setter的時(shí)候,我們是無(wú)法進(jìn)行賦值操作的,第一次初始化也不行。
- //bundle.js:8631 Uncaught TypeError: Cannot set property width of #<GetSet> which has only a getter
-
-
- get data(){
- return data;
- }
- //如果把變量定義為私有的,就可以只使用getter不使用setter。
-
- }
-
- let getSet = new GetSet();
- console.log("=====GetSet=====");
- console.log(getSet.width);
- getSet.width = 100;
- console.log(getSet.width);
- console.log(getSet._width);
- console.log(getSet.data);
-
- //存值函數(shù)和取值函數(shù)是設(shè)置在屬性的descriptor對(duì)象上的。
- var descriptor = Object.getOwnPropertyDescriptor(
- GetSet.prototype, "width");
- console.log("get" in descriptor);//true
- console.log("set" in descriptor);//true
如果把上面的get和set改成以下形式,不加下劃線報(bào)Uncaught RangeError: Maximum call stack size exceeded錯(cuò)誤。這是因?yàn)?在構(gòu)造函數(shù)中執(zhí)行this.name=name的時(shí)候,就會(huì)去調(diào)用set name,在set name方法中,我們又執(zhí)行this.name = name,進(jìn)行無(wú)限遞歸,最后導(dǎo)致棧溢出(RangeError)。
- get width(){
- console.log('獲取寬度');
- return this.width;
- }
-
- set width(width){
- console.log('設(shè)置寬度');
- this.width = width;
- }
說(shuō)明:以上width的getter和setter只是給width自定義存取值行為,開發(fā)者還是可以通過(guò)_width繞過(guò)getter和setter獲取width的值。
6.new.target屬性
new是從構(gòu)造函數(shù)生成實(shí)例的命令。ES6為new命令引入了一個(gè)new.target屬性,(在構(gòu)造函數(shù)中)返回new命令作用于的那個(gè)構(gòu)造函數(shù)。如果構(gòu)造函數(shù)不是通過(guò)new命令調(diào)用的,new.target會(huì)返回undefined,因此這個(gè)屬性可以用來(lái)確定構(gòu)造函數(shù)是怎么調(diào)用的。
- class Targettu{
- // 構(gòu)造
- constructor() {
- if(new.target !== undefined){
- this.height = 10;
- }else{
- throw new Error('必須通過(guò)new命令創(chuàng)建對(duì)象');
- }
- }
- }
- //或者(判斷是不是通過(guò)new命令創(chuàng)建的對(duì)象下面這種方式也是可以的)
- class Targettu{
- // 構(gòu)造
- constructor() {
- if(new.target === Targettu){
- this.height = 10;
- }else{
- throw new Error('必須通過(guò)new命令創(chuàng)建對(duì)象');
- }
- }
- }
-
- let targettu = new Targettu();
- //Uncaught Error: 必須通過(guò)new命令創(chuàng)建對(duì)象
- //let targettuOne = Targettu.call(targettu);
需要注意的是,子類繼承父類時(shí),new.target會(huì)返回子類。通過(guò)這個(gè)原理,可以寫出不能獨(dú)立使用、必須繼承后才能使用的類。
- // 構(gòu)造
- constructor(name,age) {
- //用于寫不能實(shí)例化的父類
- if (new.target === ExtendStuParent) {
- throw new Error('ExtendStuParent不能實(shí)例化,只能繼承使用。');
- }
- this.name = name;
- this.age = age;
- }
7.Mixin模式的實(shí)現(xiàn)
Mixin模式指的是,將多個(gè)類的接口“混入”(mix in)另一個(gè)類。它在ES6的實(shí)現(xiàn)如下。
- //MixinStu.js
- export default function mix(...mixins) {
- class Mix {}
-
- for (let mixin of mixins) {
- copyProperties(Mix, mixin);
- copyProperties(Mix.prototype, mixin.prototype);
- }
-
- return Mix;
- }
-
- function copyProperties(target, source) {
- for (let key of Reflect.ownKeys(source)) {
- if ( key !== "constructor"
- && key !== "prototype"
- && key !== "name"
- ) {
- let desc = Object.getOwnPropertyDescriptor(source, key);
- Object.defineProperty(target, key, desc);
- }
- }
- }
上面代碼的mix函數(shù),可以將多個(gè)對(duì)象合成為一個(gè)類。使用的時(shí)候,只要繼承這個(gè)類即可。
- class MixinAll extends MixinStu(B,Serializable){
-
- // 構(gòu)造
- constructor(x,y,z) {
- super(x,y);
- this.z = z;
- }
-
- }
更多的資料:點(diǎn)擊打開鏈接 點(diǎn)擊打開鏈接
Class的繼承(extends)和自定義存值(setter)取值(getter)的整個(gè)案例:
- //ExtendStuParent.js
- const ExtendStuParent = class ExtendStuParent{
-
- name;//定義name
- age;//定義age
-
- // 構(gòu)造
- constructor(name,age) {
- //用于寫不能實(shí)例化的父類
- if (new.target === ExtendStuParent) {
- throw new Error('ExtendStuParent不能實(shí)例化,只能繼承使用。');
- }
- this.name = name;
- this.age = age;
- }
-
- getName(){
- return this.name;
- }
-
- getAge(){
- return this.age;
- }
- };
-
- ExtendStuParent.prototype.width = '30cm';
-
- export default ExtendStuParent;
-
- //ExtendStu.js
- import ExtendStuParent from './ExtendStuParent';
-
- export default class ExtendStu extends ExtendStuParent{
-
- // 構(gòu)造
- constructor(name, age, height) {
- super(name,age);
- this.height = height;
- //父類和子類都有實(shí)例屬性name,但是在toString方法調(diào)用getName的時(shí)候,
- //返回的是子類的的數(shù)據(jù)
- this.name = 'LiSi 子類';
-
- //
- this.color = 'black';
- super.color = 'white';
- console.log(super.color);//undefined
- console.log(this.color);//black
- }
-
- toString(){
- console.log(super.age);//undefined
- console.log(super.width);//30cm
- return "name:"+ super.getName() + " age:"+this.age + " height:"+this.height;
- }
-
- }
參考資料:點(diǎn)擊打開鏈接
ES6 類(Class)的繼承(extends)和自定義存(setter)取值(getter)詳解
博客地址:http://blog.csdn.net/pcaxb/article/details/53784309
下載地址:http://download.csdn.net/detail/pcaxb/9717587
|