Javascript中的prototype以及__proto__

基本概念

先用構造函數創(chuàng)建一個對象

function Person() {}
var person = new Person();
person.name = 'Greg';
console.log(person.name) // Greg

在這個例子中,Person 就是一個構造函數,我們使用 new 創(chuàng)建了一個實例對象 person

prototype

首先來說說prototype屬性,不像每個對象都有__proto__[[Prototype]])屬性來標識自己所繼承的原型,只有函數才有prototype屬性。

當你創(chuàng)建函數時,Javascript會為這個函數自動添加prototype屬性,值是空對象。而一旦你把這個函數當作構造函數調用(即通過new關鍵字調用),那么Javascript就會幫你創(chuàng)建該構造函數的實例,實例繼承構造函數prototype的所有屬性和方法(實例通過設置自己的__proto__指向其構造函數的prototype來實現這種繼承)。

function Person() {}
Person.prototype.name = 'Greg';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name); // Greg
console.log(person2.name); // Greg

函數的 prototype 屬性指向了一個對象,這個對象正是調用該構造函數而創(chuàng)建的實例的原型,也就是這個例子中的 person1person2 的原型。

你可以這樣理解:每一個JavaScript對象(null除外)在創(chuàng)建的時候就會與之關聯另一個對象,這個對象就是我們所說的原型,每一個對象都會從原型"繼承"屬性。

讓我們用一張圖表示構造函數和實例原型之間的關系:

那么我們該怎么表示實例與實例原型,也就是 personPerson.prototype 之間的關系呢,這時候我們就要講到__proto__

__proto__

這是每一個JavaScript對象(除了null)都具有的一個屬性,叫__proto__,這個屬性會指向該對象的原型。

絕大部分瀏覽器都支持這個非標準的方法訪問原型,然而它并不存在于 Person.prototype 中,實際上,它是來自于 Object.prototype ,與其說是一個屬性,不如說是一個 getter/setter,當使用 obj.__proto__ 時,可以理解成返回了 Object.getPrototypeOf(obj)

function Person() {}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true

于是我們更新下關系圖:


既然實例對象和構造函數都可以指向原型,那么原型是否有屬性指向構造函數或者實例呢?

constructor

指向實例倒是沒有,因為一個構造函數可以生成多個實例,但是原型指向構造函數倒是有的,這就要講到第三個屬性:constructor,每個原型都有一個 constructor 屬性指向關聯的構造函數。

為了驗證這一點,我們可以嘗試:

function Person() {}
console.log(Person === Person.prototype.constructor); // true

當獲取 person.constructor 時,其實 person 中并沒有 constructor 屬性,當不能讀取到constructor 屬性時,會從 person 的原型也就是 Person.prototype 中讀取,正好原型中有該屬性:

var person = new Person();
console.log(person.constructor === Person); // true
console.log(person.hasOwnProperty('constructor')); // false

所以再更新下關系圖:


綜上我們已經得出:

function Person() {}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
console.log(Object.getPrototypeOf(person) === Person.prototype) // true

深入探究prototype與__proto__

對象有[[prototype]]屬性,函數對象有prototype屬性,原型對象有constructor屬性。
其中,[[Prototype]]作為對象的內部屬性,是不能被直接訪問的。所以為了方便查看一個對象的原型,Firefox和Chrome中提供了__proto__這個非標準(不是所有瀏覽器都支持)的訪問器(ECMA引入了標準對象原型訪問器Object.getPrototype(object))。

先創(chuàng)建一個構造函數

function Person() {}

函數的原型

console.log(Person.__proto__ === Function.prototype);   // true
console.log(Person.constructor === Function);  // true
console.log(typeof Person);  // function
console.log(Person);  // f Person() {}
console.log(Person.prototype.__proto__ === Object.prototype);  // true;
console.log(Person.__proto__);  // f () { [native code] }
console.log(Person.__proto__.__proto__ === Person.prototype.__proto__);  // true
console.log(Person.__proto__.__proto__ === Object.prototype);  // true
console.log(Person.prototype.constructor);  // f Person() {}
console.log('============================================');
console.log(Function.__proto__ === Function.prototype);  // true
console.log(Function.constructor === Function);  // true
console.log(typeof Function);  // function
console.log(Function);  // f Function() { [native code] }
console.log(Function.prototype);  // f () { [native code] }
console.log(Function.prototype.__proto__ === Object.prototype);  // true
console.log(Function.__proto__);   // f () { [native code] }
console.log(Function.__proto__.__proto__ === Function.prototype.__proto__);   // true
console.log(Function.__proto__.__proto__ === Object.prototype);   // true
console.log(Function.prototype.constructor);   // f Function() { [native code] }

結果分析:

  • JavaScript中有個Function對象(類似Object),這個對象本身是個函數;所有的函數(包括FunctionObject)的原型(__proto__)都是Function.prototype。
  • Function對象作為一個函數,就會有prototype屬性,該屬性將對應function () {}對象。
  • Function對象作為一個對象,就有__proto__屬性,該屬性對應Function.prototype,也就是說,Function.__proto__ === Function.prototype
  • 對于Function的原型對象Function.prototype,該原型對象的__proto__屬性將對應Object {}
  • 對于原型對象Person.prototypeconstructor,將對應Person函數本身。

Object的原型

console.log(typeof Object);  // function
console.log(Object);   // ? Object() { [native code] }
console.log(Object.prototype);  // Object {}
console.log(Object.prototype.__proto__);  // null
console.log(Object.prototype.constructor);  // ? Object() { [native code] }
console.log(Object.__proto__);  // ? () { [native code] }
console.log(Object.__proto__ === Function.prototype);  // true
console.log(Object.__proto__.prototype);  // undefined

結果分析:

  • Object對象本身是一個函數對象。
  • 既然是Object函數,就肯定會有prototype屬性,所以可以看到Object.prototype的值就是Object {}這個原型對象。
  • 反過來,當訪問Object.prototype對象的constructor這個屬性的時候,就得到了Obejct函數。
  • 另外,當通過Object.prototype.__proto__獲取Object原型的原型的時候,將會得到null,也就是說Object {}原型對象就是原型鏈的終點了。
  • Object對象作為一個對象,就有__proto__屬性,該屬性對應Function.prototype,也就是說,Object.__proto__ === Function.prototype

實例的原型

var will = new Person();
console.log(Person.prototype.__proto__ === Object.prototype);  // true
console.log(will.__proto__ === Person.prototype);  // true
console.log(will.__proto__.__proto__ === Object.prototype);  //true
console.log(will.constructor);  // ? Person() {}
console.log(will.prototype);  // undefined
console.log(Person.prototype.constructor === Person);  // true

結果分析:

  • will.__proto__ === Person.prototype,在JavaScript中,每個函數都有一個prototype屬性,當一個函數被用作構造函數來創(chuàng)建實例時,該函數的prototype屬性值將被作為原型賦值給所有對象實例(也就是設置實例的__proto__屬性),也就是說,所有實例的原型引用的是函數的prototype屬性。
  • prototype屬性是函數對象特有的,如果不是函數對象,將不會有這樣一個屬性。
  • 當通過Person.prototype.__proto__語句獲取will對象原型的原型時候,將得到Object {}對象,所有對象的原型都將追溯到Object {}對象。
  • JavaScript的原型對象中,還包含一個constructor屬性,這個屬性對應創(chuàng)建所有指向該原型的實例的構造函數。will對象本身并沒有constructor這個屬性,但是通過原型鏈查找,找到了will原型(will.__proto__)的constructor屬性,并得到了Person函數。

字面量對象的原型

var o = {};
console.log(o.constructor);  // ? Object() { [native code] }
console.log(o.__proto__ === Object.prototype);  // true
console.log(o.prototype);  // undefined

結果分析:

  • 字面量對象實質上就是Object構造函數的實例。

對比prototype和__proto__

對于prototype__proto__這兩個屬性有的時候可能會弄混,Person.prototypePerson.__proto__是完全不同的。

在這里對prototype__proto__進行簡單的介紹:

  • 對于所有的對象,都有__proto__屬性,這個屬性對應該對象的原型
  • 對于函數對象,除了__proto__屬性之外,還有prototype屬性,當一個函數被用作構造函數來創(chuàng)建實例時,該函數的prototype屬性值將被作為原型賦值給所有對象實例(也就是設置實例的__proto__屬性)

圖解實例

image.png

附上網上扣的一張圖

網上摳的一張圖

對上圖的總結

  • 所有的對象都有__proto__屬性,該屬性對應該對象的原型
  • 所有的函數對象都有prototype屬性,該屬性的值會被賦值給該函數創(chuàng)建的對象的__proto__屬性
  • 所有的原型對象都有constructor屬性,該屬性對應創(chuàng)建所有指向該原型的實例的構造函數
  • 函數對象和原型對象通過prototypeconstructor屬性進行相互關聯

重寫原型

可以通過改變函數的prototype屬性或對象的__proto__屬性來重寫原型,但會切斷現有原型與任何之前已經存在的對象實例之間的聯系;它們引用的仍然是最初的原型。

function Person() {}
var will = new Person();
Person.prototype = {
  constructor: Person,
  name: 'Greg',
  sayName: function () {
    console.log(this.name);
  }
};
will.sayName();  // error

報錯的原因就是因為will指向的原型中不包含以該名字命名的屬性。

原型對象的問題

原型模式的最大問題是由其共享的本性所導致的。

function Person() {}
Person.prototype = {
  constructor: Person,
  friends: ['a', 'b']
};
var person1 = new Person();
var person2 = new Person();

person1.friends.push('c');
console.log(person1.friends);  // ["a", "b", "c"]
console.log(person2.friends);  // ["a", "b", "c"]
console.log(person1.friends === person2.friends);  //true

在此,Person.prototype對象有一個名為friends的屬性,該屬性包含一個字符串數組。然后創(chuàng)建兩個Person實例。接著修改person1.friends引用的數組,向數組中添加一個字符串。由于friends數組存在于Person.prototype而非person1中,所以此修改也會通過person2.friends(與person1.friends指向同一個數組)反映出來。

真的是繼承嗎?

最后是關于繼承,前面我們講到“每一個對象都會從原型‘繼承’屬性”,實際上,繼承是一個十分具有迷惑性的說法,引用《你不知道的JavaScript》中的話,就是:

繼承意味著復制操作,然而 JavaScript 默認并不會復制對象的屬性,相反,JavaScript 只是在兩個對象之間創(chuàng)建一個關聯,這樣,一個對象就可以通過委托訪問另一個對象的屬性和函數,所以與其叫繼承,委托的說法反而更準確些。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容