舉個例子,來說明下為什么監聽不到數組變化
var target ={ val: 1 } let _value = target.val Object.defineProperty(target,"val",{ get:function(){ return _value }, set:function(newVal){ _value = newVal console.log("setted") } }) console.log(target.val) // 1 console.log(target.val = []) // setted [] console.log(target.val = [1,2,3]) // setted [1,2,3] console.log(target.val[1]=10) // 10 console.log(target.val.push(8)) // 4 console.log(target.val.length=5) // 5 console.log(target.val) // [1, 10, 3, 8, empty]
從本例中可以看到,當taget.val被設置為數組后,想要對數組內部進行修改,通過數組索引去賦值 target.val[1]=10 ,不會觸發set方法執行。
那么該如何實現呢?
我們先來了解下 Array.prototype.push.call() 相關知識,便於監聽數組,實現響應做鋪墊。
Array.prototype.push.apply()
var a = [1,2,3]; var b = [4,5,6]; Array.prototype.push.apply(a, b); console.log(a) //[1,2,3,4,5,6]
原生push方法接受的參數是一個參數列表,它不會自動把數組擴展成參數列表,使用apply的寫法可以將數組型參數擴展成參數列表,這樣合並兩個數組就可以直接傳數組參數了。
注:合並數組為什么不直接使用Array.prototype.concat()呢?
因為concat不會改變原數組,concat會返回新數組,而上面apply這種寫法直接改變數組a。
簡單實現監聽數組變化
let arr = [1,2,3]
console.log(arr)

打印結果看來,數組的隱式原型上掛載了一些方法,如push()、pop()、shift()、unshift()、splice()、sort()、reverse()等。
我們重新改寫下方法
let arr = [1,2,3] arr.__proto__ = { push: function() { // 這里的this指arr,即[1,2,3] return Array.prototype.push.apply(this, arguments) } } arr.push(6) console.log('修改后數組:',arr)

在官方文檔,所需監視的只有 push()、pop()、shift()、unshift()、splice()、sort()、reverse() 7 種方法。我們可以遍歷一下:
只需要監聽我們需要監聽的數據數組的一個變更,而不是針對原生Array的一個重新封裝。
會重寫Array.prototype.push方法,並生成一個新的數組賦值給數據,這樣數據雙向綁定就會觸發。
首先讓這個對象繼承 Array 本身的所有屬性,這樣就不會影響到數組本身其他屬性的使用,后面對相應的函數進行改寫,也就是在原方法調用后去通知其它相關依賴這個屬性發生了變化,這點和 Object.defineProperty 中 setter所做的事情幾乎完全一樣,唯一的區別是可以細化到用戶到底做的是哪一種操作,以及數組的長度是否變化
不會污染到原生Array上的原型方法。
首先我們將需要監聽的數組的原型指針指向newArrProto,然后它會執行原生Array中對應的原型方法,與此同時執行我們自己重新封裝的方法。
那么問題來了,這種形式咋這么眼熟呢?這不就是我們見到的最多的繼承問題么?子類(newArrProto)和父類(Array)做的事情相似,卻又和父類做的事情不同。但是直接修改__proto__隱式原型指向總感覺心里怪怪的(因為我們可能看到的多的還是prototype),心里不(W)舒(T)服(F)。
const arrayProto = Array.prototype; const arrayMethods = Object.create(arrayProto); const newArrProto = []; ['push', 'pop','shift','unshift','splice','sort','reverse'].forEach(method => { // 原生Array的原型方法 let original = arrayMethods[method]; // 將push,pop等方法重新封裝並定義在對象newArrProto的屬性上 // 注:封裝好的方法是定義在newArrProto的屬性上而不是其原型屬性,即newArrProto.__proto__ 沒有改變 newArrProto[method] = function mutator() { console.log('監聽到數組的變化啦!');
// 更新視圖,dep.notify() // 調用對應的原生方法並返回結果(新數組長度) return original.apply(this, arguments); } }) let list1 = [1, 2]; // 將我們要監聽的數組的原型指針指向上面定義的空數組對象 // newArrProto的屬性上定義了我們封裝好的push,pop等方法 list1.__proto__ = newArrProto; list1.push(3); // 監聽到數組的變化啦! 3 // list2沒有被重新定義原型指針,所以這里會正常執行原生Array上的原型方法 let list2 = [1, 2]; list2.push(3); // 3
Array.prototype.push.call()
var obj = {} console.log(Array.prototype.push.call(obj, 'a','b','c')) // 3 console.log(obj) // {0: "a", 1: "b", 2: "c", length: 3} var obj1 = { length: 5 } console.log(Array.prototype.push.call(obj1, 'a','b','c')) // 8 console.log(obj1) // {5: "a", 6: "b", 7: "c", length: 8} var obj2 = { 0: 'e', 1: 'f', length: 7 } console.log(Array.prototype.push.call(obj2, 'a','b','c')) // 10 console.log(obj2) // {0: "e", 1: "f", 7: "a", 8: "b", 9: "c", length: 10}
通過上面對比結果,我們可以看出:
1)當對象中不含有length屬性時,調用數組原型方法push,將對象轉為類數組對象,新增屬性的索引從0開始,且lengt指是新增屬性的個數
2)當對象中含有length屬性時,新增屬性的索引命名從length長度開始計算。
eg: obj1中length為5,新增加屬性的索引分別為5、6、7;obj2中length為7,新增加屬性的索引分別為7、8、9
Array.prototype.slice.call()
Array.prototype.slice.call()方法是只能在類數組上起作用的,並不能同push()方法一樣可以可以使對象轉換為帶有length屬性的類數組對象。


結論,當對象中沒有length屬性時,默認添加的新屬性索引應為0,因為a中已經有為0的key了,於是將原來的banana覆蓋了,便有了現在的結果。
