首先歡迎你們關注個人Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵。javascript
從上一篇文章響應式數據與數據依賴基本原理開始,我就萌發了想要研究Vue源碼的想法。最近看了youngwind的一篇文章如何監聽一個數組的變化發現Vue早期實現監聽數組的方式和個人實現稍有區別。而且在兩年前做者對其中的一些代碼的理解有誤,在閱讀完評論中@Ma63d的評論以後,感受收益匪淺。java
在咱們的上一篇文章中,咱們想嘗試監聽數組變化,採用的是下面的思路:git
function observifyArray(array){
//須要變異的函數名列表
var methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
var arrayProto = Object.create(Array.prototype);
_.each(methods, function(method){
arrayProto[method] = function(...args){
// 劫持修改數據
var ret = Array.prototype[method].apply(this, args);
//能夠在修改數據時觸發其餘的操做
console.log("newValue: ", this);
return ret;
}
});
Object.setPrototypeOf(array, arrayProto);
}
複製代碼
咱們是經過爲數組實例設置原型prototype
來實現,新的prototype
重寫了原生數組原型的部分方法。所以在調用上面的幾個變異方法的時候咱們會獲得相應的通知。但其實setPrototypeOf
方法是ECMAScript 6的方法,確定不是Vue內部可選的實現方案。咱們能夠大體看看Vue的實現思路。github
function observifyArray(array){
var aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
var arrayAugmentations = Object.create(Array.prototype);
aryMethods.forEach((method)=> {
// 這裏是原生Array的原型方法
let original = Array.prototype[method];
// 將push, pop等封裝好的方法定義在對象arrayAugmentations的屬性上
// 注意:是屬性而非原型屬性
arrayAugmentations[method] = function () {
console.log('我被改變啦!');
// 調用對應的原生方法並返回結果
return original.apply(this, arguments);
};
});
array.__proto__ = arrayAugmentations;
}
複製代碼
__proto__
是咱們你們的很是熟悉的一個屬性,其指向的是實例對象對應的原型對象。在ES5中,各個實例中存在一個內部屬性[[Prototype]]
指向實例對象對應的原型對象,可是內部屬性是無法訪問的。瀏覽器各家廠商都支持非標準屬性__proto__
。其實Vue的實現思路與咱們的很是類似。惟一不一樣的是Vue使用了的非標準屬性__proto__
。數組
其實閱讀過《JavaScript高級程序設計》的同窗應該還記得原型式繼承。其重要思路就是藉助原型能夠基於已有的對象建立對象。好比說:瀏覽器
function object(o){
function F(){}
F.prototype = o;
return new F();
}
複製代碼
其實咱們上面Vue的思路也是這樣的,咱們藉助原型建立的基於arrayAugmentations的新實例,使得實例可以訪問到咱們自定義的變異方法。app
上面一篇文章的做者youngwind寫文章的時候就提出了,爲何不去採用更爲常見的組合式繼承去實現,好比:函數
function FakeArray() {
Array.apply(this,arguments);
}
FakeArray.prototype = [];
FakeArray.prototype.constructor = FakeArray;
FakeArray.prototype.push = function () {
console.log('我被改變啦');
return Array.prototype.push.apply(this,arguments);
};
let list = ['a','b','c'];
let fakeList = new FakeArray(list);
複製代碼
結果發現fakeList
並非一個數組而是一個對象,做者當時這這樣認爲的:學習
構造函數默認返回的原本就是this對象,這是一個對象,而非數組。Array.apply(this,arguments);這個語句返回的纔是數組ui
咱們能不能將Array.apply(this,arguments);直接return出來呢?
若是咱們return這個返回的數組,這個數組是由原生的Array構造出來的,因此它的push等方法依然是原生數組的方法,沒法到達重寫的目的。
首先咱們知道採用new
操做符調用構造函數會依次經歷如下四個步驟:
在沒有顯式返回的時候,返回的是新對象,所以fakeList
是對象而不是數組。可是爲何不能強制返回Array.apply(this,arguments)
。其實下面有人說做者這句話有問題
這個數組是由原生的Array構造出來的,因此它的push等方法依然是原生數組的方法,沒法到達重寫的目的。
其實上面這句話自己確實沒有錯誤,當咱們給構造函數顯式返回的時候,咱們獲得的fakeList
就是原生的數組。所以調用push
方法是無法觀測到的。可是咱們不能返回的Array.apply(this,arguments)
更深層的緣由在於咱們這邊調用Array.apply(this,arguments)
的目的是爲了借用原生的Array
的構造函數將Array
屬性賦值到當前對象上。
舉一個例子:
function Father(){
this.name = "Father";
}
Father.prototype.sayName = function(){
console.log("name: ", this.name);
}
function Son(){
Father.apply(this);
this.age = 100;
}
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log("age: ", this.age);
}
var instance = new Son();
instance.sayName(); //name: Father
instance.sayAge(); //age: 100
複製代碼
子類Son
爲了繼承父類Father
的屬性和方法兩次調用Father
的構造函數,Father.apply(this)
就是爲了建立父類的屬性,而Son.prototype = new Father();
目的就是爲了經過原型鏈繼承父類的方法。所以上面所說的纔是爲何不能將Array.apply(this,arguments)
強制返回的緣由,它的目的就是借用原生的Array
構造函數建立對應的屬性。
可是問題來了,爲何沒法借用原生的Array
構造函數建立對象呢?實際上不只僅是Array
,String
、Number
、Regexp
、Object
等等JavaScript的內置類都不能經過借用構造函數的方式建立帶有功能的屬性(例如: length
)。JavaScript數組中有一個特殊的響應式屬性length
,一方面若是數組數值類型下標的數據發生變化的時候會在length
上體現,另外一方面,修改length
也會影響到數組的數值數據。由於沒法經過借用構造函數的方式建立響應式length
屬性(雖然屬性能夠被建立,但不具有響應式功能),所以在E55咱們是無法繼承數組的。好比:
function MyArray(){
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0
colors.length = 0;
console.log(colors[0]); //"red"
複製代碼
好在咱們迎來ES6的曙光,經過類class的extends,咱們就能夠實現繼承原生的數組,例如:
class MyArray extends Array {
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0
colors.length = 0;
cosole.log(colors[0]); // undefined
複製代碼
爲何ES6的extends能夠作到ES5所不能實現的數組繼承呢?這是因爲兩者的繼承原理不一樣致使的。ES5的繼承方式中,先是生成派生類型的this
(例如:MyArray),而後調用基類的構造函數(例如:Array.apply(this)),這也就是說this
首先指向的是派生類的實例,而後指向的是基類的實例。因爲原生對象(例如: Array)經過借用的方式並不能給this
賦值length
相似的具備功能的屬性,所以咱們無法實現想要的結果。
可是ES6的extends
的繼承方式倒是與之相反的,首先是由基類(Array)建立this
的值,而後再由派生類的構造函數修改這個值,所以在上面的例子中,一開始就能夠經過this
建立基類的全部內建功能並接受與之相關的功能(如length
),而後在此this
的基礎上用派生類進行擴展,所以就能夠達到咱們的繼承原生數組的目的。
不只僅如此。ES6在擴展相似上面的原生對象時還提供了一個很是方便的屬性: Symbol.species
。
Symbol.species
的主要做用就是可使得本來返回基類實例的繼承方法返回派生類的實例,舉個例子吧,好比Array.prototype.slice
返回的就是數組的實例,可是當MyArray
繼承Array
時,咱們也但願當使用MyArray
的實例調用slice
時也能返回MyArray
的實例。那咱們該如何使用呢,其實Symbol.species
是一個靜態訪問器屬性,只要在定義派生類時定義,就能夠實現咱們的目的。好比:
class MyArray extends Array {
static get [Symbol.species](){
return this;
}
}
var myArray = new MyArray(); // MyArray[]
myArray.slice(); // MyArray []
複製代碼
咱們能夠發現調用數組子類的實例myArray
的slice
方法時也會返回的是MyArray
類型的實例。若是你喜歡嘗試的話,你會發現即便去掉了靜態訪問器屬性get [Symbol.species]
,myArray.slice()
也會仍然返回MyArray
的實例,這是由於即便你不顯式定義,默認的Symbol.species
屬性也會返回this
。固然你也將this
改變爲其餘值來改變對應方法的返回的實例類型。例如我但願實例myArray
的slice
方法返回的是原生數組類型Array
,就能夠採用以下的定義:
class MyArray extends Array {
static get [Symbol.species](){
return Array;
}
}
var myArray = new MyArray(); // []
myArray.slice(); // []
複製代碼
固然了,若是在上面的例子中,若是你但願在自定義的函數中返回的實例類型與Symbol.species
的類型保持一致的話,能夠以下定義:
class MyArray extends Array {
static get [Symbol.species](){
return Array;
}
constructor(value){
super();
this.value = value;
}
clone(){
return new this.constructor[Symbol.species](this.value)
}
}
var myArray = new MyArray();
myArray.clone(); //[]
複製代碼
經過上面的代碼咱們能夠了解到,在實例方法中經過調用this.constructor[Symbol.species]
咱們就能夠獲取到Symbol.species
繼而能夠創造對應類型的實例。
上面整個的文章都是基於監聽數組響應的一個點想到的。這裏僅僅是起到拋磚引玉的做用,但願能對你們有所幫助。若有不正確的地方,歡迎你們指出,願共同窗習。