上一回聊到JS的Function類型,作了柯里化,數組去重,排序的題。javascript
介一回,偶們來聊一下用JS中的類,有些盆友可能用過ES6或者TypeScript的,知道Class語法糖,但是在ES5中並無,ES5中須要用到構造函數來模擬類。java
既然是類,確定要聊到繼承,而聊到繼承,那原型也少不了,可是,繼承、原型這些知識在網上的基礎講解已經不少了。node
因此,偶就把jquery,extjs,nodejs,underscore.js的繼承源碼拿來作個比較分析,至於模擬接口蝦米的,後面會聊滴,來吧開始咯:jquery
重溫對象,仍是先來個書上(高程3)的例子:設計模式
var o = { name:"飛狐", age:"21", sayName:function(){ alert(this.name); } };
這是以前講的對象,要修改屬性的特性,好比把name修改成不可更改,可能有的盆友說了,用以前聊過的對象凍結isFrozen()方法,是的,能夠作到,可是防篡改的方法是做用對象定義以後,若是要修改屬性的默認特性,就得用ES5的Object.defineProperty()方法了,數組
Object.defineProperty()方法接收3個參數,屬性所在對象,屬性名,描述符對象;其中描述符對象的屬性必須是:configurable,enumerable,writable,value。這是書上的描述,好像有點抽象,來吧看例子:瀏覽器
var o = {}; Object.defineProperty(o,"name",{ // 這個地方的name,就是建立的屬性, writable:false, // 這個地方定義爲只讀,不可修改 value:"飛狐" // 默認值,沒什麼好說的 }); alert(o.name); // 飛狐 o.name = "帥狐"; alert(o.name); // 飛狐
怎麼樣,配上註釋,應該不難理解吧。app
getter,setter,這倆函數具備4個屬性(配置,枚舉,訪問,寫入),對寫過java的必定很親切吧,來吧直接看例子要更直觀些:框架
var o = { _name:"帥狐", feature:"帥" }; Object.defineProperty(o,"name",{ get:function(){ // 這裏的get用於獲取 return this._name; }, // 這裏的set用於寫入,而value就是所定義屬性name的值 set:function(value){ if(value=="飛狐"){ // 這裏是修改_name的值 this._name = value; // 這裏是修改屬性feature的值 this.feature = value+this.feature; } }, enumerable: true, // 可枚舉 configurable: true, // 可配置 }); o.name = "飛狐"; alert(o.feature); // 飛狐帥
這裏簡單的改了一下書上的例子,聊到Object.defineProperty()就順便說一下AngularJS的雙向綁定,作一個知識的擴展吧:函數
AngularJS的雙向綁定受到了不少人JSer的喜好,其中有仨方法:
$scope.$apply(),$scope.$digest(),$scope.$watch()。雙向綁定離不開這仨。
玩兒過AngularJS的盆友都知道,髒值檢測scope中的對象綁狀態,一旦發生改變,$digest就>會循環監測,調用相應的方法,$watch則監聽$digest中被監聽的對象,$apply僅僅只是進入Angular context,而後經過>$digest去觸發髒檢查。其中,$watch的源碼段是介麼寫的
以下:
$watch: function (watchExp, listener, objectEquality) { //...這裏有一些屬性定義,先忽略 if(!isFunction(listener)){ // 這裏的compileToFn函數實際上是調用$parse實例來分析監控參數,返回一個函數 var listenFn = compileToFn(listener || noop, 'listener'); // 這裏的watcher是個對象,fn傳入的listener watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; }; // 這裏的watchExp是傳入的監聽對象 if(typeof watchExp == 'string' && get.constant) { var originalFn = watcher.fn; watcher.fn = function(newVal, oldVal, scope) { // 這裏的經過對象冒充,指向當前做用域 originalFn.call(this, newVal, oldVal, scope); arrayRemove(array, watcher); }; } //...這裏有一些判斷返回,也忽略 }
這裏作一個最簡單的模擬,只是爲了演示Object.defineProperty(),以下:
$watch: function (watchExp, listener, objectEquality) { var o = this.$$watchers[watchExp]; // 檢測的對象 Object.defineProperty(this, watchExp, { // this指向調用者 get: function () { return o; }, set: function (listener) { // 傳遞監聽函數 o.listener = listener; }, enumerable: true, configurable: true, }); };
Object.defineProperty是ES5的新玩意兒,不支持IE低版本。不少盆友又要疑惑了,那avalon框架就支持低版本又咋玩的嘞,實際上是使用VBScript來實現低版本IE的兼容。
(注:以上的代碼,純屬擴展,若是感受暈菜請跳過)
若是有盆友感興趣,那我再單獨寫個angular1.x源碼學習讀後感,把我讀過的Angular源碼段分享出來,O(∩_∩)O~
Object.defineProperties()方法,定義多個屬性,接收兩個對象參數,第一個是對象要操做的對象自己,第二個是要操做的對象屬性。
var o = {}; Object.defineProperties(o,{ _name:{ value:"帥狐" }, feature:{ value:"帥", writable:true // 可修改 }, name:{ get:function(){ // 這裏的get用於獲取 return this._name; }, // 這裏的set用於寫入,而value就是所定義屬性name的值 set:function(val){ if(val=="飛狐"){ // 這裏是修改_name的值 this._name = val; // 這裏是修改屬性feature的值 this.feature = val+this.feature; } } } }); alert(o.name); // 帥狐 o.name = "飛狐"; alert(o.feature); // 飛狐帥
這裏光看例子可能有點抽象,不要緊,後面講設計模式,聊到觀察者模式的時候還會聊到事件。
(1) 工廠模式,這裏就直接用書上的例子:
function createPerson(name,age){ var o = new Object(); o.name = name; o.age = age; o.feature = function(){ alert(this.name+"就是帥!"); }; return o; } var person = createPerson("飛狐",21); var person1 = createPerson("帥狐",19);
工廠模式雖然簡單,並且解決了建立多個類似對象的問題,卻無從識別對象的類型,由於所有都是Object,不像Date、Array等,因而乎構造函數模式應運而生。
(2) 構造函數模式,這裏也直接用書上的例子:
function Person(name,age){ // 雖然沒有嚴格規定,但按照慣例,構造函數的首寫字母用大寫來區別於其餘函數 this.name = name; // 直接將屬性和方法賦值給this對象 this.age = age; this.feature = function(){ alert(this.name+"就是帥!"); }; } var person = new Person("飛狐",21); var person1 = new Person("帥狐",19); // 這裏返回爲true,這正式構造函數優於工廠模式來建立對象之處 alert(person instanceof Person); alert(person instanceof Object);// true
構造函數沒有顯示建立對象:new Object(),但會隱式地自動new Object()。並且要注意一點,構造函數沒有return語句,是自動返回。
看到這裏,構造函數已經很不錯了吧,可是:
每次建立實例的時候都要從新建立一次方法,看下面的例子:
function Person(name,age){ this.name = name; this.age = age; this.feature = feature; // 把方法寫到外面 }; // 每次實例化一個對象,都會建立一次方法,並且這個feature函數是全局函數 function feature(){ alert(this.name+"就是帥!"); }; var person = new Person("飛狐",21); var person1 = new Person("帥狐",19); person.feature(); person1.feature();
能夠看出,對象的方法是相同的,而重複的建立致使了定義了多個全局函數,用書上的原話,絲毫木有封裝性可言,那啷個辦呢,因而乎,引出了原型模式。
咱們建立的每一個函數都有prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。使用原型對象的好處就是可讓全部對象實例共享它所包含的屬性及方法。
高程3上的這個解釋貌似有點繞腦殼,來吧,直接看例子:
function Person(){ }; Person.prototype.name = "飛狐"; Person.prototype.feature = function(){ alert(this.name+"就是帥!"); }; var person = new Person(); var person1 = new Person(); alert(person.feature == person1.feature); // true
看上去很不錯了,全部對象實例共享了所包含的屬性及方法,可是嘞,構造函數傳遞初始化參數木有了,並且由於共享,一個實例修改了引用,另外一個也隨之被更改了,這樣的話就能夠結合原型模式與構造函數模式使用,繼續下一個。
構造函數模式用於定義實例屬性,原型模式用於定義共享屬性,看例子:
function Person(name){ this.name = name; }; Person.prototype = { // 匿名對象 constructor:Person, // 這裏有點跳躍,默認的對象指針是指向Object的,這裏是讓指針指向自己 feature:function(){ return this.name+"就是帥!"; } }; var person = new Person("飛狐"); var person1 = new Person("帥狐"); alert(person.feature()); // 飛狐就是帥 alert(person1.feature()); // 帥狐就是帥 alert(person.feature == person1.feature); // true
看上去很不錯了,每一個實例都會有本身的一份實例屬性,但同時又共享着方法,最大限度的節省了內存。
動態原型模式是把全部信息都封裝在構造函數中,經過構造函數中初始化原型,檢測該方法是否有效而選擇是否須要初始化原型,直接看例子吧:
function Person(name){ this.name = name; if(typeof this.feature != "function"){ // 這裏的代碼只執行了一次 Person.prototype.feature = function(){ return this.name+"就是帥!"; } } }; var person = new Person("飛狐"); var person1 = new Person("帥狐"); alert(person.feature()); // 飛狐就是帥 alert(person1.feature()); // 帥狐就是帥 alert(person.feature == person1.feature); // true
金星老師說:"完美"!
在高程3的書上還介紹有寄生構造函數模式,穩妥構造函數模式,這裏咱們不一一列舉,咱如今有個大概的理解了,後面就能夠繼續裝逼繼續飛了。
這一回聊的有點兒多,先裝個逼,話說薛之謙最近有首新歌不錯喲,歌名《紳士》。
這一回講的內容比較繞腦殼,下面的內容會更繞腦殼,哈哈~~,不過不要緊,仍是那句話,一時理解不了也不要緊,先囫圇吞棗,後面的內容還會涉及,
在高程的書上講繼承,講了6種方法,在網上呢,關於JS的繼承資料多多,因此嘞,咱就裝逼一點,分析比較熱門的框架關於JS繼承的源碼,來吧:
jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, // 這裏定義的一堆先無論 target = arguments[0] || {}, // 這裏target爲arguments[0],表示取傳入的第一個參數 i = 1, length = arguments.length, // 這裏的length是傳入的參數總長度 deep = false; // 判斷第一個參數爲布爾值的狀況,如:jQuery.extend(true,o1,o2); 深拷貝,第一個值不能夠是false if ( typeof target === "boolean" ) { deep = target; // 把布爾值的target賦值給deep,至關於deep=true target = arguments[i] || {}; // target改成第二個參數o1 i++; } // 處理像string的case,如:jQuery.extend('fox',{name: 'fatfox'}) if ( typeof target !== "object" && !jQuery.isFunction(target) ) { target = {}; } // 這裏就是判斷傳入1個參數的狀況,則等於自己,最簡單的例子就是JQuery.extend(o); if (i === length ) { target = this; // 1,jQuery.extend時,this指的是jQuery; 2,jQuery.fn.extend時,this指的是jQuery.fn i--; } for ( ; i < length; i++ ) { // 判斷傳入項是有效值的時候,就賦值給options;這裏是從第二項開始的遍歷,就是被拷貝項 if ( (options = arguments[i]) != null ) { // for in 枚舉循環沒啥說的 for (name in options ) { src = target[name]; copy = options[name]; // 防止死循環 if ( target === copy ) { continue; } // deep=true爲深拷貝,且被拷貝的屬性值自己是個對象 if (deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { // 判斷被拷貝的屬性值是個數組 if (copyIsArray ) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src : []; } else { clone = src && jQuery.isPlainObject(src) ? src : {}; } // 遞歸,修改原對象屬性值 target[name] = jQuery.extend(deep, clone, copy ); // 淺拷貝的狀況,屬性值不爲undefined } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // 返回修改後的對象 return target; };
能夠看出來,JQuery的這個深拷貝和淺拷貝就是拷貝,說白了就是複製,粘貼。
這個是摘自jquery的源碼關於繼承的段兒,我加工的註釋,若是感受有難度,能夠跳過,看下一個。
_.extend = function(obj) { // each循環參數中的每個對象 // 很熟悉吧,還記得聊柯里化的時候Aarry.prototype.slice.call(arguments,1)嗎 each(slice.call(arguments, 1), function(source) { // 將對象中的所有屬性複製或覆蓋到obj對象 for(var prop in source) { obj[prop] = source[prop]; } }); return obj; };
是否是感受,很簡單粗暴。
Object.create()是ES5的新玩意兒,因此IE9如下不支持
exports.inherits = function(ctor, superCtor) { ctor.super_ = superCtor; // 子類獲得的是父類的原型,第二個參數是個對象 ctor.prototype = Object.create(superCtor.prototype, { constructor: { // 構造屬性 value: ctor, // 指針指向子類 enumerable: false, // 不可枚舉 writable: true, // 可修改 configurable: true // 可配置 // 這仨熟悉吧,描述符,對象的屬性 } }); };
這個是node.js的底層源碼,由於是nodejs,因此不用去考慮瀏覽器的兼容性,直接用新玩意兒Object.create(),代碼量少,功能強大。
那麼問題來了,要考慮瀏覽器的兼容性,那啷個辦呢,來吧,帥狐show給你看。
這個模擬ExtJS源碼實現,摘自JavaScript設計模式書上的例子。
var c = console; function extend(sub, sup) { // 建立一個函數作爲中轉函數 var F = function(){}; // 把父類的原型對象複製給中轉函數的原型對象 F.prototype = sup.prototype; // 實例化的中轉函數,實現子類的原型繼承 sub.prototype = new F(); // 還原子類的構造器 sub.prototype.constructor = sub; // 定義一個靜態屬性保存父類的原型對象 sub._super = sup.prototype; // 降級操做,防止父類原型構造器指向Object if(sup.prototype.constructor == Object.prototype.constructor){ sup.prototype.constructor = sup; } }; function Person(name){ this.name = name; } Person.prototype.getName = function(){ return this.name; } function Gentleman(name,feature){ Gentleman._super.constructor.call(this, name); this.feature = feature; this.getFeature = function(){ alert(this.name+this.feature); }; } extend(Gentleman, Person); var gm = new Gentleman("飛狐","就是帥!"); gm.getFeature(); // 飛狐就是帥
這個是模擬ExtJS的繼承實現,每一步我都寫了註釋,應該仍是不難理解吧,ExtJS底層源碼作了不少擴展,這裏只是簡單展示思路。
這一回,主要過了一下類的模擬,原型,分析了下繼承在JQuery,underscore,nodejs,ExtJS的源碼實現,感受還好吧,哈哈~~
下一回,咱主要聊一聊,接口的模擬,裝飾者模式。
話說最近假裝者過了又是琅琊榜,梅長蘇又迷到了萬千少女,不知道鍾漢良的新戲啥時候上映啊,漢良哥,快快出來挑戰胡歌吖...
注:此係飛狐原創,轉載請註明出處