第六章:類工廠 第7章:選擇器引擎

類與繼承在javascript的出現,說明javascript已經達到大規模開發的門檻了,在以前是ECMAScript4,就試圖引入類,模塊等東西,但因爲過度引入太多的特性,搞得javascript烏煙瘴氣,致使被否決。不過只是把類延時到ES6.到目前爲止,javascript尚未正真意義上的類。不過咱們能夠模擬類,曾近一段時間,類工廠是框架的標配,本章會介紹各類類實現,方便你們在本身的框架中或選擇時本身喜歡的那一類風格。javascript

1.javascript對類的支持css

在其它語言中 ,類的實例都要經過構造函數new出來。做爲一個刻意模仿java的語言。javascript存在new操做符,而且全部函數均可以做爲構造器。構造函數與普通的方法沒有什麼區別。瀏覽器爲了構建它繁花似錦的生態圈,好比Node,Element,HTMLElement,HTMLParagraphElement,顯然使用繼承關係方便一些方法或屬性的共享,因而javascript從其它語言借鑑了原型這種機制。Prototype做爲一個特殊的對象屬性存在於每個函數上。當一個函數經過new操做符new出其「孩子」——「實例」,這個名爲實例的對象就擁有這個函數的Prototype對象全部的一切成員,從而實現實現全部實例對象都共享一組方法或屬性。而javascript所謂的「類」就是經過修改這個Prototype對象,以區別原生對象及其其它定義的「類」。在瀏覽器中,node這個類基於Object修改而來的,而Element則是基於Node,而HTMLElement又基於Element....相對咱們的工做業務,咱們能夠建立本身的類來實現重用與共享html

    function A(){

    }
    A.prototype = {
        aa:"aa",
        method:function(){
        }
    };
    var a = new A;
    var b = new A;
    console.log(a.aa === b.aa);
    console.log(a.method === b.method)

通常地,我們把定義在原型上的方法叫原型方法,它爲全部的實例所共享,這有好也有很差,爲了實現差別化,javascript容許咱們直接在構造器內指定其方法,這叫特權方法。若是是屬性,就叫特權屬性。它們每個實例一個副本,各不影響。所以,咱們一般把共享用於操做數據的方法放在原型,把私有的屬性放在特權屬性中。但放於this上,仍是讓人任意訪問到,那就放在函數體內的做用域內吧。這時它就成爲名副其實的私有屬性。java

    function A() {
        var count = 0;
        this.aa = "aa";
        this.method = function() {
            return count;
        }
        this.obj = {}
    }
    A.prototype = {
        aa:"aa",
        method:function(){

        }
    };
    var a = new A;
    var b = new A;
    console.log(a.aa === b.aa);//true 因爲aa的值爲基本類型,比較值
    console.log(a.obj === b.obj) //false 引用類型,每次進入函數體都要從新建立,所以都不同。
    console.log(a.method === b.method); //false


特權方法或屬性只是只是遮住原型的方法或屬性,所以只要刪掉特權方法,就能方法到同名的原型方法或屬性node

    delete a.method;
    delete b.method;
    console.log(a.method === A.prototype.method);//true
    console.log(a.method === b.method); //true


用java的語言來講,原型方法與特權方法都屬性實例方法,在java中還有一種叫類方法與類屬性的東西。它們用javascript來模擬也很是簡單,直接定義在函數上就好了。git

    A.method2 = function(){} //類方法
    var c = new A;
    console.log(c.method2); //undefined

接下來,咱們看下繼承的實現,上面說過,Prototype上有什麼東西,它的實例就有什麼東西,不論這個屬性是後來添加的,仍是整個Prototype都置換上去的。若是咱們將這個prototype對象置換爲另外一個類的原型,那麼它就垂手可得的得到那個類的全部原型成員。es6

    function A() {};
    A.prototype = {
        aaa : 1
    }
    function B() {};
    B.prototype =  A.prototype;
    var b = new B;
    console.log(b.aaa); //=> 1;
    A.prototype.bb = 2;
    console.log(b.bb) //=> 2;

因爲是引用着同一個對象,這意味這,咱們修改A類的原型,也等同於修該了B類的原型。所以,咱們不能把一個對象賦值給兩個類。這有兩種辦法,github

方法1:經過for in 把父類的原型成員逐一賦給子類的原型
方法2是:子類的原型不是直接由父類得到,先將父類的原型賦值給一個函數,而後將這個函數的實例做爲子類的原型。數組

方法一,咱們一般要實現mixin這樣的方法,有的書稱之爲拷貝繼承,好處就是簡單直接,壞處就是沒法經過instanceof驗證。Prototype.js的extend方法就用來幹這事。瀏覽器

    function extend (des, source) { //des = destination
        for (var property in source)
            des[property] = source[property];
        return des;
    }

方法二,就在原型上動腦筋,所以稱之爲原型繼承。下面是個範本

    function A() {};
    A.prototype = {
        aa:function(){
            alert(1)
        }
    }
    function bridge() {

    };
    bridge.prototype = A.prototype;

    function B() {}
    B.prototype = new bridge();

    var a = new A;
    var b = new B;

    console.log(a == b) //false 證實成功分開原型
    console.log(A.prototype == B.prototype) //true 子類共享父類的原型方法
    console.log(a.aa === b.aa); //爲父類動態添加新的方法
    A.prototype.bb = function () {
        alert(2)
    }
    //true,繼承父類的方法
    B.prototype.cc = function (){
        alert(3)
    }
    //false 父類未必有子類的new實例
    console.log(a.cc === b.cc)
    //而且它可以正常經過javascript自帶的驗證機制instanceof
    console.log(b instanceof A) ;//true
    console.log(b instanceof B) ; //true

方法二能經過instanceof驗證,es5就內置了這種方法來實現原型繼承,它就是Object.create,若是不考慮第二個參數,它約等於下面的代碼

    Object.create = function (o) {
        function F() {}
        F.prototype = o;
        return new F();
    }


上面的方法,要求傳入一個父類的原型做爲參數,而後返回子類的原型

不過,咱們這樣仍是遺漏了一點東西——子類不僅是繼承父類的遺產,還應該有本身的東西,此外,原型繼承並無讓子類繼承父類的成員與特權成員。這些咱們都得手動添加,如類成員,咱們能夠經過上面的extend方法,特權成員咱們能夠在子類構造器中,經過apply實現。

    function inherit(init, Parent, proto){
        function Son(){
            Parent.apply(this, argument); //先繼承父類的特權成員
            init.apply(this, argument); //在執行本身的構造器
        }
    }
    //因爲Object.create是咱們僞造的,所以避免使用第二個參數
    Son.prototype = Object.create(Parent.prototype,{});
    Son.prototype.toString = Parent.prototype.toString; //處理IEbug
    Son.prototype.valueOf = Parent.prototype.valueOf; //處理IEbug
    Son.prototype.constructor = Son; //確保構造器正常指向,而不是Object
    extend(Son, proto) ;//添加子類的特有的原型成員
    return Son;

下面,作一組實驗,測試下實例的回溯機制。當咱們訪問對象的一個屬性,那麼他先尋找其特權成員,若是有同名就返回,沒有就找原型,再沒有,就找父類的原型...咱們嘗試將它的原型臨時修改下,看它的屬性會變成那個

    function A(){

    }
    A.prototype = {
        aa:1
    }
    var a = new A;
    console.log(a.aa) ; //=>1

    //將它的全部原型都替換掉
    A.prototype = {
        aa:2
    }
    console.log(a.aa); //=>1

    //因而咱們想到每一個實例都有一個constructor方法,指向其構造器
    //而構造器上面正好有咱們的原型,javascript引擎是否是經過該路線回溯屬性呢
    function B(){

    }
    B.prototype = {
        aa:3
    }
    a.constructor = B;
    console.log(a.aa) //1 表示不受影響


所以類的實例確定經過另外一條通道進行回溯,翻看ecma規範可知每個對象都有一個內部屬性[[prototype]],它保存這咱們new它時的構造器所引用的Prototype對象。在標準瀏覽器與IE11裏,它暴露了一個叫__proto__屬性來訪問它。所以,只要不動__proto__上面的代碼怎麼動,a.aa始終堅決不毅的返回1.

再看一下,new時操做發生了什麼。

1.建立了一個空對象 instance
2.instance.__proto__ = intanceClass.prototype
3.將構造函數裏面的this = instance
4.執行構造函數裏的代碼
5.斷定有沒有返回值,沒有返回值就返回默認值爲undefined,若是返回值爲複合數據類型,則直接返回,不然返回this
因而有了下面的結果。

    function A(){
        console.log(this.__proto__.aa); //1
        this.aa = 2
    }
    A.prototype = {aa:1}
    var a = new A;
    console.log(a.aa)
    a.__proto__ = {
        aa:3
    }
    console.log(a.aa) //=>2
    delete a. aa; //刪除特權屬性,暴露原型鏈上的同名屬性
    console.log(a.aa) //=>3

有了__proto__,咱們能夠將原型設計繼承設計得更簡單,咱們仍是拿上面的例子改一改,進行試驗

    function A() {}
    A.prototype = {
        aa:1
    }
    function bridge() {}
    bridge.prototype = A.prototype;

    function B(){}
    B.prototype = new bridge();
    B.prototype.constructor = B;
    var b = new B;
    B.prototype.cc = function(){
        alert(3)
    }
    //String.prototype === new String().__proto__  => true
    console.log(B.prototype.__proto__ === A.prototype) //true
    console.log(b.__proto__ == B.prototype); //true 
    console.log(b.__proto__.__proto__ === A.prototype); //true 獲得父類的原型對象

由於b.__proto__.constructor爲B,而B的原型是從bridge中得來的,而bride.prototype = A.prototype,反過來,咱們在定義時,B.prototype.__proto__ = A.prototype,就能輕鬆實現兩個類的繼承.

__proto__屬性已經加入es6,所以能夠經過防止大膽的使用

2.各類類工廠的實現。

上節咱們演示了各類繼承方式的實現,但都很凌亂。咱們但願提供一個專門的方法,只要用戶傳入相應的參數,或按照必定簡單格式就能建立一個類。特別是子類


因爲主流框架的類工廠太依賴他們龐雜的工具函數,而一個精巧的類工廠也不過百行左右

至關精巧的庫,P.js

https://github.com/jiayi2/pjs

使用版:https://github.com/jiayi2/factoryjs

這是一個至關精巧的庫,尤爲調用父類的同名方法時,它直接將父類的原型拋在你面前,連_super也省了

    var P = (function(prototype, ownProperty, undefined) {
  return function P(_superclass /* = Object */, definition) {
    // handle the case where no superclass is given
    if (definition === undefined) {
      definition = _superclass;
      _superclass = Object;
    }

    // C is the class to be returned.
    //
    // When called, creates and initializes an instance of C, unless
    // `this` is already an instance of C, then just initializes `this`;
    // either way, returns the instance of C that was initialized.
    //
    //  TODO: the Chrome inspector shows all created objects as `C`
    //        rather than `Object`.  Setting the .name property seems to
    //        have no effect.  Is there a way to override this behavior?
    function C() {
      var self = this instanceof C ? this : new Bare;
      self.init.apply(self, arguments);
      return self;
    }

    // C.Bare is a class with a noop constructor.  Its prototype will be
    // the same as C, so that instances of C.Bare are instances of C.
    // `new MyClass.Bare` then creates new instances of C without
    // calling .init().
    function Bare() {}
    C.Bare = Bare;

    // Extend the prototype chain: first use Bare to create an
    // uninitialized instance of the superclass, then set up Bare
    // to create instances of this class.
    var _super = Bare[prototype] = _superclass[prototype];
    var proto = Bare[prototype] = C[prototype] = C.p = new Bare;

    // pre-declaring the iteration variable for the loop below to save
    // a `var` keyword after minification
    var key;

    // set the constructor property on the prototype, for convenience
    proto.constructor = C;

    C.extend = function(def) { return P(C, def); }

    return (C.open = function(def) {
      if (typeof def === 'function') {
        // call the defining function with all the arguments you need
        // extensions captures the return value.
        def = def.call(C, proto, _super, C, _superclass);
      }

      // ...and extend it
      if (typeof def === 'object') {
        for (key in def) {
          if (ownProperty.call(def, key)) {
            proto[key] = def[key];
          }
        }
      }

      // if no init, assume we're inheriting from a non-Pjs class, so
      // default to using the superclass constructor.
      if (!('init' in proto)) proto.init = _superclass;

      return C;
    })(definition);
  }

  // as a minifier optimization, we've closured in a few helper functions
  // and the string 'prototype' (C[p] is much shorter than C.prototype)
})('prototype', ({}).hasOwnProperty);

咱們嘗試建立一個類:

    var Dog = P (function(proto, superProto){
        proto.init = function(name) { //構造函數
            this.name = name;
        }
        proto.move = function(meters){ //原型方法
            console.log(this.name + " moved " + meters + " m.")
        }
    });
    var a = new Dog("aaa")
    var b = new Dog("bbb"); //無實例變化
    a.move(1);
    b.move(2);

咱們在如今的狀況下,能夠嘗試建立更簡潔的定義方式

    var Animal = P (function(proto, superProto){
        proto.init = function(name) { //構造函數
            this.name = name;
        }
        proto.move = function(meters){ //原型方法
            console.log(this.name + " moved " + meters + " m.")
        }
    });
    var a = new Animal("aaa")
    var b = new Animal("bbb"); //無實例變化
    a.move(1);
    b.move(2);
    //...............
    var Snake = P (Animal, function(snake, animal){
        snake.init = function(name, eyes){
            animal.init.call(this, arguments); //調運父類構造器
            this.eyes = 2;
        }
        snake.move = function() {
            console.log('slithering...');
            animal.move.call(this, 5); //調運父類同名方法
        }
    });
    var s = new Snake("snake", 1);
    s.move();
    console.log(s.name);
    console.log(s.eyes);

私有屬性演示,因爲放在函數體內集中定義,所以安全可靠!

    var Cobra = P (Snake, function(cobra){
        var age = 1;//私有屬性
        //這裏還能夠編寫私有方法
        cobra.glow = function(){ //長大
            return age++;
        }
    });
    var c = new Cobra("cobra");
    console.log(c.glow()); //1
    console.log(c.glow()); //2
    console.log(c.glow()); //3

JS.Class

從它的設計來看,讓是繼承Base2,類似的類工廠還有mootools。
Base2的base2.__prototyping, mootools的klass.$protyping。它建立子類時頁不經過中間的函數斷開雙方的原型鏈,而是使用父類的實例來作子類的原型,這點實現的很是精巧。

simple-inheritance

做者爲john Resig ,特色是方法鏈實現的十分優雅,節儉。

體現javascript的靈活性的庫 def.js

若是有什麼庫能體現javascript的靈活性,此庫確定名列前茅。它試圖在形式上模擬Ruby的繼承形式。讓使用過ruby的人一眼看出,那個是父類,那個是子類。

下面就是Ruby的繼承示例:

    class child < Father
    #
    end

def.js能作到這個程度

    def("Animal")({
        init:function(name){
            this.name = name;
        },
        speak:function(text){
            console.log('this is a' + this.name)
        }
    });
    var animal = new Animal("Animal");
    console.log(animal.name)

    def('Dog') < Animal({
        init:function(name,age){
            this._super();//魔術般的調運了父類
            this.age = age;
        },
        run:function(s){
            console.log(s)
        }
    });
    var dog = new Dog('wangwang');
    console.log(dog.name); //wangwang

    //在命名空間上建立子類
    var namespace = {};
    def(namespace,"Shepherd") < Dog({
        init:function(){
            this._super();
        }
    });
    var shepherd = new namespace.Shepherd("Shepherd")
    console.log(shepherd.name); 

 

3.es5屬性描述符對oo庫的衝擊

es5最受人矚目的升級是爲對象引入屬性描述符,讓咱們對屬性有了更精細的控制,如這個屬性是否能夠修改,是否能夠在for in中循環出來,是否能夠刪除。這些新增的API都集中定義在Object下,基本上除了Object.keys這個方法外,其它新API,舊版本的IE都沒法模擬。因而新的API,基本不多有講解的,咱們在這裏稍微解讀下:

Obejct提供如下幾種新方法。

Object.keys
Object.getOwnPropertyNames
Object.getPrototypeOf
Object.defineProperty
Object.defineProperties
Object.getOwnPropertyDescriptor
Object.create
Object.seal
Object.freeze
Object.preventExtensions
Object.isSealed
Object.isFrozen
Object.isExtensible

其中,除了Object.keys外,舊版本的IE都沒法模擬這些新API。舊版式的標準瀏覽器,能夠用__peototype__實現Object.getPrototypeOf,結合__defineGetter__與defineSetter__來模擬Object.defineProperty。

Obejct.keys用於收集當前對象的可遍歷屬性(不包括原型鏈上的)以數組形式返回。

Object.getOwnPropertyNames用於收集當前對象不可遍歷屬性與可遍歷屬性,以數組形式返回。

    var obj = {
        aa : 1,
        toString : function() {
            return "1"
        }
    }
    if (Object.defineProperty && Object.seal) {
        Object.defineProperty(obj,"name",{
            value:2
        })
    }
    console.log(Object.getOwnPropertyNames(obj)); //=> ["aa", "toString", "name"]
    console.log(Object.keys(obj));//=> ["aa", "toString"]

    function fn(aa, bb){};
    console.log(Object.getOwnPropertyNames(fn));// => ["length", "name", "arguments", "caller", "prototype"]
    console.log(Object.keys(fn));//[]
    var reg = /\w{2,}/i;
    console.log(Object.getOwnPropertyNames(reg)); //=> ["source", "global", "ignoreCase", "multiline", "lastIndex"]
    console.log(Object.keys(reg));//[]

Object.prototypeOf返回參數對象內部屬性[[Prototype]],它在標準瀏覽器中一直使用一個私有屬性__proto__獲取(IE9 10,opera都沒有)。須要補充一下,Object的新API(除了Object.create外)有一個統一的規定,要求第一個參數不能爲數字 ,字符串,布爾,null,undefeind這五種字面量,不然拋出TypeError異常。

    console.log(Object.getPrototypeOf(function(){}) == Function.prototype ); //=>true
    console.log(Object.getPrototypeOf({}) === Object.prototype);//=>true

Object.definePrototype暴露了屬性描述的接口,以前許多內建屬性都是由JavaScript引擎在屬下操做。如,for in循環爲什麼不能遍歷出函數的arguments、length、name等屬性名,delete window.a爲什麼返回false. 這些現象終究有個解釋。它一共涉及六個可組合的配置項。
是否可重寫writable,當前值value
讀取時內部調用的函數set
寫入時內部調用函數get,
是否可遍歷enumerable,
是否能夠再次改動這些配置項configurable.

好比咱們隨便寫個對象

var obj = {x:1}

有了屬性描述符,咱們就清楚它在底下作的更多細節,它至關於es5的這個建立對象的式子:

    var obj = Object.create(Object.prototype,{
        x : {
            value : 1,
            writable : true,
            enumerable : true,
            configurable : true
        }
    })

效果對比es3和es5,就很快明白,曾經的[[ReadOnly]] , [[DontEnum]], [[DontDlelete]]改爲[[writable]], [[enumerable]],[[Configurable]]了。所以,configurable還有兼顧可否刪除的職能

這六個配置項將原有的本地屬性拆分爲兩組。數據屬性與訪問器屬性。咱們以前的方法能夠像數據屬性那樣定義。

es3時代,咱們的自定義類的屬性能夠通通看作是數據屬性。

像DOM中的元素節點的 innerHTML innerText cssText 數組的length則可歸爲訪問器屬性,對它們賦值不是單純的賦值,還會引起元素其它功能的觸發,而取值不必定直接返回咱們以前給予的值。

數據的屬性有一、二、五、6這四個配置項,訪問器有三、四、五、6這四個配置項、若是你設置了value與writable,就不能設置set,get,反之亦然。若是沒有設置。2,3,4默認爲false。第1,5,6項默認爲false.

 關於對象屬性特徵,更多請參閱http://www.cnblogs.com/ahthw/p/4272663.html 第7小節:7.屬性的特徵

var obj = {};
    Object.defineProperty(obj,"a",{
        value: 37,
        writable :true,
        enumerable :true,
        configurable: true
    });

    console.log(obj.a) //=> 37;
    obj.a = 40;
    console.log(obj.a) //=>40
    var name = "xxx";
    for(var i in obj){
        name = i
    }
    console.log(name);//=> a

    Object.defineProperty(obj,"a",{
        value:55,
        writable:false,
        enumerable:false,
        configurable:true
    })

    console.obj(obj.a);//=>55
    obj.a = 50;

    console.log(obj.a);//55
    name = "b";
    for (var i in obj){
        name = i
    }
    console.log(name); //b

Object.defineProperties就是Object.defineProperty的增強版,它能一會兒處理多個屬性。所以,若是你能模擬Object.defineProperty,它就不是問題。

    if (typeof Object.defineProperties !== 'function'){
        Object.defineProperties = function(obj, descs){
            for(var prop in descs) {
                if (descs.hasOwnProperty(porop)){
                    Object.defineProperty(obj, prop, descs[prop]);
                }
            }
            return obj;
        }
    }

使用示例

    var obj = {};
    Object.defineProperties(obj, {
        "value":{
            value :true,
            writable:false,
        },
        "name":{
            value:"John",
            writable:false
        }
    });
    var a = 1;
    for (var p in obj){
        a = p
    };
    console.log(a);// 1

Object.getOwnPropertDescriptor用於得到某對象的本地屬性的配置對象。其中,configurable,enumerable確定包含其中。視狀況再包括value,wirtable或set,get.
....
Object.preventExtensions,它是三個封鎖對象修改的API中程度最輕的,就是阻止添加本地屬性,不過若是本地屬性都被刪除了,也沒法再加回來。之前javascript對象的屬性都是任意添加的,刪除,修改其值。若是它原型改動。咱們訪問它還會有意外的驚喜。

var a = {
        aa: "aa"
    }
    Object.preventExtensions(a)
    a.bb = 2;
    console.log(a.bb);//=> undefined
    a.aa = 3; 
    console.log(a.aa); //=>3 容許修改原有的屬性
    delete a.aa;
    console.log(a.aa); //=> undefined 但容許它刪除已有的屬性

    Object.prototype.ccc = 4;
    console.log(a.ccc); //4 不能阻止添加原型屬性
    a.aa = 5;
    console.log(a.aa); //=> dundeined 不吃回頭草,估計裏邊是以白名單的方式實現的

Object.seal比Object.preventExtensions更嚴格,它不許刪除已有的本地屬性,內部實現就是遍歷一下,把本地屬性的configurable改成false

var a = {
        a : "aa"
    }
    Object.seal(a)
    a.bb = 2;
    console.log(a.bb); // =>undefined添加本地屬性失敗
    a.aa = 3;
    console.log(a.aa); //3 容許修改已有的屬性
    delete a.aa;
    console.log(a.aa) ;//=>3 但不容許刪除已有的屬性

Object.freeze無疑是最專制的(所以有人說過程式程序很專制,OO程序則自由些,顯然道格拉斯的ecma262v5想把javascript引向前者),它連原有的本地屬性也不讓修改了。內部的實現就是遍歷一下,把每一個本地屬性的writable也改爲false.

    var a = {
        aa : "aa"
    };
    Object.freeze(a);
    a.bb = 2;
    console.log(a.bb) //undefined 添加本地屬性失敗
    a.aa =3;
    console.log(a.aa) //aa 不容許它修改已有的屬性
    delete a.aa;
    console.log(a.aa); //aa 不容許刪除已經有的屬性

(isPainObject用於斷定目標是否是純淨的javascript對象,且不是其它自定義類的實例。用法與prototype.js的class.create同樣,並參照 jQuery UI提供了完美的方法鏈與靜態成員的繼承。)

總結:es5對javascript對象產生深入的影響,Object.create讓原型繼承更方便了,但在增添的字類的專有原型成員或類成員時,若是它們的屬性enumerable爲false,單純的for in循環已經無論用了,完美就要用到Object.getOwnPropertyNames。另外,訪問器屬性的複製只有經過Object.getOwnPropertyDescriptpor與Object.defineProperty才能完成

 

(本章完結

上一章:第五章:瀏覽器的嗅探和特徵偵測  下一章: 第7章:選擇器引擎

相關文章
相關標籤/搜索