構造函數其實就是一個使用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); //29 alert(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屬性。