JavaScript基礎: 類與繼承

前言

  首先歡迎你們關注個人Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵。javascript

  許久已經沒有寫東西了,由於雜七雜八的緣由最近一直沒有抽出時間來把寫做堅持下來,感受和跑步同樣,一旦鬆懈下來就很難再次撿起來。最近一直想從新靜下心來寫點什麼,選題又成爲一個讓我頭疼的問題,最近工做中偶爾會對JavaScript繼承的問題有時候會感受恍惚,意識到不少知識即便是很基礎,也須要常常的回顧和練習,不然即便再熟悉的東西也會常常讓你感到陌生,因此就選擇這麼一篇很是基礎的文章做爲今年的開始吧。   前端

  JavaScript不像Java語言自己就具備類的概念,JavaScript做爲一門基於原型(ProtoType)的語言,(推薦我以前寫的我所認識的JavaScript做用域鏈和原型鏈),時至今日,仍然有不少人不建議在JavaScript中大量使用面對對象的特性。但就目前而言,不少前端框架,例如React都有基於類的概念。首先明確一點,類存在的目的就是爲了生成對象,而在JavaScript生成對象的過程並不不像其餘語言那麼繁瑣,咱們能夠經過對象字面量語法輕鬆的建立一個對象:java

var person = {
    name: "MrErHu", 
    sayName: function(){
        alert(this.name);
    }
};
複製代碼

  一切看起來是這樣的完美,可是當咱們但願建立無數個類似的對象時,咱們就會發現對象字面量的方法就不能知足了,固然聰明的你確定會想到採用工廠模式去建立一系列的對象:   git

function createObject(name){
    return {
        "name": name,
        "sayName": function(){
            alert(this.name);
        }
    }
}
複製代碼

  可是這樣方式有一個顯著的問題,咱們經過工廠模式生成的各個對象之間並無聯繫,無法識別對象的類型,這時候就出現了構造函數。在JavaScript中構造函數和普通的函數沒有任何的區別,僅僅是構造函數是經過new操做符調用的。   github

function Person(name, age, job){
    this.name = name;
    this.sayName = function(){
        alert(this.name);
    };    
}

var obj = new Person();
obj.sayName();
複製代碼

  咱們知道new操做符會作如下四個步驟的操做:   數組

  1. 建立一個全新的對象
  2. 新對象內部屬性[[Prototype]](非正式屬性__proto__)鏈接到構造函數的原型
  3. 構造函數的this會綁定新的對象
  4. 若是函數沒有返回其餘對象,那麼new表達式中的函數調用會自動返回這個新對象

  這樣咱們經過構造函數的方式生成的對象就能夠進行類型判斷。可是單純的構造函數模式會存在一個問題,就是每一個對象的方法都是相互獨立的,而函數本質上就是一種對象,所以就會形成大量的內存浪費。回顧new操做符的第三個步驟,咱們新生成對象的內部屬性[[Prototype]]會鏈接到構造函數的原型上,所以利用這個特性,咱們能夠混合構造函數模式原型模式,解決上面的問題。前端框架

function Person(name, age, job){
    this.name = name;
}

Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}

var obj = new Person();
obj.sayName();
複製代碼

  咱們經過將sayName函數放到構造函數的原型中,這樣生成的對象在使用sayName函數經過查找原型鏈就能夠找到對應的方法,全部對象共用一個方法就解決了上述問題,即便你可能認爲原型鏈查找可能會耽誤一點時間,實際上對於如今的JavaScript引擎這種問題能夠忽略。對於構造函數的原型修改,處理上述的方式,可能還存在:   app

Person.prototype.sayName = function(){
    alert(this.name);
}
複製代碼

  咱們知道函數的原型中的constructor屬性是執行函數自己,若是你是將原來的原型替換成新的對象而且constructor對你又比較重要記得手動添加,所以第一種並不許確,由於constructor是不可枚舉的,所以更準確的寫法應該是:框架

Object.defineProperty(Person, "constructor", {
    configurable: false,
    enumerable: false,
    writable: true,
    value: Person
});
複製代碼

  到如今爲止,咱們會以爲在JavaScript中建立個類也太麻煩了,其實遠遠不止如此,好比咱們建立的類可能會被直接調用,形成全局環境的污染,好比:   函數

Person('MrErHu');
console.log(window.name); //MrErHu
複製代碼

  不過咱們迎來了ES6的時代,事情正在其變化,ES6爲咱們在JavaScript中實現了類的概念,上面的的代碼均可以用簡介的類(class)實現。   

class Person {
    constructor(name){
        this.name = name;
    }
    
    sayName(){
        alert(this.name);
    }
}
複製代碼

  經過上面咱們就定義了一個類,使用的時候同以前同樣:   

let person = new Person('MrErHu');
person.sayName(); //MrErHu
複製代碼

  咱們能夠看到,類中的constructor函數負擔起了以前的構造函數的功能,類中的實例屬性均可以在這裏初始化。類的方法sayName至關於以前咱們定義在構造函數的原型上。其實在ES6中類僅僅只是函數的語法糖:   

typeof Person  //"function"
複製代碼

  相比於上面本身建立的類方式,ES6中的類有幾個方面是與咱們自定義的類不相同的。首先類是不存在變量提高的,所以不能先使用後定義:   

let person = new Person('MrErHu')
class Person { //...... } 
複製代碼

  上面的使用方式是錯誤的。所以類更像一個函數表達式。

  其次,類聲明中的全部代碼都是自動運行在嚴格模式下,而且不能讓類脫離嚴格模式。至關於類聲明中的全部代碼都運行在"use strict"中。

  再者,類中的全部方法都是都是不可枚舉的。

  最後,類是不能直接調用的,必須經過new操做符調用。其實對於函數有內部屬性[[Constructor]][[Call]],固然這兩個方法咱們在外部是無法訪問到的,僅存在於JavaScript引擎。當咱們直接調用函數時,其實就是調用了內部屬性[[Call]],所作的就是直接執行了函數體。當咱們經過new操做符調用時,其實就是調用了內部屬性[[Constructor]],所作的就是建立新的實例對象,並在實例對象上執行函數(綁定this),最後返回新的實例對象。由於類中不含有內部屬性[[Call]],所以是無法直接調用的。順即可以提一句ES6中的元屬性 new.target     

  所謂的元屬性指的就是非對象的屬性,能夠提供給咱們一些補充信息。new.target就是其中一個元屬性,當調用的是[[Constructor]]屬性時,new.target就是new操做符的目標,若是調用的是[[Call]]屬性,new.target就是undefined。其實這個屬性是很是有用的,好比咱們能夠定義一個僅能夠經過new操做符調用的函數:

function Person(){
    if(new.target === undefined){
        throw('該函數必須經過new操做符調用');
    }
}
複製代碼

  或者咱們能夠用JavaScript建立一個相似於C++中的虛函數的函數:

class Person {
  constructor() {
    if (new.target === Person) {
      throw new Error('本類不能實例化');
    }
  }
}
複製代碼

  

繼承

  在沒有ES6的時代,想要實現繼承是一個不小的工做。一方面咱們要在派生類中建立父類的屬性,另外一方面咱們須要繼承父類的方法,例以下面的實現方法:   

function Rectangle(width, height){
  this.width = width;
  this.height = height;
}

Rectangle.prototype.getArea = function(){
  return this.width * this.height;
}

function Square(length){
  Rectangle.call(this, length, length);
}

Square.prototype = Object.create(Rectangle.prototype, {
  constructor: {
    value: Square,
    enumerable: false,
    writable: false,
    configurable: false
  }
});

var square = new Square(3);

console.log(square.getArea());
console.log(square instanceof Square);
console.log(square instanceof Rectangle);
複製代碼

  首先子類Square爲了建立父類Rectangle的屬性,咱們在Square函數中以Rectangle.call(this, length, length)的方式進行了調用,其目的就是在子類中建立父類的屬性,爲了繼承父類的方法,咱們給Square賦值了新的原型。除了經過Object.create方式,你應該也見過如下方式:   

Square.prototype = new Rectangle();
Object.defineProperty(Square.prototype, "constructor", {
    value: Square,
    enumerable: false,
    writable: false,
    configurable: false
});
複製代碼

  Object.create是ES5新增的方法,用於建立一個新對象。被建立的對象會繼承另外一個對象的原型,在建立新對象時還能夠指定一些屬性。Object.create指定屬性的方式與Object.defineProperty相同,都是採用屬性描述符的方式。所以能夠看出,經過Object.createnew方式實現的繼承其本質上並無什麼區別。      可是ES6能夠大大簡化繼承的步驟:

class Rectangle{
    constructor(width, height){
        this.width = width;
        this.height = height;
    }
    
    getArea(){
        return this.width * this.height;
    }
}

class Square extends Rectangle{
    construct(length){
        super(length, length);
    }
}
複製代碼

  咱們能夠看到經過ES6的方式實現類的繼承是很是容易的。Square的構造函數中調用super其目的就是調用父類的構造函數。固然調用super函數並非必須的,若是你默認缺省了構造函數,則會自動調用super函數,並傳入全部的參數。      不只如此,ES6的類繼承賦予了更多新的特性,首先extends能夠繼承任何類型的表達式,只要該表達式最終返回的是一個可繼承的函數(也就是講extends能夠繼承具備[[Constructor]]的內部屬性的函數,好比null和生成器函數、箭頭函數都不具備該屬性,所以不能夠被繼承)。好比:

class A{}
class B{}

function getParentClass(type){
    if(//...){
        return A;
    }
    if(//...){
        return B;
    }
}

class C extends getParentClass(//...){
}
複製代碼

  能夠看到咱們經過上面的代碼實現了動態繼承,能夠根據不一樣的判斷條件繼承不一樣的類。      ES6的繼承與ES5實現的類繼承,還有一點不一樣。ES5是先建立子類的實例,而後在子類實例的基礎上建立父類的屬性。而ES6正好是相反的,是先建立父類的實例,而後在父類實例的基礎上擴展子類屬性。利用這個屬性咱們能夠作到一些ES5沒法實現的功能:繼承原生對象。

function MyArray() {
  Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});

var colors = new MyArray();
colors[0] = "red";
colors.length  // 0

colors.length = 0;
colors[0]  // "red"
複製代碼

  能夠看到,繼承自原生對象ArrayMyArray的實例中的length並不能如同原生Array類的實例 同樣能夠動態反應數組中元素數量或者經過改變length屬性從而改變數組中的數據。究其緣由就是由於傳統方式實現的數組繼承是先建立子類,而後在子類基礎上擴展父類的屬性和方法,因此並無繼承的相關方法,但ES6卻能夠輕鬆實現這一點:

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined
複製代碼

  咱們能夠看見經過extends實現的MyArray類建立的數組就能夠同原生數組同樣,使用length屬性反應數組變化和改變數組元素。不只如此,在ES6中,咱們可使用Symbol.species屬性使得當咱們繼承原生對象時,改變繼承自原生對象的方法的返回實例類型。例如,Array.prototype.slice原本返回的是Array類型的實例,經過設置Symbol.species屬性,咱們可讓其返回自定義的對象類型:   

class MyArray extends Array {
  static get [Symbol.species](){
    return MyArray;
  }
    
  constructor(...args) {
    super(...args);
  }
}

let items = new MyArray(1,2,3,4);
subitems = items.slice(1,3);

subitems instanceof MyArray; // true
複製代碼

  最後須要注意的一點,extends實現的繼承方式能夠繼承父類的靜態成員函數,例如:   

class Rectangle{
    // ......
    static create(width, height){
        return new Rectangle(width, height);
    }
}

class Square extends Rectangle{
    //......
}

let rect = Square.create(3,4);
rect instanceof Square; // true
複製代碼
相關文章
相關標籤/搜索