JavaScript 中的類和繼承

原文發佈在個人博客javascript

咱們都知道 JavaScript 是一門基於原型的語言。當咱們調用一個對象自己沒有的屬性時,JavaScript 就會從對象的原型對象上去找該屬性,若是原型上也沒有該屬性,那就去找原型的原型,一直找原型鏈的末端也就是 Object.prototype 的原型 null。這種屬性查找的方式咱們稱之爲原型鏈。java

類的實現

因爲 JavaScript 自己是沒有的類的感念的。因此咱們若是要實現一個類,通常是經過構造函數來模擬類的實現:segmentfault

function Person(name,age){  //實現一個類
    this.name = name;
    this.age = age;
}
var you = new Person('you',23); //經過 new 來新建實例

首先新建一個 Person 的構造函數,爲了和通常的函數區別,咱們會使用 CamelCase 方式來命名構造函數。
而後經過 new 操做符來建立實例,new 操做符其實幹了這麼幾件事:ruby

  1. 建立一個繼承自 Person.prototype 的新對象函數

  2. 構造函數 Person 執行時,相應的參數傳入,同時上下文被指定爲這個新建的對象。this

  3. 若是構造函數返回了一個對象,那麼這個對象會取代 new 的結果。若是構造函數返回的不是對象,則會忽略這個返回值。prototype

返回值不是對象
function Person(name){
    this.name = name;
    return 'person'
}
var you = new Person('you');
//  you 的值: Person {name: "you"}
返回值是對象
function Person(name){
    this.name = name;
    return [1,2,3]
}
var you = new Person('you');
//  you的值: [1,2,3]

若是類的實例須要共享類的方法,那麼就須要給構造函數的 prototype 屬性添加方法了。由於 new 操做符建立的對象都繼承自構造函數的 prototype 屬性。他們能夠共享定義在類 prototype 上的方法和屬性。code

function Person(name,age){  
    this.name = name;
    this.age = age;
}
Person.prototype = {
    sayName: function(){
        console.log('My name is',this.name);
    }
}
var you = new Person('you',23);
var me = new Person('me',23);
you.sayName()   // My name is you.
me.sayName()    // My name is me.

繼承的實現

JavaScript 中經常使用的繼承方式是組合繼承,也就是經過構造函數和原型鏈繼承同時來模擬繼承的實現。對象

//Person 構造函數如上
function Student(name,age,clas){
    Person.call(this,name,age)
    this.clas = clas;
}
Student.prototype = Object.create(Person.prototype);        // Mark 1
Student.constructor = Student;      //若是不指明,則 Student 會找不到 constructor
Student.prototype.study = function(){
    console.log('I study in class',this.clas)
};
var liming = new Student('liming',23,7);
liming instanceof Person    //true
liming instanceof Student   //true
liming.sayName();       // My name is liming
liming.study();         // I study in class 7

代碼中 Mark 1 用到了 Object.create 方法。這個是 ES5 中新增的方法,用來建立一個擁有指定原型的對象。若是環境不兼容,能夠用下面這個 Polyfill 來實現(僅實現第一個參數)。繼承

if(!Object.create){
    Object.create = function(obj){
        function F(){};
        F.prototype = obj;
        return new F();
    }
}

其實就是把 obj 賦值給臨時函數 F ,而後返回一個 F 的實例。這樣經過代碼 Mark 1 Student 就獲得了 Person.prototype 上的全部屬性。有人會問了,那麼爲何不乾脆把 Person.prototype 直接賦值給 Student.prototype 呢?

是的,直接賦值是能夠達到子類共享父類 prototype 的目的,可是它破壞了原型鏈。即:子類和父類共用了同一個 prototype,這樣當某一個子類修改 prototype 的時候,其實同時也修改了父類的 prototype,那麼就會影響到全部基於這個父類建立的子類,這並非咱們想要的結果。看例子:

//Person 同上
//Student 同上
Student.prototype = Person.prototype;
Student.prototype.sayName = function(){
    console.log('My name is',this.name,'my class is',this.clas)
}
var liming = new Student('liming',23,7)
liming.sayName()        //My name is liming,my class is 7;
//另外一個子類
function Employee(name,age,salary){
    Person.call(name,age);
    this.salary = salary;
}
Employee.prototype = Person.prototype;
var emp = new Employee('emp',23,10000);
emp.sayName()       //Mark 2

大家猜 Mark 2 會輸出什麼?

咱們指望的 Mark 2 應該會輸出 "My name is emp". 但實際上報錯,爲何呢?由於咱們改寫 Student.prototype 的時候,也同時修改了 Person.prototype,最終致使 emp 繼承的 prototype 是咱們所不指望的,它的 sayName 方法是 My name is',this.name,'my class is',this.clas,這樣天然是會報錯的。

ES6 的繼承

隨着 ECMAScript 6 的發佈,咱們有了新的方法來實現繼承。也就是經過 class 關鍵字。

類的實現

class Person {
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    sayHello(){
        console.log(`My name is ${this.name},i'm ${this.age} years old`)
    }
}
var you = new Person('you',23);
you.sayHello()      //My name is you,i'm 23 years old.

繼承

ES6 裏面的繼承也很方便,經過 extends 關鍵字來實現。

class Student extends Person{
    constructor(name,age,cla){
        super(name,age);
        this.class = cla;
    }
    study(){
        console.log(`I'm study in class ${this.class}`)
    }
}
var liming = new Student('liming',23,7)
liming.study()      // I'm study in class 7.

這個繼承相比上面的 ES5 裏面實現的繼承要方便了不少,但其實原理是同樣的,提供的這些關鍵字方法只是語法糖而已,並無改變 Js 是基於原型這麼一個事實。不過 extends 這樣實現的繼承有一個限制,就是不能定義屬性,只能定義方法。要新添屬性,仍是得經過修改 prototype 來達到目的。

Student.prototype.teacher = 'Mr.Li'
var liming = new Student('liming',23,7)
var hanmeimei = new Student('hanmeimei',23,7)
liming.teacher          //Mr.Li
hanmeimei.teacher       //Mr.Li

靜態方法

ES6 還提供了 static 關鍵字,來實現靜態方法。靜態方法能夠繼承,但只能由類自己調用,不能被實例調用。

class Person{
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    static say(){
        console.log('Static')
    }
}
class Student extends Person{}
Person.say()        // Static
Student.say()       // Static
var you = new Person('you',23);
you.say()           // TypeError: liming.say is not a function

能夠看到,在實例上調用的時候會直接報錯。

Super關鍵字

在子類中能夠經過 super 來調用父類,根據調用位置的不一樣,行爲也不一樣。在 constructor 中調用,至關於調用父類的 constructor 方法,而在普通方法裏面調用則至關與調用父類自己。

class Person {
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    sayHello(){
        console.log(`My name is ${this.name},i'm ${this.age} years old`)
    }
}
class Student extends Person{
    constructor(name,age,cla){
        super(name,age);        // 必須在子類調用 this 前執行,調用了父類的 constructor
        this.class = cla;       
    }
    sayHello(){
        super.sayHello;         // 調用父類方法
        console.log('Student say')
    }
}
var liming = new Student('liming',23,7);
liming.say()        // My name is liming,i'm 23 years old.\n Student say.

總結

至此,咱們能夠看到:在 ES6 發佈之後,JavaScript 中實現繼承有了一個標準的方法。雖然它們只是語法糖,背後的本質仍是經過原型鏈以及構造函數實現的,不過在寫法上更易於咱們理解並且也更加清晰。

參考:

相關文章
相關標籤/搜索