JavaScript中的物件學習筆記(屬性操作)

EliotWang發表於2018-06-11

本文是筆者學習JavaScript時做的筆記,大部分內容來自《JavaScript權威指南》,記錄學習中的重點,並引入一些其他博文和與其他程式設計師討論的內容,供本人日常翻閱。如有疑問,請留言評論,對本文的內容想深入瞭解,請購買正版《JavaScript權威指南》。

一. 屬性的查詢和設定

  • 物件的屬性可以通過(.)或者方括號([])來訪問。
  • ES3中,點運算子後面的識別符號不能是保留字,比如o.for和o.class是非法的,因為for是js的關鍵字,class是保留字(ES6中成為關鍵字)。如果一個物件的屬性名是保留字,必須以方括號的形式訪問。ES5和ES3的某些實現對其放寬了限制。
  • 嚴格來講,方括號內的表示式必須返回字串或者返回一個可以轉換為字串的值。

二. 繼承

js物件具有“自有屬性”(own property),也有一些屬性是從原型物件繼承而來的。當查詢某個物件的屬性的時候,會沿著原型鏈向上查詢,直到查詢到null。

<賦值行為> 對o的屬性x進行賦值,如果x中已經有了x屬性且不是繼承來的,那麼這個賦值操作只改變這個已有屬性x的值。如果o中不存在屬性x,那麼賦值操作給o新增一個新的屬性x。如果整合有屬性x,那麼會在物件內重新建立一個同名的物件x。同時,在賦值的過程中,js也會檢查將要操作的屬性的合法性,如果屬性是隻讀的,那麼賦值操作將不被允許。

  • 操作屬性的時候,只會在當前物件進行操作,不會修改其原型。
  • 只要在查詢屬性的時候才會體會到繼承的存在,設定屬性與繼承無關,

三. 屬性訪問錯誤

查詢一個不存在的屬性並不會報錯,如果在o的自有屬性上或者繼承屬性上均未查詢到x,那麼將會返回一個undefined。 但是,如果物件不存在,試圖查詢這個不存在的物件的屬性,就會丟擲一個型別錯誤異常。 為了避免這種查詢錯誤,有幾種可行的方法:

	//比較冗餘的方法
	let len = undefined;
	if(book){
		if(book.subtitle) len = book.subtitle.length;
	}
	//更簡練的方法
	let len = book&&book.subtitle&&book.title.subtitle.length;
複製程式碼

總結賦值失敗的場景:

  1. o中的屬性是隻讀的
  2. o中的屬性是繼承的,且是隻讀的。
  3. o中不存在自由屬性p,o沒有使用setter方法繼承屬性p,並且o的可擴充套件性為false。

四. 刪除屬性

刪除屬性用的是delete運算子。其作用是,斷開當前物件與指定屬性的連線,但是這個屬性對應的值還當前環境有其它引用時,並不會被銷燬。

  • delete運算子只能刪除自有屬性。
  • 當delete刪除成功或沒有任何副作用時,返回true
	o = {x:1};
	delete o.x; //刪除x返回true
	delete o.x; //無事發生,返回true
	delete o.toStrung; //無事發生,返回true
	delete 1;  //表示式無意義,返回true
複製程式碼

delete不能刪除那些可配置屬性為false的屬性,但是可以刪除不可擴充套件物件的可配置屬性。某些內建物件是不可配置的,比如通過變數宣告和函式宣告建立的全域性物件的屬性。嚴格模式中,刪除一個不可配置屬性會報型別錯誤。在費嚴格模式以及一些ES3的實現中,delete會返回false:

	delete Object.prototype; //屬性為不可配置的
	var x = 1;
	delete this.x; //不能刪除這個屬性
	function f(){}; 
	delete this.f; //不能刪除全域性函式
複製程式碼

在非嚴格模式中刪除全域性物件的可配置屬性時,可以省略對全域性物件的引用,直接在delete操作符後跟隨要刪除的屬性名即可。

	this.x = 1;
	delete x;//將他刪除
複製程式碼

在嚴格模式下,delete後跟隨一個非法的運算元,會報一個語法錯誤,因此必須顯式指定物件和其屬性。

	delete x;//報型別錯誤
	delete this.x;//正常工作
複製程式碼

五. 檢測屬性

判斷某個屬性是否存在於某個物件中,是我們常用的操作。

  1. in運算子,左側為屬性名,右側為檢查物件,如果自有屬性或者繼承屬性包含檢測的值,則返回true。
  2. 物件的hasOwnProperty()方法用來檢測給定的名字是否是物件的自有屬性。對於繼承屬性將返回false。
  3. 物件的propertyIsEnumerable()方法,只有檢測這個自有屬性是可列舉的時候,才會返回true。
  4. 使用"!==undefined"來判斷屬性是否存在,但不能區分存在且值為undefined的屬性。

六. 列舉屬性

利用for/in迴圈可以遍歷目標物件所有的可列舉屬性。物件繼承的內建方法是不可列舉的,但是在程式碼中給物件新增的屬性都是可列舉的。 許多實用工具庫誒Object.prototype新增了新的方法和屬性,這些方法可以被所有的物件繼承並使用。然而在ES5之前,這些新添的方法不能被定義為不可列舉,因此他們都會被列舉出來。為了避免,需要一些方法跳過這些屬性。

	for(p in o){
		if(!o.hasOwnProperty(p)) continue; //跳過繼承屬性
	}
	for(p in o){
		if(type o[p]==="function") continue; //跳過方法
	}
複製程式碼

用來列舉屬性的工具函式

	/*
	 *列舉p中的屬性複製到o中,並返回o
	 *不會處理getter和setter以及複製屬性
	 */
	function extend(o, p){
		for(prop in p){
			o[prop] = p[prop];
		}
		return o;
	}
	//本實現並不完全,在ie中會有一些bug,但是在之後會有個更加強大的版本
	
	/*
	 *列舉p中的屬性複製到o中,並返回o
	 *不會處理getter和setter以及複製屬性
	 *o和p有同名屬性,則o的屬性將不受影響
	 */
	 function merge(o, p){
		for(prop in p){
			if(o.hasOwnProperty[prop]) continue;
			o[prop] = p[prop];
		}
		return o;
	}
	
	/*
	 *如果o中屬性在p中沒有同名屬性,則從o中刪除這個屬性
	 */
	 function restrict(o, p){
	 	for(prop in o){
	 		if(!(prop in p)) delete o[prop];
	 	}
	 	return o;
	 }
	 
	 /*
	 *如果o中屬性在p中有同名屬性,則從o中刪除這個屬性
	 */
	 function substract(o,p){
	 	for(prop in p){
	 		delete o[prop];
	 	}      
	 	return o;
	 }
	 
	 /*
	  *返回一個物件,這個物件同時有o和p的屬性
	  *如果o和p有重名的屬性,使用p的屬性
	  */
	  function union(o, p){ return extend(extend({},o),p) };
	  
	  /*
	  *返回一個物件,這個物件同時有在o和p中出現的屬性
	  *如果o和p有重名的屬性,使用p的屬性
	  */
	  function intersection(o, p){ return restrict(extend({},o),p)}
	  /*
	   *返回一個陣列,這個陣列包含的是o中可列舉的屬性的名字
	   */
	  function keys(o){
	  	if(typeof o!== "object") throw TypeError();
	  	var result = [];
	  	for(var prop in o){
	  		if(o.hasOwnProperty(prop));
	  		result.push(prop);
	  	}
	  	return result;
	  }
複製程式碼
  • 除了for/in迴圈,ES5也定義了兩個用來列舉屬性名稱的函式,第一個是Object.keys(),它返回一個陣列,這個陣列由物件中可列舉的自有屬性的名稱組成。
  • 第二個函式是,Object.hasOwnPropertyNames(),它和Object.keys()類似,只是它返回的是物件所有自有屬性的名字,而不僅僅是可列舉屬性,ES3則無法模擬,因為ES3沒有提供任何獲取物件不可列舉屬性的方法。

七. 屬性getter和setter

js物件中,屬性是由名字、值和一組特性構成的。在ES5和除了ie之外的較新的ES3實現,屬性值可以由一個或兩個方法代替,這兩個方法就是getter和setter。由getter和setter定義的屬性被稱為“存取器屬性”(accessor property),不同於“資料屬性”(data property),資料屬性只有一個簡單的值。

  • 程式訪問存取器屬性時,js呼叫getter方法。這個方法的返回值就是屬性存取表示式的值。
  • 當程式設定一個存取器屬性的值時,js呼叫setter方法,將賦值表示式右側的值當做引數傳入setter。(可以忽略該方法的返回值)
	var o = {
		data_prop:"value",
		get accessor_prop(){/*這裡是函式體*/},
		set accessor_prop(){/*這裡是函式體*/}
	}
複製程式碼

方法 存取器屬性定義為一個或兩個和屬性同名的函式,使用get和set關鍵字。以下為一個笛卡爾座標的物件。

	var p = {
		x:1.0,
		y:1.0,
		get r() { return Math.sqrt(this.x + this.y*this.x)},
		set r(newvalue){
			var oldvalue  = Math.sqrt(this.x*this.x + this.y*this.y);
			var ratio = newvalue/oldvalue;
			this.x*=ratio;
			this.y*=ratio;
		},
		get theta(){ return Math.atan2(this.y,this.x) }
	}
複製程式碼

需要注意getter和setter裡this關鍵詞的用法,js把這些函式作為當前物件的方法來呼叫,也就是說是,在函式體內this指向這個點的物件。

和資料的屬性一樣,存取器屬性是可以繼承的,因此可以將物件p當做一個點的原型。

	var q = inherit(p);
	q.x=1;
	q.y=1;
	console.log(q.r);
	console.log(q.theta);
複製程式碼

這段程式碼使用存取器屬性定義api,api提供了表示同一組資料的兩種方法(笛卡爾座標系表示法和極座標系表示法)。再來一個例子:

	//這個物件產生嚴格自增的序列號
	var serialnum = {
		$n: 0,
		get next(){ return this.$n++ },
		set next(){ 
			if(n >= this.$n) this.$n = n;
			else throw "序列號的值不能比當前值小";
		}
	}
複製程式碼

八. 屬性的特性

屬性除了包含名字和值之外,屬性還包含一些標識他們可寫,可列舉和可配置的特性。在ES3中無法設定這些特性,所有通過ES3的程式建立的屬性都是可寫的、可列舉的和可配置的,且無法對這些特性修改。ES5中定義了查詢和設定這些特性的API。這些API對於庫的開發和來說很重要,因為:

  • 可以通過這些API給原型物件新增方法,並將它們設定為不可列舉的,讓它們看起來更像內建方法。
  • 可以通過這些API給物件定義不能修改或刪除的屬性,藉此“鎖定”這個物件。

在本節,我們將存取器的getter和setter也看成是屬性的特性,那麼照這個邏輯,我們也可以把資料屬性的值,同樣看成一個屬性名和4個特性:值(value)、可寫性(writable)、可列舉性(enumerable)、可配置型(configurable)。 存取器屬性不具有值特性和可寫性,他們的可寫性是由setter方法存在與否決定的。因此存取器屬性的4個特性是,讀取(get)、寫入(set)、可列舉性和可配置性。 為了實現屬性特性的查詢和設定操作,ES5中定義了一個名為“屬性描述符”(property descriptor)的物件,這個物件代表四個特性,且描述符的屬性和它們所描述的屬性特性是同名的。因此,描述符物件的屬性有,value、writable、enumerable和configurable。

  • 通過Object.getOwnPropertyDescriptor()可以獲取某個物件特定屬性的屬性描述符。
  • 設定屬性的特性,或者想讓新建的屬性具有某種特性,則需要Object.definePeoperty(),傳入要修改的物件,或修改的屬性的名稱以及屬性描述符物件。
	var o = {};//建立一個空物件
	//新增一個不可列舉的屬性x,並賦值為1
	Object.defineProperty(o, "x", {
		value:1,
		writable:true,
		enumerable:false,
		configurable:false
	});
	
	//屬性是存在的,但不可列舉
	o.x;	//=>1
	Object.keys(o) //=>[]
	
	//現在對x進行修改,讓它變為只讀
	Object.defineProperty(o,"x",{writable:false});
	
	//現在修改這個屬性,修改失敗,但不會報錯,ES5嚴格模式下會報錯
	o.x = 2;
複製程式碼

由上例子所示,Object.defineProperty()的屬性描述物件不必包含全部四個屬性(不能用來修改繼承屬性) 還有一個進階版的方法,Object.defineProperties(),第一個引數是要修改的物件,第二個是一個對映表。

規則

  • obj不可擴充套件,則可以編輯已有的自有屬性,但不能新增新屬性。
  • 如果屬性是不可配置的,則不能修改它的可配置性和可列舉性。
  • 如果存取器屬性是不可配置的,則不能修改getter和setter方法,也不能將它轉為資料屬性。
  • 如果資料屬性是不可配置的,則不能將它轉換為存取器屬性。
  • 如果資料屬性是不可配置的,則不能將它的可行寫從false修改為true,但可以從true置為false。
  • 如果資料屬性是不可配置且不可寫的,則不能修改它的值。然而可配置但不可寫屬性的值是可以修改的。
	/*
	 *給Object.prototype新增一個不可列舉的extend()方法
	 *這個方法繼承自呼叫它的物件,將作為引數傳入的物件的屬性一一複製
	 *除了值之外,也複製屬性的所有特性,除非在目標物件中存在同名的屬性。
	 */
	 Object.defineProperty(Object.prototype,
		 "extend",	//定義Object.propertype.extend
		 {
		 	writable: true,
		 	enumerable: false,
		 	configurable: true,
		 	value: function(o){
		 		writable: true
		 		var names = Object.getOwnPropertyNmaes(o);
		 		for(var i=0;i < names.length;i++){
		 			//如果屬性已存在,則跳過
		 			if(names[i] in this) continue;
		 			var desc = Object.getOwnPropertyDescriptor(o, names[i]);
		 			//用它給this建立一個屬性
		 			Object.defineProperty(this,names[i],desc);
		 		}
		 	}
		 }
	 )
複製程式碼

相關文章