基本概念
先用構造函數創(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)建的實例的原型,也就是這個例子中的 person1
和 person2
的原型。
你可以這樣理解:每一個JavaScript
對象(null
除外)在創(chuàng)建的時候就會與之關聯另一個對象,這個對象就是我們所說的原型,每一個對象都會從原型"繼承"屬性。
那么我們該怎么表示實例與實例原型,也就是 person
和 Person.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
),這個對象本身是個函數;所有的函數(包括Function
,Object
)的原型(__proto__
)都是Function.prototype
。 -
Function
對象作為一個函數,就會有prototype
屬性,該屬性將對應function () {}
對象。 -
Function
對象作為一個對象,就有__proto__
屬性,該屬性對應Function.prototype
,也就是說,Function.__proto__ === Function.prototype
。 - 對于
Function
的原型對象Function.prototype
,該原型對象的__proto__
屬性將對應Object {}
。 - 對于原型對象
Person.prototype
的constructor
,將對應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.prototype
和Person.__proto__
是完全不同的。
在這里對prototype
和__proto__
進行簡單的介紹:
- 對于所有的對象,都有
__proto__
屬性,這個屬性對應該對象的原型 - 對于函數對象,除了
__proto__
屬性之外,還有prototype
屬性,當一個函數被用作構造函數來創(chuàng)建實例時,該函數的prototype
屬性值將被作為原型賦值給所有對象實例(也就是設置實例的__proto__
屬性)
圖解實例
附上網上扣的一張圖
對上圖的總結
- 所有的對象都有
__proto__
屬性,該屬性對應該對象的原型 - 所有的函數對象都有
prototype
屬性,該屬性的值會被賦值給該函數創(chuàng)建的對象的__proto__
屬性 - 所有的原型對象都有
constructor
屬性,該屬性對應創(chuàng)建所有指向該原型的實例的構造函數 - 函數對象和原型對象通過
prototype
和constructor
屬性進行相互關聯
重寫原型
可以通過改變函數的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)建一個關聯,這樣,一個對象就可以通過委托訪問另一個對象的屬性和函數,所以與其叫繼承,委托的說法反而更準確些。