在JavaScript 中,并沒有對抽象類和接口的支持。JavaScript 本身也是一門弱類型語言。在封裝類型方面,JavaScript 沒有能力,也沒有必要做得更多。對于JavaScript 的設計模式實現來說,不區分類型是一種失色,也可以說是一種解脫。
從設計模式的角度出發,封裝在更重要的層面體現為封裝變化。
通過封裝變化的方式,把系統中穩定不變的部分和容易變化的部分隔離開來,在系統的演變過程中,我們只需要替換那些容易變化的部分,如果這些部分是已經封裝好的,替換起來也相對容易。這可以最大程度地保證程序的穩定性和可擴展性。
javascript封裝的的基本模式有3種。
### 1、使用約定優先的原則,將所有的私有變量以_開頭
~~~
<script type="text/javascript">
/**
* 使用約定優先的原則,把所有的私有變量都使用_開頭
*/
var Person = function (no, name, age)
{
this.setNo(no);
this.setName(name);
this.setAge(age);
}
Person.prototype = {
constructor: Person,
checkNo: function (no)
{
if (!no.constructor == "string" || no.length != 4)
throw new Error("學號必須為4位");
},
setNo: function (no)
{
this.checkNo(no);
this._no = no;
},
getNo: function ()
{
return this._no;
setName: function (name)
{
this._name = name;
},
getName: function ()
{
return this._name;
},
setAge: function (age)
{
this._age = age;
},
getAge: function ()
{
return this._age;
},
toString: function ()
{
return "no = " + this._no + " , name = " + this._name + " , age = " + this._age;
}
};
var p1 = new Person("0001", "小平果", "22");
console.log(p1.toString()); //no = 0001 , name = 小平果 , age = 22
p1.setNo("0003");
console.log(p1.toString()); //no = 0003 , name = 小平果 , age = 22
p1.no = "0004";
p1._no = "0004";
console.log(p1.toString()); //no = 0004 , name =小平果 , age = 22
</script>
~~~
看完代碼,是不是有種被坑的感覺,僅僅把所有的變量以_開頭,其實還是可以直接訪問的,這能叫封裝么,當然了,說了是約定優先嘛.
下劃線的這種用法這一個眾所周知的命名規范,它表明一個屬性僅供對象內部使用,直接訪問它或設置它可能會導致意想不到的后果。這有助于防止程序員對它的無意使用,卻不能防止對它的有意使用。
這種方式還是不錯的,最起碼成員變量的getter,setter方法都是prototype中,并非存在對象中,總體來說還是個不錯的選擇。如果你覺得,這不行,必須嚴格實現封裝,那么看第二種方式。
### 2、嚴格實現封裝
~~~
<script type="text/javascript">
/**
* 使用這種方式雖然可以嚴格實現封裝,但是帶來的問題是get和set方法都不能存儲在prototype中,都是存儲在對象中的
* 這樣無形中就增加了開銷
*/
var Person = function (no, name, age)
{
var _no , _name, _age ;
var checkNo = function (no)
{
if (!no.constructor == "string" || no.length != 4)
throw new Error("學號必須為4位");
};
this.setNo = function (no)
{
checkNo(no);
_no = no;
};
this.getNo = function ()
{
return _no;
}
this.setName = function (name)
{
_name = name;
}
this.getName = function ()
{
return _name;
}
this.setAge = function (age)
{
_age = age;
}
this.
getAge = function ()
{
return _age;
}
this.setNo(no);
this.setName(name);
this.setAge(age);
}
Person.prototype = {
constructor: Person,
toString: function ()
{
return "no = " + this.getNo() + " , name = " + this.getName() + " , age = " + this.getAge();
}
}
;
var p1 = new Person("0001", "小平果", "22");
console.log(p1.toString()); //no = 0001 , name =小平果 , age = 22
p1.setNo("0003");
console.log(p1.toString()); //no = 0003 , name = 小平果 , age = 22
p1.no = "0004";
console.log(p1.toString()); //no = 0003 , name = 小平果 , age = 22
</script>
~~~
那么這與我們先前講過的其他創建對象的模式有什么不同呢,在上面的例子中,我們在創建和引用對象的屬性時總要使用this關鍵字。而在本例中,我們用var聲明這些變量。這意味著它們只存在于Person構造器中。checkno函數也是用同樣的方式聲明的,因此成了一個私用方法。
需要訪問這些變量和函數的方法只需要聲明在Person中即可。這些方法被稱為特權方法,因為它們是公用方法,但卻能夠訪問私用屬性和方法。為了在對象外部能訪問這些特權函數,它們的前面被加上了關鍵字this。因為這些方法定義于Person構造器的作用域,所以它們能訪問到私用屬性。引用這些屬性時并沒有使用this關鍵字,因為它們不是公開的。所有取值器和賦值器方法都被改為不加this地直接引用這些屬性。
任何不需要直接訪問的私用屬性的方法都可以像原來那樣在Person.prototype中聲明。像toString()方法。只有那些需要直接訪問私用成員的方法才應該被設計為特權方法。但特權方法太多又會占用過多的內存,因為每個對象實例都包含所有特權方法的新副本。
看上面的代碼,去掉了this.屬性名,嚴格的實現了封裝,只能通過getter,setter訪問成員變量了,但是存在一個問題,所有的方法都存在對象中,增加了內存的開銷。
### 3、以閉包的方式封裝
~~~
<script type="text/javascript">
var Person = (function ()
{
//靜態方法(共享方法)
var checkNo = function (no)
{
if (!no.constructor == "string" || no.length != 4)
throw new Error("學號必須為4位");
};
//靜態變量(共享變量)
var times = 0;
//return the constructor.
return function (no, name, age)
{
console.log(times++); // 0 ,1 , 2
var no , name , age; //私有變量
this.setNo = function (no) //私有方法
{
checkNo(no);
this._no = no;
};
this.getNo = function ()
{
return this._no;
}
this.setName = function (name)
{
this._name = name;
}
this.getName = function ()
{
return this._name;
}
this.setAge = function (age)
{
this._age = age;
}
this.getAge = function ()
{
return this._age;
}
this.setNo(no);
this.setName(name);
this.setAge(age);
}
})();
Person.prototype = {
constructor: Person,
toString: function ()
{
return "no = " + this._no + " , name = " + this._name + " , age = " + this._age;
}
};
var p1 = new Person("0001", "小平果", "22");
var p2 = new Person("0002", "abc", "23");
var p3 = new Person("0003", "aobama", "24");
console.log(p1.toString()); //no = 0001 , name = 小平果 , age = 22
console.log(p2.toString()); //no = 0002 , name = abc , age = 23
console.log(p3.toString()); //no = 0003 , name = aobama , age = 24
</script>
~~~
上述代碼,js引擎加載完后,會直接執行Person = 立即執行函數,然后此函數返回了一個子函數,這個子函數才是new Person所調用的構造函數,又因為子函數中保持了對立即執行函數中checkNo(no) ,times的引用,(很明顯的閉包)所以對于checkNo和times,是所有Person對象所共有的,創建3個對象后,times分別為0,1,2 。這種方式的好處是,可以使Person中需要復用的方法和屬性做到私有且對象間共享。
這里的私用成員和特權成員仍然被聲明在構造器。但那個構造器卻從原來的普通函數變成了一個內嵌函數,并且被作為包含它的函數的返回值給變量Person。這就創建了一個閉包,你可以把靜態的私用成員聲明在里面。位于外層函數聲明之后的一對空括號很重要,其作用是代碼一載入就立即執行這個函數。這個函數的返回值是另一個函數,它被賦給Person變量,Person因此成了一個構造函數。在實例華Person時,所調用的這個內層函數。外層那個函數只是用于創建一個可以用來存儲靜態成員的閉包。
在本例中,checkno被設計成為靜態方法,原因是為Person的每個實例都生成這個方法的一個新副本毫無道理。此外還有一個靜態屬性times,其作用在于跟蹤Person構造器的總調用次數。