在ES6出現(xiàn)之前,JavaScript不能真正被稱為 面向?qū)ο蟮木幊陶Z言,因為 class 僅僅作為其保留字而非關(guān)鍵字,而ES6之后,引入了class,使程序員可以用自己更加熟悉的方式創(chuàng)建對象;
至于ES6和ES5有什么區(qū)別,應(yīng)該就是上面提到的可以讓程序員更爽地coding,而程序員爽了,根據(jù) 工作量守恒定律,總有某個事物要干更多的活,沒錯,就是計算機。
為了兼容某些不支持ES6的瀏覽器,我們可以引入 Babel 庫將ES6代碼 “編譯” 成ES5之后執(zhí)行,而對于支持ES6的瀏覽器,在其JavaScript引擎中會自動進行“編譯”操作;
所以,總體上看,ES6和ES5在功能上是等效的,即用ES6能完成的任務(wù),用ES5必然能夠完成,只是在語法上,ES6提供了更多的 語法糖,讓程序員嘗到甜頭;所以為了更好的理解JavaScript對象,我們回歸初心,從ES5中窺視JavaScript所創(chuàng)建的對象世界;
對象的創(chuàng)建
對象有兩個基本元素:屬性和方法;
屬性用于存儲數(shù)據(jù),方法用于存儲代碼;接下來,我們從簡單到復(fù)雜,來理解JavaScript創(chuàng)建對象的演變史。
創(chuàng)建Object對象并賦值時代
我們可以通過 new Object 創(chuàng)建Object,并為其賦屬性和方法來創(chuàng)建對象:
// 代碼段 1
var p = new Object();
p.name = "x";
p.age = 20;
p.say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
這樣我們就得到一個含有2個屬性,1個方法的對象;
通過執(zhí)行 p.say() 得到輸出 : My name is x ,this year is 20 ;
但是,這些幾行代碼非常松散,每一行代碼都像一條單獨的語句,為了體現(xiàn)這些屬性和方法是一個整體,而不是一個個獨立的存在,我們需要進入下一個時代;
字面量賦值時代
我們通過給一個變量賦一個字面量,即可創(chuàng)建一個對象:
// 代碼段 2
var p = {
name : "x" ,
age : 20 ,
say : function(){
console.log("My name is",this.name,",this year is",this.age);
}
}
這樣,我們就創(chuàng)建好了一個對象,這個對象有了自己的屬性和方法;
通過 console.log(typeof p) ,輸出為object可知, p 的類型是一個對象;
代碼段1和代碼段2相比,代碼段2的結(jié)構(gòu)更為合理,所有的屬性和方法都用大括號包含,更利于閱讀,也體現(xiàn)了整體性,但功能上是等效的;
不過我們又發(fā)現(xiàn),每次要創(chuàng)建一個相似的對象,都需要寫一遍屬性名,非常地不優(yōu)雅,所以,我們又要進入下一個時代;
工廠模式時代
我們可以通過調(diào)用一個函數(shù)創(chuàng)建我們需要的對象,并返回該對象,從而使代碼可重用,這個函數(shù)就是傳說中的 工廠,代碼如下:
// 代碼 3
function createPerson(name,age){
var t = new Object();
t.name = name;
t.age = age;
t.say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
return t;
}
var p = createPerson("x",20);
代碼3是將代碼1變?yōu)榱斯S模式;
下面再將代碼2也變?yōu)楣S模式:
function createPerson(name,age){
return {
name:name,
age : age,
say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
};
}
var p = createPerson("x",20);
```
工廠模式的代碼要比前面兩個時代的代碼優(yōu)雅,我們只需要調(diào)用一個函數(shù)即可獲得我們想要的對象;
但是,我們又發(fā)現(xiàn)了一個新的問題(別問我為什么總是能發(fā)現(xiàn)新問題,因為就是有一雙善于觀察的眼睛,手動傲嬌 ^_^):
通過上述3中方式創(chuàng)建的對象在使用 `typeof` 時,返回的都是 `object`,而通過 `實例 instanceof 類` 只有在類為 `Object`時,才返回true,就是說,上面3中方法創(chuàng)建的對象都是無差別的對象,我們不能分辨出它們的類型;
這就麻煩了,比如我們有這么一個函數(shù):
```javascript
function seeDoctor( o ){
if(o是人){
請人醫(yī)治療
}
if(o是動物){
請獸醫(yī)治療
}
}
```
那我們創(chuàng)建的對象因為不能判斷其是人是獸,將不能選擇適合的治療方案;
要解決這個問題,有兩種思路:
- 給對象添加信息,即為每一個對象添加一個屬性 `type` ,用于指明其類型;
- 讓js解釋器能夠判斷其類型;
第一種方式比較 *丑陋*,我們需要管理更多的數(shù)據(jù),但比較容易理解;第二種是更優(yōu)雅的方法,也推動我們進入下一個時代;
### 構(gòu)造函數(shù)時代
構(gòu)造函數(shù)時代的主要任務(wù)是讓創(chuàng)建的對象自帶類別說明屬性,即通過`instanceof` 就能判斷出其所屬的類:
```javascript
// 類是一個函數(shù),約定:
// 普通函數(shù)第一個字母小寫,類函數(shù)第一個字母大寫
function Person(name,age){
this.name = name;
this.age = age;
this.say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
}
function Animal(){}
var p = new Person("x",20) ;
```
注意,創(chuàng)建對象時,必須使用關(guān)鍵字 **new** 。
此時,我們通過 `p instanceof Person`,返回的結(jié)果為 `true`,而通過 `p instanceof Animal` ,返回結(jié)果為 `false`,從而使對象實例自帶類型屬性;
完美! But,又雙叒叕發(fā)現(xiàn)了不足,我們用上述各種函數(shù)創(chuàng)建兩個對象:
```javascript
var p1 = new Person("x",20);
var p2 = new Person("y",21);
console.log(p1.say==p2.say) ; // 輸出的是false
```
我們發(fā)現(xiàn):雖然 *say* 函數(shù)的代碼相同,但兩個對象實例的 *say* 居然指向不同的代碼塊,如果我們有100個實例,相同的代碼塊就需要有100份,極大的內(nèi)存浪費,這是我們所不能忍受的,因此迫切希望下一個時代的到來!
### 構(gòu)造函數(shù)+原型時代
原型就是所有對象實例所共享的一個 **對象**,這個對象中的屬性就是共享屬性(在c++中稱為靜態(tài)變量),方法就是共享方法;
```javascript
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
var p = new Person("x",20);
```
上述代碼創(chuàng)建的對象實例 p 有自己的屬性name和age,以及共享的方法say;通過 `p.say()` 即可打印出:*My name is x ,this year is 20* ;
執(zhí)行 `p.say()` 的時候,p先搜索其自身是否有方法 `say`,如果有,就執(zhí)行,如果沒有,就搜索其原型對象是否有 `say` 方法,如果還是沒有,就搜索其原型對象的原型對象是否有say屬性(即沿著原型鏈搜索say方法,這也是繼承的實現(xiàn)機制),如果原型鏈上都沒有say方法,就拋出錯誤,否則,執(zhí)行搜索到的方法。
p通過屬性`p.__proto__` 指向原型對象 `Person.prototype` , 從而獲取原型上的所有屬性和方法;
如果p上也定義一個方法 `say`:
```javascript
p.say = function(){console.log("Hello,world");}
```
則該方法將會 **覆蓋** 原型上的say方法,即調(diào)用 `p.say` 輸出的將是 *Hello,world* ,而如果通過 `delete p.say` 刪除掉 `say` 屬性,則調(diào)用 `p.say` 時,執(zhí)行的代碼又是原型上的 say 代碼;
總結(jié):原型就是一個類所創(chuàng)建的所有對象實例共享的一個對象;
但是,原型對象的定義和構(gòu)造函數(shù)分開了,這又使結(jié)構(gòu)不太優(yōu)美,所以,我們又得進入下一個時代;
### 構(gòu)造原型時代
為了解決原型定義和構(gòu)造函數(shù)分離的問題,我們決定將原型定義放到構(gòu)造函數(shù)中,就出現(xiàn)了以下代碼:
```javascript
function Person(name,age){
this.name = name;
this.age = age;
Person.prototype.say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
}
var p = new Person("x",20);
```
OK,完成了原型定義和構(gòu)造函數(shù)的合并,結(jié)構(gòu)也變得更加優(yōu)美了,但是,又出現(xiàn)了一個問題:
每次執(zhí)行創(chuàng)建 Person 對象實例的時候,都要重新定義一遍 `Person.prototype.say` 方法,雖然這不會增加內(nèi)存泄漏(以前定義的say代碼由于沒有被引用,內(nèi)存塊將會被自動回收),但卻增加了cpu的工作量,所以我們需要進入下一個時代;
### 優(yōu)化構(gòu)造原型時代
為了避免 `Person.prototype.say` 函數(shù)的重復(fù)定義,我們可以先判斷該函數(shù)是否已定義,如果沒有定義,再對其進行定義:
```javascript
function Person(name,age){
this.name = name;
this.age = age;
if(typeof(Person.prototype.say)=="undefined"){
Person.prototype.say = function(){
console.log("My name is",this.name,",this year is",this.age);
} ;
}
}
var p = new Person("x",20);
```
通過以上7個時代的迭代,我們終于在 JavaScript中創(chuàng)建了一個基本上符合我們要求的對象;
完!