<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都是外部無法調用的。