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

本回内容介绍

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

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

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

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

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

怎么样,配上注释,应该不难理解吧。

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的书上还介绍有寄生构造函数模式,稳妥构造函数模式,这里我们不一一列举,咱现在有个大概的理解了,后面就可以继续装逼继续飞了。

《听飞狐聊JavaScript设计模式系列04》
这一回聊的有点儿多,先装个逼,话说薛之谦最近有首新歌不错哟,歌名《绅士》。

这一回讲的内容比较绕脑袋,下面的内容会更绕脑袋,哈哈~~,不过没关系,还是那句话,一时理解不了也没关系,先囫囵吞枣,后面的内容还会涉及,
在高程的书上讲继承,讲了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的源码实现,感觉还好吧,哈哈~~
下一回,咱主要聊一聊,接口的模拟,装饰者模式。

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

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

    原文作者:设计模式
    原文地址: https://segmentfault.com/a/1190000003923770
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞