javascript 的原型與原型鏈的理解
javascript中一切皆物件,但是由於沒有Class類的概念,所以就無法很好的表達物件與物件之間的關係了。
比如物件A與物件B之間,它們兩個是相對獨立的個體,互不干擾,物件A修改自身的屬性不會影響到物件B。
雖然這很好,但是有一個問題,如果物件A與物件B都有一個方法 run() ,並且程式碼也一樣,那物件A與物件B各自都獨立擁有一份 run() 方法的完整程式碼,這是需要資源去儲存的。
一旦我們程式中應用的物件過多,那這種資源消耗會是巨大的。那有沒有一種方法可以讓物件A與物件B擁有一些公共的屬性和方法,讓它們之前有某種聯絡?
我們設想一下,會不會存在一個 common物件,common物件上儲存著公共的屬性和方法,而物件A與物件B裡面有一個prototype屬性指向這個 common物件,
當然我們呼叫物件A或物件B的屬性和方法時,如果在自身物件中沒有找到,就去prototype這個屬性指向的物件上面去找。
而common物件本身也有一個prototype屬性指向更上一級的common物件,然後一直往上找啊找,直到為null,就停止。
這種不斷的從下往上找的這種路徑,就像鏈條一樣,我們稱它為 原型鏈,而那個common物件,我們稱它為 原型物件。
我們來看一個建構函式
function Base(name) { this.name = name; } let A = new Base('A'); let B = new Base('B'); //每一個函式都有一個prototype屬性,指向該函式的原型物件 console.log(Base.prototype); //當然原型物件也是一個物件,它也有一個constructor,指向建構函式 console.log(Base.prototype.constructor === Base); //每一個例項物件的constructor都指向建立它們的建構函式 console.log(A.constructor === Base); console.log(B.constructor === Base); //每一個例項物件都有一個__proto__屬性,該屬性指向建構函式的原型物件 console.log(A.__proto__ === Base.prototype); console.log(B.__proto__ === Base.prototype);
1、每一個函式都有一個prototype屬性,它指向該函式的原型物件。
2、原型物件也是物件,它也有自已的constructor,它指向建構函式Base()。換句話說,其實原型物件也是建構函式Base()的一個例項。只不過比較特殊,用來存放公共屬性和方法的。
3、每一個通過建構函式Base()建立的例項物件,都有一個constructor,指向建立它們的建構函式。
4、每一個物件,都有一個 __proto__ 屬性,指向建構函式Base()的 原型物件。換句話說,__proto__ 是將 原型 串聯起來形成鏈條的關鍵。不然物件A與物件B都無法找到原型物件上的公共屬性和方法。
function Base(name) { this.name = name; } //我們在原型物件上新增公共屬性 Base.prototype.status = '開始'; //我們在原型物件上新增公共方法 Base.prototype.run = function() { console.log(this.name + ' run ...'); }; let A = new Base('A'); let B = new Base('B'); A.run(); B.run(); console.log(A.status); console.log(B.status); //修改原型上的屬性,則例項物件也會跟著改變 Base.prototype.status = '停止'; console.log(A.status); console.log(B.status);
通過原型與原型鏈,讓物件與物件之間有了關聯關係。
那如何通過原型與原型鏈,讓一個建構函式繼承於另一個建構函式?
比如,我們要讓建構函式Child 繼承於 建構函式Base,只需要讓 Child 的 prototype 指向 Base的 原型物件,不就可以了?
function Base(name) { } Base.prototype.name = 'Base'; Base.prototype.run = function () { console.log(this.name + ' run ...'); }; function Child() { } Child.prototype = Base.prototype; //注意這個時候,Child.prototype物件的constructor屬性指向了Base //這就導致通過建構函式Child建立的例項物件,物件的constructor屬性會指向Base,而不是Child,這會導致混亂。 //所以我們重新設定Child.prototype.constructor指向Child Child.prototype.constructor = Child; let c = new Child(); console.log(c.name); c.run();
這樣有一個問題,Child.prototype 與 Base.prototype 指向同一個原型物件,任何對 Child.prototype 的修改都會反應到 Base.prototype 上面。
這時,Base.prototype.constructor 指向了 Child,這顯然是有問題。
我們只能通過一箇中間的空建構函式,來完成原型的指向。
function Base(name) { } Base.prototype.name = 'Base'; Base.prototype.run = function () { console.log(this.name + ' run ...'); }; function Child() { } //建立一箇中間的空建構函式 function Mid() { } //讓該空建構函式的prototype指向Base的原型物件 Mid.prototype = Base.prototype; //再讓Child的prototype指向該空建構函式的一個例項 Child.prototype = new Mid(); //這樣,當修改Child.prototype.constructor時,Base.prototype就不會受影響了 Child.prototype.constructor = Child; let c = new Child(); console.log(c.name); c.run(); //Base.prototype的constructor仍然指向Base,沒有受到影響 console.log(Base.prototype.constructor);
那怎麼通過原型與原型鏈,讓你一物件繼承於另一個物件呢?
比如,我們要讓物件B繼承於物件A,無非就是想要拿到物件A的屬性和方法,這麼一想,那通過把物件B的 __proto__ 指向 物件A,不就可以實現了?
let A = { name: 'A', run() { console.log(this.name + ' run ...'); } }; console.log(A.name); A.run(); let B = {}; //讓物件B的__proto__指向物件A B.__proto__ = A; //當物件B呼叫run()方法時會在自身上找,如果沒找到,則通過__proto__向上找 //由於__proto__指向物件A,所以最終會在物件A中找到run()方法 B.run(); B.__proto__.name = 'B'; console.log(A.name); console.log(B.name);
這樣有一個問題,當修改 B.__proto__.name = 'B'; 時,物件A也會受到影響。
我們可以通過ES5提供的 Object.create() 來解決此問題,Object.create()可以通過指定的 原型物件 建立一個新物件。
let A = { name: 'A', run() { console.log(this.name + ' run ...'); } }; console.log(A.name); A.run(); let B = {}; //通過Object.create()建立一個以物件A為原型物件的新物件 //讓物件B的__proto__指向該新物件 //這樣再操作B.__proto__中的屬性就與物件A無關了。 B.__proto__ = Object.create(A); B.run(); B.__proto__.name = 'B'; console.log(A.name); console.log(B.name);