天天看點

對象----《你不知道的JS》

最近在拜讀《你不知道的js》,而此篇是對于《你不知道的js》中對象部分的筆記整理,希望能有效的梳理,并且深入了解對象

一、文法

對象兩種定義形式:聲明(文字)形式、構造形式

聲明(文字)形式

var myObj = {
   key: value,
   ...
}
           

構造形式

var myObj = new Object();
myObj.key = value;
           

構造形式與文字形式生成的對象一樣

差別:文字聲明中可以添加多個鍵/值對,構造形式中必須逐個添加屬性

二、類型

在JavaScript中一共有6中主要類型:

  • string
  • number
  • boolean
  • null
  • undefined
  • object
注意:簡單基本類型(string、boolean、number、null、undefined)本身不是對象。null有時會被當做一種對象類型,typeof null傳回‘object’。實際上,null是基本類型

内置對象

内置對象:對象子類型

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

三、内容

對象的内容是由一些存儲在特定命名位置的(任意類型的)值組成,我們稱為屬性。

var myObject = {
    a: 2
};
myObject.a;// 2
myObject['a']; // 2
           

若要通路myObject中a位置上的值,需使用.操作符或[]操作符。.a文法稱為屬性通路(最常見的方式),[‘a’]文法稱為鍵通路

差別:.操作符要求屬性名滿足辨別符的命名規範,而["…"]文法可以接受任意UTF-8/Unicode字元串作為屬性名,如名稱為“Super-Fun”的屬性,就必須使用[“Super-Fun”]文法通路

由于[’…’]文法使用字元串來通路屬性,是以可以在程式中構造這個字元串,如:

var myObject = {
    a: 2
};
var idx;
if(wantA) {
    idx = "a";
}
console.log(myObject[idx]); // 2
           

在對象中,屬性名永遠都是字元串。若使用string(字面量)以外的其他值作為屬性名,那它首先會被轉換為一個字元串。

var myObject = {};
myObject[true] = 'foo';
myObject[3] = "bar";
myObject[myObject]="baz";
myObject["true"]; // 'foo'
myObject['3']; //"bar"
myObject["object object"]; // "baz"
           

1、可計算屬性名

ES6增加了可計算屬性名:

var prefix = "foo";
var myObject = {
    [prefix + "bar"]: "hello",
    [prefix + "baz"]: "world"
};
myObject["foobar"];// hello
myObject["foobaz"];// world
           

2、屬性與方法

在其他語言中,屬于對象(也稱為“類”)的函數通常被稱為“方法”,有時屬性方位也稱為“方法通路”。

無論傳回值是什麼類型,每次通路對象的屬性就是屬性通路。若屬性通路傳回一個函數,那它也并不是一個“方法”。屬性通路傳回的函數和其他函數沒有任何差別

function foo() {
    console.log("foo");
}
var someFoo = foo; // 對foo變量的引用
var myObject = {
    someFoo: foo,
}
foo; // function foo() {}
someFoo; //  function foo() {}
myObject.someFoo; // function foo() {}
           

someFoo與myObject.someFoo隻是對于同一個函數的不同引用,并不能說明這個函數是特别的或“屬于”某個對象

3、數組

數組也支援[]通路形式,通過數值下标,即存儲位置(索引)通路,是非負整數,如:0,42:

var myArray = ["foo", 42, "bar"];
myArray.length; // 3
myArray[0]; // "foo"
myArray[2]; // "bar"
           

數組也是對象,也可以給數組添加屬性:

var myArray = ["foo", 42, "bar"];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"
           

雖然添加了命名屬性,數組的length值并未發生變化。

若你試圖向數組添加一個屬性,但屬性名“看起來”像數字,那它會變成一個數值下标:

var myArray = ["foo", 42, "bar"];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3] = "baz";
           

4、複制對象

function anotherFunction() {/*..*/}
var anotherObject = {
    c: true
}
var anotherArray = [];
var myObject = {
    a: 2,
    b: anotherObject, // 引用,不是複本
    c: anotherArray, // 另一個引用
    d: anotherFunction
}
anotherArray.push(anotherObject, myObject);
           
如何準确地表示myObject的複制呢?

首先判斷它是淺複制還是深複制。

1)淺複制

複制出的新對象中a的值會複制就對象中a的值,即2,但新對象中b、c、d三個屬性其實隻是三個引用,和就對象中b、c、d引用的對象一樣

2)深複制

除了複制myObject以外還會複制anotherObject和anotherArray

問題:anotherArray引用了anotherObject和myObject,是以又需要複制,myObject,這樣會由于循環引用導緻死循環

如何解決?

1)對于json安全的對象來說:

var newObj = JSON.parse(JSON.stringify(someObj));
           

這種方法需要保證對象是json安全的,是以隻适用于部分情況。

2)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
           
注:由于Object.assign(…)使用 = 操作符來指派,是以源對象屬性的一些特性不會被複制到目标對象。

5、屬性描述符

在ES5之前,JavaScript語言本身并沒有提供可直接檢測屬性特性的方法,如判斷屬性是否是隻讀。從ES5開始,所有的屬性都具備了屬性描述符。

var myObject = {
    a: 2
}
Object.getOwnPropertyDescriptor(myObject, "a");
// {
//   value: 2,
//   writable: true, // 可寫
//   enumerable: true, // 可枚舉
//   configurable: true // 可配置
// }
           

在建立普通屬性時屬性描述符會使用預設值,可使用Object.defineProperty(…)來添加一個新屬性或修改一個已有屬性,并對特性進行設定。

var myObject = {};
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});
myObject.a; // 2
           

1)writable

決定是否可以修改屬性的值

var myObject = {};
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: false, // 不可寫
    configurable: true,
    enumerable: true
});
myObject.a = 3;
myObject.a; // 2
           

在嚴格模式下會報錯

"use strict"
var myObject = {};
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: false, // 不可寫
    configurable: true,
    enumerable: true
});
myObject.a = 3; // TypeError
           

2)Configurable

隻要屬性可配置,就可以用defineProperty(…)方法修改屬性描述符:

var myObject = {
    a: 2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty(myObject, "a", {
    value: 4,
    writable: true,
    configurable: false, // 不可配置
    enumerable: true
});
myObject.a; // 4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty(myObject, "a", {
    value: 6,
    writable: true,
    configurable: true,
    enumerable: true
}); // TypeError
           

無論是否處于嚴格模式,嘗試修改一個不可配置的屬性描述符都會出錯。

注:把Configurable 修改成false是單向操作,無法撤銷;

即便屬性是configurable:false,我們還是可以把writable的狀态由true改為false,但無法由false改為true。

除了無法修改,configurable: false還會禁止删除這個屬性:

var myObject = {
    a: 2
};
myObject.a = 2;
delete myObject.a;
myObject.a; // undefined
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: false, // 不可配置
    enumerable: true
});
myObject.a; // 2
delete myObject.a;
myObject.a; // 2
           

在本例中,delete隻用來直接删除對象的(可删除)屬性。若對象的某個屬性是某個對象/函數的最後一個引用者,對這個屬性執行delete操作後,這個對象/函數就可以被垃圾回收

3)enumerable

控制屬性是否出現在對象的屬性枚舉類中,如for…in循環,若把enumerable設定為false,屬性就不會出現在枚舉中,雖然仍可以正常通路它

6、不變性

ES5中所有方法建立的都是淺不變性,即它們隻會影響目标對象和它的直接屬性。如果目标對象引用了其他對象(數組、對象、函數等),其他對象的内容不受影響,仍是可變的:

myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push(4);
myImmutableObject.foo; // [1,2,3,4]
           

1)對象常量

結合writable:false和configurable:false就可以建立一個真正的常量屬性(不可修改、重新定義或者删除)

var myObject = {};
Object.defineProperty(myObject, "FAVORITE_NUMBER", {
    value: 42,
    writable: false,
    configurable: false, // 不可配置
});
           

2)禁止擴充

Object.preventExtensions(…):禁止一個對象添加新屬性并且保留已有屬性

var myObject = {
    a: 2
};
Object.preventExtensions(myObject);
myObject.b = 3;
myObject.b; // undefined
           

非嚴格模式下,建立屬性b會靜默失敗,在嚴格模式下,将抛出TypeError

3)密封

Object.seal(…)會建立一個“密封”對象,這個方法實際上會在一個現有對象上調用Object.preventExtensions(…)并把所有現有屬性标記為configurable:false。是以密封後不僅不能添加新屬性,也不能重新配置或删除任何現有屬性(雖然可以修改屬性的值)

4)當機

Object.freeze(…)會建立一個當機對象,實際上會在一個現有對象上調用Object.seal(…)并把所有“資料通路”屬性标記為writable:false,這樣就無法修改值

這個方法可應用在對象上的級别最高的不可變性,它會禁止對象本身及其任意直接屬性的修改,這個對象的引用的其他對象是不受影響的
深度當機方法:首先在這個對象上調用Object.freeze(…),然後周遊它引用的所有對象并在這些對象上調用Object.freeze(…),但可能會在無意中當機其他(共享)對象

7、[[Get]]

var myObject = {
    a: 2
}
myObject.a; // 2
           

myObject.a在myObject上實際是實作了[[Get]]操作。對象預設的内置[[Get]]操作首先在對象中查找是否有名稱相同的屬性,如果找到就會傳回這個屬性的值,若沒找到,按照[[Get]]實驗法的定義會執行另外一種非常重要的行為,即周遊可能存在的[[Prototype]]鍊,也就是原型鍊。

如果無論如何都沒有找到名稱相同的屬性,那[[Get]]操作會傳回undefined

var myObject = {
    a: 2
}
myObject.b; // undefined
           
注:這種方法和通路變量時是不一樣的,若你引用了一個目前詞法作用域中不存在的變量,并不會像對象屬性一樣傳回undefined,而是會抛出ReferenceError異常:
var myObject = {
    a: undefined
}
myObject.a; // undefined
myObject.b; // undefined 由于根據傳回值無法判斷出到底變量的值為undefined還是變量不存在,是以[[Get]]操作傳回了undefined
           

8、[[Put]]

[[Put]]被觸發時,實際行為取決于許多因素,包括對象中是否已經存在這個屬性(最重要的因素)

如果已經存在這個屬性,[[Put]]算法大緻會檢查下面這些内容:

1)屬性是否是通路描述符?如果是并且存在setter就調用setter

2)屬性的資料描述符中writable是否是false?如果是,在非嚴格模式下靜默注冊失敗,在嚴格模式下抛出TypeError異常

9、Getter和Setter

在ES5中可使用getter和setter部分改寫預設操作,但隻能應用在單個屬性上,無法應用在整個對象上。getter是一個隐藏函數,會在擷取屬性值時調用。setter也是一個隐藏函數,會在設定屬性時調用。

當你給一個屬性定義getter和setter或者兩者都有時,這個屬性會被定義為“通路描述符”,對于通路描述符來說,js會忽略它們的value和writable特性,取而代之的關心set和get(還有configurable和enumerable)

var myObject = {
    get a() {
        return 2;
    }
}
Object.defineProperty(
    myObject, // 目标對象
    "b", // 屬性名
    {
        get: function() {
            return this.a * 2
        },
        enumerable: true
    }
);
myOject.a; // 2
myObject.b; // 4
           

兩種方式都會在對象中建立一個不包含值的屬性,對于這個屬性的通路會自動調用一個隐藏函數,它的傳回值會被當做屬性通路的傳回值:

var myObject = {
    get a() {
        return 2;
    }
}
myOject.a = 3;
myObject.a; // 2
           

由于我們隻定義了a的getter,是以對a的值進行設定時set操作會忽略指派操作,不會抛出錯誤。

通常來說getter和setter是成對出現的

var myObject = {
    get a() {
        return this._a_;
    }
    set a(val) {
        this._a_ = val * 2;
    }
}
myOject.a = 2;
myObject.a; // 4
           

10、存在性

屬性通路傳回值可能是undefined,如何區分這是屬性中存儲的undefined,還是屬性不存在而傳回的undefined ?
var myObject = {
    a: 2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false
           

in操作符會檢查屬性是否在對象及其[[Prototype]]原型鍊中,hasOwnProperty(…)隻會檢查屬性是否在myObject對象中,不會檢查[[Prototype]]鍊

注:in是檢查某個屬性名是否存在,如:4 in [2,4,6] = false, 因為這個數組中包含的屬性名為0 ,1, 2

所有的普通對象都可以通過對于 Object.prototype 的委托來通路hasOwnProperty(…),但有的對象可能沒有連接配接到 Object.prototype (通過Object.create(null)建立),則myObject.hasOwnProperty就會失敗

此時可采用 Object.prototype.hasOwnProperty.call(myObject, “a”) 進行判斷,它借用基礎的hasOwnProperty(…)方法并把它顯示綁定在myObject上

1)枚舉

“可枚舉”相當于“可以出現在對象屬性的周遊中”

var myObject = {};
Object.defineProperty(
    myObject, 
    "a",
    // 讓a像普通對象一樣可枚舉
    { enumerable: true, value: 2 }
);
Object.defineProperty(
    myObject, 
    "B",
    // 讓b不可枚舉
    { enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty("b"); // true

for (var k in myObject) {
    console.log(k, myObject[k]);
}
// "a" 2
           
for…in枚舉不僅會包含所有索引,還會包含所有可枚舉屬性,是以最好隻在對象上引用for … in 循環,若周遊數組就使用for循環。但它無法直接擷取屬性值,需手動擷取

也可用propertyIsEnumerable(…)來區分屬性是否可枚舉

var myObject = {};
Object.defineProperty(
    myObject, 
    "a",
    // 讓a像普通對象一樣可枚舉
    { enumerable: true, value: 2 }
);
Object.defineProperty(
    myObject, 
    "B",
    // 讓b不可枚舉
    { enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable("a"); // true
myObject.propertyIsEnumerable("b"); // false

Object.keys(myObject); // ["a"]
Object.getOwnPropertyNames(myObject); // ["a", "b"]
           

propertyIsEnumerable(…)會檢查給定的屬性名是否直接存在于對象中(而非原型鍊上),并且滿足enumerable: true

Object.keys(…)會傳回數組,包含所有可枚舉屬性。Object.getOwnPropertyNames(…)隻會查找對象直接包含的屬性

四、周遊

for…in用來周遊對象的可枚舉屬性清單,如何周遊屬性值呢?

對于數值索引的數組來說,可用for循環。另外ES5也增加了一些數組輔助疊代器:forEach(…)、every(…)、some(…),他們都可以接受一個回調函數并把它應用在數組的每個元素上,差別就是它們對于回調函數傳回值的處理方式不同

forEach(…):周遊數組所有值并忽略回調函數的傳回值

every(…):會一直運作到回調函數傳回false(或“假”值)

some(…):會一直運作直到回調函數傳回true(或“真”值)

周遊數組下标時采用的數字順序,但周遊對象屬性時順序不确定,在不同的js引擎中可能不一樣

如何直接周遊值而不是數組下标?

使用for…of (ES6增加的文法)

var myArray = [1, 2, 3];
for(var v of myArray) {
    console.log(v);
}
// 1
// 2
// 3
           

for…of首先會向被通路對象請求一個疊代器對象,然後通過調用疊代器對象的next()方法來周遊所有傳回值

數組有内置的@@iterator,也可直接應用在數組上。

var myArray = [1, 2, 3];
var it = myArray[Symbol.iterator]();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // {value: 3, done: false}
it.next(); // {done: true}
           

value是周遊值,done是布爾值,表示是否還有可周遊的值

普通對象中沒有内置的@@iterator,是以無法自動完成for…of,但可以結合for…of循環與自定義疊代器來操作對象

var myObject = {
    a: 2,
    b: 3
}
Object.defineProperty(myObject, Symbol.iterator, {
  enumerable: false,
  writable: false,
  configurable: true,
  value: function() {
      var o = this;
      var idx = 0;
      var ks = Object.keys(o);
      return {
          next:function() {
              return {
                  value: o[ks[idx++]],
                  done: (idx > ks.length)
              }
          }
      }
  }
});
// 手動周遊
var it = myObject[Symbol.iterator]();
it.next(); // {value: 2, done: false}
it.next(); // {value: 3, done: false}
it.next(); // {value: undefined, done: true}

// for .. of
for (var v of myObject) {
    conosle.log(v);
}
// 2
// 3