一、建立物件
javascript中有三種方法可以建立一個物件:
- 物件字面量
var obj = {
name: 'jack',
age: 12
}
複製程式碼
- new 建構函式
var obj = new Object()
var obj1 = new Object({
name: 'jack',
age: 12
})
複製程式碼
Object.create()
var obj = Object.create({
name: 'jack',
age: 12
})
複製程式碼
需要注意的是通過Object.create()
建立的物件實際上等於將該物件的__proto__
指向Object.create()
裡面的引數物件,而obj
本身是個空物件。
var obj = Object.create({
name: 'jack',
age: 12
})
// 等價於 obj.__proto__ = { name: 'jack', age: 12 }
console.log(obj) // {}
console.log(obj.__proto__) // { name: 'jack', age: 12 }
obj.toString() // '[object Object]'
複製程式碼
如果往Object.create()
裡面傳入的是null
,則建立的物件不繼承Object
的任何方法及屬性。
var obj = Object.create(null)
console.log(obj) // {}
console.log(obj.__proto__) // undefined
obj.toString() // 報錯
複製程式碼
如果想建立一個空物件,需要傳入Object.prototype
var obj = Object.create(Object.prototype)
// 和 {} 、new Object()一樣
複製程式碼
二、物件的屬性
我們知道,物件的屬性是由名字、值和一組特性組成(屬性的特性待會介紹)。在ES5中屬性值可以用一個或兩個方法代替,這兩個方法就是getter
和setter
。由getter
和setter
定義的屬性稱為“儲存器屬性”,它不同於資料型別的屬性,資料屬性只有一個簡單的值。我們重點講解儲存器屬性。
當我們查詢儲存器屬性時會呼叫getter
方法(無引數)。這個方法返回值就是屬性存取表示式返回的值。
當我們設定儲存器屬性時會呼叫setter
方法(有引數)。這個方法修改儲存器屬性的值。
var obj = {
num: 12,
age: 13,
get num1 () {
return this.num
},
set num1 (value) {
this.num = value
},
get age1 () {
return this.age
}
}
obj.num1 // 12
obj.num1 = 120
obj.num1 // 120
obj.age1 // 13
obj.age1 = 130
obj.age1 // 13
複製程式碼
儲存器屬性定義為一個或者兩個和屬性同名的函式,這個函式定義沒有使用function
關鍵字而是使用get
和set
。
可以看出如果該屬性只有getter
方法則只能讀取該屬性不能設定該屬性,同樣如果只有setter
方法就只能設定該屬性,不能讀取該屬性,只有當兩者都有時才能正常讀取和設定屬性。
三、物件屬性的特性
每個物件的資料屬性都有四個特性(也可以說是屬性描述符),分別為:
value
屬性的值writable
可寫性,如果為false
該值將不能被修改enumerable
可列舉性,如果為false
將不能被列舉configurable
可配置性,如果為false
將不能被配置,即不能被delete
操作符刪除,不能更改這四個特性,一旦設為false
則無法再設為true
,也就是一個不可逆過程
前面我們講過儲存器屬性,每隔物件的儲存器屬性同樣也有四個特性,分別為:
get
set
enumerable
configurable
如果想要獲得一個物件某個屬性的這四個特性,可以呼叫 Object.getOwnPropertyDescriptor() 方法,該方法接受兩個引數,第一個為物件,第二個為物件的屬性
var obj = {
name: 'jack',
age: 12,
get age1 () {
return this.age1
},
set age1(value) {
this.age1 = value
}
}
// 獲取數值屬性的特性
Object.getOwnPropertyDescriptor(obj,'name')
// {value: "jack", writable: true, enumerable: true, configurable: true}
// 獲取儲存器屬性的特性
Object.getOwnPropertyDescriptor(obj,'age1')
// {enumerable: true, configurable: true, get: ƒ, set: ƒ}
// 試圖獲取不存在的屬性,返回undefined
Object.getOwnPropertyDescriptor(obj, 'sex') // undefined
// 試圖獲取原型上的屬性,返回undefined
Object.getOwnPropertyDescriptor(obj, 'toString') // undefined
複製程式碼
從上面可以看出,Object.getOwnPropertyDescriptor()
只能得到自有屬性的描述符,要想獲得繼承屬性的特性,我們可以把該物件的原型傳進去。
function Person () {
this.name = 'sillywa'
}
Person.prototype.sex = 'boy'
Person.prototype.age = 13
var person1 = new Person()
Object.getOwnPropertyDescriptor(person1.__proto__, 'sex')
// {value: "boy", writable: true, enumerable: true, configurable: true}
複製程式碼
以上可以看出,我們通過物件字面量和new
運算子建立的物件的屬性它們的writable
,enumerable
,configurable
都有true
,預設都是可寫、可列舉、可配置。如果要修改屬性的特性可以呼叫Object.defineProperty()
。
var obj = {
name: 'sillywa'
}
// 將name屬性設為不可列舉並將其值設為jack
Object.defineProperty(obj, 'name', {
value: 'jack',
enumerable: false
})
Object.getOwnPropertyDescriptor(obj, 'name')
// {value: "jack", writable: true, enumerable: false, configurable: true}
// 新增age屬性
Object.defineProperty(obj, 'age', {
value: 12
})
Object.getOwnPropertyDescriptor(obj, 'age')
// {value: 12, writable: false, enumerable: false, configurable: false}
// 將name變為儲存器屬性
Object.defineProperty(obj, 'name', {
get: function () {
return 0
}
})
Object.getOwnPropertyDescriptor(obj, 'name')
// {set: undefined, enumerable: false, configurable: true, get: ƒ}
obj.age = 78
obj.age // 12
複製程式碼
需要注意的是通過Object.defineProperty()
建立的屬性其writable
, enumerable
, configurable
都為false
。嘗試修改不寫的屬性不會報錯,但也不會修改,只有在嚴格模式下才會報錯。
如果需要同時修改和建立多個屬性,可以使用Object.defineProperties()
。
var obj = Object.defineProperties({},{
name: {
value: 'sillywa',
writable: true,
enumerable: true,
configurable: true
},
age: {
get: function () {
return 'hello' + this.name
},
set: function (value) {
this.name = 'jack'
},
enumerable: true,
configurable: true
}
})
複製程式碼
四、屬性的設定和遮蔽
我們知道當我們書寫以下程式碼時
obj.foo = 'bar'
如果obj
存在一個名為foo
的普通資料訪問屬性,這條賦值語句只會修改已有的屬性值。
如果foo
不是直接存在於obj中,[[prototype]]
鏈就會被遍歷,如果原型鏈上找不到foo
,foo
就直接被新增到obj
上。
然而,如果原型鏈上找到了foo
屬性,情況就有些不一樣了。
如果屬性foo
既出現在obj
中也在其原型鏈中,那麼obj
中包含的foo
屬性就會遮蔽原型鏈裡面的foo
屬性,這就是屬性遮蔽,原理就是屬性的查詢規則。
下面我們看一下如果foo
不直接存在於obj
中,而是在其原型鏈中時,obj.foo = 'bar'
會出現的三種情況:
- 如果原型鏈中存在名為
foo
的普通資料訪問屬性並且其writable
為true
,那麼就會直接在obj
中新增foo
屬性,它是屬性遮蔽。 - 如果原型鏈中存在
foo
,但其writabla
為false
,那麼無法修改已有屬性或者在obj
中建立遮蔽屬性。如果執行在嚴格模式下,會丟擲一個錯誤。否則這條賦值語句會被忽略,不會發生屬性遮蔽。 - 如果原型鏈上存在
foo
並且它是一個setter
,那就一定會呼叫這個setter
。foo
不會被新增到obj
中,也不會重新定義這個setter
。
大多數人認為,如果向原型鏈中已存在的屬性賦值,就一定會發生屬性遮蔽,但以上三種情況只有一種是如此。
如果希望在任何情況下都遮蔽foo
,那就不能使用=
操作符來賦值,而是使用Object.defineProperty()
來向obj
中新增foo
。
情況一:
function Person() { }
Person.prototype.foo = 'foo'
var obj = new Person()
obj.foo = 'bar'
obj.foo // 'bar'
複製程式碼
情況二:
function Person() {}
Object.defineProperty(Person.prototype,'foo',{
writable: false,
enumerable: true,
configurable: true,
value: 'foo'
})
var obj = new Person()
obj.foo = 'bar'
obj.foo // 'foo'
複製程式碼
情況三:
function Person() {}
Person.prototype = {
constructor: Person,
name: 'foo',
set foo (value) {
this.name = value
},
get foo () {
return this.name
}
}
var obj = new Person()
obj.foo = 'bar'
obj.foo // 'bar'
// obj中並沒有foo這個屬性,只是呼叫了setter
obj.hasOwnProperty('foo') // false
複製程式碼
有些情況下會隱式產生遮蔽,一定要注意,思考一下程式碼:
var obj = {
a: 2
}
var myObj = Object.create(obj)
obj.a // 2
myObj.a // 2
obj.hasOwnProperty('a') // true
myObj.hasOwnProperty('a') // false
myObj.a ++ // 隱式遮蔽
obj.a // 2
myObj.a // 3
myObj.hasOwnProperty('a') // true
複製程式碼
儘管myObj.a ++
看起來是查詢並增加obj.a
的屬性,但是別忘了++
操作符相當於myObj.a = myObj.a + 1
;因此++
操作首先會通過原型鏈查詢到obj.a
,並讀取其值為2,然後加1賦值給myObj.a
。