《javascript设计模式》读书笔记四:对象创建模式

JavaScript 是一种简洁明了的语言,其中并没有在其他语言经常使用的一些特殊语法的特征:命名空间,模块,包,私有属性,以及静态成员等。让我们用JavaScript来实现。

命名空间模式

命名空间有助于减少程序中所需要的全局变量的数量,并且同时还哟助于避免命名冲突或者过长的名字前缀。

可以为应用程序或者库创建一个全局对象,然后将所有的功能添加到全局对象中,从而在具有大量函数,对象和其他变量的情况下并不会污染全局范围。

例如:

1
2
3
4
5
6
7
8
9
10
// 构造函数
function add1() {};

// 变量
var some_var = "shud";

// 对象
var module = {};
module.data = { a:'a', b: 'b'};

这种方式将会导致很多全局变量。

1
2
3
4
5
6
// 全局变量
var MYAPP = {};
MYAPP.dd1= {};
MYAPP.some_var = "shud";
MYAPP.module = {};
module.module.data = { a:'a', b: 'b'};

这就避免了代码中的命名冲突。推荐使用这种方式。

缺点

  1. 需要输入更多的字符,每个变量都需要添加前缀。
  2. 任何闭门的代码都可以修改全局实例。
  3. 长嵌套命名意味着的属性查询时间。

1.通用命名空间函数

由于程序的复杂性。上面的做法已经变得不再健全。添加到命名空间中的属性可能已经存在,这将导致覆盖它们。因此在添一个属性或者创建一个命名空间之前,最好先检查它是否已经存在。

1
2
3
4
5
6
7
8
9
// 不健全的代码
var MYAPP = {}

// 更好的代码风格
if ( typeof MYAPP === 'undefined') {
var MYAPP = {};
}
// 或者更短的语句
var MYAPP = MYAPP || {};

但是这样每次检查读要针对一个对象或者属性。检查代码量太大。这就需要一个通用命名空间函数。

1
2
3
4
5
6
7
8
9
// 使用命名空间函数
MYAPP.namespace('MYAPP.modules.module2')

// 相当于如下代码
// var MYAPP = {
// modules: {
// module2: {}
// }
// }

一个命名空间函数的实现事例。这个实现是肥破坏性的,也就是说,,如果已经存在一个命名空间, 将不会再重新创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var MYAPP = MYAPP || {};
MYAPP.namespace = function(ns_string) {
var parts = ns_string.splict('.'),
parent = MYAPP,
i,
// 剥离最前面的多余的全局变量
if (parent[0] === 'MYAPP') {
parent = parts.slice(1);
}
for (i = 0; i < parts.length; i += 1) {
if (typeof parent[parts[i]] === 'undefined') {
parent[parts[i]] = {};
}
parent = parent[parts[i]]
}
return parent;
}

声明依赖关系

JavaScript库通常是模块化且根据命名空间组织的,这时我们能够仅包含所需的模块。它有很多优点:

  • 显式的依赖声明向您代码的用户表明了他们所需要的特定的脚本文件。
  • 在函数的顶部声明可以很容易的发现和解析依赖。
  • 解析局部变量的速度要比解析全局变量的速度要快。
  • 声明依赖在打包之后可以有更小的代码量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function test1() {
console.log(MYAPP.modules.m1);
console.log(MYAPP.modules.m2);
console.log(MYAPP.modules.m3)
console.log(MYAPP.modules.m4);
console.log(MYAPP.modules.m5);
}
// 缩减的test1
function test1() {
const modules = MYAPP.modules
console.log(modules.m1);
console.log(modules.m2);
console.log(modules.m3)
console.log(modules.m4);
console.log(MYAPPmodulesm5);
}

私有属性和方法

JavaScript并没有特殊的语法来表示私有,保护,或者公共属性和方法。在JavaScript中所有对象的成员都是公共的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 字面量的方式构造对象
var myObj = {
name: 'shuliqi',
age: 20,
getNameAndAge: function() {
console.log('1111');
}
}
console.log(myObj.name + ':' + myObj.age) // name 和 age 可以公公访问的
console.log(myObj.getNameAndAge()) // myobj.getNameAndAge也是可以公公访问的

// 构造函数创建对象
function myObj2() {
this.name = 'shuliqi';
this.age = 20;
this.getNameAndAge = function() {
console.log('1111');
}
}
var newObj = new myObj2();
console.log(newObj.name + ':' + newObj.age) // name 和 age 可以公公访问的
console.log(newObj.getNameAndAge()) // myobj.getNameAndAge也是可以公公访问的

构造函数实现私有成员

可以使用JavaScript的闭包来实现私有成员的功能。构造函数创建了一个闭包,而在这些闭包范围内部的任意变量都不会暴露给构造函数以外的代码。然而这些私有变量仍然可以用于公共方法中。

1
2
3
4
5
6
7
8
9
10
11
12
function myFun() {
// 私有成员
var name = "shuliqi";
this.getName = function() {
return name;
}
}
var newMyFun = new myFun();
// name 是私有的
console.log(newMyFun.name); // undefined
// getName 方法是公共的
console.log(newMyFun.getName()); // shuliqi

所以只需要在函数中将需要保持为私有属性的数据包装起来,并确保它对函数来说是局部变量就可以实现私有成员。

构造函数私有性失效

如果从一个特权方法中返回一个私有变量。且该变量是一个对象或者数组,那么外面的方法仍然可以访问修改该私有变量,因为它是通过引用传递的。

1
2
3
4
5
6
7
8
9
10
11
12
13
function myFun() {
var obj = {
name: "shu",
age: 20
}
this.getObj = function() {
return obj;
}
}
var newFun = new myFun();
var obj = newFun.getObj();
obj.shu = "hahah";
console.log(newFun.getObj()); // {name: "shu", age: 20, shu: "hahah"}

解决的办法构造函数里面的特权方法不要传递需要有私有性的对象和数组的引用,而是传递克隆的对象或者数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function myFun() {
var obj = {
name: "shu",
age: 20
}
this.getObj = function() {
// es6方式克隆
var newObj = Object.assign({}, obj);
return newObj;
}
}
var newFun = new myFun();
var obj = newFun.getObj();
obj.shu = "hahah";
console.log(newFun.getObj()); // {name: "shu", age: 20 } 不可以修改

对象字面量实现私有性

实现私有性,需要的只是一个能够包装私有数据的函数,因此,在使用对象字面量的情况下,可以使用额外的匿名函数创建闭包来实现私有性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj;
(function (){
// 私有成员
var name = "shuliqi";
// 实现公共部分
// 注意这里没有使用var字符
obj = {
getName: function() {
return name;
}
}
}())
console.log(obj.name) // 'undefined' name 是私有的
console.log(obj.getName()) // 'shuliqi' 特权方法是公共的

另外的一种写法

1
2
3
4
5
6
7
8
9
10
11
var obj = (function() {
// 私有成员
var name = "shuliqi";
return {
getName: function() {
return name;
}
}
}())
console.log(obj.name) // 'undefined' name 是私有的
console.log(obj.getName()) // 'shuliqi' 特权方法是公共的

原型和私有性

当私有成员与构造函数一起使用时,其中的一个缺点就是每次调用构造函数以创建对象时,这些私有成员都会被重新创建。构造函数中任何添加到this的任何成员都会有这种情况。为了避免这样重复的工作。可以把常用的方法和属性添加到构造函数的prototype属性中。这样就可以通过一个构造函数创建的多个实例共享常见的的部分数据。还可以在多个实例中共享隐藏的私有成员。即构造函数中的私有属性和对象字面量的私有属性。由于prototype仅仅是一个对象。所以可以使用对象字面量来创建它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var myObj = function () {
// 私有属性
var name = "shuliqi";
this.getName = function () {
return name;
}
}
myObj.prototype = (function() {
// 私有成员
var age = 12;
return {
getAge: function() {
return age
}
}
}());
var newObj = new myObj()
console.log(newObj.getName()) // 'shuliqi' 自身的特权方法
console.log(newObj.getAge()) // 12 原型的特权方法

模块模式

由于我们Javascript是没有包的语法的。模块模式提供了一种创建自包含非耦合代码片段的有力工具,可以将它视为黑盒功能,并且可以根据自己所需添加,替换或者删除这些模块。

模块模式是有下面这几种组合的组合。

  • 命名空间
  • 即使函数
  • 私有和特权成员
  • 声明依赖

第一步:定义一个命名空间:例如我们之前介绍的namespace()函数。

1
MYAPP.namespace('MYAPP.utilities.array');

第二步:定义该模块,对于需要保持私有性的情况,本模式可以提供就要有私有作用域的即使函数。该即时函数返回一个对象,即具有公共接口的实际模块。

1
2
3
4
5
MYAPP.utilities.array = (function() {
return {
// ... todo
}
}());

第三步:向公共接口添加一些方法

1
2
3
4
5
6
7
MYAPP.utilities.array = (function() {
return {
isArray: function(a) {
// ...
}
}
}());

最后:通过即使函数提供的私有作用域,可以根据需要声明一些私有属性和方法。在即时函数的顶部,正好也是声明模块可能有任何依赖的位置。在声明变量后,可以任意的放置有助于建立该模块的任何一性的初始化代码。最后结果是一个由即时函数返回的对象,其中该对象包含了自己模块需要的公共API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
MYAPP.namespace('MYAPP.utilities.array');
MYAPP.utilities.array = (function() {
// 依赖
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.ulang,
// 私有属性
name = "shuliqi",
age = 12,
// 私有方法
// ...
// var 变量定义结束


// 可选的一次性初始化过程
// ...

// 公共API
return {
isArray: function(a) {
// ...
},
name: name,
// ... 更多的属性和方法
}
}());

这就是模块模式的基本创建方式。

揭示模式

在模块模式里面,公共API:公共属性和方法都是写在对象字面量中, 如果想在闭包内部调用公有属性和方法,就需要通过暴露在全局变量中的对象名称去调用。

1
2
3
4
5
6
7
8
9
10
11
12
var MYAPP = (function() {
return {
// 公有属性
firstName: 'Peppa',
// 公有属性
lastName: 'Pig',
getFullName: function() {
return MYAPP.firstName + ' ' + MYAPP.lastName;
}
}
}());
console.log(MYAPP.getFullName()) // Peppa Pig

就像上面实例中展示的,getFullName 方法中需要访问公有属性 firstNamelastName,就必须通过 nameSpace 对象名去调用。这样的确很别扭,如果想要给 nameSpace 换个对象名,就需要考虑闭包内部的调用

那么使用揭示模式就可以解决这个问题了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var MYAPP = (function(){
var firstName = 'Peppa'
var lastName = 'Pig'

function getFullName() {
return firstName + ' ' + lastName
}

return {
firstName: firstName,
lastName: lastName,
getFullName: getFullName
}
})()

MYAPP.getFullName() // "Peppa Pig"

优化后的代码,我们将公有的变量和方法在返回前就做了处理,而返回的对象更加纯粹,增强了可读性。这也是揭示模块模式的优点

创建构造函数的的模块

有时候使用构造函数创建对象更为方便。当然,仍然可以使用模块模式来执行创建对象的操作。区别就是在于包装了模块的即时函数最终将会返回一个函数,而不是一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
MYAPP.namespace('MYAPP.utilities.array');
MYAPP.utilities.array = (function() {
// 依赖
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.ulang,
// 私有属性
name = "shuliqi",
age = 12,
constr;
// 私有方法
// ...
// var 变量定义结束


// 可选的一次性初始化过程
// ...
// 公共API-----构造函数
constr = function(o) {
this.elements = this.toArray(o);
}
// 公共API-----原型
constr.prototype = {
constructor: MYAPP.utilities.array,
toArray: function(o) {
// ...
}
};
}());


// 使用新构造函数的方法
var arr = new MYAPP.utilities.array(obj);
文章作者: 舒小琦
文章链接: https://shuliqi.github.io/2019/07/07/对象创建模式/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 舒小琦的Blog