1. 程式人生 > >深入學習JavaScript之物件

深入學習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

  簡單的基本型別並不是物件,它們會在使用時被物件包裝器包裝成物件

,這些簡單的資料型別是:string、numbar、undefined、boolean以及null。其中null有時會被當做一個物件型別"typeof null"會返回一個"object",但是null並不是物件型別,它是一個基本型別,這只是JavaScript設計中的一個BUG(在底層機器中,操作或者物件都是二進位制編碼,在JavaScript中規定,當二進位制前三位為0時,判定為object型別,null代表的是空物件,那麼它的二進位制編碼就都是0,所以,null自然被判定為object型別

 

  在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()來遍歷物件。