建構函式其實就是一個使用new操作符調用的函數。當使用new調用時,建構函式內用到的this對象會對指向新建立的對象執行個體,如下的例子所示:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job;}var person = ("Nicholas", 29, "Software Engineer");
上面這個例子中,Person建構函式使用this對象給三個屬性賦值:name、age和job。當和new操作符連用時,則會建立一個新的Person對象,同事會給它分配這些屬性。問題在當沒有使用new操作符來調用建構函式的情況時。由於該this對象是在運行時綁定的,所以直接調用Person(),this會映射到全域對象window上,導致錯誤對象屬性的意外增加。例如:
var person = Person("Nicholas", 29, "Software Engineer");alert(window.name); //"Nicholas"alert(window.age); //29alert(window.job); //"Software Engineer"
這裡,原本針對Person執行個體的三個屬性被加到window對象上,因為建構函式是作為普通函數調用的,忽略了new操作符。這個問題是由this對象的晚綁定造成的,在這裡this唄解析成了window對象。由於window的name屬性是用於識別連結目標和架構的,所以這裡對該屬性的偶然覆蓋可能會導致該頁面上出現其它錯誤。這個問題的解決方案就是建立一個範圍安全的建構函式。
範圍安全的建構函式在進行任何更改前,首先確認this對象是正確類型的執行個體。如果不是,那麼會建立新的執行個體並返回。請看下面的例子:
function Person(name, age, job) { if (this instanceof Person) { this.name = name; this.age = age; this.job = job; } else { return new Person(name, age, job); }}var person1 = Person("Nicholas", 29, "Software Engineer");alert(window.name); //""alert(person1.name); //"Nicholas"var person2 = new Person("Shelby", 34, "Ergonomist");alert(person2.name); //"Shelby"
這段代碼中的Person建構函式添加了一個檢查並確保this對象是Person執行個體的if語句,它表示要麼使用new操作符,要麼在現有的Person執行個體環境中調用建構函式。任何一種情況下,對象初始化都能正常進行。如果this並非Person執行個體環境中調用建構函式。任何一種情況下,對象初始化都能正常進行。如果this並非Person的執行個體,那麼會再次使用new操作符調用建構函式並返回結果。最後的結果是,調用Person建構函式時無論是否使用new操作符,都會返回一個Person的新執行個體,這就避免了在全域對象上意外設定屬性。
關於範圍安全建構函式的貼心提示。實現了這個模式後,你就鎖定了可以調用建構函式的環境。如果你使用建構函式竊模數式的繼承且不使用原型鏈,那麼這個繼承很可能被破壞。這裡有個例子:
function Polygon(sides) { if (this instanceof Polygon) { this.sides = sides; this.getArea = function () { return 0; } } else { return new Polygon(sides); }}function Rectangle(width, height) { Polygon.call(this, 2); this.width = width; this.height = height; this.getArea = function () { return this.with * this.height; };}var rect = new Rectangle(5, 10);alert(rect.sides); //undefined
在這段代碼中,Polygon建構函式是範圍安全的,然而Rectangle建構函式則不是。新建立一個Rectangle執行個體之後,這個執行個體應該通過Polygon.call()來繼承Polygon的sides屬性。但是,由於Polygon建構函式是範圍安全的,this對象並非Polygon的執行個體,所以會建立並返回一個新的Polygon對象。Rectangle建構函式中的this對象並沒有得到增長,同事Polygon.call()返回的值也沒用用到,所以Rectangel執行個體中就不會有sides屬性。
如果建構函式竊取結合使用原型鏈或者寄生組合則可以解決這個問題。考慮以下例子:
function Polygon(sides) { if (this instanceof Polygon) { this.sides = sides; this.getArea = function () { return 0; } } else { return new Polygon(sides); }}function Rectangle(width, height) { Polygon.call(this, 2); this.width = width; this.height = height; this.getArea = function () { return this.with * this.height; };}Rectangle.prototype = new Polygon();var rect = new Rectangle(5, 10);alert(rect.sides); //2
上面這段重寫的代碼中,一個Rectangle執行個體也同時是一個Polygon執行個體,所以Polygon.call()會照意願執行,最終會為Rectangle執行個體添加了sides屬性。