聲明 本系列文章內容全部梳理自以下幾個來源: 《JavaScript權威指南》 "MDN web docs" "Github:smyhvae/web" "Github:goddyZhao/Translation/JavaScript" 作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基 ...
聲明
本系列文章內容全部梳理自以下幾個來源:
- 《JavaScript權威指南》
- MDN web docs
- Github:smyhvae/web
- Github:goddyZhao/Translation/JavaScript
作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基礎上,通過自己的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,如有發現,歡迎指點下。
PS:梳理的內容以《JavaScript權威指南》這本書中的內容為主,因此接下去跟 JavaScript 語法相關的系列文章基本只介紹 ES5 標準規範的內容、ES6 等這系列梳理完再單獨來講講。
正文-對象
在 JavaScript 除了原始數據類型外,其餘均是對象,函數是對象,數組也是對象;繼承通過對象來實現,構造函數也通過對象來實現,所以對象在 JavaScript 里有著很重要的角色,理解和掌握對象的一些特性,對於掌握 JavaScript 這門語言有著很大的幫助。
Java 里對象有屬性和方法之分,但在 JavaScript 中,只存在屬性,變數是屬性,方法也是屬性,對於 JavaScript 來說,對象,其實只是一堆屬性的無序集合而已,外部可通過對象來操作各種屬性,只不過有的屬性,它的值是函數類型,所以這時可叫它為對象的方法。
對象的每個屬性,都是一個 key-value 的形式,屬性名和屬性值。而屬性,又分自有屬性和繼承屬性,自有屬性是指對象本身自己擁有的屬性,而繼承屬性是指繼承的屬性。
對象分類
一般來說,有三類對象,分別是內置對象、宿主對象、自定義對象:
- 內置對象:是指語法標準規範中內置實現的一些對象,例如函數、數組、正則、日期等這些內置對象;
- 宿主對象:是指 JavaScript 解釋器所嵌入的宿主環境,在前端里,一般來說宿主環境就是瀏覽器,瀏覽器也會定義一些內置對象,比如 HTMLElement 等;
- 自定義對象:開發人員自行實現的對象。
創建對象
創建對象有三種方式:對象直接量、構造函數、Object.create()
對象直接量
這是最簡單的一種創建對象的方式,在代碼中,直接通過 {} 形式創建一個對象,如:
var book = {
"main title": "JavaScript",
'sub-title': "The Definitive Guide",
"pages": 900,
author: {
firstname: "David",
surname: "Flanagan"
}
};
上述代碼中,等號右側 {} 代碼塊形式定義的對象方式,就叫對象直接量。代碼中,每出現一次對象直接量,會直接創建一個新的對象,對象的屬性就是對象直接量中定義的。
定義屬性時,有幾點需要註意一下,屬性名也就是 key 值,可加引號也可不加引號,但如果屬性名使用到一些保留字時,就肯定需要加引號。
屬性值可以是 JavaScript 中的任意類型,原始類型、對象類型都可以。
構造函數
構造函數就是通過 new
關鍵字和函數一起使用時,此時的函數就稱構造函數,用途在於創建一個新的對象。具體在後續講函數時會詳細講解。
這裡可以看個例子:
var o = new Object();
var o1 = new Object;
var a = new Array();
function Book() {}
var b = new Book();
通過 new 關鍵字和函數一起使用時,就可以創建新對象,例子中的 Object 和 Array 是內置的構造函數,也可以自定義構造函數,其實就是自定義一個函數,讓它和 new 關鍵字一起使用就可以了。
通過構造函數方式如何給新創建的對象添加一些初始化的屬性,這些細節和原理在函數一節中再講,這裡需要註意一點的就是,當不往構造函數傳參數時,此時括弧是允許可以省略的。
另外,第一種對象直接量的方式創建對象,其實,本質上也是通過構造函數:
var o = {name:"dasu"}
//等效於
var o = new Object();
o.name = "dasu";
對象直接量其實是一種語法糖,可以通俗的理解,JavaScript 為方便我們創建對象,封裝的一層工具,其內部的本質實現也是通過構造函數。
Object.create()
你可以把 Object.create() 理解成 Java 中的靜態方法。
通過這種方式,可以創建一個新的對象,參數是指定對象要繼承的目標對象,這個被繼承的對象,在 JavaScript 里被稱為原型。
舉個例子:
var o = Object.create(new Object()); //創建一個新對象,讓它繼承自一個空對象
通過構造函數創建的對象,其繼承關係是由構造函數決定的,而 Object.create() 方式,可自己手動指定繼承關係。當然,並不是說,構造函數就無法自己指定繼承關係。
原型
原型可以理解成 Java 中的父類概念。
在 JavaScript 中,對象也存在繼承關係,繼承的雙方都是對象,對象是從對象上繼承的,被繼承的那個對象稱作原型。所以,有一種描述說,JavaScript 是基於原型的繼承。
在 Java 中,是通過 extends 關鍵字實現繼承關係,那麼在 JavaScript 里呢?
自然也有類似的用來指定對象的繼承關係,這就取決於創建對象的方式,上面說過,創建對象有三種方式:對象直接量、構造函數、Object.create(),但由於對象直接量本質上也是通過構造函數,所以其實就兩種。
對於構造函數創建的對象來說,因為每個函數都有一個 prototype 屬性,prototype 是它的屬性名,屬性值是一個對象,這個對象就是原型,就是通過該構造函數創建出來的新對象的繼承來源。
我們可以通過修改構造函數的 prototype 屬性值來達到指定對象繼承關係的目的,如果不修改,那麼內置的構造函數如 Object 或 Array 這些都已經有預設指定的 prototype 屬性值了,也就是創建內置對象時,這個對象已經具有一定的預設繼承結構了。
對於 Object.create() 方式創建對象,參數傳入的就是子對象的原型,想讓創建出來的對象繼承自哪裡,就傳入那個對象就可以了。這個方法必須傳入一個參數,否則運行時會拋異常,但可以傳入 null,表示不繼承任何對象,所以,JavaScript 里,是允許對象沒有原型,允許對象不具有繼承結構的。
對於原型,在後續會專門有一篇來講講,這裡大概清楚些概念即可。
添加屬性
JavaScript 里的對象,其實可以通俗的理解成屬性的集合,既然是作為容器的存在,那麼其實創建完對象只是第一步,後續就是往這個集合中添加屬性,所以 JavaScript 里,對象是允許在運行期間動態添加屬性的。
添加屬性的方式,可以通過對象直接量方式,在創建對象之時,就寫在對象直接量中,或者運行期間動態添加,如:
var o = {name:"dasu"}
o.age = 24;
o.sex = "man";
o.love = "girl";
但需要註意一點的是,不像 Java 中在編寫類代碼,為類添加成員變數時,可以只聲明卻初始化。在 JavaScript 中,是不允許這樣的。
也就是說,為對象添加某個屬性時,需要直接將屬性名和屬性值都設置好,其實想想也能明白,對象無非就是屬性的集合,你見過對哪個集合進行添加數據時,是可以只對該集合設置一個 key 值的嗎?
查詢屬性
訪問對象的屬性方式很簡單,兩種:.
運算符或 []
運算符;
兩種方式都可以訪問對象的屬性,但有一個區別:
.
運算符訪問屬性的話,後面跟著的是屬性名[]
運算符訪問屬性的話,中括弧內跟著的是屬性名的字元串
仍舊以上面例子中的代碼為例:
由於通過 []
運算符訪問對象的屬性,需要的是一個屬性名的字元串形式,所以這種方式會特別靈活,你可以再 []
內放一個表達式也可以,只要表達式最後的結果是字元串,或者說可以自動類型轉換為屬性名的字元串即可,特別靈活。
而 .
運算符可能會比較習慣,但它就只能明明確確的通過屬性名來訪問了,如果你想通過某種拼接規則來生成屬性名,就只能用 []
不能使用 .
。
如果訪問對象中某個不存在的屬性時,並不會拋異常,會輸出 undefined,但如果繼續訪問不存在的屬性的屬性時,等價於訪問 undefined 原始類型值的屬性,這就會拋異常了:
ps:是不是發現,對對象的操作很像 Java 中對集合的操作?所以,有人解釋說對象是屬性的集合,這不是沒根據的。
刪除屬性
delete 是用來刪除對象上的屬性的,但它只是將該屬性名從對象上移除,並不刪除屬性值,舉個例子:
var a = [1,2,3];
var o = {name:"dasu", arr:a};
delete o.arr;
console.log(a[0]); //輸出 => 1
console.log(o.arr); //輸出 => undefined
新鍵一個對象 o,它有個屬性 aar 存儲著數組 a,當通過 delete 刪除對象 o 上的 aar 屬性後,再去訪問這個 aar 屬性,獲取的是 undefined,表明這個屬性確實被刪除了,但本質上只是將其與這個對象 o 的關聯刪除掉,並不會刪除屬性值,所以輸出數組 a 的值時還是可以訪問到的。
不過,delete 有一些局限,它並不是什麼屬性都可以刪除:
- 只能刪除自由屬性,無法刪除繼承屬性
- 不能刪除那些可配置性為 false 的屬性
屬性擁有一些特性,在下麵會講,其中有一個是可配置性,當將這個特性設置為 false 時,就無法通過 delete 來刪除。
而之前說過的,通過 var 聲明的全局變數,雖然它最後是作為全局對象的屬性存在,但它的可配置性被設為 false,所以這些全局變數才無法通過 delete 被刪除。
嘗試刪除那些無法刪除的屬性,並不會讓程式出問題,delete 表達式有一個返回值,true 表示刪除成功,false 表示刪除失敗,僅此而已,沒有其他什麼副作用。
檢測屬性
因為 JavaScript 中對象的屬性太過動態性了,在運行期間,都無法確定某個屬性到底存不存在,某個到底是不是指定對象的屬性,所以這種場景,一般都需要進行屬性的檢測。
也就是檢測對象內是否含有某個屬性,有多種方式,下麵分別來介紹:
查詢屬性的方式
之前說過,訪問對象內不存在的屬性時,會返回 undefined,可以利用這點來判斷對象是否含有要訪問的屬性。
這種方式有個缺點,如果屬性值剛好被人為的手動設置成 undefined 時,就無法區別對象究竟有沒有這個屬性。
in 運算符方式
in 運算符左側是屬性名的字元串格式,右側是對象,當右側對象含有左側字元串同名的屬性時,返回 true,用這種方式就可以很好的判斷對象是否含有某個屬性。
註意,左側必須是屬性名的字元串格式,跟 []
運算符訪問對象屬性一樣的限制要求。
但這種方式有個局限,就是無法區分這個屬性究竟是自有屬性還是繼承屬性,也就是說,繼承自原型的屬性通過該操作符同樣會返回 true。
hasOwnProperty()
上面說過,通過構造函數創建的對象,預設都會存在內置的繼承結構,不管什麼對象,這個預設的繼承結構頂端都是構造函數 Object 的 prototype 屬性值,由於它的屬性值是一個內置的匿名對象,所以,通常都直接這麼表達,對象都會繼承自 Object.prototype,直接用 Object.prototype 的描述來代表這個屬性所指向的具體對象。
所以,以後在看到諸如某某對象繼承自 Function.prototype 或 Array.prototype 之類的描述,我們要能夠清楚,它表示的是,對象的原型是 xxxx.prototype 這屬性所指向的具體對象。
Object.prototype 屬性值指向的對象中,定義了一個 hasOwnProperty()
方法,所以基本所有對象都可以使用,它是用來判斷,對象是否含有指定的自有屬性的。
首先利用上小節介紹的 in 方式來檢測,o 對象的預設繼承結構頂端是 Object.prototype,所以 o 對象繼承了它的 hasOwnProperty 屬性,第一行代碼返回 true。
這個 hasOwnProperty 屬性是個方法,調用它可以來檢測對象是否含有指定的自有屬性,參數也需要傳入屬性名的字元串格式,所以第二行代碼返回 false,第三行返回 true。
hasOwnProperty 是繼承自 Object.prototype 的屬性,由於 hasOwnProperty()
方法只能檢測自有屬性,所以第四行返回 false。
propertyIsEnumerable()
這個方法同樣是 Object.prototype 中所定義的方法,所以,同樣基本所有對象都能夠使用。
它是 hasOwnProperty()
的增強版,也就是,用於檢測對象的自有屬性且該屬性是可枚舉性的,才會返回 true。
可枚舉性是屬性的另一個特性,用來標識該屬性是否允許被遍歷,下麵會講解。
因為有一些內置屬性是不希望被枚舉出來的,所以可通過該方法來判斷。
遍歷屬性
遍歷屬性也稱枚舉屬性,也就是類似於對集合進行遍歷操作,將其所含有的屬性一個個讀取出來。
遍歷對象屬性的方式也有多種,也一一來介紹:
for-in 遍歷
var o = {name:"dasu"}
var o1 = Object.create(o); //o1 繼承自 o
o1.age = 24;
o1.sex = "man";
o1.love = "girl";
for(p in o1) {
console.log(p);
}
看看輸出的結果:
o1 繼承自 o,在 o1 內有三個自有屬性,有一個繼承屬性,通過 for-in 方式遍歷對象 o1 的屬性時,不管是自有屬性,還是繼承屬性,都會被輸出。
同時,輸出的是屬性名,並不是屬性值,所以 for-in 方式只是遍歷對象的屬性(包括繼承屬性),並返回屬性名,註意是屬性名。
通常 for-in 這種方式,可以結合 hasOwnProperty()
方法一起使用,來過濾掉繼承的屬性。
Object.keys()
這又是一個類似靜態方法的存在,註意這個方法跟上述 Object.create() 都是構造函數 Object 上的方法,而普通對象繼承的是構造函數 Object.prototype 屬性值所指向的那個原型對象,這是兩個相互獨立的對象,也就是說,通過構造函數創建出來的子對象並不是繼承構造函數對象本身。
所以在子對象中,無法使用 Object.keys() 這類構造函數對象本身的屬性,這點需要註意一下,在後續專門講繼承時會再拿出來講講。
參數傳入需要遍歷屬性的對象,通過該方法,可以獲得一個數組對象,數組內就存儲著參數傳入的對象的自有屬性且屬性是可枚舉性的,相當於 for-in 方式結合 hasOwnProperty()
的效果。
Object.getOwnPropertyNames()
該方法也是遍歷對象的自有屬性,只是它是將參數傳入的對象所擁有的所有屬性都輸出,包括那些被設置為不可枚舉的屬性,看個例子:
Object.prototype 指向了一個內置的對象,內置對象中定義了很多屬性,繼承這個原型的子對象們都可以使用這些屬性,但這些屬性都被設置為不可枚舉性,所以通過 Object.keys() 遍歷它時,得到的是一個空數組,子對象通過 for-in 方式遍歷時也讀取不到這些屬性。
這種設計是好的,但考慮到如果有某些場景是需要讀取對象自身的所有屬性,包括那些不可枚舉的,此時,就可通過 Object.getOwnPropertyNames() 來達到目的了。
屬性的特性
上面介紹中,或多或少有提到屬性的特性,屬性特性是指,屬性的一些特有行為。
屬性的特性一共有三個:可寫性、可配置性、可枚舉性
- 可寫性:表示這個屬性是否允許被更改,當設置成 false 時,這就是一個只讀屬性
- 可配置性:表示這個屬性是否允許被動態的添加或刪除,當設置成 false 時,就不允許通過 delete 來刪除
- 可枚舉性:表示這個屬性是否允許在遍歷屬性過程中被讀取,當設置成 false 時,通過 for-in 或 Object.keys 都無法遍歷到這個屬性
那麼,如果知道對象的某個屬性的這三種特性都是什麼配置呢?
針對這種情況,內置了一個叫做屬性描述符的對象,這個對象本身含有四個屬性來描述屬性:value、writable、enumerable、configurable。
- value:描述屬性值,即 key-value 中的 value
- writable:描述屬性的可寫性
- enumerable:描述屬性的可枚舉性
- configurable:描述屬性的可配置性
用來描述屬性的數據結構有了,接下去就是如何操作了,先看一下,如果獲取對象某個屬性的描述信息:
Object.getOwnPropertyDescriptor()
還是通過Object 的一個方法,接收兩個參數,第一個參數是對象,第二個參數是對象內的某個自有屬性,將會返回一個屬性描述符對象:
內置對象的很多屬性都會針對屬性的使用場景進行了不同的配置了,比如 Object.prototype 中所有屬性的 enumerable 可枚舉性都配置成 false。
但對於在代碼中,通過對象直接量創建的對象,或者自定義構造函數創建的對象等,對這些非內置對象添加的屬性,預設這三個特性都為 true,即對象添加的屬性預設都是可寫、可枚舉、可配置的。
Object.getOwnPropertyDescriptors()
這個方法也是用來獲取對象屬性的描述信息的,只是它只需一個參數即可,就是對象,然後會輸出所有自有屬性的描述信息:
這兩個方法都是只能獲取對象的自有屬性的描述信息,如果想要獲取繼承屬性的描述信息,需要先獲取原型對象,再調用這兩個方法處理原型。獲取原型對象後續講原型時會介紹,這裡知道思路就可以了。
Object.defineProperty()
有獲取對象屬性的描述信息的方法,自然有設置對象屬性的描述信息方法,所以與上面兩個方法相對應的就是 Object.defineProperty() 方法和 Object.definproperties()。
Object.defineProperty() 接收三個參數,第一參數是對象,第二個參數是需要修改屬性描述信息的屬性,第三個參數是含有屬性描述符結構的對象:
var o = {name:"dasu"}
var o1 = Object.create(o); //o1 繼承自 o
o1.age = 24;
o1.sex = "man";
o1.love = "girl";
Object.defineProperty(o1, "age", {writable:false});
Object.defineProperty(o1, "love", {enumerable:false, configurable:false});
第三個參數,你可以將四個屬性值都指定,沒指定的仍舊會使用預設的配置,再用 Object.getOwnPropertyDescriptors() 看下修改後的配置:
Object.defineProperties()
這方法作用跟上面一樣,只是它是批量處理的方法,接收兩個參數,第一個是對象,第二個是需要修改的屬性集合,如:
var o = {name:"dasu"}
var o1 = Object.create(o); //o1 繼承自 o
o1.age = 24;
o1.sex = "man";
o1.love = "girl";
Object.defineProperties(o1, {
age: {writable:false},
love: {enumerable:false, configurable:false}
})
規則
有一些規則需要註意一下:
- 如果屬性是不可配置的,那麼不能修改它的可配置性和可枚舉性,對於可寫性,只能將 true 改為 false,不能將 false 改為 true
- 如果屬性是不可配置且不可寫的,那麼不能修改這個屬性的值
- 如果屬性是可配置但不可寫的,那麼可以先將屬性修改成可寫,這時就可以修改屬性值
屬性的setter和getter
正常來說,對象的屬性由屬性的三種特性來控制屬性的操縱限制,但有一種情況是例外的,那就是通過 setter 和 getter 添加的屬性,這類屬性通常叫做存取器屬性,為了區分,將正常使用的那些屬性叫做數據屬性。
之所以叫做存取器屬性,是因為,通過這種方式添加的屬性,它的讀寫是交由 setter 和 getter 控制,並不是由屬性描述符的三種特性控制。
先來看下,如何定義一個存取器屬性:
var o = {
set name(value) {},
get name() {return "dasu"},
get age() {return 24}
}
雖然看起來有點像 Java 中的 set 方法和 get 方法,但完全是兩種不一樣的東西,首先,這裡的 set 和 get 雖然類似方法,但外部是不能通過方法來調用,第二,外部訪問這些存取器屬性,仍舊是使用 .
或 []
,如 o.age 或 o["name"]。
相比於數據屬性,存取器屬性的區別就在於,讀和寫是通過 set 和 get 控制,在定義存取器屬性時,如果沒有定義 get,那麼這個屬性就是無法讀取的,如果沒有定義 set,那麼這個屬性就是不可寫的。其餘的,可枚舉性和可配置性都跟數據屬性一樣。
也一樣是通過 Object.defineProperty() 和 Object.getOwnPropertyDescriptor() 來設置或查看存取器屬性的描述信息,唯一需要註意的是,對於數據屬性,描述符對象有四個屬性:value,writable,enumerable,configurable;但對於存取器屬性來說,沒有 value 和 writable 屬性,與之替換的是 get 和 set 兩個屬性,所以看個例子:
var o = {
set name(value) {},
get name() {return "dasu"}, //存取器屬性,可讀,可寫,但讀寫邏輯得自己實現
get age() {return 24}, //存取器屬性,只讀,讀的邏輯得自己寫
sex: "man" //數據屬性
}
假設定義了這麼個對象,有兩個存取器屬性,一個數據屬性,通過 Object.getOwnPropertyDescriptors() 看一下這些屬性的描述:
所以存取器屬性和數據屬性就在於讀和寫這兩方面的不同,看下修改描述的方式:
存取器屬性是可以換成數據屬性,同樣,數據屬性也是可以換成存取器屬性的,通過 Object.defineProperty() 在修改屬性描述信息時,使用的如果是 set 和 get,那就將數據屬性換成存取器屬性了,使用的如果是 value 和 writable,原本如果是存取器屬性,就將存取器屬性轉換成數據屬性了。
另外,它也有一些規則需要註意一下:
- 如果存取器屬性是不可配置的,則不能修改 set 和 get 方法,也不能將它轉換為數據屬性
- 如果數據屬性是不可配置的,則不能將它轉換為存取器屬性
對象的特性
對象的屬性有它的幾種特性,而對象本身也有一些特性,主要是三個:原型屬性、類屬性、可擴展性
原型屬性:表示對象繼承自哪個對象,被繼承的對象稱為子對象們的原型
類屬性:表示對象的類型信息,是一個字元串,比如數字的類屬性為 Number
可擴展性:表示是否允許對象可動態的添加屬性
原型留著後續講原型時再來細講,大概清楚對象是有繼承結構,被他繼承的對象稱作它的原型,所以通常說 JavaScript 是基於原型的繼承這些概念即可。
類屬性
類屬性,本質上就是通過調用 Object.toString() 方法來輸出對象的一些信息,這些信息中,包括了對象所屬類型的信息,對這串文本信息進行截取處理,就可以只獲取對象所屬類型的信息,所以稱這些信息為對象的類屬性。
類屬性所呈現的信息很類似於 typeOf 運算符所獲取的信息,只是類屬性會比 typeOf 更有用一些,它能夠區分所有的內置對象,以及區分 null,這些是 typeOf 所做不到的,如:
可擴展性
類似於屬性有可配置性、可寫性、可枚舉性來控制屬性的操縱限制,對象也具有可擴展性來限制對象的一些行為。
當將對象的可擴展性設置為 false 時,就無法再動態的為對象添加屬性。預設創建的新對象,都是具有可擴展性的。
不像屬性的特性那樣,還專門定義了一個屬性描述符對象來控制屬性的特性,對於對象的可擴展性,操作很簡單:
Object.isExtensible()
使用 Object.isExtensible() 來獲取對象的可擴展性描述,返回 true,表示對象是可擴展的,即可動態添加屬性。
#### Object.preventExtensions()
同樣,可使用 Object.preventExtensions() 來設置對象的不可擴展,參數傳入對象即可。這樣,這個對象就不可動態添加屬性了。
但有幾點需要註意:
- 一旦將對象設置為不可擴展,就無法再將其轉換回可擴展了
- 可擴展性只限制於對象本身,對對象的原型並不影響,在原型上添加的屬性仍可動態同步到子對象上
針對於對象的可擴展性,對象屬性的可寫性、可配置性、可枚舉性這些操作,Object 內封裝了一些便捷的方法,如:
- Object.seal():將對象設置為不可擴展,同時,將對象所有自有屬性都設置為不可配置,通常稱封閉對象。可用 Object.isSealed() 來檢測對象是否被封閉。
- Object.freeze():將對象設置為不可擴展,同時,將對象所有自有屬性不可配置且只讀,通常稱凍結對象。可用 Object.isFrozen() 來檢測對象是否被凍結。
大家好,我是 dasu,歡迎關註我的公眾號(dasuAndroidTv),公眾號中有我的聯繫方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關註,要標明原文哦,謝謝支持~