JavaScript 繼承詳解(六)
在本章中,我們將分析Prototypejs中關(guān)于JavaScript繼承的實現(xiàn)。
Prototypejs是最早的JavaScript類庫,可以說是JavaScript類庫的鼻祖。 我在幾年前接觸的第一個JavaScript類庫就是這位,因此Prototypejs有著廣泛的群眾基礎(chǔ)。
不過當(dāng)年P(guān)rototypejs中的關(guān)于繼承的實現(xiàn)相當(dāng)?shù)暮唵?,源代碼就寥寥幾行,我們來看下。
早期Prototypejs中繼承的實現(xiàn)
源碼:
var Class = { // Class.create僅僅返回另外一個函數(shù),此函數(shù)執(zhí)行時將調(diào)用原型方法initialize create: function() { return function() { this.initialize.apply(this, arguments); } } }; // 對象的擴展 Object.extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; };
調(diào)用方式:
var Person = Class.create(); Person.prototype = { initialize: function(name) { this.name = name; }, getName: function(prefix) { return prefix + this.name; } }; var Employee = Class.create(); Employee.prototype = Object.extend(new Person(), { initialize: function(name, employeeID) { this.name = name; this.employeeID = employeeID; }, getName: function() { return "Employee name: " + this.name; } }); var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan"
很原始的感覺對吧,在子類函數(shù)中沒有提供調(diào)用父類函數(shù)的途徑。
Prototypejs 1.6以后的繼承實現(xiàn)
首先來看下調(diào)用方式:
// 通過Class.create創(chuàng)建一個新類 var Person = Class.create({ // initialize是構(gòu)造函數(shù) initialize: function(name) { this.name = name; }, getName: function(prefix) { return prefix + this.name; } }); // Class.create的第一個參數(shù)是要繼承的父類 var Employee = Class.create(Person, { // 通過將子類函數(shù)的第一個參數(shù)設(shè)為$super來引用父類的同名函數(shù) // 比較有創(chuàng)意,不過內(nèi)部實現(xiàn)應(yīng)該比較復(fù)雜,至少要用一個閉包來設(shè)置$super的上下文this指向當(dāng)前對象 initialize: function($super, name, employeeID) { $super(name); this.employeeID = employeeID; }, getName: function($super) { return $super("Employee name: "); } }); var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan"
這里我們將Prototypejs 1.6.0.3中繼承實現(xiàn)單獨取出來, 那些不想引用整個prototype庫而只想使用prototype式繼承的朋友, 可以直接把下面代碼拷貝出來保存為JS文件就行了。
var Prototype = { emptyFunction: function() { } }; var Class = { create: function() { var parent = null, properties = $A(arguments); if (Object.isFunction(properties[0])) parent = properties.shift(); function klass() { this.initialize.apply(this, arguments); } Object.extend(klass, Class.Methods); klass.superclass = parent; klass.subclasses = []; if (parent) { var subclass = function() { }; subclass.prototype = parent.prototype; klass.prototype = new subclass; parent.subclasses.push(klass); } for (var i = 0; i < properties.length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) klass.prototype.initialize = Prototype.emptyFunction; klass.prototype.constructor = klass; return klass; } }; Class.Methods = { addMethods: function(source) { var ancestor = this.superclass && this.superclass.prototype; var properties = Object.keys(source); if (!Object.keys({ toString: true }).length) properties.push("toString", "valueOf"); for (var i = 0, length = properties.length; i < length; i++) { var property = properties[i], value = source[property]; if (ancestor && Object.isFunction(value) && value.argumentNames().first() == "$super") { var method = value; value = (function(m) { return function() { return ancestor[m].apply(this, arguments) }; })(property).wrap(method); value.valueOf = method.valueOf.bind(method); value.toString = method.toString.bind(method); } this.prototype[property] = value; } return this; } }; Object.extend = function(destination, source) { for (var property in source) destination[property] = source[property]; return destination; }; function $A(iterable) { if (!iterable) return []; if (iterable.toArray) return iterable.toArray(); var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; } Object.extend(Object, { keys: function(object) { var keys = []; for (var property in object) keys.push(property); return keys; }, isFunction: function(object) { return typeof object == "function"; }, isUndefined: function(object) { return typeof object == "undefined"; } }); Object.extend(Function.prototype, { argumentNames: function() { var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g, '').split(','); return names.length == 1 && !names[0] ? [] : names; }, bind: function() { if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; var __method = this, args = $A(arguments), object = args.shift(); return function() { return __method.apply(object, args.concat($A(arguments))); } }, wrap: function(wrapper) { var __method = this; return function() { return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); } } }); Object.extend(Array.prototype, { first: function() { return this[0]; } });
首先,我們需要先解釋下Prototypejs中一些方法的定義。
argumentNames: 獲取函數(shù)的參數(shù)數(shù)組
function init($super, name, employeeID) { console.log(init.argumentNames().join(",")); // "$super,name,employeeID" }
bind: 綁定函數(shù)的上下文this到一個新的對象(一般是函數(shù)的第一個參數(shù))
var name = "window"; var p = { name: "Lisi", getName: function() { return this.name; } }; console.log(p.getName()); // "Lisi" console.log(p.getName.bind(window)()); // "window"
wrap: 把當(dāng)前調(diào)用函數(shù)作為包裹器wrapper函數(shù)的第一個參數(shù)
var name = "window"; var p = { name: "Lisi", getName: function() { return this.name; } }; function wrapper(originalFn) { return "Hello: " + originalFn(); } console.log(p.getName()); // "Lisi" console.log(p.getName.bind(window)()); // "window" console.log(p.getName.wrap(wrapper)()); // "Hello: window" console.log(p.getName.wrap(wrapper).bind(p)()); // "Hello: Lisi"
有一點繞口,對吧。這里要注意的是wrap和bind調(diào)用返回的都是函數(shù),把握住這個原則,就很容易看清本質(zhì)了。
對這些函數(shù)有了一定的認(rèn)識之后,我們再來解析Prototypejs繼承的核心內(nèi)容。
這里有兩個重要的定義,一個是Class.extend,另一個是Class.Methods.addMethods。
var Class = { create: function() { // 如果第一個參數(shù)是函數(shù),則作為父類 var parent = null, properties = $A(arguments); if (Object.isFunction(properties[0])) parent = properties.shift(); // 子類構(gòu)造函數(shù)的定義 function klass() { this.initialize.apply(this, arguments); } // 為子類添加原型方法Class.Methods.addMethods Object.extend(klass, Class.Methods); // 不僅為當(dāng)前類保存父類的引用,同時記錄了所有子類的引用 klass.superclass = parent; klass.subclasses = []; if (parent) { // 核心代碼 - 如果父類存在,則實現(xiàn)原型的繼承 // 這里為創(chuàng)建類時不調(diào)用父類的構(gòu)造函數(shù)提供了一種新的途徑 // - 使用一個中間過渡類,這和我們以前使用全局initializing變量達(dá)到相同的目的, // - 但是代碼更優(yōu)雅一點。 var subclass = function() { }; subclass.prototype = parent.prototype; klass.prototype = new subclass; parent.subclasses.push(klass); } // 核心代碼 - 如果子類擁有父類相同的方法,則特殊處理,將會在后面詳解 for (var i = 0; i < properties.length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) klass.prototype.initialize = Prototype.emptyFunction; // 修正constructor指向錯誤 klass.prototype.constructor = klass; return klass; } };
再來看addMethods做了哪些事情:
Class.Methods = { addMethods: function(source) { // 如果父類存在,ancestor指向父類的原型對象 var ancestor = this.superclass && this.superclass.prototype; var properties = Object.keys(source); // Firefox和Chrome返回1,IE8返回0,所以這個地方特殊處理 if (!Object.keys({ toString: true }).length) properties.push("toString", "valueOf"); // 循環(huán)子類原型定義的所有屬性,對于那些和父類重名的函數(shù)要重新定義 for (var i = 0, length = properties.length; i < length; i++) { // property為屬性名,value為屬性體(可能是函數(shù),也可能是對象) var property = properties[i], value = source[property]; // 如果父類存在,并且當(dāng)前當(dāng)前屬性是函數(shù),并且此函數(shù)的第一個參數(shù)為 $super if (ancestor && Object.isFunction(value) && value.argumentNames().first() == "$super") { var method = value; // 下面三行代碼是精華之所在,大概的意思: // - 首先創(chuàng)建一個自執(zhí)行的匿名函數(shù)返回另一個函數(shù),此函數(shù)用于執(zhí)行父類的同名函數(shù) // - (因為這是在循環(huán)中,我們曾多次指出循環(huán)中的函數(shù)引用局部變量的問題) // - 其次把這個自執(zhí)行的匿名函數(shù)的作為method的第一個參數(shù)(也就是對應(yīng)于形參$super) // 不過,竊以為這個地方作者有點走火入魔,完全沒必要這么復(fù)雜,后面我會詳細(xì)分析這段代碼。 value = (function(m) { return function() { return ancestor[m].apply(this, arguments) }; })(property).wrap(method); value.valueOf = method.valueOf.bind(method); // 因為我們改變了函數(shù)體,所以重新定義函數(shù)的toString方法 // 這樣用戶調(diào)用函數(shù)的toString方法時,返回的是原始的函數(shù)定義體 value.toString = method.toString.bind(method); } this.prototype[property] = value; } return this; } };
上面的代碼中我曾有“走火入魔”的說法,并不是對作者的褻瀆, 只是覺得作者對JavaScript中的一個重要準(zhǔn)則(通過自執(zhí)行的匿名函數(shù)創(chuàng)建作用域) 運用的有點過頭。
value = (function(m) { return function() { return ancestor[m].apply(this, arguments) }; })(property).wrap(method);
其實這段代碼和下面的效果一樣:
value = ancestor[property].wrap(method);
我們把wrap函數(shù)展開就能看的更清楚了:
value = (function(fn, wrapper) { var __method = fn; return function() { return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); } })(ancestor[property], method);
可以看到,我們其實為父類的函數(shù)ancestor[property]通過自執(zhí)行的匿名函數(shù)創(chuàng)建了作用域。 而原作者是為property創(chuàng)建的作用域。兩則的最終效果是一致的。
我們對Prototypejs繼承的重實現(xiàn)
分析了這么多,其實也不是很難,就那么多概念,大不了換種表現(xiàn)形式。
下面我們就用前幾章我們自己實現(xiàn)的jClass來實現(xiàn)Prototypejs形式的繼承。
// 注意:這是我們自己實現(xiàn)的類似Prototypejs繼承方式的代碼,可以直接拷貝下來使用 // 這個方法是借用Prototypejs中的定義 function argumentNames(fn) { var names = fn.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g, '').split(','); return names.length == 1 && !names[0] ? [] : names; } function jClass(baseClass, prop) { // 只接受一個參數(shù)的情況 - jClass(prop) if (typeof (baseClass) === "object") { prop = baseClass; baseClass = null; } // 本次調(diào)用所創(chuàng)建的類(構(gòu)造函數(shù)) function F() { // 如果父類存在,則實例對象的baseprototype指向父類的原型 // 這就提供了在實例對象中調(diào)用父類方法的途徑 if (baseClass) { this.baseprototype = baseClass.prototype; } this.initialize.apply(this, arguments); } // 如果此類需要從其它類擴展 if (baseClass) { var middleClass = function() {}; middleClass.prototype = baseClass.prototype; F.prototype = new middleClass(); F.prototype.constructor = F; } // 覆蓋父類的同名函數(shù) for (var name in prop) { if (prop.hasOwnProperty(name)) { // 如果此類繼承自父類baseClass并且父類原型中存在同名函數(shù)name if (baseClass && typeof (prop[name]) === "function" && argumentNames(prop[name])[0] === "$super") { // 重定義子類的原型方法prop[name] // - 這里面有很多JavaScript方面的技巧,如果閱讀有困難的話,可以參閱我前面關(guān)于JavaScript Tips and Tricks的系列文章 // - 比如$super封裝了父類方法的調(diào)用,但是調(diào)用時的上下文指針要指向當(dāng)前子類的實例對象 // - 將$super作為方法調(diào)用的第一個參數(shù) F.prototype[name] = (function(name, fn) { return function() { var that = this; $super = function() { return baseClass.prototype[name].apply(that, arguments); }; return fn.apply(this, Array.prototype.concat.apply($super, arguments)); }; })(name, prop[name]); } else { F.prototype[name] = prop[name]; } } } return F; };
調(diào)用方式和Prototypejs的調(diào)用方式保持一致:
var Person = jClass({ initialize: function(name) { this.name = name; }, getName: function() { return this.name; } }); var Employee = jClass(Person, { initialize: function($super, name, employeeID) { $super(name); this.employeeID = employeeID; }, getEmployeeID: function() { return this.employeeID; }, getName: function($super) { return "Employee name: " + $super(); } }); var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan"
經(jīng)過本章的學(xué)習(xí),就更加堅定了我們的信心,像Prototypejs形式的繼承我們也能夠輕松搞定。
以后的幾個章節(jié),我們會逐步分析mootools,Extjs等JavaScript類庫中繼承的實現(xiàn),敬請期待。
相關(guān)文章
基于javascript實現(xiàn)的搜索時自動提示功能
這篇文章主要介紹了基于javascript實現(xiàn)的搜索時自動提示功能,非常實用,推薦給需要的小伙伴參考下。2014-12-12淺談通過JS攔截 pushState和replaceState事件
下面小編就為大家?guī)硪黄獪\談通過JS攔截 pushState和replaceState事件。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07JavaScript監(jiān)測ActiveX控件是否已經(jīng)安裝過的代碼
這是通用的方法,只需要把唯一的Activex的clsid和任意一個屬性或方法名傳進(jìn)來就可以判斷了。(找了兩個小時才找到 -_-!)2008-09-09javascript 中String.match()與RegExp.exec()的區(qū)別說明
最近看了javascript權(quán)威指南 里面的正則部分,match和exec方法有一些相同點和不同點,在這里寫一下加深一下印象2013-01-01layer頁面跳轉(zhuǎn),獲取html子節(jié)點元素的值方法
今天小編就為大家分享一篇layer頁面跳轉(zhuǎn),獲取html子節(jié)點元素的值方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09bootstrap導(dǎo)航欄、下拉菜單、表單的簡單應(yīng)用實例解析
這篇文章主要介紹了bootstrap導(dǎo)航欄、下拉菜單、表單的簡單應(yīng)用實例解析,非常不錯,具有參考借鑒價值,需要的朋友參考下吧2017-01-01