咱們知道對象是一個無序屬性集合,建立一個包含屬性的對象有3種方式:vue
var object1 = new Object()
object1.name = 'a'
var object2 = {}
object2.name = 'b'
var object3 = {}
Object.defineProperty(object3, 'name', {
enumerable: true,
configurable: true,
get() {
return 'c'
},
set() {
// do
}
})
複製代碼
區別咱們先講完屬性類型後再來看。git
屬性類型分爲github
ECMA規範中定義放在2對方括號中的屬性表示內部屬性數組
相同點,都有bash
[[Configurable]]
字面理解是表示屬性是否可配置——可否修改屬性;可否經過delete刪除屬性;可否把屬性修改成訪問器屬性。[[Enumerable]]
可否經過for-in
循環返回該屬性。區別閉包
[[Writable]]
是否可寫[[Value]]
屬性的值[[Get]]
取值函數[[Set]]
賦值函數接着來看屬性建立的區別app
object.name
賦值的時候,咱們實際上是對數據屬性[[Value]]
賦值,取值也是同樣object.name
取值賦值時,是經過訪問器屬性的[[Get]]
和[[Set]]
函數使用defineProperty注意點函數
// 假設咱們想修改a的值爲123
var object = { a: 1 }
Object.defineProperty(object, 'a', {
enumerable: true,
configurable: true,
get() {
// 不能在函數中引用屬性a,不然會形成循環引用
// 錯誤
return this.a + '23'
// 正確
return val + '23'
},
set(newVal) {
// 爲了在原屬性值的基礎上修改屬性,咱們能夠利用閉包的特性
// 在初始化對象的時候會調用set函數,此時將屬性(例如a)的值用閉包保存起來
// 接着取值的時候,就利用閉包中變量的值修改便可
val = newVal
}
})
// 其實也就是一個先賦值再取值修改的過程
複製代碼
以上有感於vue早期源碼學習系列之一:如何監聽一個對象的變化學習
咱們知道vue對於監測數組的變化重寫了數組的原型以達到目的,緣由是defineProperty不能檢測到數組長度的變化,準確的說是經過改變length而增長的長度不能監測到。ui
咱們須要理解2個概念,即數組長度與數組索引
數組的length
屬性,被初始化爲
enumberable: false
configurable: false
writable: true
複製代碼
也就是說,試圖去刪除和修改(並不是賦值)length屬性是行不通的。
數組索引是訪問數組值的一種方式,若是拿它和對象來比較,索引就是數組的屬性key,它與length是2個不一樣的概念。
var a = [a, b, c]
a.length = 10
// 只是顯示的給length賦值,索引3-9的對應的value也會賦值undefined
// 可是索引3-9的key都是沒有值的
// 咱們能夠用for-in打印,只會打印0,1,2
for (var key in a) {
console.log(key) // 0,1,2
}
複製代碼
當咱們給數組push值後,會給length賦值
length 和數字下標之間的關係 —— JavaScript 數組的 length 屬性和其數字下標之間有着緊密的聯繫。數組內置的幾個方法(例如 join、slice、indexOf 等)都會考慮 length 的值。另外還有一些方法(例如 push、splice 等)還會改變 length 的值。
這幾個內置的方法在操做數組時,都會改變length的值,分2種狀況
vm.$set
來添加監聽驗證數組的幾個內部方法對索引的影響
// 仍是老套路,定義一個observe方法
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function defineGet() {
console.log(`get key: ${key} val: ${val}`)
return val
},
set: function defineSet(newVal) {
console.log(`set key: ${key} val: ${newVal}`)
// 還記得咱們上面討論的閉包麼
// 此處將新的值賦給val,保存在內存中,從而達到賦值的效果
val = newVal
}
})
}
function observe(data) {
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key])
})
}
let test = [1, 2, 3]
// 初始化
observe(test)
複製代碼
console.log
時,你會發如今打印的過程當中是遍歷這個數組的
打印的過程能夠理解爲
接下來咱們作以下操做
當咱們給length賦值時,能夠看見並不會遍歷數組去賦值索引。
小結
對於defineProperty來講,處理數組與對象是一視同仁的,只是在初始化時去改寫get
和set
達到監測數組或對象的變化,對於新增的屬性,須要手動再初始化。對於數組來講,只不過特別了點,push、unshift值也會新增索引,對於新增的索引也是能夠添加observe從而達到監聽的效果;pop、shift值會刪除更新索引,也會觸發defineProperty的get和set。對於從新賦值length的數組,不會新增索引,由於不清楚新增的索引有多少,根據ecma
規範定義,索引的最大值爲2^32 - 1
,不可能循環去賦值索引的。
以上參考
引起我對這個問題的思考是
對我有所幫助是知乎@liuqipeng的回答
vue對數組的observe單獨作了處理
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
// 判斷數組實例是否有__proto__屬性,有就用protoAugment
// 而protoAugment司機就是重寫實例的__proto__
// target.__proto__ = src
// 將新的arrayMethods重寫到value上
augment(value, arrayMethods, arrayKeys)
// 而後初始化observe已存在索引的值
this.observeArray(value)
} else {
this.walk(value)
}
複製代碼
再來看如何重寫的arrayMethods
,在array.js
中,咱們能夠看到
const arrayProto = Array.prototype
// 複製了數組構造函數的原型
// 這裏須要注意的是數組構造函數的原型也是個數組
// 實例中指向原型的指針__proto__也是個數組
// 數組並無索引,由於length = 0
// 相反的擁有屬性,屬性名爲數組方法,值爲對應的函數
export const arrayMethods = Object.create(arrayProto)
// 對如下方法重寫
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
複製代碼
以下圖,當我給__proto__
索引爲0賦值時,是正常的,可是其他的屬性依舊在後面。咱們能夠這樣認爲,數組的構造函數的原型是個空數組,可是默認給你內置了幾個方法。
咱們再來看爲何只對這些方法重寫?
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// 這裏的def很重要,其實也就是用object.defineProperty從新定義屬性
// 但這裏的arrayMethods是個數組,這就是爲何上面咱們解釋
// 數組構造函數原型是個空數組可是默認了屬性方法
// 因此這裏的定義是很巧妙的
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
// ob就是observe實例
const ob = this.__ob__
let inserted
switch (method) {
// 爲何對push和unshift單獨處理?
// 咱們在上看解釋過,這2中方法會增長數組的索引,可是新增的索引位須要手動observe的
case 'push':
case 'unshift':
inserted = args
break
// 同理,splice的第三個參數,爲新增的值,也須要手動observe
case 'splice':
inserted = args.slice(2)
break
}
// 其他的方法都是在原有的索引上更新,初始化的時候已經observe過了
if (inserted) ob.observeArray(inserted)
// notify change
// 而後通知全部的訂閱者觸發回調
ob.dep.notify()
return result
})
})
複製代碼
最後,仍是貼一波博客地址爲何defineProperty不能檢測到數組長度的「變化」