听飞狐聊JavaScript设计模式系列04

本回内容介绍

上一回聊到JS的Function类型,作了柯里化,数组去重,排序的题。javascript

介一回,偶们来聊一下用JS中的类,有些盆友可能用过ES6或者TypeScript的,知道Class语法糖,但是在ES5中并无,ES5中须要用到构造函数来模拟类。java

既然是类,确定要聊到继承,而聊到继承,那原型也少不了,可是,继承、原型这些知识在网上的基础讲解已经不少了。node

因此,偶就把jquery,extjs,nodejs,underscore.js的继承源码拿来作个比较分析,至于模拟接口虾米的,后面会聊滴,来吧开始咯:jquery

1. 再谈对象

重温对象,仍是先来个书上(高程3)的例子:设计模式

var o  = {
        name:"飞狐",
        age:"21",
        sayName:function(){
            alert(this.name);
        }
    };

这是以前讲的对象,要修改属性的特性,好比把name修改成不可更改,可能有的盆友说了,用以前聊过的对象冻结isFrozen()方法,是的,能够作到,可是防篡改的方法是做用对象定义以后,若是要修改属性的默认特性,就得用ES5的Object.defineProperty()方法了,数组

2. 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

3. 访问器属性getter,setter

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~

4. Object.defineProperties()方法

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);    // 飞狐帅

这里光看例子可能有点抽象,不要紧,后面讲设计模式,聊到观察者模式的时候还会聊到事件。

5. 类的模拟

(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();

能够看出,对象的方法是相同的,而重复的建立致使了定义了多个全局函数,用书上的原话,丝毫木有封装性可言,那啷个办呢,因而乎,引出了原型模式。

6. 原型模式

咱们建立的每一个函数都有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

看上去很不错了,全部对象实例共享了所包含的属性及方法,可是嘞,构造函数传递初始化参数木有了,并且由于共享,一个实例修改了引用,另外一个也随之被更改了,这样的话就能够结合原型模式与构造函数模式使用,继续下一个。

7. 组合构造函数 + 原型模式

构造函数模式用于定义实例属性,原型模式用于定义共享属性,看例子:

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

看上去很不错了,每一个实例都会有本身的一份实例属性,但同时又共享着方法,最大限度的节省了内存。

8. 动态原型模式

动态原型模式是把全部信息都封装在构造函数中,经过构造函数中初始化原型,检测该方法是否有效而选择是否须要初始化原型,直接看例子吧:

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继承的源码,来吧:

9. JQuery的extend源码分析

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的源码关于继承的段儿,我加工的注释,若是感受有难度,能够跳过,看下一个。

10. underscore.js的_.extend源码分析

_.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;
    };

是否是感受,很简单粗暴。

11. node.js的inherits源码分析

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给你看。

12. 模拟ExtJS的继承

这个模拟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的源码实现,感受还好吧,哈哈~~
下一回,咱主要聊一聊,接口的模拟,装饰者模式。

话说最近假装者过了又是琅琊榜,梅长苏又迷到了万千少女,不知道钟汉良的新戏啥时候上映啊,汉良哥,快快出来挑战胡歌吖...


注:此系飞狐原创,转载请注明出处

相关文章
相关标签/搜索