深入學習JavaScript之物件
在JavaScript中,很多人都存在著一種誤解,認為JavaScript中萬物皆為物件,但是這其實是錯誤的。
1.1 語法
在JavaScript中定義物件有兩種語法。
①文字形式(宣告形式):
var obj={
key1:key1
key2:key2
..........
}
②構造形式:
var obj=new Object();
obj.key1=value;
從適用性方面來說使用文字(宣告)形式要比構造形式好得多 ,在文字形式中你可以一次性通過鍵值對新增多個屬性"key1:key1,key2:key2",但是在構造形式中你只能逐個新增屬性"obj.key1=key1;obj.key2=key2"。
1.2 型別
物件是JavaScript的基礎。
在JavaScript中一共有六種主要資料型別(術語是:“語言型別”)
- string
- number
- null
- object
- undefined
- boolean
簡單的基本型別並不是物件,它們會在使用時被物件包裝器包裝成物件
在JavaScript中有許多特殊的物件子型別,我們稱之為---------複雜基本型別
函式就是物件的一個子型別(從技術上來說是"可呼叫的物件")。JavaScript中的函式是"一等公民","一等公民-------------值可以作為引數進行傳遞,可以從子程式中返回,可以賦給變數"。
它們本質上與物件無差別,但是它們可以被呼叫。所以可以像操作其他物件一樣操作函式,比如:將函式當做引數傳入。
函式是可呼叫的物件
陣列也是物件的一個型別,具備一些額外的行為。陣列中內容的組織方式比一般的物件都要複雜一些。
內建物件
JavaScript中還有一些物件子型別,通常被稱為內建物件,有些內建物件的名稱跟基本資料型別一樣,但是它們之間的聯絡特別複雜。
- String
- Number
- Object
- Boolean
- Funtion
- Array
- Date
- RegExp
- Error
這些內建物件,從表現形式來說很像其他語言中的型別(type)或者是類(class),比如上述的內建物件String與Java中的String類。
但是,在JavaScript中它們只是內建物件,這些內建物件可以配合關鍵字"new"發生構造呼叫,從而構造一個對應上述內建物件子型別的新物件。
var string1="my name is zhu";
typeof string1; //string 基本資料型別
string1 instanceof String; //false
var string2=new String("my name is ming");
typeof string2; //Object
string2 instanceof String; //true
Object.prototype.toString.call(string2); //[object String]
這裡要解釋一下關鍵字"instanceof"的作用:二元操作符,判斷左邊例子是否為右邊類(內建物件)的基本例項
我們先是建立了一個基本資料型別:string的例項string,使用typeof判斷string1的基本資料型別,結果:string。
再使用new關鍵字配合內建物件"String",建立了子型別內建物件的例項:string2,使用typeof判斷string2的基本資料型別,結果:Object。
再使用關鍵字"instanceof"檢查兩個物件是否是"String"的例項。
最後子型別在內部借用了Object中的toString方法。
從以上結果來看,使用內建物件子型別搭配"new"可以建立一個基本資料型別為"Object"的物件。
注意!!!原始值"my name is zhu"並不是一個物件,而是一個字面量,其值不可改變。如果要對這個值進行操作如:獲取長度,插入某個字元等,必須轉化為"String"物件。幸好,在JavaScript中,當你要對字串字面量進行操作時,JavaScript會將此字面量轉化成一個String物件。也就意味著你不需要顯式的建立一個String物件
var string1="my name is zhu"; //構造形式
console.log(string1);
console.log(string1.length);
typeof string1; //"string"
這也就意味著能夠使用構造形式的資料型別或字面量(string、number、boolean)建立構造形式物件時,JavaScript會自行轉換該物件成內建物件子型別。
但是因為"null"以及"undefined"沒有構造形式,它們只有文字形式,因此它們不會被JavaScript轉換成內建物件。相反的,Date有構造形式卻沒有文字形式。
對於Objecrt、Array、Function以及RegExp來說它們都是物件不是字面量。在某些情況下,相比較文字形式,構造形式能夠額外的增添一些選項。
因此,兩種建立物件的用法推薦如下:
①先考慮語法簡單的文字形式
②如果需要額外增添選項再考慮構造式
Error會在丟擲異常時自動建立,也可以使用"new Error()"建立,一般來說沒什麼必要。
1.3 物件的屬性---內容
物件的內容是由一些儲存在特定命名位置的(任意型別)的值組成的,我們稱之為屬性。
雖說是內容,但是內容的儲存位置卻不在物件中。在引擎內部,值的儲存位置是多種多樣的,儲存在物件中的只有名稱,類似C語言中的指標,指向的是值的真正位置。
要訪問物件中的屬性,有兩種方法
①“屬性訪問”:通過"."操作符對物件的屬性進行訪問,例如:obj.a
②“鍵訪問”:通過"[..]"操作符對物件屬性進行訪問,例如:obj["a"];
var obj={
a:2
};
obj.a; //2 屬性訪問
obj["a"]; //2 鍵訪問
屬性訪問和鍵訪問都可以訪問物件中的屬性,它們可以訪問同一個屬性,得到同一個值。
區別在於,屬性訪問只可以訪問符合命名規範的屬性,鍵訪問可以訪問任意的UTF-8/Unicode字串。
舉個例子:要訪問"super-fun!",你用"."操作符屬性訪問會出錯"obj.super-fun!",原因是不符合識別符號命名規範,但是你使用"[...]"操作符鍵訪問就沒有任何的問題,"obj["super-fun!"]"
此外由於"[....]"使用字串來訪問屬性,因此,可以用"[...]"在程式中構造這個字串
舉個例子
var obj={
a:2,
};
var idx;
if(true){
idx="a";
}
console.log(obj[idx]); //2 注意此處鍵訪問,idx是不帶有引號的引用
在物件中,屬性名永遠是字串。如果你使用string(字面量)以外的其他值作為屬性名,那他首先會被轉換成一個字串。即使是數字也不例外,雖然在陣列下標中使用的是數字,但是在物件屬性中數字會被轉換成字串,所以千萬不要搞混了物件和陣列中的用法。
var myObject={};
//使用鍵訪問進行構造屬性
myObject[true]="foo";
myObject[3]="bar";
myObject[myObject]="baz";
//無論是怎樣型別的字面量都轉化成了字串
console.log(myObject["true"]);
console.log(myObject["3"]);
console.log(myObject["[object Object]"]);
我們在絕大多數情況下,採用的是"."操作符的屬性訪問,只有在某些特定情況下才會用"[...]"鍵訪問,比如訪問不符合規範的識別符號
1.3.1 可計算的屬性名
"[...]"鍵訪問的用處除了訪問不符合規範的識別符號之外,還能夠通過表示式計算屬性名。
比如:myObject[prefix+name]。
ES6中增加了可計算的屬性名,在文字形式宣告物件時,通過[ ]包裹一個表示式來當屬性名
var prefix="foo";
var obj={
[prefix+"bar"]:"hello",
[prefix+"baz"]:"word"
};
console.log(obj["foobar"]);
console.log(obj["foobaz"]);
可計算屬性名最常用的場景是ES6中的符號,符號是ES6中的一個新的基礎型別,本身是一個字串,包含著一個不透明無法預測的值,你不會用到這個符號的值,你只會用到這個值的名稱。
1.3.2 屬性與方法
當一個物件中的屬性是一個函式時,絕大多數人都喜歡稱這個函式為方法,這個說法有什麼問題嗎?答案是,之前我們說過函式是一個可被呼叫的物件,方法的定義是:屬於某一個物件的函式,如此一來稱呼函式為方法有點不太恰當。
函式如果是一個物件的屬性,那麼我們可以稱之為屬性訪問。
這時候你就要疑惑了,函式中不是存在著"this"關鍵字嗎?這不正是函式是方法最有利的證明嗎?
確實,有些函式具有this引用,這些this確實會指向呼叫位置的物件引用。但是這種用法從本質上來說並沒有把函式變成一個方法,因為"this"是根據呼叫位置動態繫結的,所以函式與物件的關係最多也只能說是間接關係
函式在物件中,它的返回值是一個函式。但是,無論返回值是什麼型別,每次訪問物件的屬性就是屬性訪問。如果它對的返回值是一個函式,那麼也不能叫做方法,屬性返回的函式和其他函式沒有任何區別,除了隱式繫結this的指向以外。
舉個例子
function foo() {
console.log("foo");
}
var somefoo=foo;
var obj={
somefoo:foo
};
foo(); //foo function foo={...}
somefoo(); //foo function foo={...}
obj.somefoo(); //foo function foo={...}
somefoo以及obj.somefoo是對foo()函式的不同引用,輸出是一樣的。如果foo()函式中存在著一個this,那麼當somefoo()引用foo()時,this指向全域性變數,obj.foo引用foo()的話,this指向obj內部。如果不明白的可以看我的https://blog.csdn.net/qq_41889956/article/details/83386111這篇文章。
那麼當在物件中定義一個函式時,這個函式會不會成為方法呢?
看個例子
var obj={
foo:function () {
console.log("foo");
}
};
var somefoo=obj.foo;
somefoo(); //foo funciton (){...}
obj.foo(); //foo funciton (){...}
可以看出來,在一個物件的文字形式中建立一個函式,這個函式也不能稱之為這個物件的方法。
1.3.3 陣列
陣列支援用[...]進行訪問,陣列有一套更加結構化的值儲存機制(值的型別不限制)。陣列期望的是陣列下標,陣列下標只能夠為整數,這個整數稱之為索引,比如:0或者5
var myArray=["baz","bar",3];
myArray[0]; //baz
myArray[1]; //bar
myArray[2]; //3
myArray.length; //3
陣列也是物件,雖然每個陣列下標是整數,但是你也可以給它新增屬性。
var myArray=["baz","bar",3];
myArray.bax="bax"; //添加了bax這一個屬性
console.log(myArray.bax);
console.log(myArray.length); //3 bax沒有算進陣列索引中
可以看到儘管添加了命名屬性(無論是"."操作符還是"[...]"),但是陣列的長度卻沒有發生任何變化。
你可以將陣列看成是一個物件來對待,但是我們不支援這種做法,因為在JavaScript中對,物件,陣列都進行了優化。
最好用物件來存放鍵值對,用陣列來儲存數值下標/值對。
注意如果你通過"[...]"來向陣列新增屬性時,你新增的屬性名是一個數字,那它會變成一個數組下標(因此會修改陣列內容而不是新增屬性)
var myArray=["baz","bar",3];
console.log(myArray[2]); //3
console.log(myArray.length) //3
myArray["2"]="foo";
console.log(myArray.length); //3
console.log(myArray[2]); //foo
可以看出,myArray[2]被新新增的myArray["2"]替代,原本的陣列長度為3,新增屬性後的陣列長度也為3。那麼原本的myArray[2]=3被修改成了myArray[2]="foo"。
1.3.4 複製物件
在某些情況下,我們需要賦值物件,但是複製物件往往存在著很大的問題。
賦值物件分為兩種:
①淺複製:物件中屬性引用依舊為屬性引用,屬性的值為字面量時,新的屬性掩蓋舊的屬性
②深複製:不止會複製屬性,還會複製引用的函式。
這麼說起來有點難以理解,讓我們來看看程式碼
function anotherFunction() {
/..../
}
var anotherObject={
c:true
};
var anotherArray=[];
var myObject={
a:2,
b:anotherObject, //引用,不是複製
c:anotherArray, //同樣是引用
d:anotherFunction
}
anotherArray.push(anotherObject,myObject);
如何表示myObject的複製呢?
首先,我們先判斷它是淺複製還是深複製。
對於淺複製來說,複製出的新物件中"a"的值會複製舊物件中"a"的值,也就是2,但是新物件中的"b c d"其實就是三個引用,它們的作用跟舊物件的屬性是一樣的。
對於深複製來說,這個就很複雜了,它複製的物件除了myObject之外還會複製anotherObject、anotherArray。這時就出問題了,在程式碼的最後一行"anotherArray.push(antherObject,myObject)"又再次引用了myObject,於是又會複製這一個物件,在這一個物件中我們又需要複製anotherArray,由此形成了死迴圈。
我們是應當檢測迴圈並終止迴圈(不復制深層元素)?還是應當直接報錯或者是選擇其他方法?
除此之外,我們還不能確定“複製”一個函式意味著什麼,有些人通過"toString"來序列化一個函式的原始碼(但是結果取決於JavaScript的具體實現,不同的引擎對於不同型別的函式處理方式並不完全相同)。
那麼如何解決這一個棘手的問題呢?許多的JavaScript框架都提出了自己的解決方法,但是JavaScript應當採取哪種方法作為標準呢?在很長一段時間內這個問題都沒有答案。
對於JSON安全(也就是說可以被序列化為一個JSON字串並且可以根據這個字串解析出一個結構和值一模一樣的物件)的物件來說有一種巧妙的方法。
var newObj=JSON.parse(JSON.stringify(someObj));
當然,這種方法需要保證物件是JSON安全的,所以只能適用部分情況。
相比較於深複製,淺複製要簡單得多。在ES6中定義了Object.assign(...)方法來實現淺複製。
Object.assign方法的第一個引數是目標物件,之後可以跟一個或多個源物件。它會遍歷一個或多個源物件的所有可列舉的自有鍵,並把它們複製(使用=操作法)到目標物件,最後返回目標物件。
接著上面的程式碼:
var newObj=Object.assign({},myObject);
newObj.a; //2
newObj.b===anotherObject; //true
newObj.c===anotherArray; //true
newObj.d===anotherFunction; //true
注意因為是使用=操作符進行復制,源物件屬性的一些特性(比如writable)是不會被複制到目標物件的。
1.3.5 屬性描述符
在ES5之前,JavaScript中沒有什麼方法能夠檢測屬性特性,比如判斷屬性是否可寫
但是自ES5開始,所有的屬性都具有了屬性描述符。
屬性特性符又稱“資料描述符”:描述屬性的某些特性,例如:value、writable、enumeration
思考以下程式碼:
var myObject={
a:2
};
console.log(Object.getOwnPropertyDescriptor(myObject,"a"));
// {
// value: 2, 值:2
// writable: true, 可寫:true
// enumerable: true, 可列舉:true
// configurable: true 可配置:true
// }
上述程式碼中我們建立了一個myObject物件,其中由一個屬性"a=2",在ES5以上的版本中無論是任何的屬性都帶有屬性描述符,我們使用"Object.getOwnPropertyDescriptor(...)"得到屬性預設的屬性描述符。
"Object.getOwnPropertyDescriptor(物件,"屬性")"中傳入的第一個引數為想要了解的物件,第二個引數為屬性,為想要了解的屬性。例如本例中,我們想要了解"myObject"這個物件的"a"屬性的屬性特性符有哪些。
在建立普通屬性時,普通屬性的屬性特性符是預設值(writable:true、enumerable:true、configuration:true),但是你可以使用Objcet.defineProperty(物件,"屬性名",修改體)來修改屬性特性符
var myObject={};
Object.defineProperty(myObject,"a",{
value:2,
writable:false,
enumerable:false,
configurable:true
});
console.log(myObject.a); //2
console.log(Object.getOwnPropertyDescriptor(myObject,"a")); //{value:2,writable:false,enumerable:false,configurable:falase}
利用Object.defineProperty(...)可以為物件新增屬性,並修改屬性特性符,但是正在一般情況下你不會使用此方法新增屬性,除非你想要修改屬性特性符。
下面介紹各個屬性特性符的作用
①writable
writable是決定屬性是否可被修改
writable:true----可修改
writable:false-----不可修改
var myOcject={
a:2
};
Object.defineProperty(myOcject,"a",{
writable:false
});
myOcject.a=3; //此處想要修改屬性“a”值為3
console.log(myOcject.a); //2 修改失敗,因為writable為false
我們嘗試使用"myObject.a=3"修改"a"的值,但是由於"writable:false",所以我們從輸出可以看出,我們修改失敗。
但是!!!在嚴格模式下會出錯,因為它會提示你修改了一個無法修改的屬性
"use strict";
var myOcject={
a:2
};
Object.defineProperty(myOcject,"a",{
writable:false
});
myOcject.a=3;
console.log(myOcject.a); //TypeError
執行結果
②configurable
configurable決定屬性是否能被配置,配置即為修改屬性的屬性特性符
configurable:true-------可配置
configurable:false------不可被配置
var myObject={
a:2
};
Object.defineProperty(myObject,"a",{
writable:true,
enumerable:true,
configurable:false
});
myObject.a=3;
console.log(myObject.a); //3
Object.defineProperty(myObject,"a",{
writable:true,
enumerable:false,
configurable:true
}); //TypeError
從上述結果可以看出,屬性特性符"configurable:false"時,"myObject.a=3"賦值成功,而使用Object.defineProperty(...)修改屬性特性符失敗丟擲錯誤。證明"configurable"是決定屬性特性符能否被配置
無論是處在嚴格模式下或者是非嚴格模式下,當你嘗試修改一個不可配置的屬性特性符都會出錯。
把"configurable"修改是單向的無法撤銷!!!
此處有一個小細節,及時你把"configurable"修改為false,"writable"的值依然可以從true變成false,但是無法由false變成true
當configurable:false時,你除了無法修改屬性特性符,你還無法刪除屬性!!!
var myObject={
a:2
};
console.log(myObject.a); //2
delete myObject.a; //刪除myObject.a
console.log(myObject.a); //undefined 刪除成功
Object.defineProperty(myObject,"a",{
value:3,
configurable:false
});
console.log(myObject.a); //3
delete myObject.a; //刪除myObject.a
console.log(myObject.a) //3 刪除失敗
在我們沒有將"configurable"修改為"false"時,這時的"configurable"預設為true,我們嘗試刪除"a"屬性,成功。當我們將"configurable"修改為false後,嘗試修改失敗。
因為此時屬性是不可被修改的。
③enumerable
enumerable控制的是屬性是否會出現在物件的屬性列舉中比如說"for..in"迴圈
enumerable=true-------該屬效能夠出現在物件的列舉中
enumerable=false------該屬性不能夠出現在物件的列舉中
var myObject={
c:1
};
Object.defineProperty(myObject,"a",{
value:2,
enumerable:true
});
console.log(myObject.a); //2
Object.defineProperty(myObject,"b",{
value:3,
enumerable:false
});
console.log(myObject.b); //3
console.log("a" in myObject); //true 判斷a是否在myObject中
console.log("b" in myObject); //true 判斷b是否在myObject中
for(var k in myObject){ //屬性存在於物件在中就會被輸出
console.log(k,myObject[k]); //a:2 c:1 b沒有出現
};
從結果我們可以看出,屬性"b"的屬性描述符"enumerable:false"時,在for...in迴圈中,無法發現"b"。所以列舉最通俗的說法就是物件的遍歷,可列舉就是“能否出現在物件的遍歷中”。
此處的for...in迴圈並不適合用在陣列中,因為這種列舉(遍歷)不僅會包含陣列索引還會包含所有可列舉屬性。在遍歷陣列時,最好還是使用簡單的for迴圈。
var myObject=[1,2,3];
myObject.a="a"; //陣列中新增的可列舉屬性
for(var k in myObject){ //遍歷陣列
console.log(k,myObject[k]); //0:1 1:2 2:3 a:a 本意為遍歷陣列的索引,現在變成了遍歷陣列所有可列舉屬性
}
for(var i=0;i<myObject.length;i++){
console.log(i,myObject[i]); //0:1 1:2 2:3 使用普通for遍歷正常陣列
}
也可以通過另一種方式判斷是否可列舉,那就是"Object.propertyIsEnumerable(...)"
var myObjct={};
Object.defineProperty(myObjct,"a",{
value:2,
enumerable:true
});
Object.defineProperty(myObjct,"b",{
value:3,
enumerable:false
});
console.log(myObject.propertyIsEnumerable("a")); //true
console.log(myObject.propertyIsEnumerable("b")); //false
console.log(Object.keys(myObjct)); //["a"]
console.log(Object.getOwnPropertyNames(myObjct));//["a"] ["b"]
propertyIsEnumerable(..)會檢查給定的屬性名是否直接存於物件中(而不是原型鏈中),並且滿足"enumerable:false"
"Object.keys(...)"會返回一個數組,包含所有可列舉的屬性,"Object.getOwnPropertyNames(...)"也會返回一個數組,包含所有屬性(無論可不可列舉)。這兩個函式都只會在物件中查詢,而不會設計到原型鏈。
1.3.6 不變性
在某種情況下,你會希望物件或者屬性不可被改變,在ES5中有很多方法實現。
很多方法建立的都是淺不變形,也就是說它們只會影響目標物件和它們的直接屬性。如果目標物件引用了其他物件(陣列,函式,物件)的話,其他物件的內容不受影響,但仍是可變的。
舉個例子:
myObject.foo; //[1,2,3]
myObject.foo.push(4);
myObject.foo; //[1,2,3,4]
假設程式碼中的"myObject"已經建立且不可改變,但是我了保護它裡面的可呼叫物件"foo",我們還需要用以下方法讓"foo"也不變。
①物件常量
在上一節中,我們學習了屬性描述符,我們可以利用屬性描述符,讓物件屬性不可寫,不可重定義,不可刪除,成為一個真正意義上的物件屬性常量。
為屬性新增"writable:false configurable:false"
var myOdject={};
Object.defineProperty(myOdject,"a",{
value:2,
writable:false,
configurable:false
});
console.log(myOdject.a); //2
myOdject.a=3;
console.log(myOdject.a); //2 對a修改無效
delete myOdject.a;
console.log(myOdject.a); //2
可以看出使用"writable:false configurable:false"之後,"a"屬性不可被重寫,也不可被刪除。
②禁止擴充套件
如果你希望一個已經建立的物件不能夠新增屬性且保留原來屬性,那麼就可以用到"Object.preventExtensions(.....)"
Object.preventExtentions(物件)
var myObject={
a:2
};
Object.preventExtensions(myObject); //禁止myObject物件新增新屬性
myObject.b=3;
console.log(myObject.b); //undefined
在非嚴格模式下,建立"b"屬性會出錯,在嚴格模式下會丟擲"TypeError"錯誤
③密封
密封是指:密封一個物件,使它不能夠新增屬性,且保留的屬性也不可刪除,但是屬性值可以修改
"Object.seal(...)"可以完成這個功能,從功能上看,"Object.seal(...)"就像是結合了前兩個功能(常量以及禁止拓展),這種說法也不是很對。
但是"Object.seal(...)"方法的具體實現是:對在一個傳入物件中呼叫"Object.preventExtensions(....)"再修改屬性特性符"configurable:false"。正因如此,可以修改物件屬性的值(因為沒有修改"writable")
var myObject={
a:2
};
Object.seal(myObject); //密封物件
console.log(myObject.a); //2
Object.defineProperty(myObject,"a",{
enumerable:false
}); //TypeError 嘗試修改屬性特性符失敗,證明configurable:false
myObject.b=3;
console.log(myObject.b); //undefined 嘗試新增屬性失敗,證明Object.preventExtensions(MyObject)
myObject.a=4;
console.log(myObject.a); //嘗試修改
④凍結
"Object.freeze(...)"會建立凍結一個物件,這個物件實際上是在 密封(Object.seal(...)) 的基礎上新增"writable:false",真正做到了一個屬性無法新增三處屬性,也無法修改屬性的值。
此方法是應用在一個物件上最高的不變性。它會禁止對於物件本身及其任意直接屬性的修改(不過這個物件引用其他物件不會受到影響)
你可以“深度凍結”一個物件,具體怎麼做呢?遍歷一個物件,將每個物件新增"Object.freeze(...)",如此一來這個物件的屬性,既不能被修改,也不能被刪除,重寫,更不能新增屬性,屬性特性符也不能被修改。但是很有可能因此凍結了其他的共享物件
1.3.7 [[Get]]
在我們訪問物件中的屬性時,其實是發生了很多事情的。
var myObject={
a:2
};
console.log(myObject.a); //2
我們是如何查詢物件的屬性的呢?通常的一種看法是,在物件中查詢屬性為"a"的屬性,這種說法不全對。
在語言規範中,myObject.a在myObject中,實際上是實行了[[Get]]操作(這個操作有點類似函式呼叫時的[[Get]]() )。物件內建的[[Get]]操作首先在物件內查詢是否存在相同名稱的屬性,如果找到的話就返回這個屬性。
找不到的話,按照[[Get]]演算法的定義,會到“”原型鏈”中查詢-------------其實就是遍歷"Prototype"鏈,也就是遍歷原型鏈。
如果仍找不到的話,則返回"undefined"。
var myObject={
a:2
};
console.log(myObject.b); //undefined
讓我們來分析一下"myObject.b"這一條語句執行時發生的事情。
①"myObject.b"開始執行,這時我們告訴引擎,我們需要物件"myObject"中名為"b"的屬性。
②引擎收到命令,開始執行[[Get]]操作,開始在物件"myObject"中查詢名為"b"的屬性。
③在物件"myObject"中不存在名為"b"的屬性,於是[[Get]]演算法讓引擎遍歷相關的"原型鏈"又稱"prototype"。
④原型鏈中不存在名為"b"的屬性,這時返回值"undefined"
注意,很多人會把變數和物件屬性弄混,我們之前講過"LHS"以及"RHS",這是查詢變數的兩種方式。當我們在詞法作用域中查詢變數時會使用"LHS"或者"RHS"。[[Get]]是查詢物件屬性的,與變數沒有關係
var myObject={
a:2
};
console.log(myObject.b); //[[Get]]操作 undefined
console.log(b); //ReferenceError 這裡是RHS查詢
這時會出現一個問題,便是當我們訪問一個物件的屬性,該屬性的值為"undefined",使用[[Get]]操作查詢不到屬性時返回值同樣是"undefined",那麼我們如何確定該值到底是存在還是不存在呢?
例如以下的程式碼
var myObject={
a:undefined
};
console.log(myObject.a); //undefined
console.log(myObject.b); //undefined
在1.3.10中我們介紹瞭如何區分這兩種情況。
1.3.8 [[Put]]
既然存在著[[Get]]得到屬性值,那麼也會存在[[Put]]修改屬性值。很多人認為修改屬性值(包括給屬性賦值以及建立屬性)時會觸發[[Put]]操作,但是實際情況非常複雜。
具體來說[[Put]]被觸發時,實際的行動取決於多個元素,最重要的隱式是:物件是否已經存在這個屬性
如果存在這個屬性,[[Put]]演算法大致會檢查以下內容。
①屬性是否是訪問描述符?如果是且存在"setter"就呼叫"setter"。
②屬性的屬性描述符"writable"是否是"false",是的話,在非嚴格模式下修改失敗,在嚴格模式下,會返回"TypeError"(因為嚴格模式在writable:false時禁止修改屬性值)
③如果都不是,則將該值賦給該屬性
如果不存在此屬性,[[Put]]操作更加複雜,將會同[[Get]]一樣涉及到原型鏈
1.3.9 getter和setter
物件預設的[[Get]]和[[Put]]操作可以分別控制屬性值的獲取和設定。
在ES5中可以使用getter和setter部分改寫預設操作,但是隻能應用在單個屬性上,無法應用在整個物件上。
getter是一個隱藏函式,會在獲取屬性值時呼叫。setter也是一個隱藏函式,會在設定屬性值時呼叫。
當你給一個屬性同時定義getter和setter時,這個屬性稱為"訪問描述符"(與屬性描述符相對)。對於訪問描述符來說,JavaScript會忽略它們的"value"和"writable"特性,取而代之的是關心set和get(還有enumerable和configurable)特性。
在這裡所謂的訪問描述符指的是在物件中,使用"getter"和"setter"定義的屬性。
簡單來說物件中的屬性分成兩類,一類是使用鍵值對的屬性描述符,一類是使用"getter""setter"的訪問描述符。
var myObject={
get a(){ //給a定義一個getter 這個a就是訪問描述符
return 2;
},
c:3 //屬性描述符
};
Object.defineProperty(myObject,"b",{ //新增訪問描述符b
get function(){ //給b定義一個getter
return this.a*2;
},
enumerable:true //保證b能夠在myObject中建立
});
console.log(myObject.a); //2
console.log(myObject.b); //4
我們來解析下,當我們訪問,訪問描述符的時候發生了什麼。
我們在物件"myObject"中使用"get a(){return 2}"定義了一個訪問描述符"a",當我們訪問"a"時,並不會像之前那樣呼叫[[Get]]去處理,而是呼叫一個隱藏函式"getter"這個函式會返回一個值,這個值就是該訪問描述符的值。
同理的在"Object.definedProperty(...)"中也可以建立訪問描述符,並且使用"getter""setter"定義訪問描述符。
var myObject={
get a(){
return 2;
}
};
console.log(myObject.a); //2
myObject.a=3;
console.log(myObject.a); //2
由於我們只定義了"a"的"getter",所以對"a"的值進行設定時,"set"操作會忽略賦值操作,不會丟擲錯誤。而且即使有合法的"setter",由於我們自定義的"getter"只會返回2,所以"set"是沒有意義的。
所以為了讓屬性更加合理,還應當定義"setter","setter"操作會覆蓋單個屬性預設的[[Put]](也被成為賦值操作)
通常來說"getter""setter"是成對出現的
var myObject={
get a(){
return this._a_
},
set a(val){
this._a_=val;
}
};
myObject.a=2;
console.log(myObject.a); //2
在本例中的"_a_"只是一個變數名而已沒有特殊的含義,在此程式中的作用是儲存傳入a的值。
1.3.10 存在性
在前面我們提到過,當使用[[Get]]查詢不到屬性時會返回"undefined",如果[[Get]]查詢到的屬
性值就是"undefined"的話,我們該如何區分這個屬性到底是存在還是不存在呢?
①通過"in"查詢
我們可以通過"in"關鍵字判斷該屬性是否存在於物件中
var myObject={
a:undefined
};
console.log(myObject.a); //undefined
console.log(myObject.b); //undefined
console.log("a" in myObject); //true
console.log("b" in myObject); //false
通過結果我們可以看出,("屬性名" in 物件)這一行程式碼1,可以檢測出屬性是否存在於物件中。
in關鍵字的原理是:檢查屬性是否在物件及其"prototype"原型鏈中。
注意!!!in看起來像是檢查某值是否存在,但是它只是在檢查屬性名,這點在陣列中尤為重要,例如:"3 in [1,2,3]",返回值是"false",為什麼呢?因為在陣列中屬性名是"0 1 2"沒有"3"
②hasOwnProperty(...)
我們可以通過"hasOwnProperty(...)"方法來檢測。
var myObject={
a:undefined
};
console.log(myObject.a); //undefined
console.log(myObject.b); //undefined
console.log(myObject.hasOwnProperty("a")); //true
console.log(myObject.hasOwnProperty("b")); //false
"hasOwnProperty(...)"與"in"不同,它只會檢查屬性是否在物件中,不會檢查"prototype"原型鏈。
所有的物件都可以通過對於"Object.prototype"的委託(原型鏈內容)來訪問"hasOwnProperty",但是有的物件可能沒有連線到"Object.prototype"(通過Object.create(null)來建立)。在這種情況下"hasOwnProperty"就會失敗。
這時可以藉助一個更加強力的方法來進行判斷:"Object.prototype.hasOwnProperty.call(myObject,"a")"。它藉助"call"將"hasOwnProperty"顯式繫結到"myObject"上。
1.4 遍歷
for...in迴圈只能夠遍歷陣列的屬性(會在物件及其相關的"prototype"原型鏈中查詢),而且是可列舉的屬性,而不能夠遍歷陣列的值,那麼我們想要遍歷屬性的值該怎麼做呢?
陣列可以通過基本的for迴圈遍歷陣列屬性的值
var myObject=[1,2,3];
myObject.a="a"
for(var i=0;i<myObject.length;i++){ //遍歷陣列
console.log(i,myObject[i]); //0:1 1:2 2:3 沒有a屬性
};
但是這實際上不是在遍歷陣列,而是在遍歷陣列的下標指向值。
如何解決和一個問題呢?
好在ES5中增加了專門用於陣列遍歷的迭代器,用以輔助陣列遍歷,每個迭代器都能接受一個回撥函式並把它應用在陣列的每個元素上,這幾個迭代器唯一的區別就是對回撥函式的處理不同。
①forEach(...)會遍歷陣列中的所有值並忽略回撥函式的返回值。
var myObject=[1,2,3];
myObject.forEach(function (element) {
console.log(element); //1,2,3
})
②every(...)會一直執行直到回撥函式返回"false"(或者“假”值)。此回撥函式有點像"break"處理,滿足條件之後跳出
③some(...)會一直執行到回撥函式返回"true"(或者“真”值)。此回撥函式有點像"break"處理,滿足條件之後跳出
那麼如何遍歷陣列值而不是陣列下標呢?
在ES6中增加了一種用來遍歷陣列的"for..of"迴圈語法(如果物件定義了迭代器也可以遍歷物件)
var myObject=[1,2,3];
myObject.a="a";
for (var v of myObject){
console.log(v); //1 2 3
}
下面我們來介紹一下"for..of"物件的原理
"for..of"首先會向物件請求一個迭代器物件,然後通過迭代器物件的"next()"方法來遍歷是所有返回值。
陣列中有內建的"@@iterator",因此"for...of"可以直接應用在陣列上,我們使用內建的"@@iterator"來看看它是如何工作的?
var myObject=[1,2,3];
var it=myObject[Symbol.iterator]();
console.log(it.next()); //{value: 1, done: false}
console.log(it.next()); //{value: 2, done: false}
console.log(it.next()); //{value: 3, done: false}
console.log(it.next()); //{value: undefined, done: true}
使用迭代器的"next(...)"方法會返回一串形如“{value:1,done:false}”的值,其中value是當前遍歷的值。done是一個布林值,表示事都還有可遍歷的值。
這時你會感到奇怪,在"value:3"時,"done:false"。這是否代表了還存在下個值呢?並不是,而是你必須要在呼叫一次"next(...)"得到"done:true"才能完成遍歷。
我們使用ES6中的符號symbol.iterator來獲取物件的@@iterator內部屬性。這裡的symbol是符號“也就是我們之前講過的ES6中的基礎型別,是一個字串,包含著一個不透明無法預測的值,你不會使用到這個值,你只會使用到這個值的名稱”。
引用類似iterator的特殊屬性時要使用符號名,而不是符號所包含的值,@@iterator開起來很像一個物件,但是並不是迭代器物件,而是一個返回迭代器物件的函式--------------這點特別關鍵
注意!!!在陣列中才有內建的@@iterator,普通物件中沒有,但是你可以手動給普通物件新增@@iterator,用以實現for...of迴圈
var myObject={
a:2,
b:3
};
Object.defineProperty(myObject,Symbol.iterator,{ //為普通物件新增特殊屬性符號Symbol.iterator
enumerable:false,
writable:false,
value:function () {
var o=this; //指向當前物件
var idx=0; //判斷done
var ks=Object.keys(o); //keys(...)會返回一個可列舉屬性的陣列,令ks=這個陣列
return { //iterator會返回一個迭代器物件的函式
next:function () { //定義itertor的next()方法
return{
value:o[ks[idx++]], //輸出該物件當前的值,令idx加1換下個物件
done:(idx>ks.length) //判斷idx,大於ks.length的話,輸出true。
}
}
}
}
});
//手動呼叫
var it=myObject[Symbol.iterator]();
it.next();
it.next();
it.next();
//for...of呼叫
for(var v of myObject){
console.log(v);
}
看起來為一個普通物件建立一個特殊屬性"iterator"非常複雜,但是我們仔細解刨的話,會發現非常簡單。
①建立物件"myObject"
②跟其他普通屬性一樣,使用"Object.definedProperty(...)"建立特殊屬性,但是注意這裡的屬性名只能是不帶引號的Symbol.iterator。
③屬性描述符"enumerable:false wratable:false",特殊屬性:iterator禁止被改寫,最好不列舉。
④因為"iterator"的返回值是一個函式,所以value: funcction(){...}
⑤在function中進行資料處理
⑥在function中定義一個return{...},這裡面存放next(..)函式處理。
當然你也可以不在'Object.definedProperty(...)"中定義,而是在定義物件中直接宣告鍵值對。
var myObject={
a:2,
b:3,
[Symbol.iterator]:
function(){.....}
}
for...of迴圈每一次呼叫"myObject"迭代器物件的next()方法時,內部的指標就會向前移動並返回物件屬性列表的下一個值。
1.4.1 更高級別的遍歷(使用者自定義特殊屬性)
你自己定義的物件來說,結合了for...of迴圈和自定義迭代器可以組成非常強大的物件操作工具。
舉個例子,我們可以建立一個“無限”迭代器,它永遠不會“結束”,每次都會返回一個新值(比如隨機數、遞增值,唯一表示符等),別在for...of中使用這樣的迭代器,你的程式將會被掛起
var randoms = { //構建隨機生成的迭代器
[Symbol.iterator]: function() {
return {
next: function() {
return { value: Math.random() }; //隨機生成數,每次訪問random都呼叫一次Math.random()
}
};
}
};
var randoms_pool = [];
for (var n of randoms) {
randoms_pool.push( n ); //隨機生成的數n,被隨機新增到random_pool中
// 防止無限執行!
if (randoms_pool.length === 100) break;
}
這個迭代器將會隨機生成一個新值,為什麼呢?因為在每次呼叫random時,都會呼叫一次Math.random()。將源源不斷的產生新值。
總結:建立物件有兩種方式,一種是文字(宣告)形式(var a={b:1,....}),一種是構造形式(var a=new String()),一般來說我們會使用文字形式。
許多人認為JavasScript中萬物皆為物件,這種觀點是錯誤的,物件只是JavaScript基礎型別中的一種。物件有包括function在內的子型別,不同子型別具有不同的行為,比如說[Object.Array]表示這是物件的子型別陣列。
物件就是鍵值對的集合,可以通過屬性訪問"myObject.a"訪問屬性,也可以通過鍵訪問"myObject.["a"]"來訪問屬性。無論哪種方式訪問屬性都會呼叫[[Get]]操作(設定屬性時是[[Put]]),它會在物件和原型鏈中查詢屬性,找不到時返回"undefined",區分屬性是否存在可以使用"in"和"hasOwnProperty"關鍵字,這兩種方法的區別在於前者會查詢原型鏈,後者則不會。
建立屬性時,會預設建立屬性特性符,且值都為"true",屬性特性符又稱“資料描述符”,有
wirtable:屬性是夠可寫
enumerable:屬性是否被列舉(for...in迴圈)
configurable:屬性是否被配置(單向的當為false以後就再也改不回,且屬性不可被刪除)
修改屬性特性符(也可建立屬性):Object.definedProperty(物件,“屬性”,{修改函式體})。
檢視屬性特性符:Object.getOwnPropertyDescriptor(物件,屬性)。
此外還可以通過:
Object.preventExtensions(...)禁止擴充套件物件(不能新增新屬性)
Object.seal(...)密封物件(不能新增屬性,且無法刪除屬性,但屬性值無法修改)
Object.freeze(...)凍結物件(在密封的操作下且無法修改屬性值)。
與屬性特性符相對的便是"訪問描述符"了,這個訪問描述符與普通屬性不同,它是通過"getter"函式獲取屬性值,"setter"函式設定屬性值
你要想遍歷一個物件得到屬性的值,陣列的話,你可以使用ES6中的for...of迴圈。普通物件的話,要想使用for...of迴圈可以自建"iterator"符號及迭代器,這能組成很強大的效果。
for...of迴圈會尋找內建的"iterator"或者自定義的"iterator"呼叫內部的next()來遍歷物件。