<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                <h2 id="4.1">面向對象編程概述</h2> ## 對象是什么? “面向對象編程”(Object Oriented Programming,縮寫為OOP)是目前主流的編程范式。它的核心思想是將真實世界中各種復雜的關系,抽象為一個個對象,然后由對象之間的分工與合作,完成對真實世界的模擬。 傳統的計算機程序由一系列函數或一系列指令組成,而面向對象編程的程序由一系列對象組成。每一個對象都是功能中心,具有明確分工,可以完成接受信息、處理數據、發出信息等任務。因此,面向對象編程具有靈活性、代碼的可重用性、模塊性等特點,容易維護和開發,非常適合多人合作的大型軟件項目。 那么,“對象”(object)到底是什么? 我們從兩個層次來理解。 **(1)“對象”是單個實物的抽象。** 一本書、一輛汽車、一個人都可以是“對象”,一個數據庫、一張網頁、一個與遠程服務器的連接也可以是“對象”。當實物被抽象成“對象”,實物之間的關系就變成了“對象”之間的關系,從而就可以模擬現實情況,針對“對象”進行編程。 **(2)“對象”是一個容器,封裝了“屬性”(property)和“方法”(method)。** 所謂“屬性”,就是對象的狀態;所謂“方法”,就是對象的行為(完成某種任務)。比如,我們可以把動物抽象為animal對象,“屬性”記錄具體是那一種動物,“方法”表示動物的某種行為(奔跑、捕獵、休息等等)。 雖然不同于傳統的面向對象編程語言,但是JavaScript具有很強的面向對象編程能力。本章介紹JavaScript如何進行“面向對象編程”。 ## 構造函數 “面向對象編程”的第一步,就是要生成“對象”。前面說過,“對象”是單個實物的抽象。通常需要一個模板,表示某一類實物的共同特征,然后“對象”根據這個模板生成。 典型的面向對象編程語言(比如C++和Java),存在“類”(class)這個概念。所謂“類”就是對象的模板,對象就是“類”的實例。但是,JavaScript語言的對象體系,不是基于“類”的,而是基于構造函數(constructor)和原型鏈(prototype)。這個小節介紹構造函數。 JavaScript語言使用構造函數(constructor)作為對象的模板。所謂“構造函數”,就是專門用來生成“對象”的函數。它提供模板,描述對象的基本結構。一個構造函數,可以生成多個對象,這些對象都有相同的結構。 構造函數是一個正常的函數,但是它的特征和用法與普通函數不一樣。下面就是一個構造函數。 ```javascript var Vehicle = function () { this.price = 1000; }; ``` 上面代碼中,`Vehicle`就是構造函數,它提供模板,用來生成車輛對象。為了與普通函數區別,通常將構造函數的名字的第一個字母大寫。 構造函數的特點有兩個。 - 函數體內部使用了`this`關鍵字,代表了所要生成的對象實例。 - 生成對象的時候,必需用`new`命令,調用`Vehicle`函數。 ## new命令 ### 基本用法 `new`命令的作用,就是執行構造函數,返回一個實例對象。 ```javascript var Vehicle = function (){ this.price = 1000; }; var v = new Vehicle(); v.price // 1000 ``` 上面代碼通過`new`命令,讓構造函數`Vehicle`生成一個實例對象,保存在變量`v`中。這個新生成的實例對象,從構造函數`Vehicle`繼承了`price`屬性。在`new`命令執行時,構造函數內部的`this`,就代表了新生成的實例對象,`this.price`表示實例對象有一個`price`屬性,它的值是1000。 使用`new`命令時,根據需要,構造函數也可以接受參數。 ```javascript var Vehicle = function (p) { this.price = p; }; var v = new Vehicle(500); ``` `new`命令本身就可以執行構造函數,所以后面的構造函數可以帶括號,也可以不帶括號。下面兩行代碼是等價的。 ```javascript var v = new Vehicle(); var v = new Vehicle; ``` 一個很自然的問題是,如果忘了使用new命令,直接調用構造函數會發生什么事? 這種情況下,構造函數就變成了普通函數,并不會生成實例對象。而且由于下面會說到的原因,this這時代表全局對象,將造成一些意想不到的結果。 ```javascript var Vehicle = function (){ this.price = 1000; }; var v = Vehicle(); v.price // Uncaught TypeError: Cannot read property 'price' of undefined price // 1000 ``` 上面代碼中,調用Vehicle構造函數時,忘了加上new命令。結果,price屬性變成了全局變量,而變量v變成了undefined。 因此,應該非常小心,避免出現不使用new命令、直接調用構造函數的情況。為了保證構造函數必須與new命令一起使用,一個解決辦法是,在構造函數內部使用嚴格模式,即第一行加上`use strict`。 ```javascript function Fubar(foo, bar){ "use strict"; this._foo = foo; this._bar = bar; } Fubar() // TypeError: Cannot set property '_foo' of undefined ``` 上面代碼的`Fubar`為構造函數,`use strict`命令保證了該函數在嚴格模式下運行。由于在嚴格模式中,函數內部的`this`不能指向全局對象,默認等于`undefined`,導致不加`new`調用會報錯(JavaScript不允許對`undefined`添加屬性)。 另一個解決辦法,是在構造函數內部判斷是否使用`new`命令,如果發現沒有使用,則直接返回一個實例對象。 ```javascript function Fubar(foo, bar){ if (!(this instanceof Fubar)) { return new Fubar(foo, bar); } this._foo = foo; this._bar = bar; } Fubar(1, 2)._foo // 1 (new Fubar(1, 2))._foo // 1 ``` 上面代碼中的構造函數,不管加不加new命令,都會得到同樣的結果。 ### new命令的原理 使用new命令時,它后面的函數調用就不是正常的調用,而是被new命令控制了。內部的流程是,先創造一個空對象,作為上下文對象,賦值給函數內部的this關鍵字。也就是說,this指的是一個新生成的空對象,所有針對this的操作,都會發生在這個空對象上。 構造函數之所以叫“構造函數”,就是說這個函數的目的,就是操作上下文對象(即this對象),將其“構造”為需要的樣子。如果構造函數的return語句返回的是對象,new命令會返回return語句指定的對象;否則,就會不管return語句,返回構造后的上下文對象。 ```javascript var Vehicle = function (){ this.price = 1000; return 1000; }; (new Vehicle()) === 1000 // false ``` 上面代碼中,Vehicle是一個構造函數,它的return語句返回一個數值。這時,new命令就會忽略這個return語句,返回“構造”后的this對象。 但是,如果return語句返回的是一個跟this無關的新對象,new命令會返回這個新對象,而不是this對象。這一點需要特別引起注意。 ```javascript var Vehicle = function (){ this.price = 1000; return { price: 2000 }; }; (new Vehicle()).price // 2000 ``` 上面代碼中,構造函數Vehicle的return語句,返回的是一個新對象。new命令會返回這個對象,而不是this對象。 new命令簡化的內部流程,可以用下面的代碼表示。 ```javascript function _new(/* constructor, param, ... */) { var args = [].slice.call(arguments); var constructor = args.shift(); var context = Object.create(constructor.prototype); var result = constructor.apply(context, args); return (typeof result === 'object' && result != null) ? result : context; } var actor = _new(Person, "張三", 28); ``` ## instanceof運算符 `instanceof`運算符返回一個布爾值,表示一個對象是否由某個構造函數創建。 ```javascript var v = new Vehicle(); v instanceof Vehicle // true ``` `instanceof`運算符的左邊是實例對象,右邊是構造函數。 在JavaScript之中,只要是對象,就有對應的構造函數。因此,`instanceof`運算符可以用來判斷值的類型。 ```javascript [1, 2, 3] instanceof Array // true ({}) instanceof Object // true ``` 上面代碼表示數組和對象則分別是`Array`對象和`Object`對象的實例。最后那一行的空對象外面,之所以要加括號,是因為如果不加,JavaScript引擎會把一對大括號解釋為一個代碼塊,而不是一個對象,從而導致這一行代碼被解釋為`{}; instanceof Object`,引擎就會報錯。 注意,`instanceof`運算符只能用于對象,不適合用于原始類型的值。 ```javascript var s = 'hello'; s instanceof String // false var s = new String('hello'); s instanceof String // true ``` 上面代碼中,字符串不是`String`對象的實例(因為字符串不是對象),所以返回`false`,而字符串對象是`String`對象的實例,所以返回`true`。 此外,`undefined`和`null`不是對象,所以`instanceOf`運算符總是返回`false`。 ```javascript undefined instanceof Object // false null instanceof Object // false ``` 如果存在繼承關系,也就是說,`a`是`A`的實例,而`A`繼承了`B`,那么`instanceof`運算符對`A`和`B`都返回`true`。 ```javascript var a = []; a instanceof Array // true a instanceof Object // true ``` 上面代碼表示,`a`是一個數組,所以它是`Array`的實例;同時,`Array`繼承了`Object`,所以`a`也是`Object`的實例。 利用`instanceof`運算符,還可以巧妙地解決,調用構造函數時,忘了加`new`命令的問題。 ```javascript function Fubar (foo, bar) { if (this instanceof Fubar) { this._foo = foo; this._bar = bar; } else { return new Fubar(foo, bar); } } ``` 上面代碼使用`instanceof`運算符,在函數體內部判斷`this`關鍵字是否為構造函數`Fubar`的實例。如果不是,就表明忘了加`new`命令。 ## this關鍵字 ### 涵義 構造函數內部需要用到`this`關鍵字。那么,`this`關鍵字到底是什么意思呢? `this`就是指當前對象,或者說,指函數當前的運行環境。 ```javascript this['property'] this.property ``` 上面代碼中,`this`就代表`property`屬性當前所在的對象。 再看一個例子。 ```html <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> ``` 上面代碼是一個文本輸入框,每當用戶輸入一個值,就會調用`onChange`回調函數,驗證這個值是否在指定范圍。回調函數傳入`this`,就代表傳入當前對象(即文本框),然后就可以從`this.value`上面讀到用戶的輸入值。 在JavaScript語言之中,所有函數都是在某個運行環境之中運行,`this`就是這個環境。對于JavaScipt語言來說,一切皆對象,運行環境也是對象,所以可以理解成,所有函數總是在某個對象之中運行,`this`就指向這個對象。這本來并不會讓用戶糊涂,但是JavaScript支持運行環境動態切換,也就是說,this的指向是動態的,沒有辦法事先確定到底指向哪個對象,這才是最讓初學者感到困惑的地方。 舉例來說,有一個函數`f`,它同時充當`a`對象和`b`對象的方法。JavaScript允許函數f的運行環境動態切換,即一會屬于`a`對象,一會屬于`b`對象,這就要靠`this`關鍵字來辦到。 ```javascript function f() { console.log(this.x); }; var a = { x: 'a' }; a.m = f; var b = { x:'b' }; b.m = f; a.m() // a b.m() // b ``` 上面代碼中,函數`f`可以打印出當前運行環境中`x`變量的值。當`f`屬于`a`對象時,`this`指向`a`;當`f`屬于`b`對象時,`this`指向`b`,因此打印出了不同的值。由于`this`的指向可變,所以可以手動切換運行環境,以達到某種特定的目的。 前面說過,所謂“運行環境”就是對象,this指函數運行時所在的那個對象。如果一個函數在全局環境中運行,this就是指頂層對象(瀏覽器中為window對象);如果一個函數作為某個對象的方法運行,this就是指那個對象。 可以近似地認為,this是所有函數運行時的一個隱藏參數,決定了函數的運行環境。 ### 使用場合 this的使用可以分成以下幾個場合。 **(1)全局環境** 在全局環境使用this,它指的就是頂層對象window。 ```javascript this === window // true function f() { console.log(this === window); // true } ``` 上面代碼說明,不管是不是在函數內部,只要是在全局環境下運行,this就是指全局對象window。 **(2)構造函數** 構造函數中的this,指的是實例對象。 ```javascript var O = function(p) { this.p = p; }; O.prototype.m = function() { return this.p; }; ``` 上面代碼定義了一個構造函數O。由于this指向實例對象,所以在構造函數內部定義this.p,就相當于定義實例對象有一個p屬性;然后m方法可以返回這個p屬性。 ```javascript var o = new O("Hello World!"); o.p // "Hello World!" o.m() // "Hello World!" ``` **(3)對象的方法** 當a對象的方法被賦予b對象,該方法就變成了普通函數,其中的this就從指向a對象變成了指向b對象。這就是this取決于運行時所在的對象的含義,所以要特別小心。如果將某個對象的方法賦值給另一個對象,會改變this的指向。 ```javascript var o1 = new Object(); o1.m = 1; o1.f = function (){ console.log(this.m);}; o1.f() // 1 var o2 = new Object(); o2.m = 2; o2.f = o1.f o2.f() // 2 ``` 從上面代碼可以看到,f是o1的方法,但是如果在o2上面調用這個方法,f方法中的this就會指向o2。這就說明JavaScript函數的運行環境完全是動態綁定的,可以在運行時切換。 如果不想改變this的指向,可以將o2.f改寫成下面這樣。 ```javascript o2.f = function (){ o1.f() }; o2.f() // 1 ``` 上面代碼表示,由于f方法這時是在o1下面運行,所以this就指向o1。 有時,某個方法位于多層對象的內部,這時如果為了簡化書寫,把該方法賦值給一個變量,往往會得到意想不到的結果。 ```javascript var a = { b : { m : function() { console.log(this.p); }, p : 'Hello' } }; var hello = a.b.m; hello() // undefined ``` 上面代碼表示,m屬于多層對象內部的一個方法。為求簡寫,將其賦值給hello變量,結果調用時,this指向了全局對象。為了避免這個問題,可以只將m所在的對象賦值給hello,這樣調用時,this的指向就不會變。 ```javascript var hello = a.b; hello.m() // Hello ``` **(4)Node.js** 在Node.js中,this的指向又分成兩種情況。全局環境中,this指向全局對象global;模塊環境中,this指向module.exports。 ```javascript // 全局環境 this === global // true // 模塊環境 this === module.exports // true ``` ### 使用注意點 **(1)避免多層this** 由于this的指向是不確定的,所以切勿在函數中包含多層的this。 ```javascript var o = { f1: function() { console.log(this); var f2 = function() { console.log(this); }(); } } o.f1() // Object // Window ``` 上面代碼包含兩層this,結果運行后,第一層指向該對象,第二層指向全局對象。一個解決方法是在第二層改用一個指向外層this的變量。 ```javascript var o = { f1: function() { console.log(this); var that = this; var f2 = function() { console.log(that); }(); } } o.f1() // Object // Object ``` 上面代碼定義了變量that,固定指向外層的this,然后在內層使用that,就不會發生this指向的改變。 **(2)避免數組處理方法中的this** 數組的map和foreach方法,允許提供一個函數作為參數。這個函數內部不應該使用this。 ```javascript 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 ``` 上面代碼中,foreach方法的參數函數中的this,其實是指向window對象,因此取不到o.v的值。 解決這個問題的一種方法,是使用中間變量。 ```javascript 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 ``` 另一種方法是將this當作foreach方法的第二個參數,固定它的運行環境。 ```javascript 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 ``` **(3)避免回調函數中的this** 回調函數中的this往往會改變指向,最好避免使用。 ```javascript var o = new Object(); o.f = function (){ console.log(this === o); } o.f() // true ``` 上面代碼表示,如果調用o對象的f方法,其中的this就是指向o對象。 但是,如果將f方法指定給某個按鈕的click事件,this的指向就變了。 ```javascript $('#button').on('click', o.f); ``` 點擊按鈕以后,控制臺會顯示false。原因是此時this不再指向o對象,而是指向按鈕的DOM對象,因為f方法是在按鈕對象的環境中被調用的。這種細微的差別,很容易在編程中忽視,導致難以察覺的錯誤。 為了解決這個問題,可以采用下面的一些方法對this進行綁定,也就是使得this固定指向某個對象,減少不確定性。 ## 固定this的方法 `this`的動態切換,固然為JavaScript創造了巨大的靈活性,但也使得編程變得困難和模糊。有時,需要把`this`固定下來,避免出現意想不到的情況。JavaScript提供了`call`、`apply`、`bind`這三個方法,來切換/固定`this`的指向。 ### call方法 函數的call方法,可以指定該函數內部this的指向(即函數執行時所在的作用域),然后在所指定的作用域中,調用該函數。 ```javascript var o = {}; var f = function (){ return this; }; f() === this // true f.call(o) === o // true ``` 上面代碼中,在全局環境運行函數f時,this指向全局環境;call方法可以改變this的指向,指定this指向對象o,然后在對象o的作用域中運行函數f。 再看一個例子。 ```javascript var n = 123; var o = { n : 456 }; function a() { console.log(this.n); } a.call() // 123 a.call(null) // 123 a.call(undefined) // 123 a.call(window) // 123 a.call(o) // 456 ``` 上面代碼中,a函數中的this關鍵字,如果指向全局對象,返回結果為123。如果使用call方法將this關鍵字指向o對象,返回結果為456。可以看到,如果call方法沒有參數,或者參數為null或undefined,則等同于指向全局對象。 call方法的完整使用格式如下。 ```javascript func.call(thisValue, arg1, arg2, ...) ``` 它的第一個參數就是this所要指向的那個對象,后面的參數則是函數調用時所需的參數。 ```javascript function add(a,b) { return a+b; } add.call(this,1,2) // 3 ``` 上面代碼中,call方法指定函數add在當前環境(對象)中運行,并且參數為1和2,因此函數add運行后得到3。 call方法的一個應用是調用對象的原生方法。 ```javascript var obj = {}; obj.hasOwnProperty('toString') // false obj.hasOwnProperty = function (){ return true; }; obj.hasOwnProperty('toString') // true Object.prototype.hasOwnProperty.call(obj, 'toString') // false ``` 上面代碼中,hasOwnProperty是obj對象繼承的方法,如果這個方法一旦被覆蓋,就不會得到正確結果。call方法可以解決這個方法,它將hasOwnProperty方法的原始定義放到obj對象上執行,這樣無論obj上有沒有同名方法,都不會影響結果。 ### apply方法 apply方法的作用與call方法類似,也是改變this指向,然后再調用該函數。唯一的區別就是,它接收一個數組作為函數執行時的參數,使用格式如下。 ```javascript func.apply(thisValue, [arg1, arg2, ...]) ``` apply方法的第一個參數也是this所要指向的那個對象,如果設為null或undefined,則等同于指定全局對象。第二個參數則是一個數組,該數組的所有成員依次作為參數,傳入原函數。原函數的參數,在call方法中必須一個個添加,但是在apply方法中,必須以數組形式添加。 請看下面的例子。 ```javascript function f(x,y){ console.log(x+y); } f.call(null,1,1) // 2 f.apply(null,[1,1]) // 2 ``` 上面的f函數本來接受兩個參數,使用apply方法以后,就變成可以接受一個數組作為參數。 利用這一點,可以做一些有趣的應用。 **(1)找出數組最大元素** JavaScript不提供找出數組最大元素的函數。結合使用apply方法和Math.max方法,就可以返回數組的最大元素。 ```javascript var a = [10, 2, 4, 15, 9]; Math.max.apply(null, a) // 15 ``` **(2)將數組的空元素變為undefined** 通過apply方法,利用Array構造函數將數組的空元素變成undefined。 ```javascript Array.apply(null, ["a",,"b"]) // [ 'a', undefined, 'b' ] ``` 空元素與undefined的差別在于,數組的foreach方法會跳過空元素,但是不會跳過undefined。因此,遍歷內部元素的時候,會得到不同的結果。 ```javascript var a = ["a",,"b"]; function print(i) { console.log(i); } a.forEach(print) // a // b Array.apply(null,a).forEach(print) // a // undefined // b ``` **(3)轉換類似數組的對象** 另外,利用數組對象的slice方法,可以將一個類似數組的對象(比如arguments對象)轉為真正的數組。 ```javascript 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] ``` 上面代碼的apply方法的參數都是對象,但是返回結果都是數組,這就起到了將對象轉成數組的目的。從上面代碼可以看到,這個方法起作用的前提是,被處理的對象必須有length屬性,以及相對應的數字鍵。 **(4)綁定回調函數的對象** 上一節按鈕點擊事件的例子,可以改寫成 ```javascript var o = new Object(); o.f = function (){ console.log(this === o); } var f = function (){ o.f.apply(o); // 或者 o.f.call(o); }; $("#button").on("click", f); ``` 點擊按鈕以后,控制臺將會顯示true。由于apply方法(或者call方法)不僅綁定函數執行時所在的對象,還會立即執行函數,因此不得不把綁定語句寫在一個函數體內。更簡潔的寫法是采用下面介紹的bind方法。 ### bind方法 bind方法用于將函數體內的this綁定到某個對象,然后返回一個新函數。它的使用格式如下。 ```javascript func.bind(thisValue, arg1, arg2,...) ``` 下面是一個例子。 ```javascript var o1 = new Object(); o1.p = 123; o1.m = function (){ console.log(this.p); }; o1.m() // 123 var o2 = new Object(); o2.p = 456; o2.m = o1.m; o2.m() // 456 o2.m = o1.m.bind(o1); o2.m() // 123 ``` 上面代碼使用bind方法將o1.m方法綁定到o1以后,在o2對象上調用o1.m的時候,o1.m函數體內部的this.p就不再到o2對象去尋找p屬性的值了。 bind比call方法和apply方法更進一步的是,除了綁定this以外,還可以綁定原函數的參數。 ```javascript 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函數的第一個參數,結果newAdd函數只要一個參數就能運行了。 如果bind方法的第一個參數是null或undefined,等于將this綁定到全局對象,函數運行時this指向全局對象(在瀏覽器中為window)。 ```javascript function add(x,y) { return x+y; } var plus5 = add.bind(null, 5); plus5(10) // 15 ``` 上面代碼除了將add函數的運行環境綁定為全局對象,還將add函數的第一個參數綁定為5,然后返回一個新函數。以后,每次運行這個新函數,就只需要提供另一個參數就夠了。 bind方法有一些使用注意點。 **(1)每一次返回一個新函數** bind方法每運行一次,就返回一個新函數,這會產生一些問題。比如,監聽事件的時候,不能寫成下面這樣。 ```javascript element.addEventListener('click', o.m.bind(o)); ``` 上面代碼表示,click事件綁定bind方法生成的一個匿名函數。這樣會導致無法取消綁定,所以,下面的代碼是無效的。 ```javascript element.removeEventListener('click', o.m.bind(o)); ``` 正確的方法是寫成下面這樣: ```javascript var listener = o.m.bind(o); element.addEventListener('click', listener); // ... element.removeEventListener('click', listener); ``` **(2)bind方法的自定義代碼** 對于那些不支持bind方法的老式瀏覽器,可以自行定義bind方法。 ```javascript if(!('bind' in Function.prototype)){ Function.prototype.bind = function(){ var fn = this; var context = arguments[0]; var args = Array.prototype.slice.call(arguments, 1); return function(){ return fn.apply(context, args); } } } ``` **(3)jQuery的proxy方法** 除了用bind方法綁定函數運行時所在的對象,還可以使用jQuery的$.proxy方法,它與bind方法的作用基本相同。 ```javascript $("#button").on("click", $.proxy(o.f, o)); ``` 上面代碼表示,$.proxy方法將o.f方法綁定到o對象。 **(4)結合call方法使用** 利用bind方法,可以改寫一些JavaScript原生方法的使用形式,以數組的slice方法為例。 ```javascript [1,2,3].slice(0,1) // [1] // 等同于 Array.prototype.slice.call([1,2,3], 0, 1) // [1] ``` 上面的代碼中,數組的slice方法從[1, 2, 3]里面,按照指定位置和長度切分出另一個數組。這樣做的本質是在[1, 2, 3]上面調用Array.prototype.slice方法,因此可以用call方法表達這個過程,得到同樣的結果。 call方法實質上是調用Function.prototype.call方法,因此上面的表達式可以用bind方法改寫。 ```javascript var slice = Function.prototype.call.bind(Array.prototype.slice); slice([1, 2, 3], 0, 1) // [1] ``` 可以看到,利用bind方法,將[1, 2, 3].slice(0, 1)變成了slice([1, 2, 3], 0, 1)的形式。這種形式的改變還可以用于其他數組方法。 ```javascript 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的調用形式也可以被改寫。 ```javascript function f(){ console.log(this.v); } var o = { v: 123 }; var bind = Function.prototype.call.bind(Function.prototype.bind); bind(f,o)() // 123 ``` 上面代碼表示,將Function.prototype.call方法綁定Function.prototype.bind以后,bind方法的使用形式從f.bind(o),變成了bind(f, o)。 <h2 id="4.2">封裝</h2> ## prototype對象 ### 構造函數的缺點 JavaScript通過構造函數生成新對象,因此構造函數可以視為對象的模板。實例對象的屬性和方法,可以定義在構造函數內部。 ```javascript function Cat (name, color) { this.name = name; this.color = color; } var cat1 = new Cat('大毛', '白色'); cat1.name // '大毛' cat1.color // '白色' ``` 上面代碼的`Cat`函數是一個構造函數,函數內部定義了`name`屬性和`color`屬性,所有實例對象都會生成這兩個屬性。但是,這樣做是對系統資源的浪費,因為同一個構造函數的對象實例之間,無法共享屬性。 ```javascript function Cat(name, color) { this.name = name; this.color = color; this.meow = function () { console.log('mew, mew, mew...'); }; } var cat1 = new Cat('大毛', '白色'); var cat2 = new Cat('二毛', '黑色'); cat1.meow === cat2.meow // false ``` 上面代碼中,`cat1`和`cat2`是同一個構造函數的實例。但是,它們的`meow`方法是不一樣的,就是說每新建一個實例,就會新建一個`meow`方法。這既沒有必要,又浪費系統資源,因為所有`meow`方法都是同樣的行為,完全應該共享。 ### prototype屬性的作用 在JavaScript語言中,每一個對象都有一個對應的原型對象,被稱為prototype對象。定義在原型對象上的所有屬性和方法,都能被派生對象繼承。這就是JavaScript繼承機制的基本設計。 除了這種方法,JavaScript還提供了另一種定義實例對象的方法。我們知道,構造函數是一個函數,同時也是一個對象,也有自己的屬性和方法,其中有一個prototype屬性指向另一個對象,一般稱為prototype對象。該對象非常特別,只要定義在它上面的屬性和方法,能被所有實例對象共享。也就是說,構造函數生成實例對象時,自動為實例對象分配了一個prototype屬性。 ```javascript function Animal (name) { this.name = name; } Animal.prototype.color = "white"; var cat1 = new Animal('大毛'); var cat2 = new Animal('二毛'); cat1.color // 'white' cat2.color // 'white' ``` 上面代碼對構造函數Animal的prototype對象,添加了一個color屬性。結果,實例對象cat1和cat2都帶有該屬性。 更特別的是,只要修改prototype對象,變動就立刻會體現在實例對象。 ```javascript Animal.prototype.color = "yellow"; cat1.color // 'yellow' cat2.color // 'yellow' ``` 上面代碼將prototype對象的color屬性的值改為yellow,兩個實例對象的color屬性的值立刻就跟著變了。這是因為實例對象其實沒有color屬性,都是讀取prototype對象的color屬性。也就是說,當實例對象本身沒有某個屬性或方法的時候,它會到構造函數的prototype對象去尋找該屬性或方法。這就是prototype對象的特殊之處。 如果實例對象自身就有某個屬性或方法,它就不會再去prototype對象尋找這個屬性或方法。 ```javascript cat1.color = 'black'; cat2.color // 'yellow' Animal.prototype.color // "yellow"; ``` 上面代碼將實例對象cat1的color屬性改為black,就使得它不用再去prototype對象讀取color屬性,后者的值依然為yellow。 總而言之,prototype對象的作用,就是定義所有實例對象共享的屬性和方法,所以它也被稱為實例對象的原型,而實例對象可以視作從prototype對象衍生出來的。 ```javascript Animal.prototype.walk = function () { console.log(this.name + ' is walking.'); }; ``` 上面代碼在Animal.protype對象上面定義了一個walk方法,這個方法將可以在所有Animal實例對象上面調用。 ### 原型鏈 由于JavaScript的所有對象都有構造函數,而所有構造函數都有prototype屬性(其實是所有函數都有prototype屬性),所以所有對象都有自己的prototype原型對象。 因此,一個對象的屬性和方法,有可能是定義它自身上面,也有可能定義在它的原型對象上面(就像上面代碼中的walk方法)。由于原型本身也是對象,又有自己的原型,所以形成了一條原型鏈(prototype chain)。比如,a對象是b對象的原型,b對象是c對象的原型,以此類推。因為追根溯源,最源頭的對象都是從Object構造函數生成(使用new Object()命令),所以如果一層層地上溯,所有對象的原型最終都可以上溯到Object.prototype。那么,Object.prototype有沒有原型呢?回答可以是有,也可以是沒有,因為Object.prototype的原型是沒有任何屬性和方法的null。 ```javascript Object.getPrototypeOf(Object.prototype) // null ``` 上面代碼表示Object.prototype對象的原型是null,由于null沒有任何屬性,所以原型鏈到此為止。 “原型鏈”的作用在于,當讀取對象的某個屬性時,JavaScript引擎先尋找對象本身的屬性,如果找不到,就到它的原型去找,如果還是找不到,就到原型的原型去找。以此類推,如果直到最頂層的Object.prototype還是找不到,則返回undefined。 舉例來說,如果讓某個函數的prototype屬性指向一個數組,就意味著該函數可以用作數組的構造函數,因為它生成的實例對象都可以通過prototype屬性調用數組方法。 ```javascript function MyArray (){} MyArray.prototype = new Array(); MyArray.prototype.constructor = MyArray; var mine = new MyArray(); mine.push(1, 2, 3); mine.length // 3 mine instanceof Array // true ``` 上面代碼的mine是MyArray的實例對象,由于MyArray的prototype屬性指向一個數組,使得mine可以調用數組方法(這些方法其實定義在數組的prototype對象上面)。至于最后那行instanceof表達式,我們知道instanceof運算符用來比較一個對象是否為某個構造函數的實例,最后一行表示mine為Array的實例。 ```javascript mine instanceof Array // 等同于 (Array === MyArray.prototype.constructor) || (Array === Array.prototype.constructor) || (Array === Object.prototype.constructor ) ``` 上面代碼說明了instanceof運算符的實質,它依次與實例對象的所有原型對象的constructor屬性(關于該屬性的介紹,請看下一節)進行比較,只要有一個符合就返回true,否則返回false。 ### constructor屬性 prototype對象有一個constructor屬性,默認指向prototype對象所在的構造函數。 ```javascript function P() {} P.prototype.constructor === P // true ``` 由于constructor屬性定義在prototype對象上面,意味著可以被所有實例對象繼承。 ```javascript function P() {} var p = new P(); p.constructor // function P() {} p.constructor === P.prototype.constructor // true p.hasOwnProperty('constructor') // false ``` 上面代碼表示p是構造函數P的實例對象,但是p自身沒有contructor屬性,該屬性其實是讀取原型鏈上面的`P.prototype.constructor`屬性。 constructor屬性的作用是分辨prototype對象到底定義在哪個構造函數上面。 ```javascript function F(){}; var f = new F(); f.constructor === F // true f.constructor === RegExp // false ``` 上面代碼表示,使用constructor屬性,確定變量f的構造函數是F,而不是RegExp。 ## Object.getPrototypeOf方法 Object.getPrototypeOf方法返回一個對象的原型。 ```javascript // 空對象的原型是Object.prototype Object.getPrototypeOf({}) === Object.prototype // true // 函數的原型是Function.prototype function f() {} Object.getPrototypeOf(f) === Function.prototype // true // 假定F為構造函數,f為F的實例對象 // 那么,f的原型是F.prototype var f = new F(); Object.getPrototypeOf(f) === F.prototype // true ``` ## Object.create方法 `Object.create`方法用于生成新的對象,可以替代`new`命令。它接受一個對象作為參數,返回一個新對象,后者完全繼承前者的屬性,即前者成為后者的原型。 ```javascript var o1 = { p: 1 }; var o2 = Object.create(o1); o2.p // 1 ``` 上面代碼中,`Object.create`方法在`o1`的基礎上生成了`o2`。此時,`o1`成了`o2`的原型,也就是說,`o2`繼承了`o1`所有的屬性的方法。 `Object.create`方法基本等同于下面的代碼,如果老式瀏覽器不支持`Object.create`方法,可以用下面代碼自己部署。 ```javascript if (typeof Object.create !== "function") { Object.create = function (o) { function F() {} F.prototype = o; return new F(); }; } ``` 上面代碼表示,`Object.create`方法實質是新建一個構造函數`F`,然后讓`F`的`prototype`屬性指向作為原型的對象`o`,最后返回一個`F`的實例,從而實現讓實例繼承`o`的屬性。 下面三種方式生成的新對象是等價的。 ```javascript var o1 = Object.create({}); var o2 = Object.create(Object.prototype); var o3 = new Object(); ``` 如果想要生成一個不繼承任何屬性(比如toString和valueOf方法)的對象,可以將Object.create的參數設為null。 ```javascript var o = Object.create(null); o.valueOf() // TypeError: Object [object Object] has no method 'valueOf' ``` 上面代碼表示,如果對象o的原型是null,它就不具備一些定義在Object.prototype對象上面的屬性,比如valueOf方法。 使用Object.create方法的時候,必須提供對象原型,否則會報錯。 ```javascript Object.create() // TypeError: Object prototype may only be an Object or null ``` Object.create方法生成的新對象,動態繼承了原型。在原型上添加或修改任何方法,會立刻反映在新對象之上。 ```javascript var o1 = { p: 1 }; var o2 = Object.create(o1); o1.p = 2; o2.p // 2 ``` 上面代碼表示,修改對象原型會影響到新生成的對象。 除了對象的原型,Object.create方法還可以接受第二個參數,表示描述屬性的attributes對象,跟用在Object.defineProperties方法的格式是一樣的。它所描述的對象屬性,會添加到新對象。 ```javascript var o = Object.create(Object.prototype, { p1: { value: 123, enumerable: true }, p2: { value: "abc", enumerable: true } }); o.p1 // 123 o.p2 // "abc" ``` 由于Object.create方法不使用構造函數,所以不能用instanceof運算符判斷,對象是哪一個構造函數的實例。這時,可以使用下面的isPrototypeOf方法,判讀原型是哪一個對象。 ## isPrototypeOf方法 isPrototypeOf方法用來判斷一個對象是否是另一個對象的原型。 ```javascript var o1 = {}; var o2 = Object.create(o1); var o3 = Object.create(o2); o2.isPrototypeOf(o3) // true o1.isPrototypeOf(o3) // true ``` 上面代碼表明,只要某個對象處在原型鏈上,isProtypeOf都返回true。 <h2 id="4.3">繼承</h2> ## 概述 JavaScript的所有對象,都有自己的繼承鏈。也就是說,每個對象都繼承另一個對象,該對象稱為“原型”(prototype)對象。只有`null`除外,它沒有自己的原型對象。 原型對象的重要性在于,如果`A`對象是`B`對象的原型,那么`B`對象可以拿到`A`對象的所有屬性和方法。`Object.getPrototypof`方法用于獲取當前對象的原型對象。 ```javascript var p = Object.getPrototypeOf(obj); ``` 上面代碼中,對象`p`就是對象`obj`的原型對象。 `Object.create`方法用于生成一個新的對象,繼承指定對象。 ```javascript var obj = Object.create(p); ``` 上面代碼中,新生成的`obj`對象的原型就是對象`p`。 非標準的`__proto__`屬性(前后各兩個下劃線),可以改寫某個對象的原型對象。但是,應該盡量少用這個屬性,而是用`Object.getPrototypeof()`和`Object.setPrototypeOf()`,進行原型對象的讀寫操作。 ```javascript var obj = {}; var p = {}; obj.__proto__ = p; Object.getPrototypeOf(obj) === p // true ``` 上面代碼通過`__proto__`屬性,將`p`對象設為`obj`對象的原型。 下面是一個實際的例子。 ```javascript var a = {x: 1}; var b = {__proto__: a}; b.x // 1 ``` 上面代碼中,`b`對象通過`__proto__`屬性,將自己的原型對象設為`a`對象,因此`b`對象可以拿到`a`對象的所有屬性和方法。`b`對象本身并沒有`x`屬性,但是JavaScript引擎通過`__proto__`屬性,找到它的原型對象`a`,然后讀取`a`的`x`屬性。 `new`命令通過構造函數新建實例對象,實質就是將實例對象的原型綁定構造函數的`prototype`屬性,然后在實例對象上執行構造函數。 ```javascript var o = new Foo(); // 等同于 var o = new Object(); o.__proto__ = Foo.prototype; Foo.call(o); ``` 原型對象自己的`__proto__`屬性,也可以指向其他對象,從而一級一級地形成“原型鏈”(prototype chain)。 ```javascript var a = { x: 1 }; var b = { __proto__: a }; var c = { __proto__: b }; c.x // 1 ``` 需要注意的是,一級級向上,在原型鏈尋找某個屬性,對性能是有影響的。所尋找的屬性在越上層的原型對象,對性能的影響越大。如果尋找某個不存在的屬性,將會遍歷整個原型鏈。 ### this的動作指向 不管`this`在哪里定義,使用的時候,它總是指向當前對象,而不是原型對象。 ```javascript var o = { a: 2, m: function(b) { return this.a + 1; } }; var p = Object.create(o); p.a = 12; p.m() // 13 ``` 上面代碼中,`p`對象的`m`方法來自它的原型對象`o`。這時,`m`方法內部的`this`對象,不指向`o`,而是指向`p`。 ## 構造函數的繼承 這個小節介紹,如何讓一個構造函數,繼承另一個構造函數。 假定有一個`Shape`構造函數。 ```javascript function Shape() { this.x = 0; this.y = 0; } Shape.prototype.move = function (x, y) { this.x += x; this.y += y; console.info('Shape moved.'); }; ``` `Rectangle`構造函數繼承`Shape`。 ```javascript function Rectangle() { Shape.call(this); // 調用父類構造函數 } // 另一種寫法 function Rectangle() { this.base = Shape; this.base(); } // 子類繼承父類的方法 Rectangle.prototype = Object.create(Shape.prototype); Rectangle.prototype.constructor = Rectangle; var rect = new Rectangle(); rect instanceof Rectangle // true rect instanceof Shape // true rect.move(1, 1) // 'Shape moved.' ``` 上面代碼表示,構造函數的繼承分成兩部分,一部分是子類調用父類的構造方法,另一部分是子類的原型指向父類的原型。 上面代碼中,子類是整體繼承父類。有時,只需要單個方法的繼承,這時可以采用下面的寫法。 ```javascript ClassB.prototype.print = function() { ClassA.prototype.print.call(this); // some code } ``` 上面代碼中,子類`B`的`print`方法先調用父類`A`的`print`方法,再部署自己的代碼。這就等于繼承了父類`A`的`print`方法。 ## \_\_proto\_\_屬性 `__proto__`屬性指向當前對象的原型對象,即構造函數的`prototype`屬性。 ```javascript var obj = new Object(); obj.__proto__ === Object.prototype // true obj.__proto__ === obj.constructor.prototype // true ``` 上面代碼首先新建了一個對象`obj`,它的`__proto__`屬性,指向構造函數(`Object`或`obj.constructor`)的`prototype`屬性。所以,兩者比較以后,返回`true`。 因此,獲取實例對象`obj`的原型對象,有三種方法。 - `obj.__proto__` - `obj.constructor.prototype` - `Object.getPrototypeOf(obj)` 上面三種方法之中,前兩種都不是很可靠。最新的ES6標準規定,`__proto__`屬性只有瀏覽器才需要部署,其他環境可以不部署。而`obj.constructor.prototype`在手動改變原型對象時,可能會失效。 ```javascript var P = function () {}; var p = new P(); var C = function () {}; C.prototype = p; var c = new C(); c.constructor.prototype === p // false ``` 上面代碼中,`C`構造函數的原型對象被改成了`p`,結果`c.constructor.prototype`就失真了。所以,在改變原型對象時,一般要同時設置`constructor`屬性。 ```javascript C.prototype = p; C.prototype.constructor = C; c.constructor.prototype === p // true ``` 所以,推薦使用第三種`Object.getPrototypeOf`方法,獲取原型對象。該方法的用法如下。 ```javascript var o = new Object(); Object.getPrototypeOf(o) === Object.prototype // true ``` 可以使用`Object.getPrototypeOf`方法,檢查瀏覽器是否支持`__proto__`屬性,老式瀏覽器不支持這個屬性。 ```javascript Object.getPrototypeOf({ __proto__: null }) === null ``` 上面代碼將一個對象的`__proto__`屬性設為`null`,然后使用`Object.getPrototypeOf`方法獲取這個對象的原型,判斷是否等于`null`。如果當前環境支持`__proto__`屬性,兩者的比較結果應該是`true`。 有了`__proto__`屬性,就可以很方便得設置實例對象的原型了。假定有三個對象`machine`、`vehicle`和`car`,其中`machine`是`vehicle`的原型,`vehicle`又是`car`的原型,只要兩行代碼就可以設置。 ```javascript vehicle.__proto__ = machine; car.__proto__ = vehicle; ``` 下面是一個實例,通過`__proto__`屬性與`constructor.prototype`屬性兩種方法,分別讀取定義在原型對象上的屬性。 ```javascript Array.prototype.p = 'abc'; var a = new Array(); a.__proto__.p // abc a.constructor.prototype.p // abc ``` 顯然,`__proto__`看上去更簡潔一些。 通過構造函數生成實例對象時,實例對象的`__proto__`屬性自動指向構造函數的prototype對象。 ```javascript var f = function (){}; var a = {}; f.prototype = a; var o = new f(); o.__proto__ === a // true ``` ## 屬性的繼承 屬性分成兩種。一種是對象自身的原生屬性,另一種是繼承自原型的繼承屬性。 ### 對象的原生屬性 對象本身的所有屬性,可以用Object.getOwnPropertyNames方法獲得。 ```javascript Object.getOwnPropertyNames(Date) // ["parse", "arguments", "UTC", "caller", "name", "prototype", "now", "length"] ``` 對象本身的屬性之中,有的是可以枚舉的(enumerable),有的是不可以枚舉的。只獲取那些可以枚舉的屬性,使用Object.keys方法。 ```javascript Object.keys(Date) // [] ``` ### hasOwnProperty() `hasOwnProperty`方法返回一個布爾值,用于判斷某個屬性定義在對象自身,還是定義在原型鏈上。 ```javascript Date.hasOwnProperty('length') // true Date.hasOwnProperty('toString') // false ``` `hasOwnProperty`方法是JavaScript之中唯一一個處理對象屬性時,不會遍歷原型鏈的方法。 ### 對象的繼承屬性 用Object.create方法創造的對象,會繼承所有原型對象的屬性。 ```javascript var proto = { p1: 123 }; var o = Object.create(proto); o.p1 // 123 o.hasOwnProperty("p1") // false ``` ### 獲取所有屬性 判斷一個對象是否具有某個屬性(不管是自身的還是繼承的),使用in運算符。 ```javascript "length" in Date // true "toString" in Date // true ``` 獲得對象的所有可枚舉屬性(不管是自身的還是繼承的),可以使用for-in循環。 ```javascript var o1 = {p1: 123}; var o2 = Object.create(o1,{ p2: { value: "abc", enumerable: true } }); for (p in o2) {console.info(p);} // p2 // p1 ``` 為了在`for...in`循環中獲得對象自身的屬性,可以采用hasOwnProperty方法判斷一下。 ```javascript for ( var name in object ) { if ( object.hasOwnProperty(name) ) { /* loop code */ } } ``` 獲得對象的所有屬性(不管是自身的還是繼承的,以及是否可枚舉),可以使用下面的函數。 ```javascript function inheritedPropertyNames(obj) { var props = {}; while(obj) { Object.getOwnPropertyNames(obj).forEach(function(p) { props[p] = true; }); obj = Object.getPrototypeOf(obj); } return Object.getOwnPropertyNames(props); } ``` 用法如下: ```javascript inheritedPropertyNames(Date) // ["caller", "constructor", "toString", "UTC", "call", "parse", "prototype", "__defineSetter__", "__lookupSetter__", "length", "arguments", "bind", "__lookupGetter__", "isPrototypeOf", "toLocaleString", "propertyIsEnumerable", "valueOf", "apply", "__defineGetter__", "name", "now", "hasOwnProperty"] ``` ## 對象的拷貝 如果要拷貝一個對象,需要做到下面兩件事情。 - 確保拷貝后的對象,與原對象具有同樣的prototype原型對象。 - 確保拷貝后的對象,與原對象具有同樣的屬性。 下面就是根據上面兩點,編寫的對象拷貝的函數。 ```javascript function copyObject(orig) { var copy = Object.create(Object.getPrototypeOf(orig)); copyOwnPropertiesFrom(copy, orig); return copy; } function copyOwnPropertiesFrom(target, source) { Object .getOwnPropertyNames(source) .forEach(function(propKey) { var desc = Object.getOwnPropertyDescriptor(source, propKey); Object.defineProperty(target, propKey, desc); }); return target; } ``` ## 多重繼承 JavaScript不提供多重繼承功能,即不允許一個對象同時繼承多個對象。但是,可以通過變通方法,實現這個功能。 ```javascript function M1(prop) { this.hello = prop; } function M2(prop) { this.world = prop; } function S(p1, p2) { this.base1 = M1; this.base1(p1); this.base2 = M2; this.base2(p2); } S.prototype = new M1(); var s = new S(111, 222); s.hello // 111 s.world // 222 ``` 上面代碼中,子類`S`同時繼承了父類`M1`和`M2`。當然,從繼承鏈來看,`S`只有一個父類`M1`,但是由于在`S`的實例上,同時執行`M1`和`M2`的構造函數,所以它同時繼承了這兩個類的方法。 <h2 id="4.4">模塊化編程</h2> 隨著網站逐漸變成"互聯網應用程序",嵌入網頁的JavaiScript代碼越來越龐大,越來越復雜。網頁越來越像桌面程序,需要一個團隊分工協作、進度管理、單元測試等等......開發者不得不使用軟件工程的方法,管理網頁的業務邏輯。 JavaScript模塊化編程,已經成為一個迫切的需求。理想情況下,開發者只需要實現核心的業務邏輯,其他都可以加載別人已經寫好的模塊。 但是,JavaScript不是一種模塊化編程語言,ES5不支持"類"(class),更遑論"模塊"(module)了。ES6正式支持"類"和"模塊",但還沒有成為主流。JavaScript社區做了很多努力,在現有的運行環境中,實現模塊的效果。 ## 原始寫法 模塊就是實現特定功能的一組方法。只要把不同的函數(以及記錄狀態的變量)簡單地放在一起,就算是一個模塊。 ```javascript function m1(){ //... } function m2(){ //... } ``` 上面的函數m1()和m2(),組成一個模塊。使用的時候,直接調用就行了。 這種做法的缺點很明顯:"污染"了全局變量,無法保證不與其他模塊發生變量名沖突,而且模塊成員之間看不出直接關系。 為了解決上面的缺點,可以把模塊寫成一個對象,所有的模塊成員都放到這個對象里面。 ```javascript var module1 = new Object({  _count : 0,  m1 : function (){   //...  },  m2 : function (){  //...  } }); ``` 上面的函數m1()和m2(),都封裝在module1對象里。使用的時候,就是調用這個對象的屬性。 ```javascript module1.m1(); ``` 但是,這樣的寫法會暴露所有模塊成員,內部狀態可以被外部改寫。比如,外部代碼可以直接改變內部計數器的值。 ```javascript module1._count = 5; ``` ## 使用構造函數封裝私有變量 可以利用構造函數,封裝私有變量。 ```javascript function StringBuilder() { var buffer = []; this.add = function (str) { buffer.push(str); }; this.toString = function () { return buffer.join(''); }; } ``` 這種方法將私有變量封裝在構造函數中,違反了構造函數與實例對象相分離的原則。并且,非常耗費內存。 ```javascript 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),將相關的屬性和方法封裝在一個函數作用域里面,可以達到不暴露私有成員的目的。 ```javascript var module1 = (function(){  var _count = 0;  var m1 = function(){   //...  };  var m2 = function(){   //...  };  return {   m1 : m1,   m2 : m2  }; })(); ``` 使用上面的寫法,外部代碼無法讀取內部的_count變量。 ```javascript console.info(module1._count); //undefined ``` module1就是JavaScript模塊的基本寫法。下面,再對這種寫法進行加工。 ## 放大模式 如果一個模塊很大,必須分成幾個部分,或者一個模塊需要繼承另一個模塊,這時就有必要采用"放大模式"(augmentation)。 ```javascript var module1 = (function (mod){  mod.m3 = function () {   //...  };  return mod; })(module1); ``` 上面的代碼為module1模塊添加了一個新方法m3(),然后返回新的module1模塊。 在瀏覽器環境中,模塊的各個部分通常都是從網上獲取的,有時無法知道哪個部分會先加載。如果采用上面的寫法,第一個執行的部分有可能加載一個不存在空對象,這時就要采用"寬放大模式"(Loose augmentation)。 ```javascript var module1 = ( function (mod){  //...  return mod; })(window.module1 || {}); ``` 與"放大模式"相比,“寬放大模式”就是“立即執行函數”的參數可以是空對象。 ## 輸入全局變量 獨立性是模塊的重要特點,模塊內部最好不與程序的其他部分直接交互。 為了在模塊內部調用全局變量,必須顯式地將其他變量輸入模塊。 ```javascript var module1 = (function ($, YAHOO) {  //... })(jQuery, YAHOO); ``` 上面的module1模塊需要使用jQuery庫和YUI庫,就把這兩個庫(其實是兩個模塊)當作參數輸入module1。這樣做除了保證模塊的獨立性,還使得模塊之間的依賴關系變得明顯。 立即執行函數還可以起到命名空間的作用。 ```javascript (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都是外部無法調用的。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看