JavaScript基礎(4)—— 包羅萬象的對象(下)

  上一章主要了解了對象的增刪改查,引出了原型鏈和繼承的概念,本章我們來好好聊一聊對象的屬性,在對象的屬性中有一些比較好用的方法和特性,瞭解這些方法可能對你寫代碼不會有什麼幫助,但如果你要封裝一個大型的,好用的對象——比如Vue,你可能就需要用到裏面的許多工具了。

1.檢測屬性

有時候我們需要檢測一個對象中是否包含某個屬性,這個時候我們可以通過in運算符、hasOwnPreperty()或propertyIsEnumerable()方法來完成這個工作,當然你也可以通過點(.)或方括號([])運算符查詢來做到這一點。下面我們通過簡單的代碼來回顧下這幾種檢測方式。

let o = {x:1}
o.y === undefined // true 最簡單的屬性查詢方式
'x' in o // true 注意要用引號把屬性值包起來
'toString' in o // true in運算符可以查詢到繼承屬性
o.toString() // '[Object Object]' 直接查詢也可以查詢到繼承屬性

o.hasOwnProperty('toString') 
// false hasOwnProperty用來檢測對象的自有屬性,對於繼承屬性返回false

o.propertyIsEnumerable('toString') 
// false propertyIsEnumerable是hasOwnProperty的增強版
// 只有檢測到可枚舉型的自有屬性爲true時返回true。通常由JavaScript創建的對象默認是可枚舉的。

  需要注意的時,hasOwnProperty()方法只能檢測到對象的自有屬性,這看起來好像有點雞肋,但某些情況下這個方法十分有用,當某個對象繼承自一個十分龐大的父對象時,你可能不完全清楚繼承來的有哪些屬性值,這時候你想訪問該對象的某個屬性又不想受到父對象干擾的時候就可以用這個方法檢測該對象的自有屬性。關於propertyIsEnumerble方法,人如其名,該方法檢測的屬性不僅是自有屬性,還得是可枚舉的,關於可枚舉屬性我們會在後面有所介紹。

2.枚舉屬性

  除了檢測對象是否存在,我們還需要檢測對象是否可枚舉。當我們用for / in 循環遍歷對象的屬性時,不但可以遍歷對象的自有屬性,還可以遍歷對象繼承的屬性,我們在使用for / in的時候基本上都只遍歷到了自己想要的屬性,說明Object.prototype裏面的屬性都是不可枚舉的,這點需要注意一下,下面來看一個例子,來證明下for / in 不但可以遍歷自有屬性還可以遍歷繼承屬性。

// 繼承 這個方法在上一章已經寫過,這裏再回顧一下
function inherit(p){
    if(p===null){
		throw TypeError()
	}
	if(Object.create){
		return Object.create(p)
	}
	if(typeof(p)!=='object'&&typeof(p)!=='function'){
		throw TypeError()
	}
	function fn(){}
	fn.prototype = p
	return new fn()
}

let o = {x:1}
let p = inherit(o)
p.y = 2
for(let key in p){
	console.log(key) //第一次是自有屬性y,第二次是繼承的可枚舉屬性x
}

  由於for/in循環有時候會遍歷到一些我們不想要的屬性(如從某個菜逼寫的構造函數裏繼承的一些可枚舉屬性),因此我們需要對這些屬性進行過濾,得到我們想要的自有屬性。事實上ES5提供的Object.keys()方法已經幫我們過濾了繼承的可枚舉屬性,實現的思路如下所示

Object.prototype.keys= function(o){
  if(typeof(o)!=='object') {throw TypeError()}
  let	arr = [] //用與存儲key值
	for(let prop in o){
		if(o.hasOwnProperty(prop)){
			arr.push(prop)
		}
	}
	return arr
}

let o = {x:1}
let p = inherit(o)
p.y = 2
Object.keys(p) // ['y']

3.屬性的監聽器getter和setter

  我們都知道Vue數據雙向綁定的核心就是改寫對象屬性的get和set方法,在ES5種,屬性值可以用一個或者兩個方法代替,這兩個方法就是getter和setter,由getter和setter定義的屬性稱作“存儲器屬性”,我個人更喜歡把他稱作“被監聽屬性”,既然有了“存儲器屬性”的官方說法,那麼普通的屬性我們稱之爲數據屬性。

  當程序查詢一個“被監聽屬性”(在後面我都會用被監聽屬性代替存儲器屬性的官方說法)時,JavaScript就會去調用該屬性的getter方法,注意這個方法是沒有參數的,這個方法的返回值就是被監聽屬性的值。當程序設置(改寫)一個被監聽的屬性的值的時候,JavaScript就會調用該屬性的setter方法,並把賦值表達式右側的值當作參數傳入setter。

  總而言之,被監聽屬性可以人爲改寫讀取和設置屬性時的行爲,這樣我們可以在讀取和設置屬性的時候對屬性以及和屬性相關的操作爲所欲爲之爲所欲爲,有了這兩個方法,我們就可以實時監聽數據的變化和讀取了。如果屬性同時具有getter和setter方法,那麼他具有讀/寫屬性,如果他只有getter方法,那麼他是一個只讀屬性,如果他只有setter方法,那麼他是一個只寫屬性,讀取只寫屬性永遠返回undefined。

  說了這麼多,下面我們來簡單使用下屬性的getter和setter方法做一些爲所欲爲的操作

var rect = {
	'width':3,
	'height':4,
	// 讀取對角線
	get diagonal(){
		return Math.sqrt(this.width*this.width + this.height*this.height)
	},
	// 改對角線
	set diagonal(newVal){
		var oldVal = Math.sqrt(this.width*this.width + this.height*this.height)
		var ratio = newVal/oldVal
		this.width *= ratio
		this.height *= ratio
	},
	// theta只讀
	get theta(){return Math.atan2(this.height,this.width)}
}
rect.diagonal // 5
rect.diagonal = 10
rect.width //6
rect.height //8

  上面這段代碼裏的this關鍵字都指向當前對象,你只需要知道這一點就可以了,當我們訪問對角線屬性的時候,返回的是一個表達式的計算值,當我們改寫對角線的時候,他的寬高信息也會做出相應的變化。通過屬性的getter和setter方法對數據進行監聽在許多場景中都有實際應用,這裏不過多介紹了(因爲本章的內容實在是有點多!)

4.屬性的三大特性

  對象的屬性除了key和value之外,還包含了一些標識他們是否可寫,可枚舉和可配置的特性,你也可以稱之爲屬性的三大狀態值。這些特性值在所有的屬性中都存在,但只有通過特殊的API才能進行設置,瞭解這些API對開發業務並沒有什麼幫助,但如果你想開發一個API庫,這些方法就顯得格外重要了。

  還記得我們在將for/in枚舉屬性的時候,繼承屬性會被for/in循環打印出來的問題嗎?你肯定不能指望使用你構建的API工具的用戶還知道如何使用hasOwnProperty()吧,因此你需要將這些可能被繼承的屬性和方法設置成不可枚舉的,這可以讓他們看起來更像是一個內置方法。你還可以通過將對象的屬性設置成不可寫的來鎖定這個屬性的值。

  JavaScript把數據屬性的值(value)和被監聽屬性的get,set方法作爲屬性的特性,以此將他們分爲

  數據屬性的四大特性:值(value),可寫性(writable),可枚舉型(enumerable)和可配置性(configurable)

  由於被監聽屬性的讀寫性由get和set決定。

  被監聽屬性的四大特性:可讀性(get),可寫性(set),可枚舉型(enumerable)和可配置性(configurable)

  介紹完了屬性的特性,那麼JavaScript提供了哪些API來操作屬性的特性呢?首先ES5定義了一個名爲“屬性描述符”的對象,用於查詢某個對象的某個屬性的屬性描述符,你可以通過調用Object.getOwnPropertyDescriptor(obj,key)進行查詢。

Object.getOwnPropertyDescriptor({x:1},'x')
//{value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(rect,'diagonal')
//{get: ƒ, set: ƒ, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor({x:1},'toString')
//undefined

  需要注意的是,Object.getOwnPropertyDescriptor()方法只能得到自有屬性的描述符,想要獲得繼承屬性的特性,需要遍歷原型鏈,這在後面會講到如何實現。

  說完了如何查詢屬性的特性,我們再來聊聊如何設置屬性的特性。調用Object.defineProperty()就可以實現對象某個屬性的設置。下面來看一個簡單的示例:

let o = {}
Object.defineProperty(o,'x',{
	value:1,
	writable:true,
	enumerable:false, //設置x屬性不可枚舉
	configurable:true
})
o.x // 1
Object.keys(o) // [] x屬性不可枚舉

//下面我們對屬性x做修改,讓他變爲只讀屬性
Object.defineProperty(o,'x',{writable:false})

//試圖修改這個屬性的值
o.x = 2 //操作失敗但不報錯,嚴格模式下會拋出類型錯誤異常
o.x //1

  在上述例子中,我們設置了x屬性不可枚舉且不可寫,那麼我們是不是沒有辦法修改x的值了吶?並不是!由於x屬性依然是可配置的,我們還是可以調用Object.defineProperty方法修改他的讀取值。方法如下

Object.defineProperty(o,'x',{value:2})
o.x //2

  從上面的寫法可以看出,傳入Object.defineProperty的屬性描述符對象不必包含所有四個特性。還需要注意一個點,該方法只能修改或新建自有屬性,不能操作繼承的屬性。

  如果需要同時修改或者創建多個屬性,我們可以使用Object.defineProperties(),第一個參數是要修改的對象,第二個參數是一個映射表,示例如下:

var p = Object.defineProperties({},{
    x:{value:1,writable:true,enumerable:true,confitable:true},
    y:{value:2,writable:true,enumerable:true,confitable:true}
    ...
})

 

  本章主要介紹了對象屬性的一些操作方法,關於對象的三個屬性(原型,類型,可擴展性),可以瞭解一下,關於對象的方法,比較常用的對象序列化操作可以瞭解一下,本章有時間的話,我會補一下對象的原型屬性,其他內容就不再多說了,多說無益,感興趣的可以點個關注,也可以入駐個人粉絲羣708637831

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章