大多数的开发者在使用Java或C#等基于类的语言的过程中学会了面向对象编程。由于JavaScript没有对类的正式支持(当然ES2015支持了),这些开发者在学习JavaScript时往往会迷失方向。JavaScript不需要在开头就定义好各种类,你可以在写代码過程中根據需要創建數據結構。由於JavaScript缺少類,也就缺少用於對類進行分組的包。在Java中,包和類的名字不僅定義了對象的類型,也在工程中列出文件和目錄的層次結構,JavaScript編程就好像從一塊空白石板開始:你可以在上面組織任何你想要的東西。有些開發者選擇模仿其他語言的結構,也有一下人則利用JavaScript的靈活性來創建一些新的東西,對沒有掌握JavaScript的人來說,這種選擇的自由令人崩潰,然而你一旦熟悉了它,你會發現JavaScript是一個無比靈活的語言,可以很輕鬆的適應你的編程習慣。
爲了便於開發者的從傳統的面向對象語言過渡,JavaScript把對象作爲語言的中心。幾乎所有JavaScript的數據要麼是一個對象要麼從對象中獲取。其實就連函數在JavaScript中也視爲對象,這使得它們成爲了JavaScript的一等公民。
使用和理解對象是整個JavaScript的關鍵。你可以在任何時候創建對象,在任何時候給對象添加,刪除屬性。JavaScript對象是如此的靈活,可以創造出其他語言不具有的獨特而有趣的模式。
本章致力於鑑別和使用兩種JavaScript基本數據類型:原始類型和引用類型。雖然兩種都是通過對象進行訪問,但是理解它們行爲之間的區別是非常重要的。
1.1 什麼是類型
JavaScript雖然沒有類的概念,但依然存在兩種類型:原始類型和引用類型。原始類型保存爲簡單數據值。引用類型則保存爲對象,其本質是指向內存位置的引用。
爲了讓開發者都能夠把原始類型和引用類型按相同的方式處理,JavaScript花費了很大努力來保證語言的一致性。
其他編程語言用棧存儲原始類型,用堆存儲引用類型,JavaScript則完全不同:它使用一個變量對象追蹤變量的生存期。原始值被直接保存在變量對象內,而引用值則作爲一個指針保證在變量對象內,該指針指向實際對象在內存在的存儲位置。雖然看上去原始值和引用值一樣,但是它們還是有區別的,本章稍後會介紹。
當然,原始類型和引用類型還有其他區別。
1.2 原始類型
原始類型代表照原樣保存的一些簡單數據,如true和25.JavaScript共有5種原始類型,如下:
boolean 布爾,值爲true或false
number 數字,值爲任何整型或浮點數值
string 字符串,值爲由單引號或雙引號擴出的單個字符或連續字符(JavaScript不區分字符類型)
null 空類型,該原始值有且僅有一個值:null
undefined 未定義,該原始類型也是有且僅有一個值:undefined(undefined會被賦給一個還沒有初始化的變量)
前三種(boolean,number,string)表現的行爲類似,而後兩種(null和undefined)則有一點區別,本章後面將會討論。所有原始類型的值都有字面形式。字面形式是不被保存在變量種的值,如硬編碼的姓名或價格。下面是每種類型使用字面形式的例子。
//strings
var name ="Nicholas";
var selection="a";
//number
var count=5;
var cost=1.51;
//boolean
var found=true;
//null
var object=null;
//undefined
var flag=undefined;
var ref;//assigned undefined automatically
JavaScript和許多其他語言一樣,原始類型的變量直接保存原始值(而不是一個指向對象的指針)。當你將原始值賦值給一個變量時,該值將被賦值到變量中。也就是說,如果你使一個變量等於另一個變量時,每個變量有它自己的一份數據拷貝。例如,
var color1="red";
var color2=color1;
這裏,color1被賦值爲"red"。變量color2被賦予color1的值,這樣變量color2中就保存了"red"。雖然color1和color2具有同樣的值,但是兩者毫無關聯,改變color1的值是不影響color2,反之亦然。這是因爲存在兩個不同的存儲地址,每個變量擁有一個。
因爲每個含有原始值的變量使用自己的存儲空間,一個變量的改變不會影響到其他變量。例如
var color1="red";
var color2=color1;
console.log(color1);//red
console.log(color2);//red
color1="blue";
console.log(color1);//blue
console.log(color2);//red
這段代碼中,color1被改爲“blue,而color2還保有原來的值"red”。
1.2.1 鑒別原始類型
鑒別原始類型的最佳方法是使用typeof操作符。它可以被用在字符串,數字,布爾和未定義類型。下面是typeof對不同原始類型的輸出。
console.log(typeof "nicholas"); //"string"
console.log(typeof 10);// number
console.log(typeof 5.1 ); //number
console.log(typeof true); //boolean
console.log(typeof undefined) undefined
正如我們所期望的那樣,對於字符串,typeof將返回"string",對於數字將返回'number'(無論整型還是浮點型),對於布爾類型將返回‘boolean’,對於未定義類型將返回'undefined'。對於空類型則有一些棘手。
下面那行代碼的運行結果困擾了很多開發者
console.log(typeof null);//object
當你運行typeof null時,結果是object。但是這是為什麼呢?(其實這已經被設計和維護javascript的委員會TC39認定是一個錯誤。在邏輯上,你可以認為null是一個空的對象指針,所以結果為‘object’,但這還是令人困惑)
判斷一個值是否為空類型嘴賤的方法是直接和null比較,如下。
console.log(value===null) //true or false
非強制轉換比較:
三等號在進行比較時不會將變量強制轉換為另一種類型
1.3 引用類型
引用類型是指javascript中的對象,同時也是你在該語言中能找到的最接近類的東西。引用值時引用類型的實例,也是對象的同義詞(本章後面將用對象指代引用值)。對象是屬性的無序曆表。屬性包含鍵(始終是字符串)和值。如果一個屬性的值是函數,它就被稱為方法。javascript中函數其實是引用值,除了函數可以運行以外,一個包含數組的屬性和一個包含函數的屬性沒什麼區別。
當然,在使用對象前,你必須先創建它們。
1.3.1 創建對象
有時候,把javascript對象想像成哈希表可以幫助你更好的理解對象的結構。
javascript有好幾種方法可以創建對象,或者說實例化對象。第一種是使用new操作符合構造函數。構造函數就是通過new操作符來創建對象的函數--任何函數都可以是構造函數。根據命名規範,javascript中的構造函數用首字母大寫來跟非構造函數進行區分。例如下列代碼實例化一個通用對象,並將它的引用保存在object中。
var object = new Object();
因為引用類型不在變量中直接保存對象,所以本例中的object變量實際上並不包含對象的實例,而是一個指向內存中實際對象所在位置的指針(或者說引用)。這是對象和原始值之間的一個基本差別,原始值是直接保存在變量中的。
當你將一個對象賦值給變量時,實際是賦值給這個變量一個指針。這意味著,將一個變量賦值給另一個變量時,兩個變量各獲得了一份指針的拷貝,指向內存中的同一個對象。例如,
var object1=new Object();
var object2=object1;
這段代碼先用new創建了一個對象並將其引用保存在object1中。然後將object1的值賦值給object2.兩個變量都指向第一行被創建的那個對象實例。
1.3.2 對象的引用解除
javascript語言有垃圾收集的功能,因此當你使用引用類型時無需擔心內存分配問題。但最好在不使用對象時將其引用解除,讓垃圾收集器對那塊內存進行釋放。解除引用的最佳手段是將對象變量重置為null。
var object1=new Object();
//do something
object1=null;//dereference
這裏,對象object1被創建然後使用,最後設置為null。當內存中的對象不再被引用後,垃圾收集器會把那塊內存挪作它用(在那些使用幾百萬對象的巨型程序裡,對象引用解除尤其重要)
1.3.3 添加刪除屬性
在javascript中,對象另一個有趣的地方是你可以隨時添加和刪除其屬性。例如,
var object1=new Object();
var object2=object1;
object1.myCustomProperty="Awesome!";
console.log(object2.myCustomProperty);//"Awesome!"
這裏,object1上增加了myCustomProperty屬性,值為"Awesome!”。該屬性也可以被object2訪問到,因為object1和object2指向同一個對象。
除了通用對象引用類型以外,javascript還有其他一些內建類型任你使用。
1.4 內建類型實例化
你已經見過如何使用new Object()創建和使用通用對象。Object類型只是javascript提供的少量內建引用類型之一。其他內建類型各有它們的特殊用途,可在任何時候被實例化。
這些內建類型如下:
Array 數組類型,以數字為索引的一組值的有序列表
Data 日期和和時間類型
Error 運行期錯誤類型(還有一些更特別的錯誤例子類型)
Function 函數類型
Object 通用對象類型
RegExp 正則表達式類型
可以用new來實例化每一個內建引用類型,如下:
var items =new Array();
var now =new Date();
var error =new Error("Something bad happened");
var func =new Function("console.log('Hi')");
var object=new Object();
var re=new RegExp("\\d+");
1.4.1 字面形式
內建引用類型有字面形式。字面形式允許你在不需要使用new操作符合構造函數顯示創建對象的情況下生成引用值(你曾在本章前面見過原始類型的字面形式,包括字符串,數字,布爾,空類型和為定義)。
1.4.2 對象和數組字面形式
要用對象字面形式創建對象,可以在大括號內定義一個新對象及其屬性。屬性的組成包括一個表示符或字符串,一個冒號以及一個值。多個屬性值之間用逗號分割。例如
var book ={
name:"The Principles of Object-Oriented JavaScript",
year:2014
};
屬性名字也可以用字符串表示,特別是當你希望名字中包含空格或其他特殊字符時。
var book={
"name":"The Principles of Object-Oriented JavaScript",
"year":2014
};
本例等價於前一個例子,僅在語法上有區別,下面是另一個等價寫法。
var book=new Object();
book.name="The Principles of Object-Oriented JavaScript";
book.year=2014;
雖然3例的結果是一致的:一個具有兩個屬性的對象。寫法完全取決於你。
注意:雖然使用字面形式並沒有調用new Object(),但是javascript引擎背後做的工作和new Object()一樣,除了密友調用構造函數,其他引用 類型的字面形式也是如此。
定義數組的字面形式是在中括號內使用逗號區分的人士數量的值。例如:
var colors=["red","blue","green"];
console.log(colors[0]);//''red"
這段代碼等價於
var colors=new Array("red","blue","green");
console.log(colors[0]);
1.4.3 函數字面形式
基本上都要用字面形式來定義函數。考慮到在可維護性,易讀性和調試上的巨大挑戰,通常不會有人使用函數的構造函數,因此很少看到用字符串表示的代碼而不是實際的代碼。
使用字面形式創建函數更方便也更不容易出錯。如下例:
function reflect(value){
return value;
}
//is the same
var reflect=new Function("value","return value;");
1.4.4正則表達式字面形式
javascript允許使用字面形式而不是RexExp構造函數定義正則表達式。它們看上去類似Perl中的正則表達式;模式被包含在兩個“/”之間,第二個“/”後是由單字符表示的額外的選項。
var number =/\d+/g;
// is the same as
var numbers = new RegExp("\\d+","g");
使用字面形式比較方便的一個原因是你不需要擔心字符串中的轉億字符。如果使用RegExp構造函數,傳入模式的參數時一個字符串,你需要對任何反斜槓進行轉義。在javascript中,除非需要通過一個或多個字符串動態構造函數表達式,否則都建議使用字面形式而不是構造函數。
總之,除了函數,對內建類型沒有什麼正確或錯誤的實例化方法。很多開發者喜歡字面形式,另一些則喜歡用構造函數,你可以選擇能令你更舒服的那種。
1.5 訪問屬性
屬性時對象中保存的名字和值的配對。點號時javascript中訪問屬性的最通用的做法(就跟許多面向對象的語言一樣),不過也可以用中括號訪問javascript對象的屬性。
例如,下面的代碼使用點號。
var array=[];
array.push(12345);
也可以如下例用中括號,方法的名字現在由中括號中的字符串表示。
var array=[];
array["push"](12345);
在需要動態決定訪問那個屬性的時候,這個語法特別游泳。例如下例中的中括號允許你用變量而不是字符串字面形式來制定訪問的屬性。
var array=[];
var method="push";
array[method](12345);
在這段代碼中,變量method的值時“push”,因此在array上調用了push()方法。這種能力及其有用,你會在本書中隨處看到這種用法。記住一點:除了語法不同,在性能或其他方面點號和中括號都大致相同,唯一的區別在於中括號允許你在屬性名字上使用特殊字符。開發者通常認為點號更加容易讀,所以你更多的看到點號而不是中括號。
1.6 鑒別引用類型
函數是最容易鑒別的引用類型,因為對眼熟使用typeof操作符時,返回值是“function”。
function reflect(value){
return value;
}
console.log(typeof reflect);//function
對其他引用類型的鑒別則較為棘手,因為對於非函數的引用類型,typeof返回“object”。在處理很多不同類型的時候幫不上什麼忙,為了更方便的鑒別引用類型,可以使用javascript的instanceof操作符。
instanceof 操作符以一個對象和一個構造函數為參數。如果對象是構造函數所制定的類型的一個實例,instanceof返回ture;否則返回false,如下例:
var items =[];
var object={};
function reflect(value){
return value;
}
console.log(items instanceof Array); //true
console.log(object instanceof Object);//true
console.log(reflect instanceof Function); //true
本例用instanceof和構造函數測試了幾個值,它們真正的類型都正確鑒別出來(即使該構造函數並沒有被用於創造該變量)。
instanceof操作符可鑒別繼承類型。這意味著所有對象都是object的實例,因為所有引用類型都繼承自object。
1.7 鑒別數組
雖然,instanceof可以鑒別數組,但是有一個例外會影響網頁開發者:javascript的值可以在同一個網頁的不同框架之間傳來傳去。當你試圖鑒別一個引用值的類型的時候,這就有有可能成為一個問題,因為每一個頁面擁有它自己的上下文-Object,Array以及其他內建類型的版本。結果,當你把一個數組從一個框架傳到另一個框架時,instanceof就無法識別它,因為那個數組時來自不同框架的array的實例。
為了解決這個問題,ECMAScript5引入了array.isArray()來明確鑒別一個值是否為array的實例,無論該值來自哪裏該方法對來自任何上下文的數組都返回true。如果你的環境兼容ECMAScript 5,Array。isArray()是鑒別數組的最佳辦法。
var items=[];
console.log(Array.isArray(items));//true
大多數環境都在瀏覽器和node中指出這個方法,ie8或更糟的版本不支持該方法
1.8 原始封裝類型
javascript中一個最讓人困惑的部分可能就是原始封裝類型。原始封裝類型共有三種(String,Number and Boolean) 這些特殊引用類型的存在使得原始類型用起來和對象一樣方便。(如果你不得不用獨特的語法和或切換為基於過程的編成方式來獲取一個字符串,那就太讓人困惑了)。
當獲取字符串,數字或布爾值,原始封裝類型將被自動創建。例如,下面代碼第一行,一個原始字符串的值被賦給name,第二行代碼把name當成一個對象,使用點號調用了charAt方法
var name ="Nicholas";
var firstChar=name.charAt(0);
console.log(firstChar);
這是在背後發生的事情如下:
//what the Javascript engine does
var name ="Nicholas";
var temp =new String(name);
var firstChar=temp.CharAt(0);
temp =null;
console.log(firstChar);
由於第二行把字符串當成對象使用,javascript引擎創建了一個字符串實體讓charAt(0)可以工作。字符串對象的存在僅僅用於該語句並在隨後被銷毀(一個稱為自動打包的過程)。為了測試這多一點。試著給字符串添加一個屬性看看它是不是對象。
var name="Nicholeas";
name.last="Zakas";
console.log(name.last); //undefined
這段代碼試圖給字符串name添加last屬性,代碼運行時沒有錯誤,但是屬性消失了,到底放生了什麼?你可以在任何時候給一個真的對象添加屬性,屬性會保留至你手動刪除他們。原始封裝類型的屬性會消失時因為添加屬性的對象立刻就被銷毀了。
下面是在javascript引擎中實際發生的事情。
//what the javascript engine does
var name="Nicholas";
var temp = new String(name);
temp.last="Zakas";
temp =null;
var temp = new String(name);
console.log(temp.last);
temp=null;
實際上是在一個立刻就被銷毀的臨時對象上而不是字符串上添加了新的屬性,之後當你試圖訪問該屬性時,另一個不同的臨時對象被創建,而新屬性是不存在的。雖然原始封裝類型會被自動創建,在這些值上進行instanceof檢查對應類型的返回值都是false。
var name ="Nicholas";
var count =0;
var found =false;
console.log(name instanceof String ); //false
console.log(count instanceof Number) //false
console.log(found instanceof Boolean) //false
這是因爲臨時對象僅在值被讀取時創建。instanceof操作符並沒有真的讀取任何東西,也沒有臨時對象的創建,於是它告訴我們這些值並不屬於原始封裝類型。你也可以手動創建原始封裝類型,但是有某些副作用。
手動創建原始封裝類型實際會創建出一個object,這意味着typeof無法鑑別出來你實際保存的數據的類型。
另外,使用String,Number和Boolean對象和使用原始值有一定區別。例如,下列代碼使用了Boolean對象,對象的值是false,但是console.log("Found")亦然會被執行,這是因爲inyige對象在條件判斷語句中總是被認爲是true,無論該對象的值是不是等於false。
var found=new Boolean(false);
if(found){
console.log("Found");//this excutes
}
手工創建的原始封裝類型在其他地方很容易讓人誤解,在大多數情況下都只會導致錯誤,所以,除非有特殊情況,否則你應該避免這麼做。
1.9 總結
javascript中雖然沒有類,但是有類型。每個變量或數據都有一個對應的原始類型或引用類型。5種原始類型(字符串,數字,布爾,空類型和未定義)的值會被直接保存在變量對象中,除了空類型,都可以用typefo來鑑別,空類型必須直接跟null進行比較才能鑑別。
引用類型是javascript中最接近類的東西,而對戲則是引用類型的實例。可以用new操作符或字面形式創建新對象。通常可以用點號訪問屬性和方法,也可以用中括號,函數在javascript中也是對象,可以用typeof來鑑別它們。至於其他引用類型,你應該用instanceof和一個構造函數來鑑別。
爲了讓原始類型看上去更像引用類型,javascript提供了3種原始封裝類型:String和Number 還有Bolean,javascript會在背後創建這些對象使你能夠像使用普通對象那樣使用原始值,但是這些臨時對象在使用它們的語句結束的時候立刻被銷毀了,雖然你可以自己創建原始封裝類型的實例,但是它們太容易讓人誤解了,所以最好別這麼幹。