(78)Wangdao.com第十五天_JavaScript 面向對象

面向對象編程(Object Oriented Programming,縮寫爲 OOP)web

是目前主流的編程範式。編程

是單個實物的抽象,數組

是一個容器,封裝了屬性(property)和方法(method),屬性是對象的狀態,方法是對象的行爲(完成某種任務)。瀏覽器

將真實世界各類複雜的關係,抽象爲一個個對象,而後由對象之間的分工與合做,完成對真實世界的模擬。安全

每個對象都是功能中心,具備明確分工,能夠完成接受信息、處理數據、發出信息等任務。數據結構

對象能夠複用,經過繼承機制還能夠定製。app

具備靈活、代碼可複用、高度模塊化等特色,容易維護和開發,比起由一系列函數或指令組成的傳統的過程式編程(procedural programming),更適合多人合做的大型軟件項目。模塊化

 

  • 構造函數

「類」 就是對象的模板,對象就是 「類」 的實例。函數

JavaScript 語言的對象體系不是基於「類」的,而是基於構造函數(constructor)和原型鏈(prototype)。ui

  • 使用構造函數(constructor)做爲對象的模板,描述實例對象的基本結構。。
  • 所謂 」構造函數」,就是專門用來生成實例對象的函數。
  • 一個構造函數,能夠生成多個實例對象,這些實例對象都有相同的結構。
  • 爲了與普通函數區別,構造函數名字的第一個字母一般大寫。
  • 特色:
    • 函數體內部使用了 this 關鍵字,表明所要生成的實例
    • 生成對象的時候,必須使用 new 命令
  • new 命令
    • 經過new命令,讓構造函數生成一個實例對象
    • new命令執行時,構造函數內部的this,就表明了新生成的實例對象
    • 使用new命令時,根據須要,構造函數也能夠接受參數
      • var Vehicle = function (p) {
            this.price = p;
        };
        
        var v = new Vehicle(500);

        new命令自己就能夠執行構造函數,因此後面的構造函數能夠帶括號,也能夠不帶括號。可是爲了表示是函數調用,推薦使用括號

      • var Vehicle = function (){
            this.price = 1000;
        };
        
        var v = Vehicle();    // 將構造函數當成普通函數調用,不抱錯
        v    // undefined    // 可是變量v變成了undefined
        price    // 1000    // price屬性變成了全局變量
        
        
        // 一個解決辦法是: // 構造函數內部使用嚴格模式,即第一行加上use strict。 // 這樣的話,一旦忘了使用new命令,直接調用構造函數就會報錯。
        function Fubar(foo, bar){  'use strict'; this._foo = foo; // 因爲 嚴格模式中,函數內部的 this 不能指向全局對象,默認等於undefined this._bar = bar; } Fubar(); // TypeError: Cannot set property '_foo' of undefined

        // 另外一個解決辦法:
        // 造函數內部判斷是否使用new命令,若是發現沒有使用,則直接返回一個實例對象。
        thisundefinednew
        function Fubar(foo, bar) { if (!(this instanceof Fubar)) { return new Fubar(foo, bar); } this._foo = foo; this._bar = bar; } 
        // 此時無論加不加new命令,都會獲得一樣的結果 Fubar(1, 2)._foo // 1 (new Fubar(1, 2))._foo // 1
        new

         

  • new 命令的原理

使用new命令時,它後面的函數依次執行下面的步驟

1. 建立一個空對象,做爲將要返回的對象實例

2. 將這個空對象的隱式原型對象 __proto__,指向構造函數的 prototype 屬性

3. 將這個空對象賦值給函數內部的 this 關鍵字

4. 開始執行構造函數內部的代碼

 

    • 若是構造函數內部有 return 語句,並且 return 後面跟着一個對象,new 命令會返回 return 語句指定的對象;不然,就會無論 return 語句,返回 this 對象
      • 若是對普通函數(內部沒有this關鍵字的函數)使用new命令,則會返回一個空對象
      • 由於new命令老是返回一個對象,要麼是實例對象,要麼是return語句指定的對象
        function getMessage() {
            return 'this is a message';
        }
        
        var msg = new getMessage();
        
        msg    // {}
        typeof msg    // "object"

         

    • new 命令簡化的內部流程,能夠用下面的代碼表示
      • function _new(/* 構造函數 */ constructor, /* 構造函數參數 */ params) {
            // 將 arguments 對象轉爲數組
            var args = [].slice.call(arguments);
          
            // 取出構造函數
            var constructor = args.shift();
            
            // 建立一個空對象,繼承構造函數的 prototype 屬性
            var context = Object.create(constructor.prototype);
        
            // 執行構造函數
            var result = constructor.apply(context, args);
        
            // 若是返回結果是對象,就直接返回,不然返回 context 對象
            return (typeof result === 'object' && result != null) ? result : context;
        }
        
        // 實例
        var actor = _new(Person, '張三', 28);

         

  • 函數內部可使用 new.target 屬性
    • 若是當前函數是 new 命令調用,new.target 指向當前函數,不然爲 undefined
      • function f() {
            console.log(new.target === f);
        }
        
        f();    // false    // 指向 
        new f();    // true    // 指向 當前函數new.targetundefinednew.target

         

    • 使用這個屬性,能夠判斷函數調用的時候,是否使用new命令
      • function f() {
            if (!new.target) {
                throw new Error('請使用 new 命令調用!');
            }
            // ...
        }
        
        f();     // Uncaught Error: 請使用 new 命令調用!

         

  • Object.create() 建立實例對象
    • 有時拿不到構造函數,只能拿到一個現有的對象
    • 可使用 Object.create() 方法,生成新的實例對象
      • var Person= {
            name: '張三',
            age: 38,
            greeting: function() {
                console.log('Hi! I\'m ' + this.name + '.');
            }
        };
        
        var person2 = Object.create(Person);
        
        person2.name;    // 張三
        person2.greeting();    // Hi! I'm 張三.

         

 

  • this 關鍵字

this除了能夠在構造函數中表示實例對象,還能夠用在其餘場合

可是無論什麼場合,this 老是返回一個對象

  • this 的指向是可變的
    • 只要函數被賦給另外一個變量,this 的指向就會變
      • var A = {
            name: '張三',
            describe: function () {
                return '姓名:'+ this.name;
            }
        };
        
        var name = '李四';    // window.name
        var f = A.describe;    // window.f
        f();    // "姓名:李四"

         

    • 根據據當前所在的對象不一樣,this 的指向也不一樣
      • function f() {
            return '姓名:'+ this.name;
        }
        
        var A = {
            name: '張三',
            describe: f
        };
        
        var B = {
            name: '李四',
            describe: f
        };
        
        A.describe();    // "姓名:張三"
        B.describe();    // "姓名:李四"

         

    • 一個網頁編程的例子
      • <input type="text" name="age" size=3 onChange="validate(this, 18, 99);">
        
        <script>
        function validate(obj, lowval, hival){
            if ((obj.value < lowval) || (obj.value > hival))
                console.log('Invalid Value!');
        }
        </script>
        // 一個文本輸入框,每當用戶輸入一個值,就會調用回調函數,驗證這個值是否在指定範圍。
        // 瀏覽器會向回調函數傳入當前對象,所以 就表明傳入當前對象(即文本框),而後就能夠從 上面讀到用戶的輸入值onChangethisobj.value

         JavaScript 支持運行環境動態切換,也就是說,this的指向是動態的,沒有辦法事先肯定到底指向哪一個對象

  • this 實質
  • var obj = { foo:  5 };
    // 原始的對象以字典結構保存,每個屬性名都對應一個屬性描述對象。
    // 舉例來講,上面例子的 foo 屬性,其實是如下面的形式保存的
    
    {
      foo: {
        [[value]]: 5
        [[writable]]: true
        [[enumerable]]: true
        [[configurable]]: true
      }
    }

     

    • 有 this 的設計,跟內存裏面的數據結構有關係
      • 因爲函數是一個單獨的值,因此它能夠在不一樣的環境(上下文)執行
      • 因爲函數能夠在    不一樣的運行環境(上下文)  執行,因此須要有一種機制,可以在函數體內部得到當前的運行環境(context)
      • 因此,this 就出現了,它的設計目的就是在函數體內部,指代函數當前的運行環境。
        • var f = function () {
              console.log(this.x);
          }
          
          var x = 1;
          var obj = {
              ff: f,
              x: 2,
          };
          
          // 單獨執行
          f();    // 1
          
          // obj 環境執行
          obj.ff();    // 2

           

    • 幾個使用場合下,this 的指向
      • 全局環境
        • 全局環境使用this,它指的就是頂層對象window
        • 不論是不是在函數內部,只要是在全局環境下運行,this 就是指頂層對象window 
      • 構造函數
        • 構造函數中的 this ,指的是實例對象構造函數中的 this ,指的是實例對象
      • 方法調用時
        • this 指向調用該對象的對象。
      • call() 和 apply() 時
        • 指向指定的對象

 

    • 因爲 this 的指向是不肯定的,因此切勿在函數中包含多層的 this 
      • 使用一個變量固定 this 的值,而後內層函數調用這個變量,是很是常見的作法,請務必掌握
      • var o = {
            f1: function () {
                console.log(this);
                var f2 = function () {
                    console.log(this);
                }();
            }
        }
        
        o.f1();    
        // Object
        // Window

         

      • 使用一個變量固定 this 的值,而後內層函數調用這個變量
        var o = {
            f1: function() {
                console.log(this);
        var that = this; var f2 = function() { console.log(that); }(); } } o.f1() // Object // Object


        // 以上代碼等價於
        var temp = function () {
        console.log(this);
        };
        var o = { f1: function () { console.log(this); var f2 = temp(); } }
         

         

      • 嚴格模式下,若是函數內部的 this 指向頂層對象,就會報錯。

 

    • 避免數組處理方法中的 this 
      • 數組的mapforeach方法,容許提供一個函數做爲參數。這個函數內部不該該使用this
      • 若是有
        • 使用中間變量固定this
        • 將 this 看成 foreach( , this) 方法的第二個參數,固定它的運行環境
        • var o = {
              v: 'hello',
              p: [ 'a1', 'a2' ],
              f: function f() {
                  this.p.forEach(function (item) {
                      console.log(this.v + ' ' + item);
                  });
              }
          }
          
          o.f()
          // undefined a1
          // undefined a2
          
          
          
          // 解決方法1    使用變量固定 this
          var o = {
              v: 'hello',
              p: [ 'a1', 'a2' ],
              f: function f() {
                  var that = this;
                  this.p.forEach(function (item) {
                      console.log(that.v+' '+item);
                  });
              }
          }
          
          o.f()
          // hello a1
          // hello a2
          
          
          
          // 解決方法2    forEach( , this)
          var o = {
              v: 'hello',
              p: [ 'a1', 'a2' ],
              f: function f() {
                  this.p.forEach(function (item) {
                      console.log(this.v + ' ' + item);
                  }, this);
              }
          }
          
          o.f()
          // hello a1
          // hello a2

           

    • 回調函數中的 this 每每會改變指向,最好避免使用
        • var o = new Object();
          o.f = function () {
              console.log(this === o);
          }
          
          // jQuery 的寫法
          $('#button').on('click', o.f);

          上面代碼中,點擊按鈕之後,控制檯會顯示false。緣由是此時this再也不指向o對象,而是指向按鈕的 DOM 對象,由於f方法是在按鈕對象的環境中被調用的。

 

    • 固定 this 指向

JavaScript 提供了callapplybind這三個方法,來切換/固定this的指向

      • Function.prototype.call()
    • 函數實例的call方法,能夠指定函數內部this的指向(即函數執行時所在的做用域),而後在所指定的做用域中,調用該函數
      • var obj = {};
        
        var f = function () {
            return this;
        };
        
        f() === window    // true
        f.call(obj) === obj    // true

         

    • call 方法的參數,應該是一個對象。若是參數爲空、null和undefined,則默認傳入全局對象

 

    • 若是 call 方法的參數是一個原始值,那麼這個原始值會自動轉成對應的包裝對象,而後傳入 call 方法
      • var f = function () {
            return this;
        };
        
        f.call(5)
        // Number {[[PrimitiveValue]]: 5}

         

    • call 方法還能夠接受多個參數    func.call(thisObj, arg1, arg2, ...)
      • 後面的參數是函數調用時所需的參數
      • function add(a, b) {
            return a + b;
        }
        
        add.call(this, 1, 2);    // 3

         

    • 應用 之 調用對象的原生方法
      • var obj = {};
        obj.hasOwnProperty('toString');    // false
        
        
        // 覆蓋掉繼承的 hasOwnProperty 方法
        obj.hasOwnProperty = function () {
            return true;
        };
        obj.hasOwnProperty('toString');    // true
        // hasOwnProperty是obj對象繼承的方法,若是這個方法一旦被覆蓋,就不會獲得正確結果
        
        
        // 將hasOwnProperty方法的原始定義放到obj對象上執行,這樣不管obj上有沒有同名方法,都不會影響結果
        Object.prototype.hasOwnProperty.call(obj, 'toString');    // false

 

      • Function.prototype.apply()
    • 與 call 方法相似,也是改變 this 指向,而後再調用該函數。
    • 惟一的區別就是,它只有 2 個參數,第二個參數接收一個數組做爲函數執行時的參數    func.call(thisObj, [arg1, arg2, ...])
      • 第一個參數也是 this 所要指向的那個對象,若是設爲null或undefined,則等同於指定全局對象
    • 不少有趣的應用
      • 找出數組最大元素
        • var a = [10, 2, 4, 15, 9];
          
          // 結合使用apply方法和Math.max方法,就能夠返回數組的最大元素
          Math.max.apply(null, a);    // 15

           

      • 將數組的空元素變爲 undefined
        • // 利用 Array構造函數 和 apply結合
          // 將數組的空元素變成undefined
          Array.apply(null, ['a', ,'b']);    // [ 'a', undefined, 'b' ]

          空元素與undefined的差異在於,數組的forEach方法會跳過空元素,可是不會跳過undefined

      • 轉換相似數組的對象
        • 利用數組對象的slice方法,將一個相似數組的對象(好比arguments對象)轉爲真正的數組
        • // 前提是
              //被處理的對象必須有length屬性
              //以及相對應的數字鍵
          Array.prototype.slice.apply({0: 1, length: 1});    // [1]
          Array.prototype.slice.apply({0: 1});    // []
          Array.prototype.slice.apply({0: 1, length: 2});    // [1, undefined]
          Array.prototype.slice.apply({length: 1});    // [undefined]

           

      • 綁定回調函數的對象
        • var o = new Object();
          
          o.f = function () {
              console.log(this === o);
          }
          
          var f = function (){
              o.f.apply(o);    // 或者 o.f.call(o);
          };
          
          // jQuery 的寫法
          $('#button').on('click', f);
          
          //點擊按鈕之後,控制檯將會顯示true。
          // 因爲apply方法(或者call方法)不只綁定函數執行時所在的對象,還會當即執行函數,所以不得不把綁定語句寫在一個函數體內。
          // 更簡潔的寫法是採用下面介紹的bind方法。

 

      • Function.prototype.bind()
    • 用於將函數體內的 this 綁定到某個對象,而後返回一個新函數
        • var d = new Date();
          d.getTime() // 1481869925657
          
          var print = d.getTime;
          print() // Uncaught TypeError: this is not a Date object.
          
          // 由於getTime方法內部的this,綁定Date對象的實例,賦給變量print之後,內部的this已經不指向Date對象的實例了

          bind方法能夠解決這個問題

          • var print = d.getTime.bind(d);
            print();    // 1481869925657

            bind 方法將 getTime 方法內部的 this 綁定到 d 對象,這時就能夠安全地將這個方法賦值給其餘變量了

    • bind 方法的參數就是所要綁定 this 的對象
    • 下面是一個更清晰的例子
      • var counter = {
            count: 0,
            inc: function () {
                this.count++;
            }
        };
        
        var func = counter.inc.bind(counter);
        func();
        counter.count    // 1

        counter.inc() 方法被賦值給變量 func 。這時必須用 bind 方法將 inc 內部的 this,綁定到 counter,不然就會出錯

    • this綁定到其餘對象也是能夠的
      • var counter = {
            count: 0,
            inc: function () {
                this.count++;
            }
        };
        
        var obj = {
            count: 100
        };
        var func = counter.inc.bind(obj);
        func();
        obj.count    // 101

         

    • bind 還能夠接受更多的參數,將這些參數綁定原函數的參數
      • var add = function (x, y) {
            return x * this.m + y * this.n;
        }
        
        var obj = {
            m: 2,
            n: 2
        };
        
        var newAdd = add.bind(obj, 5);
        newAdd(5)    // 20

        bind() 方法除了綁定 this 對象,還將 add 函數的第一個參數 x 綁定成 5,而後返回一個新函數 newAdd(),這個函數只要再接受一個參數 y 就能運行了

    • 若是bind() 方法的第一個參數是 null 或 undefined,等於將 this 綁定到全局對象,函數運行時 this 指向頂層對象(瀏覽器爲window)
      • function add(x, y) {
            return x + y;
        }
        
        var plus5 = add.bind(null, 5);
        plus5(10);    // 15

        上面代碼中,函數 add 內部並無 this,使用 bind() 方法的主要目的是綁定參數 x,之後每次運行新函數 plus5,就只須要提供另外一個參數 y 就夠了。

      • 並且由於 add 內部沒有 this,因此 bind() 的第一個參數是 null,不過這裏若是是其餘對象,也沒有影響

 

    • bind() 使用注意
      • 每一次返回一個新函數
        • bind方法每運行一次,就返回一個新函數,這會產生一些問題。
        • 好比,監聽事件的時候,不能寫成下面這樣
          • element.addEventListener('click', o.m.bind(o));
            // 上面代碼中,click事件綁定bind方法生成的一個匿名函數。
            
            // 這樣會致使沒法取消綁定,因此,下面的代碼是無效的
            element.removeEventListener('click', o.m.bind(o));
            
            
            
            // 正確寫法
            var listener = o.m.bind(o);
            element.addEventListener('click', listener);
            //  ...
            element.removeEventListener('click', listener);

             

      • 結合回調函數使用
        • 回調函數是 JavaScript 最經常使用的模式之一,
        • 可是一個常見的錯誤是,將包含 this 的方法直接看成回調函數
          • var counter = {
                count: 0,
                inc: function () {
                    'use strict';
                    this.count++;
                }
            };
            
            function callIt(callback) {
                callback();
            }
            
            // 上面代碼中,callIt方法會調用回調函數。
            // 這時若是直接把counter.inc傳入,調用時counter.inc內部的this就會指向全局對象。 callIt(counter.inc.bind(counter)); // 使用bind方法將counter.inc綁定counter之後,就不會有這個問題,this老是指向counter counter.count
            // 1

             

        • 某些數組方法能夠接受一個函數看成參數。這些函數內部的this指向,極可能也會出錯
          • var obj = {
                name: '張三',
                times: [1, 2, 3],
                print: function () {
                    this.times.forEach(function (n) {
                        console.log(this.name);
                    });
                }
            };
            
            obj.print();    // 沒有任何輸出
            // obj.print內部this.times的this是指向obj的,這個沒有問題。
            
            // 可是,forEach方法的回調函數內部的this.name倒是指向全局對象,致使沒有辦法取到值。
            obj.print = function () {
                this.times.forEach(function (n) {
                    console.log(this === window);
                });
            };
            
            obj.print()
            // true
            // true
            // true

            解決方法

            • // 經過bind方法綁定this
              obj.print = function () {
                  this.times.forEach(function (n) {
                      console.log(this.name);
                  }.bind(this));
              };
              
              obj.print()
              // 張三
              // 張三
              // 張三

               

      • 結合call方法使用
        • 利用bind方法,能夠改寫一些 JavaScript 原生方法的使用形式,以數組的slice方法爲例
          • [1, 2, 3].slice(0, 1);    // [1] // 等同於
            Array.prototype.slice.call([1, 2, 3], 0, 1);    // [1]

            call方法實質上是調用Function.prototype.call方法,所以上面的表達式能夠用bind方法改寫

            • var slice = Function.prototype.call.bind(Array.prototype.slice);
              slice([1, 2, 3], 0, 1);    // [1]
            • var push = Function.prototype.call.bind(Array.prototype.push);
              var pop = Function.prototype.call.bind(Array.prototype.pop);
              
              var a = [1 ,2 ,3];
              push(a, 4)
              a    // [1, 2, 3, 4]
              
              pop(a)
              a    // [1, 2, 3]

               

        • 若是再進一步,將 Function.prototype.call 方法綁定到 Function.prototype.bind 對象,就意味着 bind 的調用形式也能夠被改寫
          • function f() {
                console.log(this.v);
            }
            
            var o = { v: 123 };
            var bind = Function.prototype.call.bind(Function.prototype.bind);
            bind(f, o)();    // 123

            因此bind方法就能夠直接使用,不須要在函數實例上使用

相關文章
相關標籤/搜索