最近在拜讀《你不知道的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