javascript oo實現
很久很久以前,我還是個phper,第一次接觸javascript覺得好神奇。跟傳統的oo類概念差別很大。記得剛畢業面試,如何在javascript裏面實現class一直是很熱門的面試題,當前面試百度就被問到了,當年作為一個小白只是網上隨便搜搜應付了下。= =現在發現當時知道的還是太少太少。今天整理了下javascript的oo實現,發現知道的越多,越發現知識真是無窮無盡。
原始時代最簡單的oo實現
javascript雖然沒有class的概念,但是它的函數卻是可以new出來一個對象的。所以一個最簡單的class就可以用function來模擬出來。
function Animal(name){ this.name = name; this.run = function(){ console.log(this.name + "is running!!"); } } var pet = new Animal("pet"); pet.run();//petis running!!
這樣 pet就有了屬性,有了方法,不過這種寫法毫無繼承性,擴展性。比如我們要實現個dog類,只能把屬性方法再寫一遍。而且每個new出來的對象都有自己的方法,造成資源浪費。
在javascript裏面有個原型鏈的概念,每一個函數都有一個prototype對象屬性。這樣通過這個函數new出來的對象會自動具有__proto__屬性指向函數的prototype對象。說白了所有的實例對象都會共用一個prototype對象,並且調用一個屬性或者方法時在自己上面找不到,就會找__proto__對象有沒有,之後一直往上追溯一直到找到為止。具體表現為:
function Animal(name){ this.name = name; } Animal.prototype.run = function(){ console.log(this.name + "is running!!"); } var a = new Animal("a"); var b = new Animal("b"); console.log(Animal.prototype) //Animal {} console.log(Animal.prototype instanceof Object) //true prototype是個對象 console.log(Animal.prototype.constructor == Animal)//true console.log(a.__proto__ == Animal.prototype) //true __proto__在new的時候會自動加載在實例對象上。在現代瀏覽器裏可以看到 console.log(b.__proto__ == Animal.prototype) //true console.log(a.__proto__.__proto__) //Object {} 最後會找到最上面的boject對象 console.log(a.__proto__.run == a.run) //true console.log(a.__proto__.run == Animal.prototype.run) //true
所以,在prototype對象上定義的方法會被所有實例共享,這不就是復用嗎?
於是有了基於原型鏈的繼承的寫法:
function Animal(name){ this.name = name; } Animal.prototype.run = function(){ console.log(this.name + "is running!!"); } function Dog(name){ //調用父類的構造函數,通過改變this指向將屬性賦值到新的實例對象 Animal.call(this,name); } Dog.prototype = new Animal(); var dog = new Dog("dog"); dog.run();//dog is running!!
可以看到我們將Animal的實例對象暫且叫做a,作為 Dog的prototype,這樣 Dog的實例對象dog的__proto__指向Dog的prototype也就是a,a的__proto__再指向Animal的prototype對象,這個對象上有run方法。於是我們調用dog.run()的時候會一層層的往上追溯一直找到run方法執行。於是通過原型鏈我們就讓 Dog繼承了Animal的方法run。
需要註意的是,如果在子類的prototype對象上也有run方法,就會覆蓋父類的,因為查找時在自己上面就找到了,就不會向上回溯了。
上面是原型鏈方法的繼承。而屬性我們則是通過調用父類的構造函數來賦值的。因為屬性不能所有的實例都公用,應該每個人都有自己的一份,所以不能放在原型上。
上面就是原始時代最簡單的類繼承了。
石器時代的oo實現
這個時代javascript變得比較重要了,作為非常有用的特性,oo開始被很多人研究。
首先上面的那種簡單oo實現方式,其實是有很多問題的。
1.沒有實現傳統oo該有的super方法來調用父類方法。
作為oo,怎麽能沒有super呢。作為我們前端界宗師一般的人物。Douglas 有一篇經典文章。不過貌似有很多問題。國內的玉伯分析過。在這裏
最後Douglas總結出來:
我編寫 JavaScript 已經 8 個年頭了,從來沒有一次覺得需要使用 uber 方法。在類模式中,super 的概念相當重要;但是在原型和函數式模式中,super 的概念看起來是不必要的。現在回顧起來,我早期在 JavaScript 中支持類模型的嘗試是一個錯誤。
2.直接將父類實例作為子類的原型,簡單粗暴造成多余的原型屬性。還有construct的問題。
這個問題主要是之前代碼裏面這一句造成的:
Dog.prototype = new Animal();
//var dog = new Dog("dog");
//console.log(dog.__proto__) Animal {name: undefined}
執行new Animal()就會執行animal的構造函數,就會在Dog.prototype生成多余的屬性值,這邊是name。而一般屬性值為了復用是不能放在原型對象上的。並且由於dog有自己的name屬性,原型上的是多余的。
還有construct的問題。
console.log(dog.constructor == Animal) //true
console.log(dog.constructor == Dog) //false
顯然這不是我們希望看到的。
所以我們要對上面做些改良:
var F = function(){};
F.prototype = Animal.prototype;
Dog.prototype = new F();
Dog.prototype.constructor = Dog;
我們可以封裝下:
function objCreate(prototype){
var F = function(){};
F.prototype = prototype;
return new F();
}
function inherit(subclass,parentclass){
subclass.prototype = objCreate(parentclass.prototype);
subclass.prototype.constructor = subclass;
}
於是繼承可以寫成:
function Animal(name){
this.name = name;
}
Animal.prototype.run = function(){
console.log(this.name + "is running!!");
}
function Dog(name){
//調用父類的構造函數,通過改變this指向將屬性賦值到新的實例對象
Animal.call(this,name);
}
inherit(Dog,Animal);
var dog = new Dog("dog");
dog.run();//dog is running!!
當年大學畢業面試,也就到這個程度了。 = =
工業時代的oo實現
這個時代,各種javascript類庫像雨後春筍般湧現了出來。
上面最後給出的方案,使用起來還是很不便,比如需要自己手動維護在構造函數裏調用父類構造函數。同時繼承寫法對不了接原理的比較容易出錯。
這個時候湧現了一大堆的類庫的實現:
1.首先有些類庫決定跳出傳統oo的思維。不一定非要實現傳統oo的繼承。歸根到底我們是為了復用。於是出現了很多輕量級的復用方式。
比如jquery的extend:http://api.jquery.com/jQuery.extend/
還有kissy的mix:http://docs.kissyui.com/1.3/docs/html/api/seed/kissy/mix.html?highlight=mix#seed.KISSY.mix
還有kissy的argument:http://docs.kissyui.com/1.3/docs/html/api/seed/kissy/augment.html
還有很多很多,說白了都是對象級別上的混入達到復用的地步。大部分情況下已經足夠了。
2.當然還是有人對類的繼承有需求的。
下面我們看下kissy的extend的實現方式。其他類庫實現方式類似,kissy的我覺得算是比較有代表性了。為了演示,做了些小修改。
//這個就是我們之前實現的方法,為了演示做了些改動主要是處理了construct的問題
function objCreate(prototype,construct){
var F = function(){};
F.prototype = prototype;
var newPro = new F();
newPro.construct = construct;//維護構造函數的改變
return newPro;
}
//mix是個輔助方法,這邊給個最簡單的實現,其實kissy裏面的復雜的多。這邊不考慮深度遍歷等等,只是最簡單的實現。
function mix(r, s) {
for (var p in s) {
if (s.hasOwnProperty(p)) {
r[p] = s[p]
}
}
}
//下面是kissy的實現r代表子類 s代表父類,px代表最後會混入子類原型上的屬性,sx代表會混入子類函數上面的屬性,也就是可以當做靜態方法。
//http://docs.kissyui.com/1.3/docs/html/api/seed/kissy/extend.html?highlight=extend#seed.KISSY.extend
function extend (r, s, px, sx) {
if (!s || !r) {
return r;
}
var sp = s.prototype,
rp;
//針對父類生成一個原型。跟之前我們寫的一致
rp = createObject(sp, r);
//不是簡單的直接復制原型對象,而是先把以前原型的方法跟要繼承的合並之後再一起賦值
r.prototype = S.mix(rp, r.prototype);
//為子類增加superclass屬性,指向一個父類對象,這樣就可以調用父類的方法了。這邊是實現比較巧妙的地方
r.superclass = createObject(sp, s);
//下面就是往原型還有函數上混入方法了
// add prototype overrides
if (px) {
S.mix(rp, px);
}
// add object overrides
if (sx) {
S.mix(r, sx);
}
return r;
}
有了kissy的extend我們可以這麽用:
function Animal(name){
this.name = name;
}
Animal.prototype.run = function(){
console.log(this.name + "is running!!");
}
function Dog(name){
//Animal.call(this,name);
//因為kissy的封裝 這邊可以這麽用
Dog.superclass.construct.call(this,name);
}
extend(Dog,Animal,{
wang:function(){
console.log("wang wang!!")
}
})
var dog = new Dog("dog");
dog.run();//dog is running!!
dog.wang();//wang wang!!
相對之前的變得清晰了很多,也更易用了。
現代科技時代的oo實現
前面的寫法,目前雖然還是有很多人用,不過也漸漸過時了。上面的寫法還是不夠清晰,定義屬性,方法都很分散,也沒有多繼承,等特性。我們需要像傳統oo一樣具有一個類工廠,可以生成一個類,屬性都定義在裏面。同時具有繼承的方法。
而隨著javascript成為前端唯一的語言,一代代大神前仆後繼。終於開始湧現出了各種神奇的寫法,下面羅列下一些我覺得特別好的實現,加上原理註釋。
John Resig的實現方式
作為jquery的作者。John Resig在博客裏記錄了一種class的實現,原文在此
調用方法:
var Person = Class.extend({
init: function(isDancing){
this.dancing = isDancing;
},
dance: function(){
return this.dancing;
}
});
var Ninja = Person.extend({
init: function(){
this._super( false );
},
dance: function(){
// Call the inherited version of dance()
return this._super();
},
swingSword: function(){
return true;
}
});
var p = new Person(true);
p.dance(); // => true
var n = new Ninja();
n.dance(); // => false
n.swingSword(); // => true
// Should all be true
p instanceof Person && p instanceof Class &&
n instanceof Ninja && n instanceof Person && n instanceof Class
源碼解讀:
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*/
// Inspired by base2 and Prototype
(function(){
//initializing是為了解決我們之前說的繼承導致原型有多余參數的問題。當我們直接將父類的實例賦值給子類原型時。是會調用一次父類的構造函數的。所以這邊會把真正的構造流程放到init函數裏面,通過initializing來表示當前是不是處於構造原型階段,為true的話就不會調用init。
//fnTest用來匹配代碼裏面有沒有使用super關鍵字。對於一些瀏覽器`function(){xyz;}`會生成個字符串,並且會把裏面的代碼弄出來,有的瀏覽器就不會。`/xyz/.test(function(){xyz;})`為true代表瀏覽器支持看到函數的內部代碼,所以用`/\b_super\b/`來匹配。如果不行,就不管三七二十一。所有的函數都算有super關鍵字,於是就是個必定匹配的正則。
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
// 超級父類
this.Class = function(){};
// Create a new Class that inherits from this class
// 生成一個類,這個類會具有extend方法用於繼續繼承下去
Class.extend = function(prop) {
//保留當前類,一般是父類的原型
//this指向父類。初次時指向Class超級父類
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
//開關 用來使原型賦值時不調用真正的構成流程
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
//這邊其實就是很簡單的將prop的屬性混入到子類的原型上。如果是函數我們就要做一些特殊處理
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
//通過閉包,返回一個新的操作函數.在外面包一層,這樣我們可以做些額外的處理
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
// 調用一個函數時,會給this註入一個_super方法用來調用父類的同名方法
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
//因為上面的賦值,是的這邊的fn裏面可以通過_super調用到父類同名方法
var ret = fn.apply(this, arguments);
//離開時 保存現場環境,恢復值。
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
// 這邊是返回的類,其實就是我們返回的子類
function Class() {
// All construction is actually done in the init method
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
// 賦值原型鏈,完成繼承
Class.prototype = prototype;
// 改變constructor引用
Class.prototype.constructor = Class;
// 為子類也添加extend方法
Class.extend = arguments.callee;
return Class;
};
})();
相當簡單高效的實現方式,super的實現方式非常亮
P.js的實現
源地址:https://github.com/jneen/pjs
pjs的一大亮點是支持私有屬性,他的類工廠傳遞的是函數不是對象。
調用方式:
//可以生成一個可繼承的對象,P接收一個函數,這個函數會傳入生成後的class的原型。
var Animal = P(function(animal) {
animal.init = function(name) { this.name = name; };
animal.move = function(meters) {
console.log(this.name+" moved "+meters+"m.");
}
});
//繼承Animal。後面的snake,animal分別是前面Snake和Animal的原型。程序直接把這些對象暴露給你了。於是靈活度很高。
var Snake = P(Animal, function(snake, animal) {
snake.move = function() {
console.log("Slithering...");
animal.move.call(this, 5);
};
});
var Horse = P(Animal, function(horse, animal) {
//真正的私有屬性,外面沒法調用到
var test = "hello world";
horse.move = function() {
console.log(test);
console.log("Galloping...");
//調用父類的方法,so easy!!
animal.move.call(this, 45);
};
});
//工廠方式生成對象,可以不用new
var sam = Snake("Sammy the Python")
, tom = Horse("Tommy the Palomino")
;
sam.move()
tom.move()
源碼解讀:
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;
}
//最後返回的類就是這個,也就是我們需要的子類。這個類可以用new生成實例,也可以直接調用生成實例
function C() {
//判斷,是new的話this instanceof C就是true。否則我們自己手動new一下Bare。Bare就是為了實現這種類工廠的生成類的方式
var self = this instanceof C ? this : new Bare;
self.init.apply(self, arguments);
return self;
}
//這個就是用來實現不用new生成類的方式
function Bare() {}
C.Bare = Bare;
//將父類的原型賦值給Bare
//這邊prototype就是個字符串“prototype”變量,主要為了壓縮字節少點,所以作者還單獨傳成變量進來 = =
var _super = Bare[prototype] = _superclass[prototype];
//再生成這個空函數的實例賦值給C,Bare的原型,同時在C.p存下來
//這樣C,Bare都公用一個原型
var proto = Bare[prototype] = C[prototype] = C.p = new Bare;
var key;
//改變constructor指向
proto.constructor = C;
//上面幾部其實還是實現的通用的繼承實現方式,新建個空函數,將父類的原型賦給這個空函數再生成實例賦值給子類的原型。萬變不離其宗。原理都一樣
//增加extend方法。這是個語法糖,本質上還是調用P來實現,只不過第一個參數是調用者C
C.extend = function(def) { return P(C, def); }
//下面是最關鍵的地方,寫的有點繞。這邊分為這幾步
//傳入definition 執行 function(def){}
// 執行C.open = C
// return C.open 其實就是 renturn C 返回最終的生成類
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);
}
// 如果是對象,就直接混入到原型
if (typeof def === 'object') {
for (key in def) {
if (ownProperty.call(def, key)) {
proto[key] = def[key];
}
}
}
//確保有init函數
if (!('init' in proto)) proto.init = _superclass;
return C;
})(definition);
}
})('prototype', ({}).hasOwnProperty);
阿拉蕾的實現方式
這是支付寶的庫阿拉蕾的實現,我覺得是最不錯的一種方式:
源地址:https://github.com/aralejs/class/blob/master/class.js
// The base Class implementation.
function Class(o) {
//這個判斷用來支持 將一個已有普通類轉換成 阿拉蕾的類
if (!(this instanceof Class) && isFunction(o)) {
//原理是給這個函數增加extend,implement方法
return classify(o)
}
}
//用來支持 commonjs的模塊規範。
module.exports = Class
// Create a new Class.
//
// var SuperPig = Class.create({
// Extends: Animal,
// Implements: Flyable,
// initialize: function() {
// SuperPig.superclass.initialize.apply(this, arguments)
// },
// Statics: {
// COLOR: 'red'
// }
// })
//
//
//用於創建一個類,
//第一個參數可選,可以直接創建時就指定繼承的父類。
//第二個參數也可選,用來表明需要混入的類屬性。有三個特殊的屬性為Extends,Implements,Statics.分別代表要繼承的父類,需要混入原型的東西,還有靜態屬性。
Class.create = function(parent, properties) {
//創建一個類時可以不指定要繼承的父類。直接傳入屬性對象。
if (!isFunction(parent)) {
properties = parent
parent = null
}
properties || (properties = {})
//沒有指定父類的話 就查看有沒有Extends特殊屬性,都沒有的話就用Class作為父類
parent || (parent = properties.Extends || Class)
properties.Extends = parent
// 子類構造函數的定義
function SubClass() {
// 自動幫忙調用父類的構造函數
parent.apply(this, arguments)
// Only call initialize in self constructor.
//真正的構造函數放在initialize裏面
if (this.constructor === SubClass && this.initialize) {
this.initialize.apply(this, arguments)
}
}
// Inherit class (static) properties from parent.
//parent為Class就沒必要混入
if (parent !== Class) {
//將父類裏面的屬性都混入到子類裏面這邊主要是靜態屬性
mix(SubClass, parent, parent.StaticsWhiteList)
}
// Add instance properties to the subclass.
//調用implement將自定義的屬性混入到子類原型裏面。遇到特殊值會單獨處理,真正的繼承也是發生在這裏面
//這邊把屬性也都弄到了原型上,因為這邊每次create或者extend都會生成一個新的SubClass。所以倒也不會發生屬性公用的問題。但是總感覺不大好
implement.call(SubClass, properties)
// Make subclass extendable.
//給生成的子類增加extend和implement方法,可以在類定義完後,再去繼承,去混入其他屬性。
return classify(SubClass)
}
//用於在類定義之後,往類裏面添加方法。提供了之後修改類的可能。類似上面defjs實現的open函數。
function implement(properties) {
var key, value
for (key in properties) {
value = properties[key]
//發現屬性是特殊的值時,調用對應的處理函數處理
if (Class.Mutators.hasOwnProperty(key)) {
Class.Mutators[key].call(this, value)
} else {
this.prototype[key] = value
}
}
}
// Create a sub Class based on `Class`.
Class.extend = function(properties) {
properties || (properties = {})
//定義繼承的對象是自己
properties.Extends = this
//調用Class.create實現繼承的流程
return Class.create(properties)
}
//給一個普通的函數 增加extend和implement方法。
function classify(cls) {
cls.extend = Class.extend
cls.implement = implement
return cls
}
// 這裏定義了一些特殊的屬性,阿拉蕾遍歷時發現key是這裏面的一個時,會調用這裏面的方法處理。
Class.Mutators = {
//這個定義了繼承的真正操作代碼。
'Extends': function(parent) {
//這邊的this指向子類
var existed = this.prototype
//生成一個中介原型,就是之前我們實現的objectCreat
var proto = createProto(parent.prototype)
//將子類原型有的方法混入到新的中介原型上
mix(proto, existed)
// 改變構造函數指向子類
proto.constructor = this
// 改變原型 完成繼承
this.prototype = proto
//為子類增加superclass屬性,這樣可以調用父類原型的方法。
this.superclass = parent.prototype
},
//這個有點類似組合的概念,支持數組。將其他類的屬性混入到子類原型上
'Implements': function(items) {
isArray(items) || (items = [items])
var proto = this.prototype, item
while (item = items.shift()) {
mix(proto, item.prototype || item)
}
},
//傳入靜態屬性
'Statics': function(staticProperties) {
mix(this, staticProperties)
}
}
// Shared empty constructor function to aid in prototype-chain creation.
function Ctor() {
}
// 這個方法就是我們之前實現的objectCreat,用來使用一個中介者來處理原型的問題,當瀏覽器支持`__proto__`時可以直接使用。否則新建一個空函數再將父類的原型賦值給這個空函數,返回這個空函數的實例
var createProto = Object.__proto__ ?
function(proto) {
return { __proto__: proto }
} :
function(proto) {
Ctor.prototype = proto
return new Ctor()
}
// Helpers 下面都是些輔助方法,很簡單就不說了
// ------------
function mix(r, s, wl) {
// Copy "all" properties including inherited ones.
for (var p in s) {
//過濾掉原型鏈上面的屬性
if (s.hasOwnProperty(p)) {
if (wl && indexOf(wl, p) === -1) continue
// 在 iPhone 1 代等設備的 Safari 中,prototype 也會被枚舉出來,需排除
if (p !== 'prototype') {
r[p] = s[p]
}
}
}
}
var toString = Object.prototype.toString
var isArray = Array.isArray || function(val) {
return toString.call(val) === '[object Array]'
}
var isFunction = function(val) {
return toString.call(val) === '[object Function]'
}
var indexOf = Array.prototype.indexOf ?
function(arr, item) {
return arr.indexOf(item)
} :
function(arr, item) {
for (var i = 0, len = arr.length; i < len; i++) {
if (arr[i] === item) {
return i
}
}
return -1
}
萬變不離其宗,本質上還是我們之前的繼承方式,只是在上面再封裝一層,更加清晰,明白了。
還有很多很多的實現,這邊就不一一列舉了。
未來科技的oo實現
其實 es6已經開始重視emcsript的oo實現了。不過還沒定案,就算定案了,也不知道嘛時候javascript會實現。再加上一大堆瀏覽器的跟進。不知道什麽時候才能用的上。不過了解下最新的規範還是很有必要的。
目前nodejs裏面已經實現了 inherite方法用來實現類繼承,類似我們上面的那種實現。
而es6(harmony)實現了class關鍵字用來創建類,並且具有類該有的一系列方法。如下:
class Monster {
// The contextual keyword "constructor" followed by an argument
// list and a body defines the body of the class’s constructor
// function. public and private declarations in the constructor
// declare and initialize per-instance properties. Assignments
// such as "this.foo = bar;" also set public properties.
constructor(name, health) {
public name = name;
private health = health;
}
// An identifier followed by an argument list and body defines a
// method. A “method” here is simply a function property on some
// object.
attack(target) {
log('The monster attacks ' + target);
}
// The contextual keyword "get" followed by an identifier and
// a curly body defines a getter in the same way that "get"
// defines one in an object literal.
get isAlive() {
return private(this).health > 0;
}
// Likewise, "set" can be used to define setters.
set health(value) {
if (value < 0) {
throw new Error('Health must be non-negative.')
}
private(this).health = value
}
// After a "public" modifier,
// an identifier optionally followed by "=" and an expression
// declares a prototype property and initializes it to the value
// of that expression.
public numAttacks = 0;
// After a "public" modifier,
// the keyword "const" followed by an identifier and an
// initializer declares a constant prototype property.
public const attackMessage = 'The monster hits you!';
}
可以看到具有了傳統oo裏面的大部分關鍵字,私有屬性也得到了支持。
繼承也很容易:
class Base {}
class Derived extends Base {}
//Here, Derived.prototype will inherit from Base.prototype.
let parent = {};
class Derived prototype parent {}
原文在這裏:http://h3manth.com/content/classes-javascript-es6
結語
雖然es6已經實現了正規的class關鍵字。不過等到真正能用上也不知道是何年馬月了。不過規範提供了方向,在es6還沒出來之前,n多大神前仆後繼實現了自己的class方式,分析源碼可以學到的還是很多,僅僅一個類的實現就可以摳出這麽多的類容,程序員還是應該多探索,不能只停留在表面。
原文地址:https://github.com/purplebamboo/blog/issues/14
javascript oo實現