對象----《你不知道的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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章