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