類與繼承在javascript的出現,說明javascript已經達到大規模開發的門檻了,在之前是ECMAScript4,就試圖引入類,模塊等東西,但由於過分引入太多的特性,搞得javascript烏煙瘴氣,導致被否決。不過只是把類延時到ES6.到目前為止,javascript還沒有正真意義上的類。不過我們可以模擬類,曾近一段時間,類工廠是框架的標配,本章會介紹各種類實現,方便大家在自己的框架中或選擇時自己喜歡的那一類風格。
1.javascript對類的支持
在其它語言中 ,類的實例都要通過構造函數new出來。作為一個刻意模仿java的語言。javascript存在new操作符,並且所有函數都可以作為構造器。構造函數與普通的方法沒有什麼區別。浏覽器為了構建它繁花似錦的生態圈,比如Node,Element,HTMLElement,HTMLParagraphElement,顯然使用繼承關系方便一些方法或屬性的共享,於是javascript從其它語言借鑒了原型這種機制。Prototype作為一個特殊的對象屬性存在於每一個函數上。當一個函數通過new操作符new出其“孩子”——“實例”,這個名為實例的對象就擁有這個函數的Prototype對象所有的一切成員,從而實現實現所有實例對象都共享一組方法或屬性。而javascript所謂的“類”就是通過修改這個Prototype對象,以區別原生對象及其其它定義的“類”。在浏覽器中,node這個類基於Object修改而來的,而Element則是基於Node,而HTMLElement又基於Element....相對我們的工作業務,我們可以創建自己的類來實現重用與共享。
function A(){
}
A.prototype = {
aa:"aa",
method:function(){
}
};
var a = new A;
var b = new A;
console.log(a.aa === b.aa);
console.log(a.method === b.method)
一般地,我們把定義在原型上的方法叫原型方法,它為所有的實例所共享,這有好也有不好,為了實現差異化,javascript允許我們直接在構造器內指定其方法,這叫特權方法。如果是屬性,就叫特權屬性。它們每一個實例一個副本,各不影響。因此,我們通常把共享用於操作數據的方法放在原型,把私有的屬性放在特權屬性中。但放於this上,還是讓人任意訪問到,那就放在函數體內的作用域內吧。這時它就成為名副其實的私有屬性。
function A() {
var count = 0;
this.aa = "aa";
this.method = function() {
return count;
}
this.obj = {}
}
A.prototype = {
aa:"aa",
method:function(){
}
};
var a = new A;
var b = new A;
console.log(a.aa === b.aa);//true 由於aa的值為基本類型,比較值
console.log(a.obj === b.obj) //false 引用類型,每次進入函數體都要重新創建,因此都不一樣。
console.log(a.method === b.method); //false
特權方法或屬性只是只是遮住原型的方法或屬性,因此只要刪掉特權方法,就能方法到同名的原型方法或屬性。
delete a.method; delete b.method; console.log(a.method === A.prototype.method);//true console.log(a.method === b.method); //true
用java的語言來說,原型方法與特權方法都屬性實例方法,在java中還有一種叫類方法與類屬性的東西。它們用javascript來模擬也非常簡單,直接定義在函數上就行了。
A.method2 = function(){} //類方法
var c = new A;
console.log(c.method2); //undefined
接下來,我們看下繼承的實現,上面說過,Prototype上有什麼東西,它的實例就有什麼東西,不論這個屬性是後來添加的,還是整個Prototype都置換上去的。如果我們將這個prototype對象置換為另一個類的原型,那麼它就輕而易舉的獲得那個類的所有原型成員。
function A() {};
A.prototype = {
aaa : 1
}
function B() {};
B.prototype = A.prototype;
var b = new B;
console.log(b.aaa); //=> 1;
A.prototype.bb = 2;
console.log(b.bb) //=> 2;
由於是引用著同一個對象,這意味這,我們修改A類的原型,也等同於修該了B類的原型。因此,我們不能把一個對象賦值給兩個類。這有兩種辦法,
方法1:通過for in 把父類的原型成員逐一賦給子類的原型
方法2是:子類的原型不是直接由父類獲得,先將父類的原型賦值給一個函數,然後將這個函數的實例作為子類的原型。
方法一,我們通常要實現mixin這樣的方法,有的書稱之為拷貝繼承,好處就是簡單直接,壞處就是無法通過instanceof驗證。Prototype.js的extend方法就用來干這事。
function extend (des, source) { //des = destination
for (var property in source)
des[property] = source[property];
return des;
}
方法二,就在原型上動腦筋,因此稱之為原型繼承。下面是個范本
function A() {};
A.prototype = {
aa:function(){
alert(1)
}
}
function bridge() {
};
bridge.prototype = A.prototype;
function B() {}
B.prototype = new bridge();
var a = new A;
var b = new B;
console.log(a == b) //false 證明成功分開原型
console.log(A.prototype == B.prototype) //true 子類共享父類的原型方法
console.log(a.aa === b.aa); //為父類動態添加新的方法
A.prototype.bb = function () {
alert(2)
}
//true,繼承父類的方法
B.prototype.cc = function (){
alert(3)
}
//false 父類未必有子類的new實例
console.log(a.cc === b.cc)
//並且它能夠正常通過javascript自帶的驗證機制instanceof
console.log(b instanceof A) ;//true
console.log(b instanceof B) ; //true
方法二能通過instanceof驗證,es5就內置了這種方法來實現原型繼承,它就是Object.create,如果不考慮第二個參數,它約等於下面的代碼。
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
}
上面的方法,要求傳入一個父類的原型作為參數,然後返回子類的原型
不過,我們這樣還是遺漏了一點東西——子類不只是繼承父類的遺產,還應該有自己的東西,此外,原型繼承並沒有讓子類繼承父類的成員與特權成員。這些我們都得手動添加,如類成員,我們可以通過上面的extend方法,特權成員我們可以在子類構造器中,通過apply實現。
function inherit(init, Parent, proto){
function Son(){
Parent.apply(this, argument); //先繼承父類的特權成員
init.apply(this, argument); //在執行自己的構造器
}
}
//由於Object.create是我們偽造的,因此避免使用第二個參數
Son.prototype = Object.create(Parent.prototype,{});
Son.prototype.toString = Parent.prototype.toString; //處理IEbug
Son.prototype.valueOf = Parent.prototype.valueOf; //處理IEbug
Son.prototype.constructor = Son; //確保構造器正常指向,而不是Object
extend(Son, proto) ;//添加子類的特有的原型成員
return Son;
下面,做一組實驗,測試下實例的回溯機制。當我們訪問對象的一個屬性,那麼他先尋找其特權成員,如果有同名就返回,沒有就找原型,再沒有,就找父類的原型...我們嘗試將它的原型臨時修改下,看它的屬性會變成那個。
function A(){
}
A.prototype = {
aa:1
}
var a = new A;
console.log(a.aa) ; //=>1
//將它的所有原型都替換掉
A.prototype = {
aa:2
}
console.log(a.aa); //=>1
//於是我們想到每個實例都有一個constructor方法,指向其構造器
//而構造器上面正好有我們的原型,javascript引擎是不是通過該路線回溯屬性呢
function B(){
}
B.prototype = {
aa:3
}
a.constructor = B;
console.log(a.aa) //1 表示不受影響
因此類的實例肯定通過另一條通道進行回溯,翻看ecma規范可知每一個對象都有一個內部屬性[[prototype]],它保存這我們new它時的構造器所引用的Prototype對象。在標准浏覽器與IE11裡,它暴露了一個叫__proto__屬性來訪問它。因此,只要不動__proto__上面的代碼怎麼動,a.aa始終堅定不毅的返回1.
再看一下,new時操作發生了什麼。
1.創建了一個空對象 instance
2.instance.__proto__ = intanceClass.prototype
3.將構造函數裡面的this = instance
4.執行構造函數裡的代碼
5.判定有沒有返回值,沒有返回值就返回默認值為undefined,如果返回值為復合數據類型,則直接返回,否則返回this
於是有了下面的結果。
function A(){
console.log(this.__proto__.aa); //1
this.aa = 2
}
A.prototype = {aa:1}
var a = new A;
console.log(a.aa)
a.__proto__ = {
aa:3
}
console.log(a.aa) //=>2
delete a. aa; //刪除特權屬性,暴露原型鏈上的同名屬性
console.log(a.aa) //=>3
有了__proto__,我們可以將原型設計繼承設計得更簡單,我們還是拿上面的例子改一改,進行試驗
function A() {}
A.prototype = {
aa:1
}
function bridge() {}
bridge.prototype = A.prototype;
function B(){}
B.prototype = new bridge();
B.prototype.constructor = B;
var b = new B;
B.prototype.cc = function(){
alert(3)
}
//String.prototype === new String().__proto__ => true
console.log(B.prototype.__proto__ === A.prototype) //true
console.log(b.__proto__ == B.prototype); //true
console.log(b.__proto__.__proto__ === A.prototype); //true 得到父類的原型對象
因為b.__proto__.constructor為B,而B的原型是從bridge中得來的,而bride.prototype = A.prototype,反過來,我們在定義時,B.prototype.__proto__ = A.prototype,就能輕松實現兩個類的繼承.
__proto__屬性已經加入es6,因此可以通過防止大膽的使用
2.各種類工廠的實現。
上節我們演示了各種繼承方式的實現,但都很凌亂。我們希望提供一個專門的方法,只要用戶傳入相應的參數,或按照一定簡單格式就能創建一個類。特別是子類。
由於主流框架的類工廠太依賴他們龐雜的工具函數,而一個精巧的類工廠也不過百行左右
相當精巧的庫,P.js
https://github.com/jiayi2/pjs
使用版:https://github.com/jiayi2/factoryjs
這是一個相當精巧的庫,尤其調用父類的同名方法時,它直接將父類的原型拋在你面前,連_super也省了。
var P = (function(prototype, ownProperty, undefined) {
return function P(_superclass /* = Object */, definition) {
// handle the case where no superclass is given
if (definition === undefined) {
definition = _superclass;
_superclass = Object;
}
// C is the class to be returned.
//
// When called, creates and initializes an instance of C, unless
// `this` is already an instance of C, then just initializes `this`;
// either way, returns the instance of C that was initialized.
//
// TODO: the Chrome inspector shows all created objects as `C`
// rather than `Object`. Setting the .name property seems to
// have no effect. Is there a way to override this behavior?
function C() {
var self = this instanceof C ? this : new Bare;
self.init.apply(self, arguments);
return self;
}
// C.Bare is a class with a noop constructor. Its prototype will be
// the same as C, so that instances of C.Bare are instances of C.
// `new MyClass.Bare` then creates new instances of C without
// calling .init().
function Bare() {}
C.Bare = Bare;
// Extend the prototype chain: first use Bare to create an
// uninitialized instance of the superclass, then set up Bare
// to create instances of this class.
var _super = Bare[prototype] = _superclass[prototype];
var proto = Bare[prototype] = C[prototype] = C.p = new Bare;
// pre-declaring the iteration variable for the loop below to save
// a `var` keyword after minification
var key;
// set the constructor property on the prototype, for convenience
proto.constructor = C;
C.extend = function(def) { return P(C, def); }
return (C.open = function(def) {
if (typeof def === 'function') {
// call the defining function with all the arguments you need
// extensions captures the return value.
def = def.call(C, proto, _super, C, _superclass);
}
// ...and extend it
if (typeof def === 'object') {
for (key in def) {
if (ownProperty.call(def, key)) {
proto[key] = def[key];
}
}
}
// if no init, assume we're inheriting from a non-Pjs class, so
// default to using the superclass constructor.
if (!('init' in proto)) proto.init = _superclass;
return C;
})(definition);
}
// as a minifier optimization, we've closured in a few helper functions
// and the string 'prototype' (C[p] is much shorter than C.prototype)
})('prototype', ({}).hasOwnProperty);
我們嘗試創建一個類:
var Dog = P (function(proto, superProto){
proto.init = function(name) { //構造函數
this.name = name;
}
proto.move = function(meters){ //原型方法
console.log(this.name + " moved " + meters + " m.")
}
});
var a = new Dog("aaa")
var b = new Dog("bbb"); //無實例變化
a.move(1);
b.move(2);
我們在現在的情況下,可以嘗試創建更簡潔的定義方式
var Animal = P (function(proto, superProto){
proto.init = function(name) { //構造函數
this.name = name;
}
proto.move = function(meters){ //原型方法
console.log(this.name + " moved " + meters + " m.")
}
});
var a = new Animal("aaa")
var b = new Animal("bbb"); //無實例變化
a.move(1);
b.move(2);
//...............
var Snake = P (Animal, function(snake, animal){
snake.init = function(name, eyes){
animal.init.call(this, arguments); //調運父類構造器
this.eyes = 2;
}
snake.move = function() {
console.log('slithering...');
animal.move.call(this, 5); //調運父類同名方法
}
});
var s = new Snake("snake", 1);
s.move();
console.log(s.name);
console.log(s.eyes);
私有屬性演示,由於放在函數體內集中定義,因此安全可靠!
var Cobra = P (Snake, function(cobra){
var age = 1;//私有屬性
//這裡還可以編寫私有方法
cobra.glow = function(){ //長大
return age++;
}
});
var c = new Cobra("cobra");
console.log(c.glow()); //1
console.log(c.glow()); //2
console.log(c.glow()); //3
以上所述就是本文的全部內容了,希望大家能夠喜歡。