js面向對象編程
js通過構造函數結合原型鏈的方式來定義類和實現繼承。
雖然es6新增了類的概念,使得js面向對象編程的語法更加簡潔和清晰。但es6中的類只是一個語法糖,並沒有改變js中「類是特殊函數」和其「原型繼承」的本質。學習es5中定義類和實現繼承的方式,有助於理解這個本質。
本文介紹了es5中類定義和繼承的方式,並簡單對比了es6中的類語法。
- 對象創建(定義類)
- 繼承
- ES6面向對象語法
對象創建(定義類)
組合(構造函數與原型)模式 是目前認同度最高的創建對象的方式。先看代碼:
// 構造函數
function Person(name) {
this.name = name;
}
// 原型
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
}
}
// 實例化
var person1 = new Person(Jack);
組合模式包含構造函數模式和原型模式兩部分。
構造函數
構造函數與普通函數並無區別,只是使用new關鍵字調用。
使用new調用構造函數時,會經歷4個步驟:
- 創建一個對象
- 將構造函數的作用域賦給新建的對象(因此this指向了這個新對象)
- 執行構造函數中的代碼
- 返回新對象
js中的函數一般包含 [[Call]]和[[Construct]] 兩個內部方法。當通過函數調用符調用時(直接調用),運行[[Call]]中的代碼;通過new運算符調用時,調用[[Contruct]]創建一個對象。(實現[[Construct]]方法的對象稱作構造器。)
原型
prototype與[[prototype]]
每個函數都有一個prototype屬性(事實上只有其作為構造函數使用時才有用),用來定義它的實例對象的原型;
每個對象都有一個內部屬性[[prototype]], 指向其原型對象。
當使用new調用函數,創建實例化對象時,函數的prototype對象的引用被複制到實例的[[prototype]]中。
使用「[[]]」包裹的屬性為內部屬性,由底層引擎實現,本不暴露到js層面。但chrome中提供了__proto__屬性來訪問[[prototype]]的值
constructor
函數的原型對象prototype默認帶有兩個屬性: constuctor和__proto__。 constructor指向函數本身,__proto__指向Object
當使用對象字面量定義的對象,只有默認有一個指向Object的__proto__屬性。
因此,在使用字面量方式重寫Person.prototype時,constructor被丟掉了,需要手動添加。
事實上,prototype上默認的constructor屬性是不可枚舉的,準確的寫法應該是:
Person.prototype = {
sayName: function() {
console.log(this.name);
}
}
Object.defineProperty(Person.prototype, constructor, {
enumberable: false,
value: Person
})
其他模式及特點 * (有興趣看) *
工廠模式
「工廠模式」顧名思義,就是把函數當做一個(生產對象的)工廠,把對象當成產品。對象的創建和初始化操作就是一個標準生產線。
// 工廠函數
function personFactory(name, age) {
var person = new Object();
person.name = name;
person.age = age;
person.sayName = function() {
console.log(this.name);
}
return person;
}
// 創建對象
var person1 = personFactory(Jack, 21);
缺點:
- 無法通過instanceof判斷實例類別。(對象識別問題)
- 每次創建對象都要重新定義屬性和方法。(無法共享屬性和方法)
構造函數模式
構造函數模式可以解決對象識別問題。
構造函數通過new關鍵字調用。函數內不需手動創建 / 返回對象。通過this給實例添加屬性 / 方法。
// 構造函數
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log(this.name);
}
}
// 創建對象
var person1 = new Person(Jack, 20);
// 判斷實例的類型
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
構造函數可以使用instanceof校驗實例的類型。原理如下:
拿instanceof左側的對象的原型鏈上的節點 依次 與右側函數的prototype屬性做比較。如果發現有兩者相同(指向同一個對象),則停止比較,返回true。遍歷到原型鏈頂部時,仍未找到相同情況,則返回false。參考:instanceof運算符,[[hasInstance]]
缺點:
- 每次創建對象都要重新定義屬性和方法。(不能共享屬性和方法)
原型模式
通過原型模式可以定義共享的屬性和方法。
function Person() {}
// 定義共享屬性|方法
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person();
person1.name = Jack;
var person2 = new Person();
person2.name = Alice;
person1.sayName(); // Jack
person2.sayName(); // Alice
console.log(person1.sayName === person2.sayName); // true
例子中將公用方法sayName定義在Person的prototype。由於所有Person的實例化對象都指向這同一個原型,所以sayName方法被所有實例共享。
然而定義私有屬性name時,就顯得麻煩了。
上面講到的「組合構造函數和原型模式」就是結合構造函數與原型模式的優勢。在構造函數內方便地定義私有屬性,而通過原型模式定義公有屬性和方法。
動態原型模式
獨立地定義構造函數和原型的方式,會讓一些人(尤其是有其他OO語言開發經驗的人)感到困惑。動態原型模式便是為了解決這個問題,它把所有的信息都封裝在構造函數中。即在構造函數中動態地定義原型。
function Person(name, age) {
this.name = name;
this.age = age;
// 動態原型
if(typeof this.sayName != "function") { // 沒找到sayName方法,說明原型還沒有定義
Person.prototype.sayName = function() {
console.log(this.name);
}
}
}
var person1 = new Person(Jack, 19);
person1.sayName(); // Jack
寄生構造函數模式
寄生構造函數模式通常用於改造內建對象。假設我們想創建一個具有額外方法的特殊數組,又不能直接修改Array構造函數。就可以使用這個模式:
function SpecialArray() {
var values = new Array();
values.push.apply(values, arguments);
values.toPipedString = function() {
return this.join("|");
}
return values;
}
var colors = new SpecialArray(red, blue, green);
console.log(colors.toPipedString()); // "red|blue|green"
可以看出SpecialArray函數與工廠函數沒有區別,不過它使用new操作符調用,故SpecialArray實際上是一個構造函數。
構造函數在不返回值的情況下,會默認返回新的對象實例。而如果在構造函數末尾添加一個return語句,可以重寫調用構造函數時的返回值。
需要強調的是,SpecialArray的使用方式與原生的Array並無區別。這也是寄生構造函數模式追求的目標。