前言
面向對象編程是每次面試必問的知識點,而前端js如何實現繼承每次命中率高達80%
這不,近兩天我們面試時候,同事就問道面試者此問題,但是,不論之前自己做的回答,還是面試者的回答,基本都不太令人滿意
很大的原因是多數時候前端並不需要實現繼承,就jquery來說也基本上是一碼到底,沒有實現繼承,據我所知,也就prototype與ext實現過繼承
所以繼承對前端來說似乎不太適用
近兩年來情況有所變化,SPA的興起以及前端邏輯的復雜化,讓前端代碼愈發的多,愈發的重,所以繼承慢慢的進入了一些初級一點的前端視野
所以,好好的了解如何實現繼承,繼承的幾個用法,是非常有意義的,就算只是為面試都是很有用的
文章只是個人見解,有誤請提出,demo未做檢測,有誤請提出
實現繼承
當一個函數被創建時,Function構造函數產生的函數會隱式的被賦予一個prototype屬性,prototype包含一個constructor對象
而constructor便是該新函數對象(constructor意義不大,但是可以幫我們找到繼承關系)
每個函數都會有一個prototype屬性,該屬性指向另一對象,這個對象包含可以由特定類型的所有實例共享的屬性和方法
每次實例化后,實例內部都會包含一個[[prototype]](__proto__)的內部屬性,這個屬性指向prototype
① 我們通過isPrototypeOf來確定某個對象是不是我的原型
② hasOwnPrototype 可以檢測一個屬性是存在實例中還是原型中,該屬性不是原型屬性才返回true
var Person = function (name, age) { this.name = name; this.age = age; }; Person.prototype.getName = function () { return this.name; }; var y = new Person('葉小釵', 30);
通俗一點來說,prototype是一模板,新創建對象就是對他一個拷貝,里面的屬性或者方法都會賦值給實例
這里說是模板賦值其實不太合理,反正由類產生的所有實例的__proto__都會共享一個prototype,這里我做一個例子
我們在斷點情況下是沒有name2屬性的,但是我們如果在斷點下加上這個代碼的話,a.name2,就有值了
Klass.prototype.name2 = '222';
所以,這里說模板,不如說是指針指向,都是共享一個對象;繼承的情況的話就是這樣

(function () { var Person = function (name) { this.name = name; }; //Person.prototype = {};//這句將影響十分具有constructor屬性 Person.prototype.getName = function () { return this.name; }; var Student = function (name, sex, id) { this.name = name || '無名氏'; this.sex = sex || '不明'; this.id = id || '未填'; //學號 }; //相當於將其prototype復制了一次,若是包含constructor的話將指向Person Student.prototype = new Person(); Student.prototype.getId = function () { return this.id; } var y = new Person(); var s = new Student; var s1 = y instanceof Person; var s2 = s instanceof Student; var s3 = s instanceof Person; var s4 = Student.prototype.constructor === Person; var s5 = Student.constructor === Person; var s6 = Student.constructor === Function; var s = ''; })();
prototype實現繼承
我們在具體項目中,真正復雜一點的項目可能就會對繼承進行封裝,讓自己更好的使用,我們下面就來看看prototype怎么干的
PS:我這里做了一點小的修改:
1 var Class = (function () { 2 function subclass() { }; 3 4 //我們構建一個類可以傳兩個參數,第一個為需要繼承的類, 5 //如果沒有的話就一定會有第二個對象,就是其原型屬性以及方法,其中initialize為構造函數的入口 6 function create() { 7 8 //此處兩個屬性一個是被繼承的類,一個為原型方法 9 var parent = null; 10 var properties = $A(arguments); 11 12 if (Object.isFunction(properties[0])) 13 parent = properties.shift(); 14 15 //新建類,這個類最好會被返回,構造函數入口為initialize原型方法 16 function klass() { 17 this.initialize.apply(this, arguments); 18 } 19 20 //擴展klass類的“實例”對象(非原型),為其增加addMethods方法 21 Object.extend(klass, Class.Methods); 22 23 //為其指定父類,沒有就為空 24 klass.superclass = parent; 25 26 //其子類集合(require情況下不一定准確) 27 klass.subclasses = []; 28 29 //如果存在父類就需要繼承 30 if (parent) { 31 //新建一個空類用以繼承,其存在的意義是不希望構造函數被執行 32 //比如 klass.prototype = new parent;就會執行其initialize方法 33 subclass.prototype = parent.prototype; 34 klass.prototype = new subclass; 35 parent.subclasses.push(klass); 36 } 37 38 //遍歷對象(其實此處這樣做意義不大,我們可以強制最多給兩個參數) 39 //注意,此處為一個難點,需要謹慎,進入addMethods 40 for (var i = 0, length = properties.length; i < length; i++) 41 klass.addMethods(properties[i]); 42 43 if (!klass.prototype.initialize) 44 klass.prototype.initialize = Prototype.emptyFunction; 45 46 klass.prototype.constructor = klass; 47 return klass; 48 } 49 50 /** 51 由於作者考慮情況比較全面會想到這種情況 52 var Klass = Class.create(parent,{},{}); 53 后面每一個對象的遍歷都會執行這里的方法,我們平時需要將這里直接限定最多兩個參數 54 */ 55 function addMethods(source) { 56 57 //當前類的父類原型鏈,前面被記錄下來了 58 var ancestor = this.superclass && this.superclass.prototype; 59 60 //將當前對象的鍵值取出轉換為數組 61 var properties = Object.keys(source); 62 63 //依次遍歷各個屬性,填充當前類(klass)原型鏈 64 for (var i = 0, length = properties.length; i < length; i++) { 65 66 //property為鍵,value為值,比如getName: function(){}的關系 67 var property = properties[i], value = source[property]; 68 69 /**************** 70 這里有個難點,用於處理子類中具有和父類原型鏈同名的情況,仍然可以調用父類函數的方案(我這里只能說牛B) 71 如果一個子類有一個參數叫做$super的話,這里就可以處理了,這里判斷一個函數的參數使用了正則表達式,正如 72 var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(','); 73 ****************/ 74 if (ancestor && Object.isFunction(value) && value.argumentNames()[0] == "$super") { 75 76 //將當前函數存下來 77 var method = value; 78 /**************** 79 第一步: 80 81 這里是這段代碼最難的地方,需要好好閱讀,我們首先將里面一塊單獨提出 82 value = (function (m) { 83 return function () { return ancestor[m].apply(this, arguments); }; 84 })(property) 85 這里很牛B的構建了一個閉包(將方法名傳了進去),任何直接由其父類原型中取出了相關方法 86 然后內部返回了該函數,此時其實重寫了value,value 87 ***這里***有一個特別需要注意的地方是,此處的apply方法不是固定寫在class上的,是根據調用環境變化的,具體各位自己去理解了 88 89 第二步: 90 首先value被重新成其父類的調用了,此處可以簡單理解為(僅僅為理解)$super=value 91 然后下面會調用wrap操作vaule將,我們本次方法進行操作 92 wrap: function (wrapper) { 93 var __method = this; 94 return function () { 95 return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); 96 } 97 } 98 可以看出,他其實就是將第一個方法(value)作為了自己方法名的第一個參數了,后面的參數不必理會 99 ****************/ 100 value = (function (m) { 101 return function () { return ancestor[m].apply(this, arguments); }; 102 })(property).wrap(method); 103 } 104 //為其原型賦值 105 this.prototype[property] = value; 106 } 107 return this; 108 } 109 110 return { 111 create: create, 112 Methods: { 113 addMethods: addMethods 114 } 115 }; 116 })();
下面來一個簡單的例子:

1 var Person = Class.create({ 2 initialize: function (name) { 3 this.name = name; 4 }, 5 getName: function () { 6 console.log('我是父類'); 7 return this.name; 8 }, 9 getAge: function () { 10 return this.age; 11 } 12 }); 13 14 var Employee = Class.create(Person, { 15 initialize: function ($super, name, age) { 16 $super(name); 17 this.age = age; 18 }, 19 getName: function ($super) { 20 return '我是子類:' + $super(); 21 } 22 }); 23 24 var C = Class.create(Employee, { 25 getAge: function ($super) { 26 return $super(); 27 } 28 }); 29 30 var y = new C("葉小釵", 25); 31 console.log(y.getName() + ': ' + y.getAge());
這里,我們根據自己的需求重寫寫了下繼承相關代碼,表現基本與上述一致,各位可以自己試試
PS:當然如果有問題請指出
簡化prototype繼承
1 var arr = []; 2 var slice = arr.slice; 3 4 function create() { 5 if (arguments.length == 0 || arguments.length > 2) throw '參數錯誤'; 6 7 var parent = null; 8 //將參數轉換為數組 9 var properties = slice.call(arguments); 10 11 //如果第一個參數為類(function),那么就將之取出 12 if (typeof properties[0] === 'function') 13 parent = properties.shift(); 14 properties = properties[0]; 15 16 function klass() { 17 this.initialize.apply(this, arguments); 18 } 19 20 klass.superclass = parent; 21 klass.subclasses = []; 22 23 if (parent) { 24 var subclass = function () { }; 25 subclass.prototype = parent.prototype; 26 klass.prototype = new subclass; 27 parent.subclasses.push(klass); 28 } 29 30 var ancestor = klass.superclass && klass.superclass.prototype; 31 for (var k in properties) { 32 var value = properties[k]; 33 34 //滿足條件就重寫 35 if (ancestor && typeof value == 'function') { 36 var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(','); 37 //只有在第一個參數為$super情況下才需要處理(是否具有重復方法需要用戶自己決定) 38 if (argslist[0] === '$super' && ancestor[k]) { 39 value = (function (methodName, fn) { 40 return function () { 41 var scope = this; 42 var args = [function () { 43 return ancestor[methodName].apply(scope, arguments); 44 } ]; 45 return fn.apply(this, args.concat(slice.call(arguments))); 46 }; 47 })(k, value); 48 } 49 } 50 51 klass.prototype[k] = value; 52 } 53 54 if (!klass.prototype.initialize) 55 klass.prototype.initialize = function () { }; 56 57 klass.prototype.constructor = klass; 58 59 return klass; 60 }
如此,我們就完成了自己的繼承了
實戰繼承
知道原型可以實現繼承是皮毛,知道各個庫是怎樣干的也只是入門
因為,要在項目中用到才能算真正掌握繼承,這里我們便來點實戰的小例子
這里我寫一個簡單的view用於下面各種繼承

1 var AbstractView = create({ 2 initialize: function (opts) { 3 opts = opts || {}; 4 this.wrapper = opts.wrapper || $('body'); 5 6 //事件集合 7 this.events = {}; 8 9 this.isCreate = false; 10 11 }, 12 on: function (type, fn) { 13 if (!this.events[type]) this.events[type] = []; 14 this.events[type].push(fn); 15 }, 16 trigger: function (type) { 17 if (!this.events[type]) return; 18 for (var i = 0, len = this.events[type].length; i < len; i++) { 19 this.events[type][i].call(this) 20 } 21 }, 22 createHtml: function () { 23 throw '必須重寫'; 24 }, 25 create: function () { 26 this.root = $(this.createHtml()); 27 this.wrapper.append(this.root); 28 this.trigger('onCreate'); 29 this.isCreate = true; 30 }, 31 show: function () { 32 if (!this.isCreate) this.create(); 33 this.root.show(); 34 this.trigger('onShow'); 35 }, 36 hide: function () { 37 this.root.hide(); 38 } 39 }); 40 41 var Alert = create(AbstractView, { 42 43 createHtml: function () { 44 return '<div class="alert">這里是alert框</div>'; 45 } 46 }); 47 48 var AlertTitle = create(Alert, { 49 initialize: function ($super) { 50 this.title = ''; 51 $super(); 52 53 }, 54 createHtml: function () { 55 return '<div class="alert"><h2>' + this.title + '</h2>這里是帶標題alert框</div>'; 56 }, 57 58 setTitle: function (title) { 59 this.title = title; 60 this.root.find('h2').html(title) 61 } 62 63 }); 64 65 var AlertTitleButton = create(AlertTitle, { 66 initialize: function ($super) { 67 this.title = ''; 68 $super(); 69 70 this.on('onShow', function () { 71 var bt = $('<input type="button" value="點擊我" />'); 72 bt.click($.proxy(function () { 73 alert(this.title); 74 }, this)); 75 this.root.append(bt) 76 }); 77 } 78 }); 79 80 var v1 = new Alert(); 81 v1.show(); 82 83 var v2 = new AlertTitle(); 84 v2.show(); 85 v2.setTitle('我是標題'); 86 87 var v3 = new AlertTitleButton(); 88 v3.show(); 89 v3.setTitle('我是標題和按鈕的alert');
http://sandbox.runjs.cn/show/ddlp7nlt
結語
希望這次繼承的文章對各位有幫助,此外文中錯誤請指出
親愛的道友們,我其實在我們團隊只是中等水平,我們上海攜程無線是一個優秀的團隊,
如果你現在正在找工作,請加入我們吧!!!
在我們團隊,你可以肆無忌憚的黑自己的老大,你會體會到和諧的氛圍,當然妹子多多的!
要求:靠譜前端,吃苦耐勞,待遇剛剛的!!!
需要的朋友可以私信我
順便推廣道友的jquery技術交流群:239147101