《JavaScript物件導向精要》之一:基本型別和引用型別

明易發表於2018-12-16

1.1 什麼是型別

原始型別 儲存為簡單資料值。 引用型別 儲存為物件,其本質是指向記憶體位置的引用。

為了讓開發者能夠把原始型別和引用型別按相同的方式處理,JavaScript 花費了很大的努力來保證語言的一致性。

其他程式語言用棧存原始型別,用對儲存引用型別。

而 JavaScript 則完全不同:它使用一個變數物件追蹤變數的生存期。

原始值被直接儲存在變數物件內,而引用值則作為一個指標儲存在變數物件內,該指標指向實際物件在記憶體中的儲存位置。

1.2 原始型別

原始型別代表照原樣儲存的一些簡單資料。

JavaScript 共有5種原始型別:

  • boolean 布林,值為 true or false
  • number 數字,值為任何整型或浮點數值
  • string 字串,值為由單引號或雙引號括住的單個字元或連續字元
  • null 空型別,僅有一個值:null
  • undefined 未定義,只有一個值:undefined(undefined 會被賦給一個還沒有初始化的變數)

JavaScript 和許多其他語言一樣,原始型別的變數直接儲存原始值(而不是一個指向物件的指標)。

var color1 = "red";
var color2 = color1;

console.log(color1); // "red"
console.log(color2); // "red"

color1 = "blue";

console.log(color1); // "blue"
console.log(color2); // "red"
複製程式碼

鑑別原始型別

鑑別原始型別的最佳方式是使用 typeof 操作符。

console.log(typeof "Nicholas"); // "string"
console.log(typeof 10);         // "number"
console.log(typeof true);       // "boolean"
console.log(typeof undefined);  // "undefined"
複製程式碼

至於空型別(null)則有些棘手。

console.log(typeof null); // "object"
複製程式碼

對於 typeof null,結果是"object"。(其實這已被設計和維護 JavaScript 的委員會 TC39 認定是一個錯誤。在邏輯上,你可以認為 null 是一個空的物件指標,所以結果為"object",但這還是很令人困惑。)

判斷一個值是否為空型別(null)的最佳方式是直接和 null 比較:

console.log(value === null); // true or false
複製程式碼

注意:以上這段程式碼使用了三等號(全等 ===),因為三等號(全等)不會將變數強制轉換為另一種型別。

console.log("5" == 5); // true
console.log("5" === 5); // false

console.log(undefined == null); // true
console.log(undefined === null); // false
複製程式碼

原始方法

雖然字串、數字和布林值是原始型別,但是它們也擁有方法(null 和 undefined 沒有方法)。

var name = "Nicholas";
var lowercaseName = name.toLowerCase(); // 轉為小寫

var count = 10;
var fixedCount = count.toFixed(2); // 轉為10.00

var flag = true;
var stringFlag = flag.toString(); // 轉為"true"

console.log("YIBU".charAt(0)); // 輸出"Y"
複製程式碼

儘管原始型別擁有方法,但它們不是物件。JavaScript 使它們看上去像物件一樣,以此來提高語言上的一致性體驗。

1.3 引用型別

引用型別是指 JavaScript 中的物件,同時也是你在該語言中能找到最接近類的東西。

引用值是引用型別的例項,也是物件的同義詞(後面將用物件指代引用值)。

物件是屬性的無序列表。屬性包含鍵(始終是字串)和值。如果一個屬性的值是函式,它就被稱為方法。

除了函式可以執行以外,一個包含陣列的屬性和一個包含函式的屬性沒有什麼區別。

建立物件

有時候,把 JavaScript 物件想象成雜湊表可以幫助你更好地理解物件結構。

JavaScript 有好幾種方法可以建立物件,或者說例項化物件。第一種是使用 new 操作符和建構函式。

建構函式就是通過 new 操作符來建立物件的函式——任何函式都可以是建構函式。根據命名規範,JavaScript 中的建構函式用首字母大寫來跟非建構函式進行區分。

var object = new Object();
複製程式碼

因為引用型別不再變數中直接儲存物件,所以本例中的 object 變數實際上並不包含物件的例項,而是一個指向記憶體中實際物件所在位置的指標(或者說引用)。這是物件和原始值之間的一個基本差別,原始值是直接儲存在變數中。

當你將一個物件賦值給變數時,實際是賦值給這個變數一個指標。這意味著,將一個變數賦值給另外一個變數時,兩個變數各獲得了一份指標的拷貝,指向記憶體中的同一個物件。

var obj1 = new Object();
var obj2 = obj1;
複製程式碼

物件引用解除

JavaScript 語言有垃圾收集的功能,因此當你使用引用型別時無需擔心記憶體分配。但最好在不使用物件時將其引用解除,讓垃圾收集器對那塊記憶體進行釋放。解除引用的最佳手段是將物件變數設定為 null

var obj1 = new Object();
// dosomething
obj1 = null; // dereference
複製程式碼

新增刪除屬性

在 JavaScript 中,你可以隨時新增和刪除其屬性。

var obj1 = new Object();
var obj2 = obj1;

obj1.myCustomProperty = "Awsome!";
console.log(obj2.myCustomProperty); // "Awsome!" 因為 obj1 和 obj2 指向同一個物件。
複製程式碼

1.4 內建型別例項化

內建型別如下:

  • Array 陣列型別,以數字為索引的一組值的有序列表
  • Date 日期和時間型別
  • 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();
複製程式碼

字面形式

內建引用型別有字面形式。字面形式允許你在不需要使用 new 操作符和建構函式顯示建立物件的情況下生成引用值。屬性的可以是識別符號或字串(若含有空格或其他特殊字元)

var book = {
  name: "Book_name",
  year: 2016
}
複製程式碼

上面程式碼與下面這段程式碼等價:

var book = new Object();
book.name = "Book_name";
book.year = 2016;
複製程式碼

雖然使用字面形式並沒有呼叫 new Object(),但是 JavaScript 引擎背後做的工作和 new Object() 一樣,除了沒有呼叫建構函式。其他引用型別的字面形式也是如此。

1.5 訪問屬性

可通過 .中括號 訪問物件的屬性。 中括號 [] 在需要動態決定訪問哪個屬性時,特別有用。因為你可以用變數而不是字串字面形式來指定訪問的屬性。

1.6 鑑別引用型別

函式是最容易鑑別的引用型別,因為對函式使用 typeof 操作符時,返回"function"。

function reflect(value){
  return value;
}
console.log(typeof reflect); // "function"
複製程式碼

對其他引用型別的鑑別則較為棘手,因為對於所有非函式的引用型別,typeof 返回 object。為了更方便地鑑別引用型別,可以使用 JavaScript 的 instanceof 操作符。

var items = [];
var obj = {};
function reflect(value){
  return value;
}

console.log(items instanceof Array); // true;
console.log(obj instanceof Object); // true;
console.log(reflect instanceof Function); // true;
複製程式碼

instanceof 操作符可鑑別繼承型別。這意味著所有物件都是 Oject 的例項,因為所有引用型別都繼承自 Object

雖然 instanceof 可以鑑別物件型別(如陣列),但是有一個列外。JavaScript 的值可以在同一個網頁的不用框架之間傳來傳去。由於每個網頁擁有它自己的全域性上下文—— Object、Array 以及其他內建型別的版本。所以當你把一個物件(如陣列)從一個框架傳到另外一個框架時,instanceof 就無法識別它。

1.8 原始封裝型別

原始封裝型別有 3 種:String、Number 和 Boolean。 當讀取字串、數字或布林值時,原始封裝型別將被自動建立。

var name = "Nicholas";
var firstChar = name.charAt(0); // "N"
複製程式碼

這在背後發生的事情如下:

var name = "Nichola";
var temp = new String(name);
var firstChar = temp.charAt(0);
temp = null;
複製程式碼

由於第二行把字串當成物件使用,JavaScript 引擎建立了一個字串的實體讓 charAt(0) 可以工作。字串物件的存在僅用於該語句並在隨後銷燬(一種被稱為自動打包的過程)。為了測試這一點,試著給字串新增一個屬性看看它是不是物件。

var name = "Nicholas";
name.last = "Zakas";

console.log(name.last); // undefined;
複製程式碼

下面是在 JavaScript 引擎中實際發生的事情:

var name = "Nicholas";
var temp = new String(name);
temp.last = "Zakas";
temp = null; // temporary object destroyed

var temp = new String(name);
console.log(temp.last);
temp = null;
複製程式碼

新屬性 last 實際上是在一個立刻就被銷燬的臨時物件上而不是字串上新增。之後當你試圖訪問該屬性時,另一個不同的臨時物件被建立,而新屬性並不存在。

雖然原始封裝型別會被自動建立,在這些值上進行 instanceof 檢查對應型別的返回值卻是 false。 這是因為臨時物件僅在值被讀取時建立instanceof 操作符並沒有真的讀取任何東西,也就沒有臨時物件的建立。

當然你也可以手動建立原始封裝型別。

var str = new String("me");
str.age = 18;

console.log(typeof str); // object
console.log(str.age); // 18
複製程式碼

如你所見,手動建立原始封裝型別實際會建立出一個 object。這意味著 typeof 無法鑑別出你實際儲存的資料的型別。

另外,手動建立原始封裝型別和使用原始值是有一定區別的。所以儘量避免使用。

var found = new Boolean(false);
if(found){
  console.log("Found"); // 執行到了,儘管物件的值為 false
}
複製程式碼

這是因為一個物件(如 {} )在條件判斷語句中總被認為是 true;

1.9 總結

第一章的東西都是我們一些比較熟悉的知識。但是也有一些需要注意的地方:

  • 正確區分原始型別和引用型別
  • 對於 5 種原始型別都可以用 typeof 來鑑別,而空型別必須直接跟 null 進行全等比較。
  • 函式也是物件,可用 typeof 鑑別。其它引用型別,可用 instanceof 和一個建構函式來鑑別。(當然可以用 Object.prototype.toString.call() 鑑別,它會返回 [object Array]之類的)。
  • 為了讓原始型別看上去更像引用型別,JavaScript提供了 3 種封裝型別。JavaScript會在背後建立這些物件使得你能夠像使用普通物件那樣使用原始值。但這些臨時物件在使用它們的語句結束時就立刻被銷燬。雖然可手動建立,但不建議。

相關文章