JS雖然是一個面向對象的語言,可是不是典型的面嚮對象語言。Java/C++的面向對象是object - class
的關係,而JS是object - object
的關係,中間經過原型prototype鏈接,父類和子類造成一條原型鏈。本文經過分析JS的對象的封裝,再探討正確實現繼承的方式,而後討論幾個問題,最後再對ES6新引入的類class
關鍵字做一個簡單的說明。html
JS的類實際上是一個函數function,因爲不是典型的OOP的類,所以也叫僞類。理解JS的類,須要對JS裏的function有一個比較好的認識。首先,function自己就是一個object,能夠看成函數的參數,也能夠看成返回值,跟普通的object無異。而後function能夠看成一個類來使用,例如要實現一個String類git
1
2
3
4
5
6
7
|
var MyString = function(str){
this.content = str;
};
var name = new MyString("hanMeimei");
var addr = new MyString("China");
console.log(name.content + " live in " + addr.content);
|
第一行聲明瞭一個MyString的函數,獲得一個MyString類,同時這個函數也是MyString的構造函數。第5行new一個對象,會去執行構造函數,this指向新產生的對象,第2行給這個對象添加一個content的屬性,而後將新對象的地址賦值給name。第6行又去新建一object,注意這裏的this指向了新的對象,所以新產生的content和前面是不同的。github
上面的代碼在瀏覽器運行有一點問題,由於這段代碼是在全局做用域下運行,定義的name變量也是全局的,所以實際上執行var name = new MyString(「」)等同於window.name = new MyString(「」),因爲name是window已經存在的一個變量,做爲window.open的第二個參數,可用來跨域的時候傳數據。但因爲window.name不支持設置成自定義函數的實例,所以設置無效,仍是保持默認值:值爲」[object Object]」的String。解決辦法是把代碼的運行環境改爲局部的,也就是說用一個function包起來:web
1
2
3
4
|
(function(){
var name = new MyString("hanMeimei");
console.log(name.content); //正確,輸出hanMeimei
})();
|
因此今後處看到,代碼用一個function包起來,不去污染全局做用域,仍是挺有必要的。接下來,回到正題。chrome
JS裏的每個function都有一個prototype屬性,這個屬性指向一個普通的object,即存放了這個object的地址。這個function new出來的每一個實例都會被帶上一個指針(一般爲__proto__)指向prototype指向的那個object。其過程相似於:編程
1
2
|
var name = new MyString(); //產生一個對象,執行構造函數
name.__proto__ = MyString.prototype; //添加一個__proto__屬性,指向類的prototype(這行代碼僅爲說明)
|
以下圖所示,name和addr的__proto__指向MyString的prototype對象:設計模式
能夠看出在JS裏,將類的方法放在function的prototype裏面,它的每一個實例都將得到類的方法。 跨域
如今爲MyString添加一個toString的方法:瀏覽器
1
2
3
|
MyString.prototype.toString = function(){
return this.content;
};
|
MyString的prototype對象(object)將會添加一個新的屬性。app
這個時候實例name和addr就擁有了這個方法,調用這個方法:
1
2
|
console.log(name.toString()); //輸出hanMeimei
console.log(name + " lives in " + addr); //「+」鏈接字符時,自動調用toString,輸出hanMeimei lives in China
|
這樣就實現了基本的封裝——類的屬性在構造函數裏定義,如MyString的content;而類的方法在函數的prototype裏添加,如MyString的toString方法。
這個時候,考慮一個基礎的問題,爲何在原型上添加的方法就能夠被類的對象引用到呢?由於JS首先會在該對象上查找該方法,若是沒有找到就會去它的原型上查找。例如執行name.toString(),第一步name這個object自己沒有toString(只有一個content屬性),因而向name的原型對象查找,即__proto__指向的那個object,發現有toString這個屬性,所以就找到了。
要是沒有爲MyString添加toString方法呢?因爲MyString其實是一個Function對象,上面定義MyString語法做用等效於:
1
2
|
//只是爲了示例,應避免使用這種語法形式,由於會致使兩次編譯,影響效率
var MyString = new Function("str", "this.content = str");
|
經過比較MyString和Function的__proto__,能夠從側面看出MyString實際上是Function的一個實例:
1
2
|
console.log(MyString.__proto__); //輸出[Function: Empty]
console.log(Function.__proto__); //輸出[Function: Empty]
|
MyString的__proto__的指針,指向Function的prototype,經過瀏覽器的調試功能,能夠看到,這個原型就是Object的原型,以下圖所示:
由於Object是JS裏面的根類,全部其它的類都繼承於它,這個根類提供了toString、valueOf等6個方法。
所以,找到了Object原型的toString方法,查找完成並執行:
1
|
console.log(name.toString()); //輸出{ content: 'hanMeimei' }
|
到這裏能夠看到,JS裏的繼承就是讓function(如MyString)的原型的__proto__指向另外一個function(如Object)的原型。基於此,寫一個自定義的類UnicodeString繼承於MyString
1
|
<span style="font-size: 15px;"><span style="color: #0000ff;">var</span> UString = <span style="color: #0000ff;">function</span>(){ };</span>
|
實現繼承:
1
|
UString.prototype = MyString.prototype; //錯誤實現
|
注意上面的繼承方法是錯誤的,這樣只是簡單的將UString的原型指向了MyString的原型,即UString和MyString使用了相同的原型,子類UString增刪改原型的方法,MyString也會相應地變化,另一個繼承MyString如AsciiString的類也會相應地變化。依照上文分析,應該是讓UString的原型裏的的__proto__屬性指向MyString的原型,而不是讓UString的原型指向MyString。也就是說,得讓UString有本身的獨立的原型,在它的原型上添加一個指針指向父類的原型:
1
|
UString.prototype.__proto__ = MyString.prototype; //不是正確的實現
|
由於__proto__不是一個標準的語法,在有些瀏覽器上是不可見的,若是在Firefox上運行上面這段代碼,Firefox會給出警告:
mutating the [[Prototype]] of an object will cause your code to run very slowly; instead create the object with the correct initial [[Prototype]] value using Object.create
合理的作法應該是讓prototype等於一個object,這個object的__proto__指向父類的原型,所以這個object需要是一個function的實例,而這個function的prototype指向父類的原型,因此得出如下實現:
1
2
3
4
5
6
7
|
Object.create = function(o){
var F = function(){};
F.prototype = o;
return new F();
};
UString.prototype = Object.create(MyString.prototype);
|
代碼第2行,定義一個臨時的function,第3行讓這個function的原型指向父類的原型,第4行返回一個實例,這個實例的__proto__就指向了父類的prototype,第7行再把這個實例賦值給子類的prototype。繼承的實現到這裏基本上就完成了。
可是還有一個小問題。正常的prototype裏面會有一個constructor指向構造函數function自己,例如上面的MyString:
這個constructor的做用就在於,可在原型裏面調用構造函數,例如給MyString類增長一個copy拷貝函數:
1
2
3
4
5
6
7
8
|
MyString.prototype.copy = function(){
// return MyString(this.content); //這樣實現有問題,下面再做分析
return new this.constructor(this.content); //正確實現
};
var anotherName = name.copy();
console.log(anotherName.toString()); //輸出hanMeimei
console.log(anotherName instanceof MyString); //輸出true
|
問題就於:Object.create的那段代碼裏第7行,徹底覆蓋掉了UString的prototype,取代的是一個新的object,這個object的__proto__指向父類即MyString的原型,所以UString.prototype.constructor在查找的時候,UString.prototype沒有constructor這個屬性,因而向它指向的__proto__查找,找到了MyString的constructor,所以UString的constructor其實是MyString的constuctor,以下所示,ustr2其實是MyString的實例,而不是指望的UString。而不用constructor,直接使用名字進行調用(上面代碼第2行)也會有這個問題。
1
2
3
4
5
|
var ustr = new UString();
var ustr2 = ustr.copy();
console.log(ustr instanceof UString); //輸出true
console.log(ustr2 instanceof UString); //輸出false
console.log(ustr2 instanceof Mystring); //輸出true
|
因此實現繼承後須要加多一步操做,將子類UString原型裏的constructor指回它本身:
1
|
UString.prototype.constructor = UString;
|
在執行copy函數裏的this.constructor()時,實際上就是UString()。這時候再作instanseof判斷就正常了:
1
|
console.log(ustr2 instanceof Ustring); //輸出true
|
能夠把相關操做封裝成一個函數,方便複用。
基本的繼承核心的地方到這裏就結束了,接下來還有幾個問題須要考慮。
第一個是子類構造函數裏如何調用父類的構造函數,直接把父類的構造函數看成一個普通的函數用,同時傳一個子類的this指針:
1
2
3
4
5
6
7
|
var UString = function(str){
// MyString(str); //不正確的實現
MyString.call(this, str);
};
var ustr = new UString("hanMeimei");
console.log(ustr + ""); //輸出hanMeimei
|
注意第3行傳了一個this指針,在調用MyString的時候,這個this就指向了新產生的UString對象,若是直接使用第2行,那麼執行的上下文是window,this將會指向window,this.content = str等價於window.content = str。
第二個問題是私有屬性的實現,在最開始的構造函數裏定義的變量,其實例是公有的,能夠直接訪問,以下:
1
2
3
4
5
6
|
var MyString = function(str){
this.content = str;
};
var str = new MyString("hello");
console.log(str.content); //直接訪問,輸出hello
|
可是典型的面向對象編程裏,屬性應該是私有的,操做屬性應該經過類提供的方法/接口進行訪問,這樣才能達到封裝的目的。在JS裏面要實現私有,得藉助function的做用域:
1
2
3
4
5
6
7
8
|
var MyString = function(str){
this.sayHi = function(){
return "hi " + str;
}
};
var str = new MyString("hanMeimei");
console.log(str.sayHi()); //輸出hi, hanMeimei
|
可是這樣的一個問題是,必須將函數的定義放在構造函數裏,而不是以前討論的原型,致使每生成一個實例,就會給這個實例添加一個如出一轍的函數,形成內存空間的浪費。因此這樣的實現是內存爲代價的。若是產生不少實例,內存空間會大幅增長,這個問題是不可忽略的,所以在JS裏面實現屬性私有不太現實,即便在ES6的class語法也沒有實現。可是能夠給類添加靜態的私有成員變量,這個私有的變量爲類的全部實例所共享,以下面的案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var Worker;
(function(){
var id = 1000;
Worker = function(){
id++;
};
Worker.prototype.getId = function(){
return id;
};
})();
var worker1 = new Worker();
console.log(worker1.getId()); //輸出1001
var worker2 = new Worker();
console.log(worker2.getId()); //輸出1002
|
上面的例子使用了類的靜態變量,給每一個worker產生惟一的id。同時這個id是不容許worker實例直接修改的。
第三個問題是虛函數,在JS裏面討論虛函數是沒有太大的意義的。虛函數的一個很大的做用是實現運行時的動態,這個運行時的動態是根據子類的類型決定的,可是JS是一種弱類型的語言,子類的類型都是var,只要子類有相應的方法,就能夠傳參「多態」運行了。比強類型的語言如C++/Java做了很大的簡化。
最後再簡單說下ES6新引入的class關鍵字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
//須要在strict模式運行
'use strict';
class MyString{
constructor(str){
this.content = str;
}
toString(){
return this.content;
}
//添加了static靜態函數關鍵字
static concat(str1, str2){
return str1 + str2;
}
}
//extends繼承關鍵字
class UString extends MyString{
constructor(str){
//使用super調用父類的方法
super(str);
}
}
var str1 = new MyString("hello"),
str2 = new MyString(" world");
console.log(str1); //輸出MyString {content: "hello"}
console.log(str1.content); //輸出hello
console.log(str1.toString()); //輸出hello
console.log(MyString.concat(str1, str2));//輸出hello world
var ustr = new UString("ustring");
console.log(ustr); //輸出MyString {content: "ustring"}
console.log(ustr.toString()); //輸出ustring
|
從輸出的結果來看,新的class仍是沒有實現屬性私有的功能,見第27行。而且從第26行看出,所謂的class其實就是編譯器幫咱們實現了上面複雜的過程,其本質是同樣的,可是讓代碼變得更加簡化明瞭。一個不一樣點是,多了static關鍵字,直接用類名調用類的函數。ES6的支持度還不高,最新的chrome和safari已經支持class,firefox的支持性還不太好。
最後,雖然通常的網頁的JS不少都是小工程,看似不須要封裝、繼承這些技術,可是若是若是可以用面向對象的思想編寫代碼,無論工程大小,只要應用得當,甚至結合一些設計模式的思想,會讓代碼的可維護性和擴展性更高。因此平時能夠嘗試着這樣寫。
原博客園地址:http://www.cnblogs.com/yincheng/p/4943789.html
參考:
1. Professional Javascript for web developers(JavaScript高級程序設計) 第6章 Object – Oriented Programming
2. The Node Craftsman Book第一部分 Object-oriented JavaScript
3. Why is it necessary to set the prototype constructor Stackoverflow