JavaScript中的繼承實現(1)

前言:學習過面向對象語言(java、c++)的童鞋都瞭解面向對象的概念,也確定知道面嚮對象語言的特徵:封裝、繼承和多態,但JavaScript並不是面向對象,而是基於對象,這些概念咱們沒法直接應用到JavaScript的對象機制中,這節咱們主要學習JavaScript開發者是如何曲線救國實現面嚮對象語言的繼承特性java

1.類的概述?c++

  類:類是面嚮對象語言的基礎。類比如模型,好比說動物是一個類別很空泛不具體,擁有不少的特徵,可是咱們具體不知道它是會飛、爬、遊。app

  對象:那麼對象比如是類的一個具體體現,好比狗這個動物就是真實存在的,全部的特徵是固定的,會跑、會汪汪叫等。函數

  面向對象:面向對象更多強調的是數據和操做數據的行爲是互相關聯的,具體的實現就是將數據和關聯的行爲封裝爲一個類,基於類完成面嚮對象語言的開發。性能

  舉例:表示單詞或短語的字符一般稱爲字符串。但咱們每每關心的不是數據自己,而是和數據關聯的操做。因此就將數據(字符)和行爲(計算長度、增刪數據等)封裝成了統一的String類,全部的字符串就是String類的一個實例。學習

2.何爲繼承?繼承有什麼好處?優化

  在現實生活中咱們常會使用繼承一詞,好比:小鳥繼承了媽媽的特徵。能夠繼承媽媽的屬性(羽毛、四肢等),也能夠繼承媽媽的行爲(鳴叫、飛等),程序中的繼承與其相似。this

  繼承:子類在不須要重寫父類屬性和方法的前提下繼承了父類的屬性和方法,而且能夠直接調用它們spa

  好處:提升了代碼的複用性。prototype

3.JavaScript中繼承的實現

  在其餘語言(java)中,子類繼承父類獲得只是父類的一個副本,類的繼承實質上就是類的複製。可是在JavaScript中只有對象,對象之間不存在類之間複製的功能。那麼JavaScript開發者又是如何實現面嚮對象語言的繼承行爲的。

  1>構造函數繼承

    在子類中經過call/apply方法完成子類對父類的繼承。

        // 定義名爲Person的構造函數,構造函數只綁定屬性name
        function Person(name) {
            this.name = name
            this.getName = function() {
                return this.name;
            }
        }

        // 定義構造函數Bar(),定義屬性name和label
        function Student(name, hobby){
            // 經過call改變this的指向,繼承Person中的屬性name和方法getName()
            Person.call(this, name)
            this.hobby = hobby;
            this.getHobby = function () {
                return this.hobby
            }
        }
        var a = new Student("小明", "basketBall");
        console.log(a.getName());
        console.log(a.getHobby());

  上例中子類Student經過Person.call(this.name)繼承了父類Person的name屬性和getName()方法,咱們在Student的實例中能夠直接調用name屬性和getName()方法。

  分析:<1>Person全部實例中的getName()方法行爲都是同樣的,每實例化一個對象都會定義一個行爲相同的getName()方法,但咱們但願全部實例都共享同一個getName()方法,全部getName()最好的方式是定義在原型上。

  缺點:

    <1>.每一個實例都有相同行爲的getName()方法形成內存的浪費。

    <2>.作不到代碼的複用,最佳作法getName()方法全部實例共用同一個。

    <3>.形成父類多餘屬性的繼承

  2>原型prototype模式繼承

    函數都有prototype屬性,在設計構造函數時,咱們將不須要共享的屬性和方法,定義在構造函數裏面,須要共享的屬性和方法,都放在prototype中;

        // 將不共享的name屬性定義在Person函數中
        function Person(name) {
            this.name = name
        }
        // 將須要共享的getName方法定義在原型上
        Person.prototype.getName = function(){
            return this.name
        }

        // 定義構造函數Student(),定義屬性name和label
        function Student(name, hobby){
            // 經過call改變this的指向,繼承Person中的屬性name
            Person.call(this, name)
            this.hobby = hobby;
        }
        // Student繼承了Person中的方法
        Student.prototype = new Person()
        // 修正Student的構造函數的指向
        Student.prototype.constructor = Student
        // Student定義getHobby的方法
        Student.prototype.getHobby = function() {
            return this.hobby
        }

        var stu = new Student("小明", "basketBall");
        console.log(stu.getName());
        console.log(stu.getHobby());
        

  分析:

  1>.代碼中Student.prototype = new Person(),Person實例賦值給Student.prototype,Student.prototype原先的值被刪除掉。
  2>.Student.prototype.constructor = Student作了什麼呢?每一個Prototype對象都有一個Constructor屬性,若是沒有Student.prototype = new Person(),

    Student.prototype的constructor是指向Student的,執行這行後constructor指向了Person,更重要的是每一個實例有一個constructor屬性默認調用prototype中的constructor屬性
     調用Student.prototype = new Person()後,stu.constructor也指向了Person。
        console.log(Student.prototype.constructor === Person)   // true
        console.log(stu.constructor === Person)                 // true
        console.log(stu.constructor === Student.prototype.constructor)  //true

    這就致使了繼承鏈的混亂,由於stu是Student的實例,而不是Person的實例,因此咱們須要手動修正Student.prototype.constructor = Student。

   3>.上例中對於Student.prototype = new Person(),咱們可使用Student.prototype = Person.prototype替換,與第一種相比不須要執行Person函數(提升性能)也不須要建立實例(節省內存),同時縮短了原型鏈的調用。

    缺點:致使Student.prototype和Person.prototype指向同一個對象引用,修改其中任意一個另外一個就會被影響。好比:Student.prototype.constructor = Student會致使Person.prototype.constructor也是Student。

console.log(Person.prototype.constructor === Student)   //  true

  3>prototype模式改進

    因爲Student.prototype = new Person()和Student.prototype = Person.prototype兩種方式都存在缺點,這裏咱們使用空對象做爲中介進行優化。

        /*
            1>F是空函數對於性能的損耗和內存的浪費忽略不計
            2>不會致使child.prototype和parent.prototype指向同一個引用
        */
        function extend (parent, child) {
            function F (){}
            F.prototype = parent.prototype
            child.prototype = new F()
            child.prototype.constructor = child
        }

        // 將不共享的name屬性定義在Person函數中
        function Person(name) {
            this.name = name
        }
        // 將須要共享的getName方法
        Person.prototype.getName = function(){
            return this.name
        }

        // 定義構造函數Student(),定義屬性name和hobby
        function Student(name, hobby){
            // 經過call改變this的指向,繼承Person中的屬性name和方法getName()
            Person.call(this, name)
            this.hobby = hobby;
        }

        extend(Person, Student)
        Student.prototype.getHobby = function() {
            return this.hobby
        }

        var stu = new Student("小明", "basketBall");
        console.log(stu.getName());             //小明
        console.log(stu.getHobby());            //basketball
        console.log(Student.prototype.constructor)  //Student
        console.log(Person.prototype.constructor)   //Person

  分析:

    1>上例中咱們封裝了extend方法,extend中咱們仍是用空構造函數F做爲中介,避免了以前實現方式中的缺點。   

     1>F是空函數對於性能的損耗和內存的浪費忽略不計
      2>不會致使child.prototype和parent.prototype指向同一個引用

    2>對於咱們本身封裝的extend方法,咱們其實可使用ES5中提供的Object.create(proto,properties)代替,實現的效果是同樣的。

    Student.prototype = Object.create(Person.prototype)

    注意:Object.create()方法的內部實現和咱們本身封裝的extend方法相似。

相關文章
相關標籤/搜索