2011-11-11 175 views
35

請考慮以下代碼。JavaScript繼承和構造函數屬性

function a() {} 
function b() {} 
function c() {} 

b.prototype = new a(); 
c.prototype = new b(); 

console.log((new a()).constructor); //a() 
console.log((new b()).constructor); //a() 
console.log((new c()).constructor); //a() 
  • 爲什麼不更新的B和C的構造?
  • 我做繼承錯了嗎?
  • 什麼是更新構造函數的最佳方法?

此外,請考慮以下內容。

console.log(new a() instanceof a); //true 
console.log(new b() instanceof b); //true 
console.log(new c() instanceof c); //true 
  • 鑑於(new c()).constructor等於a()Object.getPrototypeOf(new c())a{ },怎麼可能爲instanceof知道new c()c一個實例?

http://jsfiddle.net/ezZr5/

+3

你需要更新構造函數的任何原因?如果我假裝財產不存在,我發現我的生活更容易。 – hugomg

+0

我很難把它作爲一個副本來關閉 - 所有其他questinos都是如此冗長...... – hugomg

+3

'c.prototype'是'b()'而'b.prototype'是'a()',因此'c.prototype'是'a()' –

回答

63

好吧,讓我們玩一個小智力遊戲:

從上面的圖片我們可以看到:

  1. 當我們創建一個像function Foo() {}功能,JavaScript的創建Function實例。
  2. 每個Function實例(構造函數)都有一個屬性prototype這是一個指針。構造函數的prototype屬性指向其原型對象。
  3. 原型對象有一個屬性constructor這也是一個指針。
  4. 原型對象的constructor屬性指向它的構造函數。
  5. 當我們創建Foo的新實例(如new Foo())時,JavaScript會創建一個新對象。
  6. 實例的內部[[proto]]屬性指向構造函數的原型。

現在,問題出現了,爲什麼JavaScript沒有將constructor屬性附加到實例對象而不是原型。試想一下:

function defclass(prototype) { 
    var constructor = prototype.constructor; 
    constructor.prototype = prototype; 
    return constructor; 
} 

var Square = defclass({ 
    constructor: function (side) { 
     this.side = side; 
    }, 
    area: function() { 
     return this.side * this.side; 
    } 
}); 

var square = new Square(10); 

alert(square.area()); // 100 

正如你可以看到constructor屬性爲原型的另一種方法,就像上面的例子areaconstructor屬性的特殊之處在於它用於初始化原型的實例。否則,它與原型的其他方法完全相同。

定義的原型constructor財產由於下列原因是有利的:

  1. 這是邏輯上是正確的。例如考慮Object.prototype。 的constructor屬性指向Object。如果在實例上定義constructor屬性,則Object.prototype.constructor將爲undefined,因爲Object.prototypenull的實例。
  2. 它與其他原型方法沒有區別。這使得new的工作更容易,因爲它不需要在每個實例上定義constructor屬性。
  3. 每個實例共享相同的constructor屬性。因此它是高效的。

現在,當我們談論繼承,我們有以下情形:

從上面的圖片我們可以看到:

  1. 派生構造函數的prototype屬性設置爲基礎構造函數的實例。
  2. 因此派生構造函數實例的內部[[proto]]屬性也指向它。
  3. 因此,派生構造函數實例的constructor屬性現在指向基礎構造函數。

至於instanceof算子,與流行的看法相反,它並不依賴於實例的constructor屬性。從上面我們可以看出,這會導致錯誤的結果。

instanceof運算符是一個二元運算符(它有兩個操作數)。它在一個實例對象和一個構造函數上運行。作爲上Mozilla Developer Network解釋,它只是執行以下操作:

function instanceOf(object, constructor) { 
    while (object != null) { 
     if (object == constructor.prototype) { //object is instanceof constructor 
      return true; 
     } else if (typeof object == 'xml') { //workaround for XML objects 
      return constructor.prototype == XML.prototype; 
     } 
     object = object.__proto__; //traverse the prototype chain 
    } 
    return false; //object is not instanceof constructor 
} 

爲了把它從Bar只是如果Foo繼承,那麼原型鏈的Foo實例是:

  1. foo.__proto__ === Foo.prototype
  2. foo.__proto__.__proto__ === Bar.prototype
  3. foo.__proto__.__proto__.__proto__ === Object.prototype
  4. foo.__proto__.__proto__.__proto__.__proto__ === null

正如您所看到的,每個對象都繼承自Object的構造函數。原型鏈在內部[[proto]]屬性指向null時結束。

instanceof函數簡單地遍歷實例對象的原型鏈(第一操作數),並且每個對象的內部屬性[[proto]]比較的構造函數的prototype屬性(第二個操作數)。如果它們匹配,則返回true;否則如果原型鏈結束,則返回false

+4

另請參閱http://joost.zeekat.nl/constructors-considered-mildly-confusing.html – Christoph

+3

+1我寧願使用Object.getPrototypeOf而不是使用'.__ proto__'。 – pimvdb

+3

爲了便於說明,我只使用了'__proto__'屬性。這就是在Mozilla開發者網絡上解釋的方式。儘管如此,'__proto__'屬性確實比'Object.getPrototypeOf'方法具有更快的優勢(無需函數調用開銷),並且它被所有主流瀏覽器實現。我使用'Object.getPrototypeOf'的唯一原因是爲了解決像Rhino這樣不支持'__proto__'屬性的實現。我們都有自己的喜好。我更喜歡'__proto__'屬性,因爲它更具可讀性。乾杯。 =) –

12

默認情況下,

function b() {} 

然後b.prototype具有被自動設置爲b一個.constructor屬性。不過,您目前覆蓋原型,從而丟棄變量:

b.prototype = new a; 

然後b.prototype沒有一個.constructor財產了;它被覆蓋擦除。它確實繼承自a雖然和(new a).constructor === a,因此(new b).constructor === a(它指的是原型鏈中的相同屬性)。

最好做的就是將其設置手動算賬:

b.prototype.constructor = b; 

你也可以做一個小功能是:

function inherit(what, from) { 
    what.prototype = new from; 
    what.prototype.constructor = what; 
} 

http://jsfiddle.net/79xTg/5/

+1

這都是事實。但是你不會知道某個對象是*繼承*。 'new b()instanceof a'在你使用'$ .extend'時返回'false',如果他需要檢查繼承,這在OP的情況下可能是不受歡迎的。 –

+0

@Robert Koritnik:好點,但'b instanceof b'總是假的,因爲構造函數不是它自己的一個實例。擴展函數似乎適用於'new b instanceof b',除非我誤會你:http://jsfiddle.net/79xTg/3/。 – pimvdb

+0

我編輯了我的評論。這當然是一個錯字。讓我編輯你的例子:http://jsfiddle.net/79xTg/4/'$ .extend'不是繼承,而是'原型'副本。因此你不能使用它來檢查繼承。 –

4

constructor是有規律的,非 - 函數對象的prototype屬性的默認值的可枚舉屬性。因此,分配到prototype將失去財產。

instanceof仍然可以工作,因爲它不使用constructor,而是掃描功能的prototype屬性的(當前)值的對象的原型鏈,即foo instanceof Foo相當於

var proto = Object.getPrototypeOf(foo); 
for(; proto !== null; proto = Object.getPrototypeOf(proto)) { 
    if(proto === Foo.prototype) 
     return true; 
} 
return false; 

在ECMAScript3 ,因爲用戶定義的屬性總是可枚舉的(即對for..in可見),所以無法設置與內置行爲相同的constructor屬性。

這改變了ECMAScript5。但是,即使您手動設置constructor,您的代碼仍然存在問題:特別是將prototype設置爲父類的實例是個壞主意 - 父類構造函數在子類的類別中不應調用'被定義,而是當創建子實例時。

下面是應該怎麼做一些ECMAScript5示例代碼:

function Pet(name) { 
    this.name = name; 
} 

Pet.prototype.feed = function(food) { 
    return this.name + ' ate ' + food + '.'; 
}; 

function Cat(name) { 
    Pet.call(this, name); 
} 

Cat.prototype = Object.create(Pet.prototype, { 
    constructor : { 
     value : Cat, 
     writable : true, 
     enumerable : false, 
     configurable : true 
    } 
}); 

Cat.prototype.caress = function() { 
    return this.name + ' purrs.'; 
}; 

如果你堅持ECMAScript3,你需要使用自定義clone() function,而不是Object.create(),將無法使constructor不可枚舉:

Cat.prototype = clone(Pet.prototype); 
Cat.prototype.constructor = Cat;