JavaScript 類型

JavaScript 類型

對於 JavaScript 類型,可以簡單地概括爲:相對於強類型語言來說,它是弱(鬆散)類型的語言;有基本類型和引用類型,他們是區別是一個有固定空間存在於棧內存中,一個沒有固定空間保存在堆內存中並且在棧內存中保存了一個指向實現位置的指針。

市面上很多書都有不小的篇幅在講。這篇文章會講幾個方面,這些方面可能會需要你對 JavaScript 已經有了一些簡單的瞭解,特別是 JavaScript 的類型。如果還不一解,可以隨手拿起一本關於 JavaScript 的書翻翻,再來看本文。

一、基本類型與引用類型

  • 基本類型:Undefined / Null / Boolean / Number / String
  • 引用類型:Object / Array / Function / Date / RegExp / Error / Map / Set ...

爲什麼引用類型沒有枚舉完呢,因爲這裏面你瞭解這麼多就夠了,至少在我講的這篇中這些已經足夠。其他的可能很少會用到,甚至像 Map 、Set 這樣的也不是所有瀏覽器都支持。

二、JavaScript 類型的判斷

在 JavaScript 有兩個 operator 可以用以判斷類型。他們是 typeofinstanceof,不過圈子很小,它們混的可不是那麼好,是出了名的不靠譜。少數情況也是對的,很多情況下是不靠譜的。看看就知道了:

// 靠譜的時候:
typeof 'sofish' // object
new String('sofish') instanceof String // true

// 不靠譜的時候:
typeof [] // object
typeof null // object 
'sofish' instanceof String // false

呃~ 可能很多初學的 JavaScript 程序員會因此爆粗口。還大部分人在需要用 JS 的時候已經有了 jQuery 等這樣的庫,他們都做了封裝,讓你可以方便地檢測類型。當然,事實上要檢測也不麻煩,因爲那句「在 JavaScript 中,一切都是對象」,當然像很多文檔中說到的,undefined 其實和 NaN, Infinity 都只是一個全局屬性。你大概知道就可以了。但「對象」可以幫到我們:

/* 檢測對象類型
 * @param: obj {JavaScript Object}
 * @param: type {String} 以大寫開頭的 JS 類型名
 * @return: {Boolean}
 */
function is(obj, type)  {
  return Object.prototype.toString.call(obj).slice(8, -1) === type;
}

這樣的話,我們就可以利用 is 這個函數來幫我們搞定類型判斷了,並且這個簡單的函數有很好的兼容性,可以用到你的項目中去。情況如:

is('sofish', 'String') // true
is(null, 'Null') // true
is(new Set(), 'Set') // true

三、JavaScript 類型的轉換

在 JavaScript 中,變量(屬性)的類型是可以改變的。最常看到的是 StringNumber 之間的轉換。如何把 1 + '2' 變成 12 呢?這裏面有必要理解一下 + 號 operator,它是一個數學運算符,同時也是 JavaScript 中的字符串連字符。所以新手會經常會看到一個有趣的現象,當使用 + 號的時候有時計算出來的不是想要的,而用 - 號卻總能得到「正確」的答案。

1 + '2' // '12'
1 + (+'2') // 3
1- '2' // -1

這裏面其實就是因爲 + 的雙重角色導致的。在上面的代碼中,可以注意到第二條表達式在 String 前面運用了一個 + 號,強制把它的類轉換爲 Number。而對於 JavaScript 的類型轉換理解,大多數情況下,只要理解 + 具有雙重角色就可以了。其他的可以理解類,類似都是可以用賦值/重載來修改的,甚至包括 Error:

var err = new Error();
console.log(err instanceof Error); // true

err = 'sofish';
console.log(err); // 'sofish'

四、JavaScript 引用類型

這一點是本文的一個難點。相於基本類型,引用可以爲其添加屬性和方法;引用類似的值是一個引用,把一個引用類型的值賦給一個變量,他們所指向的是同一存儲在堆內存中的值。變量(屬性)可以重載,但複製會是一件很有趣的事情,後面我們會詳細來說。

1. 添加屬性和方法

下面的代碼我們將會看到,假設我們對一個基本類似賦值,它並不會報錯,但在獲取的時候卻是失效的:

var arr = [1,2,3];
arr.hello = 'world';
console.log(arr.hello); // 'world'

var str = 'sofish';
str.hello = 'world';
console.log(str.hello); // undefined

2. 引用類型值的操作

由於引用類型存儲在棧內存中的是一個引用,那麼當我們指向的同一個原始的值,對值的操作將會影響所有引用;這裏有一個例是,重新賦值(並非對值的直接操作)會重新創建一個對象,並不會改變原始值。比如:

var arr = [1,2,3], sofish = arr;
sofish.push('hello world');
console.log(arr); // [1, 2, 3, 'hello world']

// 非相同類型
sofish = ['not a fish']; // 當 sofish 類似改變時,不會改變原始值
console.log(arr);// [1, 2, 3, 'hello world']

3. 引用類型值的複製

對原始值的操作會影響所有引用,而這不一定是我們想要的,有時候我們需要複製一個全新的對象,操作的時候不影響其他引用。而一般情況也,像 Date / Function / RegExp ... 都很少有具體的操作,主要是像 ArrayObject 會有添加項、屬性等操作。所以我們主要需要理解的是如何複製 ArrayObject 對象。

3.1 數組的複製

Array 對象中,存在 slice 方法返回一個截取的數組,在 ES5 中 filter 等也返回一個新的數組,那麼我們可能利用這個方法來進行復制。

var arr = [1, 2, 3];
var sofish = arr.slice();

// 對新的數組進行操作並不會影響到原始數組
sofish.push('hello world');
console.log(arr); // [1, 2, 3] 

3.2 對象的複製

Array 的複製中我們使用的是 slice 方法,實際上對於 ArrayObject 中都可以利用 for ... in 循環來進行遍歷並賦值來進行復制。

var obj = { name: 'sofish' }, sofish = {}, p;
for (p in obj) sofish[p] = obj[p];

// 對新的對象操作並不會影響原始值
sofish.say = function() {};
console.log(obj); // { name: 'sofish' }

3.3 Shadow / Deep Copy

像上面的操作,就是我們常說的淺拷貝(Shadow Copy)。不過在 ArrayObject 都可以有多層(維),像這樣的拷貝只考慮到最上面一層的值,在可能存在的值中的 ArrayObject 都還是指向了原始對象。比如:

var arr = [1, { bio: 'not a fish' } ], sofish = [], p;
for(p in arr) {
  sofish[p] = arr[p];
}

// 對 `sofish` 中包含的對象 `cat` 的操作會影響原始值
sofish[1].bio = 'hackable';
console.log(arr);//  [1, cat: { bio: 'hackable' } ]

那麼如何做呢?來一個 copy() 函數解決這個問題:

/* 複製對象
 * @param: obj {JavaScript Object} 原始對象
 * @param: isDeep {Boolean} 是否爲深拷貝
 * @return: {JavaScript Object} 返回一個新的對象
 */
function copy(obj, isDeep) {
	var ret = obj.slice ? [] : {}, p;
	// 配合 is 函數使用
	if(!isDeep && is(obj, 'Array')) return obj.slice();
	for(p in obj) {
	    var prop = obj[p];
		if(!obj.hasOwnProperty(p)) continue;
		if(is(prop, 'Object') || is(prop, 'Array')) {
		  ret[p] = copy(prop, isDeep);
		} else {
			ret[p] = prop;
		}
	}
	return ret;
}

這樣,我們就可以通過 copy(obj, isDeep) 函數來複制一個 Array 或者 Object 。可以測試一下:

var arr = [1, {bio: 'not a fish'}];
var sofish = copy(arr);

// 淺拷貝對於第一層的操作不影響原始值,但影響第二層
sofish.push('cat'); 
console.log(arr); //  [1, {bio: 'not a fish'}]
sofish[1].bio = 'hello world';
console.log(arr) //  [1, {bio: 'hello world'}]

// 深拷貝則不會影響原始值
sofish = copy(arr, 1);
sofish[1].bio = 'foo or bar'
console.log(arr); // [1, {bio: 'hello world'}]

到此。你基本上要了解的關於類型的比較難的點,應該是都基本瞭解了。當然,複製是最麻煩的一個點,除了經常需要操作的 ArrayObject 來說,還有 Date / Function / RegExp 的複製。給幾篇參考文章吧,也都很有趣:

  1. Passing Objects to Functions By Value(https://www.htmlgoodies.com/beyond/javascript/passing-objects-to-functions-by-value.html)
  2. Performance: Regex Clone(https://jsperf.com/regexp-clone)
  3. JavaScript: clone a function(https://stackoverflow.com/questions/1833588/javascript-clone-a-function)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章