一、前言

惰性十足,这篇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);

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

睡觉吧,要收收心了。

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/echolun/p/10404011.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!

相关课程