JavaScript與大部分客戶端語言有幾點明顯的不同:
JS是 動態解釋性語言,沒有編譯過程,它在程序運行過程中被逐行解釋執行
JS是 弱類型語言,它的變量沒有嚴格類型限制
JS是面向對象語言,但 沒有明確的類的概念(雖然有class關鍵字,然而目前並沒有什么卵用)
JS雖然沒有類,但可以通過一些方法來模擬類以及實現類的繼承。
一切皆對象,還先從對象說起。
1、對象(Object)
ECMA-262對對象的定義是:無序屬性的集合,其屬性可以包含基本值、對象或者函數。
直觀點描述,就是由多個鍵值對組成的散列表。
JS創建對象的方法和其它語言大同小異:
// 通過構造函數創建 var zhangsan = new Object(); zhangsan.name = "張三"; zhangsan.age = 20; zhangsan.sayHi = function() { alert("Hi, I'm " + this.name); }; // 通過對象字面量創建 var lisi = { name: "李四", age: 21, sayHi: function() { alert("Hi, I'm " + this.name); } };
當需要大量創建相同結構的對象時,可以使用 對象工廠(Object Factory):
// 對象工廠 function createPerson(name, age) { return { name: name, age: age, sayHi: function() { alert("Hi, I'm " + this.name); } }; } var zhangsan = createPerson("張三", 20); var lisi = createPerson("李四", 21);
但通過這種方式創建出來的實例,不能解決類型識別問題,只知道它是一個對象,但具體什么?無法判斷:
zhangsan instanceof ? lisi.constructor = ?
這時,“類”就登場了。
2、類(Class)
2.1、構造函數模式
事實上,JS中每個函數(function)本身就是一個構造函數(constructor),就是一個類:
// 構造函數模式 function Person(name, age) { this.name = name; this.age = age; this.sayHi = function() { alert("Hi, I'm " + this.name); }; } var zhangsan = new Person("張三", 20); var lisi = new Person("李四", 21); alert(zhangsan instanceof Person); // true alert(lisi.constructor === Person); // true
這里面其實有個問題:
alert(zhangsan.sayHi === lisi.sayHi); // false
多個實例中的同名方法並不相等,也就是說存在多個副本。而這些行為是相同的,應該指向同一個引用才對。
為了解決這個問題,JS為每個函數分配了一個 prototype(原型)屬性,該屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。
2.2、原型模式
原型(Prototype):指向一個對象,作為所有實例的基引用(base reference)。
// 構造函數+原型組合模式 function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayHi = function() { alert("Hi, I'm " + this.name); }; var zhangsan = new Person("張三", 20); var lisi = new Person("李四", 21); alert(zhangsan.sayHi === lisi.sayHi); // true
在Person中,sayHi 是 原型成員(prototype),name 和 age 是 特權成員(privileged),它們都是 公共成員(public)。
注:“特權”是道格拉斯提出的名詞。道格拉斯·克羅克福德(Douglas Crockford),Web界人稱道爺,JSON創立者,《JavaScript語言精粹》作者,JSLint、JSMin、ADsafe開發者。
類的原型帶有一個 constructor 屬性,指向該類的構造函數(如果重新分配了原型指針,需要手動添加 constructor 屬性);類的實例上會自動生成一個屬性指向該類原型(在Chrome上可以通過“__proto__”訪問到該對象,而IE上該屬性則是不可見的)。
Person、Person的原型、Person的實例間的關系如下:
需要注意的是,原型成員保存引用類型值時需謹慎:
Person.prototype.friends = []; zhangsan.friends.push("王五"); alert(lisi.friends); // ["王五"]
張三的基友莫名其妙就變成李四的基友了,所以 friends 應該添加為特權成員,而不是原型成員。
2.3、類的結構
綜上所述,JS中的類的結構大致如下:
- 類由構造函數和原型組成
- 構造函數中可以聲明私有成員和添加特權成員
- 原型中可以添加原型成員
- 私有成員可以被特權成員訪問而對原型成員不可見
- 特權成員和原型成員都是公共成員
3、繼承(Inherit)
在JS中繼承是如何實現的呢?
3.1、拷貝繼承
最簡單直接的方式莫過於 屬性拷貝:
// 拷貝繼承 function extend(destination, source) { for (var property in source) { destination[property] = source[property]; } } extend(SubClass.prototype, SuperClass.prototype);
這種方式雖然實現了原型屬性的繼承,但有一個非常明顯的缺陷:子類實例無法通過父類的 instanceof 驗證,換句話說,子類的實例不是父類的實例。
3.2、原型繼承
在 Chrome 的控制台中查看 HTMLElement 的原型,大致如下:
可以清晰看到,HTMLElement 的原型是 Element 的實例,而 Element 的原型又是 Node 的實例,從而形成了一條 原型鏈(Prototype-chain),JS的原生對象就是通過原型鏈來實現繼承。
這里順道說下解釋器對實例屬性的查找過程:
- 在特權屬性中查找
- 特權屬性中沒找到,再到原型屬性中查找
- 原型屬性中沒找到,再到原型的原型屬性中查找
- 直到根原型還沒找到,返回 undefined
這就說明為什么我們自定義的類明明沒有聲明 toString() 方法,但仍然可以訪問到,因為所有對象都繼承自 Object。
因此,我們也可以通過原型鏈來實現繼承:
// 原型鏈繼承 function User(name, age, password) { // 繼承特權成員 Person.call(this, name, age); this.password = password; } // 繼承原型 User.prototype = new Person(); // 修改了原型指針,需重新設置 constructor 屬性 User.prototype.constructor = User; var zhangsan = new User("張三", 20, "123456"); zhangsan.sayHi(); // Hi, I'm 張三
運行正常,貌似沒什么問題,但其實里面還是有些坑:
父類的構造函數被執行了 2 次:繼承特權成員時 1 次,繼承原型時又 1 次。
父類初始化兩次,這有時會導致一些問題,舉個例子,父類構造函數中有個alert,那么創建子類實例時,會發現有兩次彈框。
不僅如此,還導致了下面的問題。從控制台中查看子類的實例,結構如下:
可以看到子類的原型中也包含了父類的特權成員(直接創建了一個父類實例,當然會有特權成員),只不過因為解釋器的屬性查找機制,被子類的特權成員所覆蓋,只要子類的特權成員被刪除,原型中相應的成員就會暴露出來:
delete zhangsan.name; alert(zhangsan.name); // 此時訪問到的就是原型中的name
那怎么辦呢?對此道爺提供了一個很實用的解決方案—— 原型式寄生組合繼承。
3.3、原型式寄生組合繼承
我們的目的是子類原型只繼承父類的原型,而不要特權成員,原理其實很簡單:創建一個臨時的類,讓其原型指向父類原型,然后將子類原型指向該臨時類的實例即可。實現如下:
function inheritPrototype(subClass, superClass) { function Temp() {} Temp.prototype = superClass.prototype; subClass.prototype = new Temp(); subClass.prototype.constructor = subClass; } inheritPrototype(User, Person);
因為臨時類的構造函數是空實現,子類在繼承原型時自然不會執行到父類的初始化操作,也不會繼承到一堆亂七八糟的特權成員。
再看下 zhangsan 的結構:
此時,子類實例的原型鏈大致如下:
總結
修改后的代碼整理如下:
// 用於子類繼承父類原型的工具函數 function inheritPrototype(subClass, superClass) { function Temp() {} Temp.prototype = superClass.prototype; subClass.prototype = new Temp(); subClass.prototype.constructor = subClass; } // 父類 function Person(name, age) { // 特權成員(每個實例都有一份副本) this.name = name; this.age = age; } // 原型成員(所有實例共享) Person.prototype.sayHi = function() { alert("Hi, I'm " + this.name); }; // 子類 function User(name, age, password) { // 繼承父類特權成員(在子類中執行父類的初始化操作) Person.call(this, name, age); // 添加新的特權成員 this.password = password; } // 繼承父類原型 inheritPrototype(User, Person); // 重寫父類原型方法 User.prototype.sayHi = function() { alert("Hi, my name is " + this.name); }; // 擴展子類原型 User.prototype.getPassword = function() { return this.password; };
到此為止,我們已經比較完美地實現了類和類的繼承。
但每個類、每個子類、每個子類的子類,都要這么分幾步寫,也是很蛋疼的。對象有對象工廠,類當然也可以搞個 類工廠(Class Factory),江湖上已有不少現成的類工廠,讓我們可以從統一規范的入口來生成自定義類。(見:JavaScript幾種類工廠實現原理剖析)