本文詳細講述了JavaScript繼承的特性與實踐應用。分享給大家供大家參考,具體如下:
繼承是代碼重用的模式。JavaScript 可以模擬基于類的模式,還支持其它更具表現力的模式。但保持簡單通常是最好的策略。
JavaScript 是基于原型的語言,也就是說它可以直接繼承其他對象。
JavaScript 的原型不是直接讓對象從其他對象繼承,而是插入一個多余的間接層:通過構造函數來產生對象。
當一個函數被創建時,Function 構造器產生的函數對象會運行這樣類似的代碼:
this.prototype = {constructor : this};
新的函數對象新增了一個 prototype 屬性,它是一個包含了 constructor 屬性且屬性值為該新函數的對象。
當采用構造器調用模式,即用 new 去調用一個函數時,它會這樣執行:
Function.method('new', function (){ var that = Object.create(this.prototype);//創建一個繼承了構造器函數的原型對象的新對象 var other = this.apply(that, arguments);//調用構造器函數,綁定 this 到新對象 return (typeof other === 'object' && other) || that;//如果構造器函數的返回值不是對象,就直接返回這個新對象});
我們可以定義一個構造器,然后擴充它的原型:
//定義構造器并擴充原型var Mammal = function (name) { this.name = name;};Mammal.prototype.get_name = function () { return this.name;};Mammal.prototype.says = function () { return this.saying || '';};
然后構造實例:
var myMammal = new Mammal('Herb the mammal');console.log(myMammal.get_name());//Herb the mammal
構造另一個偽類來繼承 Mammal(定義構造器函數并替換它的 prototype):
var Cat = function (name) { this.name = name; this.saying = 'meow';};Cat.prototype = new Mammal();
擴充原型:
Cat.prototype.purr = function (n) { var i, s = ''; for (i = 0; i < n; i += 1) { if (s) { s += '-'; } s += 'r'; } return s;};Cat.prototype.get_name = function () { return this.says() + ' ' + this.name + ' ' + this.says();};var myCat = new Cat('Henrietta');console.log(myCat.says());//meowconsole.log(myCat.purr(5));//r-r-r-r-rconsole.log(myCat.get_name());//meow Henrietta meow
我們使用 method 方法定義了 inherits 方法,來隱藏上面這些丑陋的細節:
/** * 為 Function.prototype 新增 method 方法 * @param name 方法名稱 * @param func 函數 * @returns {Function} */Function.prototype.method = function (name, func) { if (!this.prototype[name])//沒有該方法時,才添加 this.prototype[name] = func; return this;};Function.method('inherits', function (Parent) { this.prototype = new Parent(); return this;});
這兩個方法都返回 this,這樣我們就可以以級聯的方式編程啦O(∩_∩)O~
var Cat = function (name) { this.name = name; this.saying = 'meow';}.inherits(Mammal).method('purr', function (n) { var i, s = ''; for (i = 0; i < n; i += 1) { if (s) { s += '-'; } s += 'r'; } return s; }).method('get_name', function () { return this.says() + ' ' + this.name + ' ' + this.says(); });var myCat = new Cat('Henrietta');console.log(myCat.says());//meowconsole.log(myCat.purr(5));//r-r-r-r-rconsole.log(myCat.get_name());//meow Henrietta meow
雖然我們有了行為很像“類”的構造器函數,但沒有私有環境,所有的屬性都是公開的,而且不能訪問父類的方法。
如果在調用構造函數時忘記加上 new 前綴,那么 this 就不會被綁定到新對象上,而是被綁定到了全局變量?。。∵@樣我們不但沒有擴充新對象,還破壞了全局變量環境。
這是一個嚴重的語言設計錯誤!為了降低出現這個問題的概率,所有的構造器函數都約定以首字母大寫的形式來命名。這樣當我們看到首字母大寫的形式的函數,就知道它是構造器函數啦O(∩_∩)O~
當然,更好的策略是根本不使用構造器函數。
有時候,構造器需要接受一大堆參數,這很麻煩。所以在編寫構造器時,讓它接受一個簡單的對象說明符會更好:
var myObject = maker({ first: f, middle: m, last: l});
現在這些參數可以按照任意的順序排列咯,而且構造器還能夠聰明地為那些沒有傳入的參數使用默認值,代碼也變得更易閱讀啦O(∩_∩)O~
基于原型的繼承指的是,一個新對象可以繼承一個舊對象的屬性。首先構造出一個有用的對象,然后就可以構造出更多與那個對象類似的對象。
/** * 原型 */var myMammal = { name: 'Herb the mammal', get_name: function () { return this.name; }, says: function () { return this.saying || ''; }};//創建新實例var myCat = Object.create(myMammal);myCat.name = 'Henrietta';myCat.saying = 'meow';myCat.purr = function (n) { var i, s = ''; for (i = 0; i < n; i += 1) { if (s) { s += '-'; } s += 'r'; } return s;};myCat.get_name = function () { return this.says() + ' ' + this.name + ' ' + this.says();};console.log(myCat.says());//meowconsole.log(myCat.purr(5));//r-r-r-r-rconsole.log(myCat.get_name());//meow Henrietta meow
這里用到了 create 方法來創建新的實例:
Object.create = function (o) { var F = function () { }; F.prototype = o; return new F(); }
目前為止看到的繼承模式的問題是:無法保護隱私,對象的所有屬性都是可見的。有一些無知的程序員會使用偽裝私有的模式,即給一個需要私有的屬性起一個古怪的名字,并希望其他使用代碼的程序員假裝看不到它們!
其實有更好的方法:應用模塊模式。
我們先構造一個生成對象的函數,它有這些步驟:
①. 創建新對象。這有四種方式:
【1】構造一個對象字面量。
【2】調用一個構造器函數。
【3】構造一個已存在對象的新實例。
【4】調用任意一個會返回對象的函數。
②. 定義私有實例變量與方法。
③. 為這個新對象擴充方法,這些方法擁有特權去訪問這些參數。
④. 返回這個新對象。
函數化構造器的偽代碼如下:
var constructor = function (spec, my){ var that, 其他私有變量; my = my || {}; //把共享的變量和函數添加到 my 中 that = 一個新對象 //添加給 that 的特權方法 return that;};
spec 對象包含了需要構造一個新實例的所有信息,它可以被用到到私有變量或者其他函數中。
my 對象為在一個繼承鏈中的構造器提供了共享的容器,如果沒有傳入,那么會創建一個 my 對象。
創建特權方法的方式是:把函數定義為私有方法,然后再把它們分配給 that:
var methodical = function (){ ...};that.methodical = methodical;
這樣分兩步定義的好處是:私有的 methodical 不受這個實例被改變的影響。
現在,我們把這個模式應用到 mammal 示例中:
var mammal = function (spec) { var that = {}; that.get_name = function () { return spec.name; }; that.says = function () { return spec.saying || ''; }; return that;};var myMammal = mammal({name: 'Herb'});console.log(myMammal.get_name());//Herbvar cat = function (spec) { spec.saying = spec.saying || 'meow'; var that = mammal(spec); that.purr = function (n) { var i, s = ''; for (i = 0; i < n; i += 1) { if (s) { s += '-'; } s += 'r'; } return s; }; that.get_name = function () { return that.says() + ' ' + spec.name + ' ' + that.says(); }; return that;};var myCat = cat({name: 'Henrietta'});console.log(myCat.says());//meowconsole.log(myCat.purr(5));//r-r-r-r-rconsole.log(myCat.get_name());//meow Henrietta meow
函數化模式還能調用父類的方法。這里我們構造一個 superior 方法,它會返回調用某個方法名的函數:
//返回調用某個方法名的函數Object.method('superior', function (name) { var that = this, method = that[name]; return function () { return method.apply(that, arguments); };});
現在創建一個 coolcat,它擁有一個可以調用父類方法的 get_name:
var coolcat = function (spec) { var that = cat(spec), super_get_name = that.superior('get_name'); that.get_name = function (n) { return 'like ' + super_get_name() + ' baby'; }; return that;};var myCoolCat = coolcat({name: 'Bix'});console.log(myCoolCat.get_name());//like meow Bix meow baby
函數化模式有很大的靈活性,而且可以更好地實現封裝、信息隱藏以及訪問父類方法的能力。
如果對象所有的狀態都是私有的,那么就稱為防偽對象。這個對象的屬性可以被替換或刪除,但這個對象的狀態不受影響。如果用函數化模式來創建對象,并且這個對象的所有方法都不使用 this 或 that,那么這個對象就是持久性的,它不會被入侵。除非存在特權方法,否則不能訪問這個持久性對象的內部狀態。
可以構造一個能夠給任何對象添加簡單事件處理特性的函數。這里,我們給這個對象添加一個 on 方法,fire 方法和私有的事件注冊對象:
var eventuality = function (that) { var registry = {}; /** * 觸發事件 * * 使用 'on' 方法注冊的事件處理程序將被調用 * @param 可以是包含事件名稱的字符串,或者是一個擁有 type 屬性(值為事件名稱)的對象。 */ that.fire = function (event) { var array, func, handler, i, type = typeof event === 'string' ? event : event.type; //如果這個事件已被注冊,則遍歷并依序執行 if (registry.hasOwnProperty(type)) { array = registry[type]; for (i = 0; i < array.length; i += 1) { handler = array[i];//處理程序包含一個方法和一組可選的參數 func = handler.method; if (typeof func === 'string') {//如果方法是字符串形式的名稱,則尋找它 func = this[func]; } //調用它。如果處理程序包含參數,則傳遞過去,否則就傳遞事件對象 func.apply(this, handler.parameters || [event]); } } return this; }; /** * 注冊一個事件 * @param type * @param method * @param parameters */ that.on = function (type, method, parameters) { var handler = { method: method, parameters: parameters }; if (registry.hasOwnProperty(type)) {//如果已存在,就新增數組項 registry[type].push(handler); } else {//新增 registry[type] = [handler]; } return this; }; return that;};
可以在任何單獨對象上調用 eventuality,授予它事件處理方法。也可以在 that 被返回前,在構造函數中調用它:
eventuality(that);
JavaScript 弱類型的特性在此是一個巨大的優勢,因為我們無須處理對象繼承關系中的類型O(∩_∩)O~
希望本文所述對大家JavaScript程序設計有所幫助。
新聞熱點
疑難解答