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