本文僅探討如何合理的使用 function 在 javascript中實現一個面向對象設計的類。總所周知,javascript 並不能實現一個真正意義上的類,比如 protect 比如 函數重載。
下面開始由淺入深的討論 function 作為類來使用如何盡可能的模擬傳統的面向對象設計。
還有一篇相關博文(關於 class)可對比閱讀:js面向對象設計之class類。
下面的 Class01 一個最簡單的類。
function Class01( val, pVal ) {
this.val = val; /*實例可直接讀寫的屬性*/
var pVal = pVal; /*實例無法直接讀寫的屬性*/
}
Class01.prototype.printVal = function () {
var _this = this;
/*此處並不會出現this丟失,但推薦這么做*/
console.log( _this.val );
};
對於實例無法直接讀寫的屬性,需要提供接口,首先想到的是提供實例方法,如下面這個例子的 getpVal 和 setpVal。
這樣做的弊端十分明顯,每一個實例都將擁有getpVal和setpVal,是否可以在原型上定義getpVal和setpVal,先來試試。
function Class01( val, pVal ) {
this.val = val; /*實例可直接讀寫的屬性*/
var pVal = pVal; /*實例無法直接讀寫的屬性*/
this.getpVal = function () {
return pVal;
};
this.setpVal = function ( v ) {
pVal = v;
return this;
}
}
下面的例子是通過原型方法讀取“私有”屬性pVal,試過發現,運行報錯,pVal並不存在,路子都被堵死了,采用這種方式定義“私有”屬性將只能通過實例方法讀寫。
這種方式顯然不能在工程中使用,下面介紹另外一種方法
function Class01( val, pVal ) {
this.val = val; /*實例可直接讀寫的屬性*/
var pVal = pVal; /*實例無法直接讀寫的屬性*/
}
Class01.prototype.printVal = function (){
var _this = this;
console.log( _this.val );
};
Class01.prototype.getpVal = function (){
console.log( pVal );
};
var ins01 = new Class01( 1, 2 );
ins01.getpVal(); /*此處報錯*/
采用高階函數,return function的方式,在Class01的內部只定義可直接讀寫的屬性,而把“私有”屬性或方法放到Class作用域外部
var Class01 = ( function () {
var pValue = ''; /*實例無法直接讀寫的屬性*/
function hello (){
console.log( '歡迎來到nDos的博客' );
}
function Class( val, pVal ){
this.val = val; /*實例可直接讀寫的屬性*/
pValue = pVal;
}
Class.prototype.printVal = function (){
var _this = this;
console.log( _this.val );
};
Class.prototype.getpVal = function (){
console.log( pValue );
return pValue;
};
Class.prototype.setpVal = function ( v ){
pValue = v;
return this;
};
Class.prototype.sayHello = function (){
hello();
return this;
};
return Class;
} )();
var ins01 = new Class01( 1, 2 );
ins01.getpVal();
ins01.setpVal( '2222' ).getpVal();
ins01.sayHello();
小問題:實例 ins01 是由 Class 實例化而來,還是由 Class01 實例化而來。
console.log( ins01.constructor.name ); /* 此處是 Class */
console.log( ins01 instanceof Class01 ); /* 此處是 true */
顯然在這里會在類的使用過程中造成疑惑,也會使得項目的實例的歸屬產生問題。可以把 Class01 直接改為 Class 即可。
小tips:在 Google 開發者工具中找到 Class.__proto__.constructor["[[Scopes]]"]
在這里可以看到閉包、全局作用域等信息。
至此,function 類中已經可以實現實例屬性、實例私有屬性和方法、原型方法。
實例方法一般不會在類中定義,而是在實例化之后有客戶自行添加。
原型上也一般不會定義屬性,原因在於原型屬性並不能讀寫(對於數值字符串和布爾值),若可讀寫會影響所有實例。
小tips:對於私有屬性和方法,建議將它們寫入到一個對象當中,進行塊狀管理,在大型項目中代碼結構清晰一些。
var pV = { pVal:'', hello:() => console.log('ok') }
傳統面向對象設計中類還存在靜態方法和靜態屬性,在 javascript 中也可以輕松實現。
靜態方法只能通過類調用,靜態屬性也只能如此使用。靜態屬性和方法,類的實例無法獲取。
var Class = ( function (){
/*代碼略*/
Class.prototype.nDos = {
name: 'nDos',
sayHello: hello
};
Class.prototype.noChangeVal = '實例拿我沒辦法';
Class.staticVal = 'Class的靜態屬性';
Class.staticMethod = function (){
console.log( 'Class的靜態方法' );
};
return Class;
} )();
var ins01 = new Class( 1, 2 ),
ins02 = new Class( 'a', 'b' );
ins01.nDos.name = 'ins01 say hello nDos'; /*對於數組也是一樣的*/
console.log( 'ins02中的name值:' + ins02.nDos.name );
try {
ins01.noChangeVal = '實例1改變原型屬性值';
console.log( ins01.noChangeVal );
console.log( ins02.noChangeVal );
ins01.prototype.noChangeVal = '我就是想改變它'; /*報錯*/
} catch ( e ) {
console.Error( e );
}
總結:
1、靜態屬性和原型屬性,都可以用來儲存實例化次數,同樣也可以用來儲存每個實例的引用。
此處建議盡量使用靜態屬性,不使用原型屬性的原因在於原型屬性容易被實例屬性覆蓋。
2、顯然 function 也可以被當作函數執行,在實際項目中為了防止這種情況發生,需要加入防御性代碼:
if ( this.constructor.name !== 'Class' ) {
throw new Error( '類只能被實例化' );
}
就算這么做了,還是可以繞過去:
function fakeClass () {
Class01.call(this, '1', '2');
}
fakeClass.prototype = Class.prototype;
var ins = new fakeClass();
當然這算是繼承里邊的內容。
3、為避免2中出現的情況,就是加入以下代碼:
if ( !new.target || new.target.name !== 'Class' ) {
throw new Error( '類只能被實例化' );
}
4、只有函數才能被實例化,實例化之后得到的變量是實例並不是函數或者說是 object。
5、function 類實例化過程,實際上是函數體的執行過程。這個執行過程也就是初始化的過程。
在這種過程當中可以做相當多的事情,比如 上述的防御性代碼、實例(this)的傳遞與儲存、使用 Object.assign 給this擴充功能(Mixin)
以及加入鈎子函數,讓類消費者自行決定如何實例化等
6、function 類一般不會有 return 當然也可以存在。若沒有 return,函數會自動返回 this 做為類的實例。
若存在 return,該類生成的內容便可比較靈活豐富,讀者可自行想象,比如通過不同的鈎子函數返回不同的內容。
結尾是學習用源代碼:
var Class = ( function (){
var pValue = ''; /*實例無法直接讀寫的屬性*/
function hello (){
console.log( '歡迎來到nDos的博客' );
}
function Class( val, pVal ){
if ( this.constructor.name !== 'Class' ){
throw new Error( '類只能被實例化' );
}
if ( !new.target || new.target.name !== 'Class' ){
throw new Error( '類只能被實例化' );
}
this.val = val; /*實例可直接讀寫的屬性*/
pValue = pVal;
}
Class.prototype.printVal = function (){
var _this = this;
/*盡管此處並不會出現this丟失的情況,但推薦總是這么做*/
console.log( _this.val );
};
Class.prototype.getpVal = function (){
console.log( pValue );
return pValue;
};
Class.prototype.setpVal = function ( v ){
pValue = v;
return this;
};
Class.prototype.sayHello = function (){
hello();
return this;
};
Class.prototype.nDos = {
name: 'nDos',
sayHello: hello
};
Class.prototype.noChangeVal = '實例拿我沒辦法';
Class.staticVal = 'Class的靜態屬性';
Class.staticMethod = function (){
console.log( 'Class的靜態方法' );
};
return Class;
} )();
var ins01 = new Class( 1, 2 ),
ins02 = new Class( 'a', 'b' );
ins01.nDos.name = 'ins01 say hello nDos'; /*對於數組也是一樣的*/
console.log( 'ins02中的name值:' + ins02.nDos.name );
try {
ins01.noChangeVal = '實例1改變原型屬性值';
console.log( ins01.noChangeVal );
console.log( ins02.noChangeVal );
/* ins01.prototype.noChangeVal = '我就是想改變它'; 報錯*/
} catch ( e ) {
console.error( e );
}
function fakeClass(){
Class.call( this, '1', '2' );
}
fakeClass.prototype = Class.prototype;
var ins = new fakeClass();