ES6 類(Class)的繼承(extends)和自定義存(setter)取值(getter)詳解,好文章,可以作爲高級程序員面試題

ES6 類(Class)的繼承(extends)和自定義存(setter)取值(getter)詳解
ES6的Class之間可以通過extends關鍵字實現繼承,這比ES5的通過修改原型鏈實現繼承,要簡單很多,這也是平常大多數面嚮對象語言的方式。
1.類的super方法
子類必須在constructor方法中調用super方法,否則新建實例時會報錯。如果子類在constructor方法中使用了this初始化實例屬性,調用super方法必須放到this初始化實例屬性的前面。這是因爲子類沒有自己的this對象,而是繼承父類的this對象,然後對其進行加工。如果不調用super方法,子類就得不到this對象。
//ExtendStu.js
//正確   構造
constructor(name, age, height) {
    super(name,age);
    this.height = height;
}
 
//錯誤  構造
constructor(name, age, height) {
    this.height = height;
    super(name,age);
    //SyntaxError: 'this' is not allowed before super()
}
ES5的繼承,實質是先創造子類的實例對象this,然後再將父類的方法添加到this上面(Parent.apply(this))。ES6的繼承機制完全不同,實質是先創造父類的實例對象this(所以必須先調用super方法),然後再用子類的構造函數修改this。如果子類沒有定義constructor方法,這個方法會被默認添加。
(1)super作爲函數時,指向父類的構造函數。super()只能用在子類的構造函數之中,用在其他地方就會報錯。
注意:super雖然代表了父類ExtendStuParent的構造函數,但是返回的是子類ExtendStu的實例,即super內部的this指的是ExtendStu,因此super()在這裏相當於ExtendStuParent.prototype.constructor.call(this)。
(2)super作爲對象時,指向父類的原型對象。
toString(){
    return "name:"+ super.getName() + " age:"+this.age + " height:"+this.height;
}
上面代碼中,子類ExtendStu當中的super.getName(),就是將super當作一個對象使用。這時,super指向ExtendStuParent.prototype,所以super.getName()就相當於ExtendStuParent.prototype.getName()。
由於super指向父類的原型對象,所以定義在父類實例上的屬性,是無法通過super調用的。
toString(){
    console.log(super.age);//undefined
    return "name:"+ super.getName() + " age:"+this.age + " height:"+this.height;
}
age是父類的實例屬性,子類通過是super不能調用
//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定義在父類的原型對象上,所以width可以通過super取到。

ES6 規定,通過super調用父類的方法時,super會綁定子類的this。由於綁定子類的this,所以如果通過super對某個屬性賦值,這時super就是this,賦值的屬性會變成子類實例的屬性。
// 構造
constructor(name, age, height) {
    super(name,age);
    this.height = height;
    //父類和子類都有實例屬性name,但是在toString方法調用getName的時候,
    //返回的是子類的的數據
    this.name = 'LiSi 子類';
 
    //
    this.color = 'black';
    super.color = 'white';
    console.log(super.color);//undefined
    console.log(this.color);//black
}
最後,由於對象總是繼承其他對象的,所以可以在任意一個對象中,使用super關鍵字。
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
說明;ExtendStu繼承ExtendStuParent之後,ExtendStu創建的對象同時是ExtendStu和ExtendStuParent兩個類的實例。

2.類的prototype屬性和__proto__屬性
Class作爲構造函數的語法糖,同時有prototype屬性和__proto__屬性,因此同時存在兩條繼承鏈。
(1)子類的__proto__屬性,表示構造函數的繼承,總是指向父類。
(2)子類prototype屬性的__proto__屬性,表示方法的繼承,總是指向父類的prototype屬性。
console.log(ExtendStu.__proto__ === ExtendStuParent);//true
console.log(ExtendStu.prototype.__proto__ === ExtendStuParent.prototype);//true
//繼承的原理
// ExtendStu的實例繼承ExtendStuParent的實例
Object.setPrototypeOf(ExtendStu.prototype, ExtendStuParent.prototype);
// ExtendStu的實例繼承ExtendStuParent的靜態屬性
Object.setPrototypeOf(ExtendStu, ExtendStuParent);
作爲一個對象,子類(ExtendStu)的原型(__proto__屬性)是父類(ExtendStuParent);作爲一個構造函數,子類(ExtendStu)的原型(prototype屬性)是父類的實例。
console.log(ExtendStuParent.__proto__ === Function.prototype);//true
console.log(ExtendStuParent.prototype.__proto__ === Object.prototype);//true
ExtendStuParent作爲一個基類(即不存在任何繼承),就是一個普通函數,所以直接繼承Funciton.prototype。但是,ExtendStuParent調用後返回一個空對象(即Object實例),所以ExtendStuParent.prototype.__proto__指向構造函數(Object)的prototype屬性。
console.log(Object.getPrototypeOf(ExtendStu) === ExtendStuParent  );//true
Object.getPrototypeOf方法可以用來從子類上獲取父類。因此,可以使用這個方法判斷,一個類是否繼承了另一個類。

3.實例的__proto__屬性
子類實例的__proto__屬性的__proto__屬性,指向父類實例的__proto__屬性。也就是說,子類的原型的原型,是父類的原型。
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
因此,通過子類實例的__proto__.__proto__屬性,可以修改父類實例的行爲。
b.__proto__.__proto__.getName = ()=>{
    console.log('給父類添加一個函數');
};
b.getName();//給父類添加一個函數
a.getName();//給父類添加一個函數
說明:使用子類的實例給父類添加getName方法,由於B繼承了A,所以B和A中都添加了getName方法。

4.原生構造函數的繼承
原生構造函數是指語言內置的構造函數,通常用來生成數據結構。ECMAScript的原生構造函數大致有下面這些。
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
以前,這些原生構造函數是無法繼承的,比如,不能自己定義一個Array的子類。
function MyArray() {
  Array.apply(this, arguments);
}
 
MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});
上面代碼定義了一個繼承Array的MyArray類。但是,這個類的行爲與Array完全不一致。
var colors = new MyArray();
colors[0] = "red";
colors.length  // 0
 
colors.length = 0;
colors[0]  // "red"
之所以會發生這種情況,是因爲子類無法獲得原生構造函數的內部屬性,通過Array.apply()或者分配給原型對象都不行。原生構造函數會忽略apply方法傳入的this,也就是說,原生構造函數的this無法綁定,導致拿不到內部屬性。ES5是先新建子類的實例對象this,再將父類的屬性添加到子類上,由於父類的內部屬性無法獲取,導致無法繼承原生的構造函數。比如,Array構造函數有一個內部屬性[[DefineOwnProperty]],用來定義新屬性時,更新length屬性,這個內部屬性無法在子類獲取,導致子類的length屬性行爲不正常。
ES6允許繼承原生構造函數定義子類,因爲ES6是先新建父類的實例對象this,然後再用子類的構造函數修飾this,使得父類的所有行爲都可以繼承。下面是一個繼承Array的例子。
//ExtendsArray.js
class ExtendsArray extends Array{}
 
let extendsArray = new ExtendsArray();
console.log("=====ExtendsArray=====");
extendsArray[0] = '數據1';
console.log(extendsArray.length);
上面代碼定義了一個ExtendsArray類,繼承了Array構造函數,因此就可以從ExtendsArray生成數組的實例。這意味着,ES6可以自定義原生數據結構(比如Array、String等)的子類,這是ES5無法做到的。上面這個例子也說明,extends關鍵字不僅可以用來繼承類,還可以用來繼承原生的構造函數。因此可以在原生數據結構的基礎上,定義自己的數據結構。

5.Class的取值函數(getter)和存值函數(setter)
取值函數(getter)和存值函數(setter)可以自定義賦值和取值行爲。當一個屬性只有getter沒有setter的時候,我們是無法進行賦值操作的,第一次初始化也不行。如果把變量定義爲私有的(定義在類的外面),就可以只使用getter不使用setter。
//GetSet.js
let data = {};
 
class GetSet{
 
    // 構造
    constructor() {
        this.width = 10;
        this.height = 20;
    }
 
    get width(){
        console.log('獲取寬度');
        return this._width;
    }
 
    set width(width){
        console.log('設置寬度');
        this._width = width;
    }
    //當一個屬性只有getter沒有setter的時候,我們是無法進行賦值操作的,第一次初始化也不行。
    //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);
 
//存值函數和取值函數是設置在屬性的descriptor對象上的。
var descriptor = Object.getOwnPropertyDescriptor(
    GetSet.prototype, "width");
console.log("get" in descriptor);//true
console.log("set" in descriptor);//true
如果把上面的get和set改成以下形式,不加下劃線報Uncaught RangeError: Maximum call stack size exceeded錯誤。這是因爲,在構造函數中執行this.name=name的時候,就會去調用set name,在set name方法中,我們又執行this.name = name,進行無限遞歸,最後導致棧溢出(RangeError)。
get width(){
    console.log('獲取寬度');
    return this.width;
}
 
set width(width){
    console.log('設置寬度');
    this.width = width;
}
說明:以上width的getter和setter只是給width自定義存取值行爲,開發者還是可以通過_width繞過getter和setter獲取width的值。

6.new.target屬性
new是從構造函數生成實例的命令。ES6爲new命令引入了一個new.target屬性,(在構造函數中)返回new命令作用於的那個構造函數。如果構造函數不是通過new命令調用的,new.target會返回undefined,因此這個屬性可以用來確定構造函數是怎麼調用的。
class Targettu{
    // 構造
    constructor() {
        if(new.target !== undefined){
            this.height = 10;
        }else{
            throw new Error('必須通過new命令創建對象');
        }
    }
}
//或者(判斷是不是通過new命令創建的對象下面這種方式也是可以的)
class Targettu{
    // 構造
    constructor() {
        if(new.target === Targettu){
            this.height = 10;
        }else{
            throw new Error('必須通過new命令創建對象');
        }
    }
}
 
let targettu = new Targettu();
//Uncaught Error: 必須通過new命令創建對象
//let targettuOne = Targettu.call(targettu);
需要注意的是,子類繼承父類時,new.target會返回子類。通過這個原理,可以寫出不能獨立使用、必須繼承後才能使用的類。
// 構造
constructor(name,age) {
    //用於寫不能實例化的父類
    if (new.target === ExtendStuParent) {
        throw new Error('ExtendStuParent不能實例化,只能繼承使用。');
    }
    this.name = name;
    this.age = age;
}

7.Mixin模式的實現
Mixin模式指的是,將多個類的接口“混入”(mix in)另一個類。它在ES6的實現如下。
//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函數,可以將多個對象合成爲一個類。使用的時候,只要繼承這個類即可。
class MixinAll extends MixinStu(B,Serializable){
 
    // 構造
    constructor(x,y,z) {
        super(x,y);
        this.z = z;
    }
 
}
更多的資料:點擊打開鏈接   點擊打開鏈接

Class的繼承(extends)和自定義存值(setter)取值(getter)的整個案例:
//ExtendStuParent.js
const ExtendStuParent = class ExtendStuParent{
 
    name;//定義name
    age;//定義age
 
    // 構造
    constructor(name,age) {
        //用於寫不能實例化的父類
        if (new.target === ExtendStuParent) {
            throw new Error('ExtendStuParent不能實例化,只能繼承使用。');
        }
        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{
 
    // 構造
    constructor(name, age, height) {
        super(name,age);
        this.height = height;
        //父類和子類都有實例屬性name,但是在toString方法調用getName的時候,
        //返回的是子類的的數據
        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;
    }
 
}

————————————————
版權聲明:本文爲CSDN博主「預見才能遇見」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/pcaxb/article/details/53784309

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章