##**理解對象**##
---
###**屬性類型**
> ?JavaScript中有兩種屬性類型 分別是 數據屬性和訪問器屬性
#### **數據屬性**
數據屬性具有四個特征值
[Configurble] 表示能否修改特性或者是通過delete重新定義屬性 默認為true
[Enumerable] 表示能否通過forin遍歷循環返回屬性
[Writable] 表示能否修改這個屬性值 默認為true
[Value] 包含這個屬性的數據值
> 如果要修改屬性的特征值 那么可以通過以下的方法
```
var person = {
? ?name:"zhang"
}
Object.defineProperty(person,"name",{
? ?writable:false,
? ?value:"zhao" //小寫
});
person.name = "wang";
console.log(person.name)
```
> 但要注意的是如果你修改了屬性的Configurble 你將無法再修改這個屬性的特性。
####**訪問器屬性**
> 訪問器屬性不包含數據值,它們包含一塊getter和setter函數,在讀取和調用時會調用這些函數,從而負責返回 和 處理。訪問器有下面四個特性。
訪問器有下面四個特性。
> [Configurble] 表示能否修改特性或者是通過delete重新定義屬性 默認為true
? [Enumerable] 表示能否通過forin遍歷循環返回屬性
? [Get] 在讀取時調用的函數 ,默認為undefined
? [Set] 在寫入時調用的函數, 默認為undefined
```
var person = {
? ?name:"zhang",
? ?_year:2004,
? ?edition:1
}
Object.defineProperty(person,"year",{
? ?get: function () {
? ? ? ?return this._year;
? ?},
? ?set: function (newValue) {
? ? ? ?if(newValue > 2004){
? ? ? ? ? ?this._year = newValue; //這里的設置新值 設置的是_year的值
? ? ? ? ? ?this.edition += newValue - 2004;//每次更新同時去設置版本
? ? ? ?}
? ?}
});
//這里的year就是一個典型的**訪問器屬性**,他不保存實際的value 但是有擁有getset方法,他只能通過這種方法去定義。而數據函數則是修改特性需要調用,生成是不需要的
person.year = 2005; //其實是調用了year屬性的set方法 然后實質上設置了_year的值 并且跟新版本
alert(person.year); //調用year的get方法 返回了_year的值
alert(person.edition);
//所以實質上 _year 和 year 是兩個完全不同的值.這是訪問器屬性的常見用法 get set一個屬性 影響其他屬性。
```
####**定義多個屬性**
> 在一些情況下我們需要為一個對象的多個屬性設置特性,當然js也為我們提供了方便的方法 `Object.defineProperties()`.它提供了兩個參數,第一個參數代表了你要修改的對象,第二個參數也是一個對象 其中的屬性和第一個對象中需要修改的一一對應
```
var cat = {};
Object.defineProperties(cat,{
? ?_year:{
? ? ? ?writable:true,
? ? ? ?value:2004
? ?},
? ?edtions:{
? ? ? ?wirtable:true,
? ? ? ?value:1
? ?},
? ?year:{
? ? ? ?get: function () {
? ? ? ? ? ?return this._year;
? ? ? ?},
? ? ? ?set: function (newValue) {
? ? ? ? ? ?if(newValue > 2004){
? ? ? ? ? ? ? ?this._year = newValue; //這里的設置新值 設置的是_year的值
? ? ? ? ? ? ? ?this.edition += newValue - 2004;//每次更新同時去設置版本
? ? ? ? ? ?}
? ? ? ?}
? ?}
});
alert(cat.year);
alert(cat.edtions);
```
####**讀取屬性的特性**
> 既然有定義方法,自然有讀取方法 `Object.getOwnPropertyDescriptor()` 獲得自身特性描述符,返回一個具有四個屬性的對象,讀取不同對象的時候,他的屬性也不一樣。具體看上面的列表。
###**創建對象**
> 雖然用Obj構造函數或對象字面量可以創建單個對象,但是當你需要創建大量對象的時候,就會非常的麻煩并且產生大量重復的代碼。
####**工廠模式**
> 用函數來封裝特定接口創建對象,但這種方法無法知道他屬于什么類型的對象
```
function createPerson(name,age,job) {
? ?var o = new Object();
? ?o.name = name;
? ?o.age = age;
? ?o.job = job;
? ?o.sayName = function () {
? ? ? ?alert(this.name);
? ?}
? ?return o;
}
var tom = createPerson("tom",19,"doctor");
```
####**構造函數模式**
> 我們都知道,我們可以通過構造函數快捷構造特定的對象 比如Object和Array,此外,我們可以創建自己的構造函數。
```
function Person(name,age,job) {
? ?this.name = name;
? ?this.age = age;
? ?this.job = job;
? ?this.sayName = function () {
? ? ? ?alert(name);
? ?}
}
var tom = new Person("tom",90,"doctor");
var jerry = new Person("jerry",18,"teacher");
```
> 以上是一個典型的構造函數 我們可以和工廠模式做一個簡單的對比
> 第一 沒有顯示的創建對象
> 第二 直接通過this來給對象添加屬性
> 第三 沒有通過return把對象返回
> 第四 構造函數的首字母應大寫
既然生成了構造函數,我們可以通過new關鍵字來調用這個構造函數,當new的時候 其實發現了以下步驟
> 1. 創建了一個新的對象
> 2. ?把這個構造函數的作用域連接到這個新生成的對象 也就是說 這個函數內的this 指向新對象
> 3. ?執行構造函數的代碼(添加屬性和方法)
> 4. ?返回構造完畢的對象
在上方的例子中tom和jerry分別保存了一個**Person的一個實例** 他們其實都有一個constructor(構造函數)屬性,該屬性指向了他們的**構造函數Person**.
但實質上這種方法可能并不是非常的保險,我們可以使用更加可靠的`instanceof`操作符 就像下面這樣
```
console.log(tom instanceof Person);//true
console.log(tom instanceof Object);//true
console.log(jerry instanceof Person);//true
console.log(jerry instanceof Object);//true
//所有對象都繼承自Object
```
**構造函數模式相比工廠模式最大的優勢在于他提供了一個訪問對象類型的方法**
> 構造函數也是函數,這意味這個函數能夠被new的方式調用,同樣可以被當成普通的方式來調用,在普通情況下 他的所有特征會和普通函數表現的完全一樣。以下是一個簡單的例子。
```
Person("drivd",66,"ko");
console.log(window.name);//drivd
var cat = {};
Person.call(cat,"cat",10,"catch mourse");
console.log(cat.name);
//或者使用以下的方法 在對象內把函數置入到對象之中
cat.fnc = Person;
cat.fnc("wang",15,22);
```
> 首先是一個在全局內調用這個Person函數的情況下 Person函數正常執行 這里的**this指向了**運行他的作用域對象 也就是**window對象**。所以window對象的name就被設置成了drivd.
> 第二是在其他對象的作用域情況下,通過call方法來增加作用域 進入函數時**this指向為cat對象**。
**構造函數的問題**
```
this.sayName = function () {
? ? ? ?alert(name);
? ?}
```
> 在創建實例時,都會調用構造函數內的方法,但是每次在構造方法的時候,它實質上是**創建一個新的函數**(也就是一個對象)**然后讓sayName指向這個new出來的函數**,不同的Person實例中的方法雖然函數名和做的事情完全相同,但他們并不指向同一個函數對象。所以當你比較(==)兩個相同類型不同實例的相同方法時,**他們并不相同**。
>
> 所以我們需要一種能夠把一些函數封裝 然后在構造函數內部可以直接引用,這樣就可以讓不通實例的相同方法的指針指向同一個函數。也許把方法寫在全局變量之中,在內部調用是種方法。他達到了兩者方法里面存放一個指針 并且指向了同一個函數,但這種方法的**封裝性**實在差勁,并且并不是很好管理(如果需要非常多函數的情況下)。
####**原型模式**
>我們所創建的所有對象都擁有著一個屬性`prototype`,他包含了一個指針,指向了這個對象的原型對象,而這個原型對象的作用是提供一種類型的不同實例所共享的方法和屬性。讓我們接下來深入了解原型的本質。
**理解原型的本質**
> 首先當我們創建的一個新的函數,馬上按特定規則創造一個當前函數的`prototype`屬性,這個屬性指向了函數的原型對象。而原型對象也會生成`constructor`屬性,他指向了prototype屬性所在的函數。而在沒有實例生成的時候 這個原型屬性里其實除了`constructor`沒有任何東西
>
> 第二步發生在實例調用構造函數的過程中,創建的實例時,實例內部也會產生一個prototype屬性,然后內部產生一個指針,他指向了構造函數所指向的原型對象,(盡管實例的這個屬性對腳本不可見,不可直接訪問)。要注意的是,這個連接存在于實例與原型對象之間,這連接與構造函數并無太大關系。
>
> 對于實例可以用`Person.prototype.isPrototypeOf(Person1)` 的方法來查看是否和某個構造函數的原型建立了鏈接, 如果兩者確實有鏈接,返回true,
>
> 在EMCAScript中新增加了一個方法 它能讓我們快速的返回一個對象的原型對象 `Object.getPrototypeOf(person1)`
>
> 當實例的屬性被我們讀取時,他首先訪問自己本身有沒有這個屬性,如果沒有的情況下,就繼續查詢他的原型,如果在原型中找到了這個屬性,他的值就會被返回。
>
> 如果在某個實例的自身和他的原型中有同名的屬性,那么自身的屬性將會屏蔽原型的屬性。所以即使刪除這個屬性,修改的也永遠是其自身的屬性,他不會修改原型的屬性,我們可以通過刪除對象自身的某個屬性,來讓取消對原型屬性的屏蔽
>
> `person1.hasOwnProperty("name")`繼承自Object 他會檢查參數是否是對象其中的一個屬性,他不會去檢查對象的原型。
```
function Person() {
}
Person.prototype.name = "wang";
Person.prototype.age = 20;
Person.prototype.job = "T";
Person.prototype.sayName = function () {
? ?alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
person1.name = "zhang";
person1.sayName();//這里的sayName會查找到原型并且原型方法內的this指向為person1
alert(person1.hasOwnProperty("name")); //true
```
**原型與in操作符**
> 其實在之前我們就在forin中使用了in操作符 其實在檢測某個屬性是否屬于某個對象的時候,這個操作符同樣可以使用,他的特性與`hasOwnProperty()`不同,如果該屬性屬于該對象的原型對象,同樣會返回true,如果原型不曾擁有,返回false。
>
> 現在我們有了檢測屬性的兩種方法 1 檢測屬性是否屬于對象本身 2 檢測屬性對象是否屬于對象本身或原型。那么我們缺少第三種。檢測對象是否屬于原型。其實實現的方法很簡單 無法就是達成了兩個條件 **第一** hasOwnProperty()返回false,該屬性在本身不存在,**第二** in操作符返回true,在原型中存在。所以以下就是一個簡單方法
```
function hasPrototypeProperty(obj,name) {
? ?return !obj.hasOwnProperty(name) && (name in obj);//如果條件都達成 返回true
}
```
> 在我們通過forin循環遍歷屬性的時候,原型中的屬性也會被遍歷出來,(除非原型中屬性的特效標記為無法遍歷),但是這一點在IE8以一下版本中有一個BUG,**屏蔽不可枚舉屬性的實例屬性不會出現在forin循環之中**,也就是說,如果原型中的某個方法屏蔽in循環,然后在實例自身中覆蓋同名方法,連這個方法自身也會屏蔽In.無法發現。
>要取得一個對象中所有可以通過forin循環的屬性,可以通過`Object.keys()`方法,他接受一個對象參數,返回一個包含所有可forin遍歷的屬性標識符字符串數組。他的順序是forin中的出場順序
>
>如果想取得一個對象內的所有實例屬性 可以使用`Object.getOwnPropertyNames(Person)`他的參數是一個對象,他的返回值會包含非常多的屬性,比如caller argunments coustrusta
>當然上方的返回值都只會是自身的屬性,不會去查找他們的原型。
**更簡單的原型語法**
在很多時候重復的調用.prototype屬性的封裝性并不是很好,我們可以通過創建一個新對象并設置的方法來
```
function Person() {
}
Person.prototype = {
? ?name:"null",
? ?age:0,
? ?job:"job"
? ?sayName:function () {
? ? ? ?alert(this.name);
? ?}
}
```
這個方法 唯一問題在于 Person的原型的constructor不再指向Person構造函數,因為我們實質上創建了一個新的對象并把構造函數的prototype指針指向了他,之前默認創建的prototype對象已經被覆蓋,新對象里也沒有constructor這個屬性,辦法就是在新的原型對象內重新指定一下constructor
**原型的動態性**
原型的動態性主要體現在增加和重定向兩個方面
```
function Person() {
}
var person1 = new Person();
Person.prototype.name = "person1";
alert(person1.name);
```
> 上面的例子首先創建一個person的實例person1 然后給person的原型添加的name屬性。即使增加屬性在創建實例之后,但依舊可以正常訪問原型中的屬性。這是因為獲取值只是查詢,他在自身內沒找到就回到原型中去找,找到了,返回。
```
Person.prototype = {
? ?name:"name"
}
alert(person1.name);//undefined 因為他訪問的仍舊是創建時綁定的原型對象。
```
> 但是在這種情況下,就會發生無法訪問的情況。因為實例原型的連接出現在構造函數的那一刻,他被綁定成原來的原型對象,而后來的原型對象被重新更改成另一個新創建的對象,也就是說,這個實例對象的原型仍舊是剛開始創建的那個原型對象。 和上面的例子對比,一個是在新的原型對象內創建屬性,而另一個則是在原本的原型中去添加屬性。
**原生對象的原型**
不僅僅是自創建的對象,就連很多原生對象(比如Object Array String)都使用了原型的方式來封裝方法,通過原生對象的原型不僅可以獲取原生方法的應用,也可以為原生對象添加新的方法。
```
String.prototype.startsWith = function (tex) {
? ?return this.indexOf(tex) == 0;
}
var text = "Hello World";
alert(text.startsWith("World"));
```
以上就為基本包裝類的原型添加了一個方法
**原型對象的問題**
原生模式其實也存在一些問題,首先他沒有傳參數然后初始化的環節,也就是說原型中的值默認值都相同而且會被共享。假設我們兩個實例的原型相同,且內部有一個數組,被兩個共享,那么我們操作其中一個實例的該數組,會改變到另外一個實例的該數組。(如果重新創建 則創建在對象內部 和原型無關)
**組合使用構造函數和原型模式**
構造函數用于定義實例屬性,而原型模式用來定義需要方法和共享的屬性
```
function Person(name,age) {
? ?this.name = name;
? ?this.age = age;
? ?this.friends = ["tom","jerry"]
}
Person.prototype = {
? ?sayName:function () {
? ? ? ?alert(this.name);
? ?}
}
var person1 = new Person("wang",15);
var person2 = new Person("zhang",12);
```
**動態原型模式**
如果把分離的原型和構造函數可能會造成一些困惑,可以把初始化原型和構造函數封裝在一起
```
function Person(name,age) {
? ?this.name = name;
? ?this.age = age;
? ?this.friends = ["tom","jerry"]
? ?if(this.sayName != "function"){
? ? ? ?Person.prototype.sayName = function () {
? ? ? ? ? ?alert(this.name);
? ? ? ?}//不能調用對象字面量的方式去重寫原型,只能增加方法,不然第一個創建的對象會被隔絕
? ?}
}
```
> 以上的原型初始化只會執行一次,在第一次調用構造函數生成實例時,this指向的是new出來的對象,這個對象有原型,但這個原型內部沒有sayName函數。所以會執行一次初始化,而第二次開始new出來的對象原型中已經有sayName函數,以后就不會調用了,