(79)Wangdao.com第十五天_JavaScript 對象的繼承_prototype原型對象_封裝_函數式編程

javascript 內置了許多 function 函數(){...}javascript

js 執行首先就會執行本身內置的函數定義 (function Function、function Object)html

對象的繼承java

大部分面向對象的編程語言,都是經過「類」(class)實現對象的繼承。c++

傳統上,JavaScript 語言的繼承不經過 class,而是經過「原型對象」(prototype)實現,稱之爲 JavaScript 的原型鏈繼承面試

JavaScript 繼承機制的設計思想就是,原型對象 prototype 的全部屬性和方法,都能被實例對象共享編程

ES6 引入了 class 語法數組

  • 構造函數的缺點
    • 同一個構造函數的多個實例之間,沒法共享屬性,從而形成對系統資源的浪費
      • function Cat(name, color) {
            this.name = name;
            this.color = color;
            this.bar = function () {
                console.log('喵喵');
            };
        }
        
        var cat1 = new Cat('大毛', '白色');
        var cat2 = new Cat('二毛', '黑色');
        
        cat1.meow === cat2.meow    // false

        cat1 和 cat2 是同一個構造函數的兩個實例,它們都具備 bar 方法。因爲 bar 方法是生成在每一個實例對象上面,因此兩個實例就生成了兩次。瀏覽器

    • 也就是說,每新建一個實例,就會新建一個 bar方法。這既沒有必要,又浪費系統資源,由於全部 bar 方法都是一樣的行爲,徹底應該共享
    • 這個問題的解決方法,就是 JavaScript 的原型對象(prototype)

 

  • JavaScript 的原型對象
    • JavaScript 繼承機制的設計思想就是,原型對象的全部屬性和方法,都能被實例對象共享
    • 屬性和方法定義在原型上,那麼全部實例對象就能共享,不只節省了內存,還體現了實例對象之間的聯繫
    • JavaScript 規定,每一個函數都有一個 prototype 屬性,指向一個對象。
      • 對於普通函數來講,該屬性基本無用。
      • 可是,對於構造函數來講,生成實例的時候,該屬性會自動成爲實例對象的原型
        • function Animal(name) {
              this.name = name;
          }
          Animal.prototype.color = 'white';
          
          var cat1 = new Animal('大毛');
          var cat2 = new Animal('二毛');
          
          cat1.color    // 'white'
          cat2.color    // 'white'
      • 原型對象的做用,就是定義全部實例對象共享的屬性和方法。
  • 在函數建立時,都會默認建立 顯示原型對象

 

讀取屬性/方法,沿着原型鏈找安全

設置屬性/ 方法,只會查看和影響自身編程語言

全部函數都具備 prototype 顯式原型屬性,指向一個對象____原型對象 

  •  function Dog(){};

全部對象都是某個構造函數的實例,都擁有 __proto__隱式原型屬性

  •  var wangCai = new Dog("旺財", "2");

注意:

  • 因爲 函數 也是一個對象,因此 函數 也擁有一個 __proto__隱式原型屬性,由原生底層語言實現

constructor    構造函數____等於函數自己

__proto__    隱式原型屬性____指向 該對象的構造函數的原型對象 prototype 隱式原型屬性 指向 上一級對象的原型對象) 

  • 同一構造函數的 全部實例對象 都有一個 隱式原型 指向同一個原型對象

  • 也就是說,能夠在 構造函數 定義時,存放一些實例對象 公共方法 公共屬性

 

  • 底層 native code 用 c/c++ 實現
  • 全部 函數 有一個 prototype 顯示原型屬性

全部函數都是 function Function(){...} 的實例

  • 全部 實例對象 都有一個 __proto__ 隱式原型屬性 (包括原型對象都有一個 __proto__ )
  • 全部 原型對象 都有 constructor 屬性指向 構造函數

 

    • 原型鏈
      • 當調用某對象的屬性或者方法時,沿着 __proto__ 這條鏈向上查找,
          • 首先在自身做用域中尋找,
          • 而後到原型對象中尋找,
          • 再到原型對象的原型對象中尋找,直到找到 Object 。
          • 若是始終沒找到就返回 undefined。
      • 若是對象自身和它的原型,都定義了一個同名屬性,那麼優先讀取對象自身的屬性____這叫作 「覆蓋」(overriding)
      • JavaScript 規定,全部對象都有本身的 隱式原型屬性 __proto__        指向 new 構造函數的 原型對象
        • 一方面,任何一個對象,均可以充當其餘對象的原型;
        • 另外一方面,因爲原型對象也是對象,因此它也有本身的原型。
        • 所以,就會造成一個「原型鏈」(prototype chain):對象到原型,再到原型的原型……
      • Object.prototype 的原型是 null ,所以,原型鏈的盡頭就是 null 
      • 所尋找的屬性在越上層的原型對象,對性能的影響越大。若是尋找某個不存在的屬性,將會遍歷整個原型鏈。
          • /* 面試題 */
            var A = function() {
            
            }
            
            A.prototype.n = 1
            
            var b = new A()
            
            A.prototype = {    // 改變的 只是一個地址值,而不會改變 真正對象 的存在(b 始終指向那個 原始的 prototype 對象)
                n: 2,
                m: 3
            }
            
            var c = new A()
            console.log(b.n, b.m, c.n, c.m)
      • 舉例來講,若是讓構造函數的 prototype 屬性指向一個數組,就意味着實例對象能夠調用數組方法
        • prototype 對象有一個 constructor 屬性,默認指向 prototype 對象所在的構造函數
        • constructor 屬性的做用
          • 能夠得知某個實例對象,究竟是哪個構造函數產生的
          • 有了 constructor 屬性,就能夠從一個實例對象新建另外一個實例
            • function Constr() {}
              var x = new Constr();
              
              var y = new x.constructor();
              y instanceof Constr;    // true

              這使得在實例方法中,調用自身的構造函數成爲可能

              • Constr.prototype.createCopy = function () {
                    return new this.constructor();
                };
      • constructor 屬性表示原型對象與構造函數之間的關聯關係,若是修改了原型對象,通常會同時修改 constructor 屬性,防止引用的時候出錯
      • 要麼將 constructor 屬性從新指向原來的構造函數,要麼只在原型對象上添加方法,這樣能夠保證 instanceof 運算符不會失真
      • 若是不能肯定 constructor 屬性是什麼函數,還有一個辦法:經過 name 屬性,從實例獲得構造函數的名稱。
        • function Foo() {}
          var f = new Foo();
          f.constructor.name    // "Foo"
    • 全部 函數 都是 function Function(){} 的實例對象,

        • 包括 Object 的構造函數        的 __proto__ 都指向 Function 的原型對象

        • 甚至 Function 本身的構造函數        的 __proto__ 都指向 Function 的原型對象
            • console.log(Object instanceof Function);    // true
              console.log(Object instanceof Object);    // true
              console.log(Function instanceof Function);    // true
              console.log(Function instanceof Object);    // true
    • instanceOf 運算符
          • A instanceof B            若是 B 函數的顯示原型對象 A 對象的 __proto__ 原型鏈上,則返回 true,不然返回 false
      • 返回一個布爾值,表示對象是否爲某個構造函數的實例
        • var v = new Vehicle();
          v instanceof Vehicle;    // true

          實例對象 instanceOf 構造函數

          • 它會檢查右邊構建函數的原型對象(prototype),是否在左邊對象的原型鏈上
            v instanceof Vehicle
            // 等同於
            Vehicle.prototype.isPrototypeOf(v)
      • 有一種特殊狀況,就是左邊對象的原型鏈上,只有null對象。這時,instanceof 判斷會失真
        • var obj = Object.create(null);
          typeof obj;    // "object"
          Object.create(null) instanceof Object;    // false

          所以,只要一個對象的原型不是null,instanceof運算符的判斷就不會失真。

      • 使用 instanceOf 判斷一個變量的類型
        • var x = [1, 2, 3];
          var y = {};
          x instanceof Array    // true
          y instanceof Object    // true

          注意,instanceof 運算符只能用於對象,不適用原始類型的值。

      • 對於undefined和null,instanceOf運算符老是返回false
      • 利用 instanceof 運算符,還能夠巧妙地解決,調用構造函數時,忘了加 new 命令的問題
        • function Fubar (foo, bar) {
              if (this instanceof Fubar) {
                  this._foo = foo;
                  this._bar = bar;
              } else {
                  return new Fubar(foo, bar);
              }
          }
    • 構造函數的繼承

讓一個構造函數繼承另外一個構造函數,是很是常見的需求。這能夠分紅兩步實現

  • 在子類的構造函數中,調用父類的構造函數
  • 讓子類的原型指向父類的原型,這樣子類就能夠繼承父類原型
    • function Sub(value) {
          Super.call(this);
          Sub.prototype = Object.create(Super.prototype);
          Sub.prototype.constructor = Sub;
      
          this.name = value;
          Sub.prototype.method = '...';
      }

      另一種寫法是 Sub.prototype 等於一個父類實例,可是子類會具備父類實例的方法。有時,這可能不是咱們須要的,因此不推薦使用這種寫法

      • Sub.prototype = new Super();

         

  • 有時只須要單個方法的繼承,這時能夠採用下面的寫法
    • ClassB.prototype.print = function() {
          ClassA.prototype.print.call(this);
          // some code
      }

      這就等於繼承了父類A的 print 方法

 

  • 封裝——函數式編程
  • 在 ES6 加入 class 關鍵字以前的編程方式
  • /**** 旨在實現封裝的前提下,最少佔用內存 ****/
    // 封裝 父類
    function Parent(name, age){
        this.name = name;
        this.age = age;
    };
    
    Parent.prototype.setName = function(name){
        this.name = name;
    };
    
    Parent.prototype.setAge = function(age){
        this.age = age;
    };
    
    // 封裝 子類
    function Child(name, age){
        Parent.call(this, name, age);    // 繼承父類的屬性
        this.isCrying = false;
    };
    
    Child.prototype = new Parent();    // 繼承父類的方法
    Child.prototype.constructor = Child;    // 修正構造器指向

 

    • 多重繼承
      • JavaScript 不提供多重繼承功能,即不容許一個對象同時繼承多個對象。可是,能夠經過變通方法,實現這個功能
        • function M1() {
              this.hello = 'hello';
          }

          function M2() {
              this.world = 'world';
          }

          function S() {
              M1.call(this);
              // 繼承 M1
              S.prototype = Object.create(M1.prototype);

              M2.call(this);
              // 繼承鏈上加入 M2
              Object.assign(S.prototype, M2.prototype);

              // 指定構造函數
              S.prototype.constructor = S;
          }

          var s = new S();
          s.hello    // 'hello'
          s.world    // 'world'

          子類 S 同時繼承了父類 M1 和 M2 。這種模式又稱爲 Mixin(混入)

 

    • 模塊
      • JavaScript 不是一種模塊化編程語言,ES6 纔開始支持 「類」 和 「模塊」
      • ES5 中傳統方法實現模塊
        • 模塊是實現特定功能的一組屬性和方法的封裝
          • 簡單的作法是把模塊寫成一個對象,全部的模塊成員都放到這個對象裏面
          • 可是,這樣的寫法會暴露全部模塊成員,內部狀態能夠被外部改寫。好比,外部代碼能夠直接改變內部計數器的值。
            • var module1 = new Object({
                 _count : 0,
                 m1 : function (){
                    //...
                 },
                 m2 : function (){
                     //...
                 }
              });

               

        • 能夠利用構造函數,封裝私有變量
          • function StringBuilder() {
                var buffer = [];
            
                this.add = function (str) {
                     buffer.push(str);
                };
            
                this.toString = function () {
                    return buffer.join('');
                };
            }
          • 構造函數有雙重做用,既用來塑造實例對象,又用來保存實例對象的數據
          • 背了構造函數與實例對象在數據上相分離的原則(即實例對象的數據,不該該保存在實例對象之外。同時,很是耗費內存)
          • 解決:
            • function StringBuilder() {
                  this._buffer = [];
                  StringBuilder.prototype = {
                      constructor: StringBuilder,
                      add: function (str) {
                          this._buffer.push(str);
                      },
                      toString: function () {
                          return this._buffer.join('');
                      }
                  };
              }

              以上代碼將私有變量放入實例對象中,好處是看上去更天然,可是它的私有變量能夠從外部讀寫,不是很安全

 

        • 使用 「當即執行函數」(Immediately-Invoked Function Expression,IIFE),將相關的屬性和方法封裝在一個函數做用域裏面,能夠達到不暴露私有成員的目的
            • var module1 = (function () {  var age = 0;  var getAge = function () {   return this.age;
                };   var setAge = function (age) {    this.age = age;
                };   return {     getAge : getAge,    setAge : setAge  }; })();

              使用上面的寫法,外部代碼沒法直接獲取內部的_count變量。

 

        • 模塊的 放大模式 (argumentation)
          • 若是一個模塊很大,必須分紅幾個部分,或者一個模塊須要繼承另外一個模塊,這時就有必要採用「放大模式」。
            • var module1 = (function (mod){
                 mod.m3 = function () {
                    //...
                 };
                 return mod;
              })(module1);

              上面的代碼爲 module1 模塊添加了一個新方法 m3(),而後返回新的 module1 模塊。

          • 在瀏覽器環境中,模塊的各個部分一般都是從網上獲取的,有時沒法知道哪一個部分會先加載。
          • 若是採用上面的寫法,第一個執行的部分有可能加載一個不存在空對象,這時就要採用 "寬放大模式"(Loose augmentation)
            • var module1 = (function (mod) {
                  //...
                  return mod;
              })(window.module1 || {});

              與"放大模式"相比,「寬放大模式」就是「當即執行函數」的參數能夠是空對象

 

        • 輸入全局變量
          • 獨立性是模塊的重要特色,模塊內部最好不與程序的其餘部分直接交互
          • 爲了在模塊內部調用全局變量,必須顯式地將其餘變量輸入模塊
            • var module1 = (function ($, YAHOO) {
                 //...
              })(jQuery, YAHOO);

               

            • 上面的module1模塊須要使用 jQuery 庫和 YUI 庫,就把這兩個庫(實際上是兩個模塊)看成參數輸入module1

            • 這樣作除了保證模塊的獨立性,還使得模塊之間的依賴關係變得明顯

          • 當即執行函數還能夠起到命名空間的做用
            • (function($, window, document) {
              
                  function go(num) {
                  }
              
                  function handleEvents() {
                  }
              
                  function initialize() {
                  }
              
                  function dieCarouselDie() {
                  }
              
                  //attach to the global scope
                  window.finalCarousel = {
                      init : initialize,
                      destroy : dieCouraselDie
                  }
              
              })( jQuery, window, document );

               

            • 上面代碼中,finalCarousel 對象輸出到全局,對外暴露 init() 和 destroy() 接口,

            • 內部方法 go()、handleEvents()、initialize()、dieCarouselDie() 都是外部沒法調用的。

相關文章
相關標籤/搜索