• 精读JavaScript模式(七),命名空间模式,私有成员与静态成员


    一、前言

    惰性十足,这篇2月19号就开始写了,拖到了现在,就是不愿意花时间把看过的东西整理一下,其它的任何事都比写博客要有吸引力,我要反省自己。

    从这篇开始,是关于JS对象创建模式的探讨,JS语言简单直观,并没有模块,包,私有属性,静态成员等语法特性。而这一大章将介绍一些有用的模式,例如命名空间,依赖声明,模块模式以及沙箱模式等。这些能帮助我们更好的组织代码,减轻全局污染问题。

    二、命名空间模式(Namespace Pattern)

    命名空间可以减少全局变量的数量,还能有效避免命名冲突以及名称前缀的滥用,命名是个很头疼的事情,我想大家都有这种情况,命名到词穷。

    JS默认语法是不支持命名空间,不过很好实现,命名空间很实用,对于类库,应用,插件编写,我们都可以为其创建一个全局对象,然后将所有的功能都添加到这个对象上,而不是到处申明大量的全局函数,对象等,这样看着很乱。

    const MYAPP = {};
    MYAPP.Parent = function () {};
    MYAPP.number = 4;
    MYAPP.modules = {};
    MYAPP.modules.data = {};

    上述代码中MYAPP就是命名空间对象,名称随意取,但是通常采用大写,还需要注意的是,一般大写的变量都表示常量

    很明显这样的写法在命名冲突上可能性就大大降低了,但是也存在一些问题,例如在MYAPP.modules.data这里代码量就明显增加了,整体会增加文件的大小,其次,在获取某个方法会属性时,得从MYAPP一层层往下读,读取较慢。而且该全局实例可能被误操作修改,虽然我们理论上说了这是常量不该被修改。

    三、通用命名空间函数(思想是好的,但很鸡肋)

    有个问题,当程序的复杂性提升我们很难保证命名空间的创建是否已存在,不小心修改了已存在的变量是很麻烦的事情,因此创建前的检查行为是更为安全的。

    var MYAPP = MYAPP || {}

    这个知识点后续内容我选择跳过了,原书中的观点是对于命名空间的创建最好做个检查,然后提供了一个通用的命名空间检测检查函数,我经过了测试,发现提供的函数永远返回一个空对象,并没达到预期的检查效果;其次,我认为创建一个对象每次都要检测真的过于繁琐,即便是封装一个检查函数,我还得调用。例如a.b.c.d.e,我不可能对于每层都做一个检测是否存在,安全创建思想是好的,但个人觉得过于鸡肋了。

     四、对象的私有属性和方法

    JS并没有专门提供保护私有成员,方法的语法,我们在全局创建一个对象,是可以轻易访问到对象的所有属性的。

    let obj = {
      num:1,
      getNum:function () {
        console.log(this.num);
      }
    };
    //在函数外部可以轻易访问
    console.log(obj.num);//1
    obj.getNum();//1

    即便是构造函数,也是如此。

    // 构造函数
    function GetNum() {
      this.num = 2;
      this.getNum = function () {
        console.log(this.num); 
      }
    }
    let Num = new GetNum();
    console.log(Num.num);//2
    Num.getNum();//2

    如何做到对象属性私有化,我们可以使用闭包做到这一点,只有闭包内部函数才可以访问到内部变量,外部无法直接访问。

    function GetNum() {
      let num = 3;
      this.getNum = function () {
        console.log(num);
      }
    }
    let Num = new GetNum();
    console.log(Num.num);//undefined
    Num.getNum();//3

    除了调用getNum方法以外,我们并不能直接访问到num变量,所以一般我们称getNum方法为特权方法,因为它拥有访问num属性的特殊权限。

     来聊聊特权方法权限的问题,假设我们的闭包返回的是一个对象,而非一个字符串。通过特权方法可以修改影响到闭包内部的本地变量。

    function BoxInfo() {
      let boxSize = {
        widht:200,
        height:300,
        color:'yellow'
      };
      this.getBox = function () {
        return boxSize;
      }
    }
    //实例一个对象得到box1
    let box1 = new BoxInfo(),
      size = box1.getBox();
    //我们修改size的颜色
    size.color = 'bule';
    //再取一次size信息,可以看到size颜色已被修改
    size1 = box1.getBox();
    console.log(size1)//{widht: 200, height: 300, color: "bule"}
    //如果在修改后你想取到没修改的初始数据,你只能再次new一个实例
    let box2 = new BoxInfo(),
      size2 = box2.getBox();
    console.log(size2);//{widht: 200, height: 300, color: "yellow"}

    取得实例修改颜色,后续再读取对象发现颜色已改变,这是肯定的,毕竟对象的赋值只是赋予了值的引用地址而非值本身,这种随意修改数据的做法不太安全,针对这个问题,我们可以使用“最低授权原则”,永远不要给出比需求更多的东西。

    比如需求是要访问boxSize的height与color属性,那么特权方法不再是可以访问整个对象,而是只能访问到长与颜色属性,像这样:

    this.getBox = function () {
      return {
        height:boxSize.height,
        color:boxSize.color
      }
    }

    我们只提供需求需要的属性,拼装为全新的对象返回,后续无论你怎么修改,我们永远得到的是最初的原始数据。

    或者,当我们第一次得到box1实例时,深拷贝一份,作为原属性不再动用它,那另一份数据就随便你玩了。“最低授权原则”这个思想我觉得还是蛮不错的。

     除了通过构造函数创建私有成员外,我们也可以通过对象字面量结合自调函数来达到目的。

    (function () {
        var name = "时间跳跃";
        myobj = {
            getName : function () {
                return name;
            }
        }
    })();
    let myName = myobj.getName();
    console.log(myName);//时间跳跃

    这种实现方式就是通过自调函数创建了一个独立的作用域,外部无法访问,但是可以通过函数内部的对象访问到私有属性name,思想上是差不多的。

    五、原型和私有成员(属性)

    使用构造函数创建私有成员有个弊端,或者说使用构造函数创建实例时都会存在的弊端,每当调用一次构造函数,私有成员都会被创建一次。

    这是因为每次new一个构造函数,都隐性的创建了一个空对象赋予给this,然后复制构造函数this上的属性方法,最终返回this,这点在精读JS模式三这篇文章的第四个知识点有说,有疑惑可以去看看。

    同理,哪怕是在创造私有成员时,如果这个成员很多地方都会用到,那就没必要加载构造函数中被反复创建,直接将此成员添加在prototype上。

    function Mine() {
        let name = "echo";
        this.getName = function () {
            console.log(name);
        };
    };
    //假设age属性每个实例都需要使用,就不要加在上方构造函数了,每次new都要创建,没必要
    Mine.prototype = (function () {
        var age = 26;
        return {
            getAge : function () {
                console.log(age);
            }
        };
    })();
    let me = new Mine();
    me.getName();//echo
    me.getAge();//26

     六、静态成员(属性和方法)

    1.构造函数的静态方法

    当我们希望某个方法只有构造函数自身可以使用,实例无法继承使用,此方法就应该使用静态方法。

    而在JS中并没有专门创建静态成员的语法,但我们可以通过构造函数添加属性的方法来添加静态方法。

    let Func = function () {};
    //这是func的静态方法
    Func.myName = function () {
        console.log('My name is echo');
    };
    //这是func的实例方法
    Func.prototype.myAge = function () {
        console.log('My age is 26');
    };
    Func.myName();
    let me = new Func();
    me.myAge();

    在上述代码中,我为函数Func添加了一个静态方法myName和一个实例方法myAge。

    myName方法之所以是静态方法是因为Func函数可以直接调用,它不需要指定一个对象去调用它,也不需要实例调用。但myAge方法则需要实例调用。当然相对的,函数Fcun无法直接调用实例方法,就像实例无法直接调用静态方法。

    Func.myAge()//无法找到
    me.myName()//无法找到

     当然我们也可以将静态方法添加在原型链上,像这样(其实看到这里,我所理解的静态方法就是直接添加在函数上的方法,照常理说实例是无法使用的)

    Func.prototype.myName = Func.myName
    let me = new Func();
    me.myName()//My name is echo

    但区别在于,通过Func调用myName函数时,函数this指向Func函数,但通过后者实例调用时,this指向了实例me,这是有区别的。

    2.构造函数的静态属性与私有静态属性

    静态属性添加与静态方法相同,直接添加在构造函数上。

    let Parent = function () {};
    //静态方法
    Parent.sayAge = function () {
        console.log(this.age);
    };
    //静态属性
    Parent.age = 26;
    Parent.sayAge()//26

    什么是私有静态属性呢?有两大特点,第一,此属性在所有由同一构造函数创建的对象中可共享;第二,不允许在构造函数外部访问。

    let KissMe = (function() {
      let counter = 0;
      return function() {
        console.log(counter += 1);
      };
    })();
    console.log(KissMe);
    let one = new KissMe();//1
    let two = new KissMe();//2
    let three = new KissMe();//3

    上述代码中,我定义了一个记录亲吻我(实例)次数的构造函数,其中变量counter外部无法访问,且三次调用得到的实例共享counter,因为第二次调用时counter已经变成了1而非0,那么我们可以说counter就是一个私有的静态属性。

    仔细看代码,其实就是一个构造函数被包裹在了一个自调函数中,去掉外层自调函数来看,这个实现的本质就是一个全局变量counter以及一个使用此变量的函数。

    let counter = 0;
    let KissMe = function() {
      console.log((counter += 1));
    };
    console.log(KissMe);
    let one = KissMe(); //1
    let two = KissMe(); //2
    let three = KissMe(); //3

    有没有发现,假设我们想知道一个构造函数被new了多少次,或者想知道这个实例是构造函数的第几个孩子,这个简单的实现就能计算出次数。(也许真的会用到)

    上面自调函数的例子说是构造函数其实有点牵强,毕竟我们new KissMe的时候函数都已经执行完毕了,都没有通过实例调用方法的机会了,所以我们改改代码。结果还是一样,只是更像构造函数模式了。

    另外,私有成员和私有静态成员的区别是,私有成员在每次实例中都是一个新的,并不会共享,很明显私有静态成员第二个实例受到了第一个实例调用时的影响。

    let KissMe = (function() {
      let counter = 0,
        getNum = function() {
          counter += 1;
        };
      getNum.prototype.getLastId = function() {
        console.log(counter);
      };
      return getNum;
    })();
    
    let one = new KissMe();
    one.getLastId();//1
    let two = new KissMe();
    one.getLastId();//2
    let three = new KissMe();
    one.getLastId();//3

    通过上面的例子我们可以看到,静态属性(公有或私有)可以包含和实例无关的方法或数据,创建实例时,这些私有属性不会被反复创建,但实例却可以使用,我感觉与原型链继承比较像,但与原型链添加方法的不同在于,最终执行时this指向不同,前面举例有说。

    七、有趣的对象链式调用模式

    假设我们需要连续调用一个对象上的多个方法,且操作的数据有所关联,我们就可以在每次调用时,直接将this作为函数调用的返回值,从而避免每次调用返回值作为下次调用函数参数的繁琐。

    let obj = {
      value: 1,
      plus: function(a) {
        this.value += a;
        return this;
      },
      reduce: function() {
        this.value -= 1;
        return this;
      },
      multiply: function() {
        this.value *= 2;
        console.log(this.value);
      }
    };
    obj.plus(3).reduce().multiply();//6

    这个挺像promise链式结构的写法,每次promise的执行都返回一个新的promise对象,这里就是每次返回了this,因为操作的全是this,这个就不多说了。

    使用链式调用模式很明显能节约代码量,其实阅读起来更像一个句子,更容易将函数之间的调用关联起来。其实这种模式非常常见,比如我们常用的JQ获取DOM的写法:

    document.getElementById("#echo").appendChild(new node);

     那么到这里,第五章的内容大概就看完了,其实博客中我省略了比较多的东西,比如沙箱模式,模块模式等,在看之前我还是有所期待的,但在实际阅读中,这几个知识点的收货是极少的,一方面是例子难懂以及存在错误,其实可能我个人境界还不是太高,看了也无法立刻在实际开发中实践出来,所以更多是记录了一些我个人觉得有意义的东西,哪怕是多知道了一句概念。

    睡觉吧,要收收心了。

  • 相关阅读:
    20175226 2018-2019-2 《Java程序设计》第二周学习总结
    存储管理-页面置换算法(页面淘汰算法)
    存储管理-存储组织
    进程管理-死锁问题
    操作系统-进程管理
    操作系统:进程管理、存储管理、文件管理、作业管理、设备管理
    第十一章 集合框架
    匿名内部类
    第10章 java常用类
    第8章 反射
  • 原文地址:https://www.cnblogs.com/echolun/p/10404011.html
Copyright © 2020-2023  润新知