第四章 对象的创建
命名空间
在ESM模块化还未出来之前,用于减少全局变量名污染的一种做法就是使用命名空间,其做法也非常简单,就是创建一个全局的变量,然后将内容都赋值给这个变量,从而减少对全局变量的使用。
//创建命名空间
var MYAPP = {};
//构造函数
MYAPP.Parent = function(){};
MYAPP.Child = function(){};
// 一个变量
MYAPP.some_var =1;
// 一个对象容器
MYAPP.modules ={};
// 嵌套的对象
MYAPP.modules.module1 ={};
MYAPP.modules.module1.data ={a:1, b:2};
MYAPP.modules.module2 ={};
命名空间常常使用大写来进行声明,这也和之前所说的常量一样,为了突出显示,方便区分。
命名空间可以进行嵌套,一个命名空间里包含其他命令空间,但是随着命名空间的增多,相同名称的空间可能会存在冲突。一般的做法就是不进行覆盖,代码如下:
//创建命名空间
var MYAPP = MYAPP || {};
虽然这可以有效的避免代码自身的命名冲突,但是它也是有缺点的:
- 代码量的增加,每个变量的使用都需要增加命名空间的前缀
- 命名空间本身就是全局变量,随时可能被修改
深度的嵌套会增加属性值查询的时间
这些缺点在ESM模块化之后都得到了解决,在本文后续也有不使用模块化的解决方式。
随着ES6标准的推行,为此我觉得没有太大的必要去深入了解这个,点到为止。
依赖声明
将你需要的依赖声明在函数的顶部,是一个很好的习惯,因为它可以很明确的告诉开发者,你使用了哪些依赖,甚至于我们可以将一些全局依赖保存为局部的变量,此时就不用每次都往外层去查找需要的属性,加上局部变量总是会优先使用,从而减少了属性查询时间。
function test() {
var doc = document;
var name = "test";
var div = doc.createElement("div");
}
私有属性和方法
JavaScript是没有私有属性的,哪怕是ts,它最终生成的代码也不会对你使用了修饰符的属性做特殊操作。
class Test {
private getName() {
return "test";
}
}
ts转换为js:
var Test = /** @class */ (function () {
function Test() {
}
Test.prototype.getName = function () {
return "test";
};
return Test;
}());
你会发现getName
并没有被特殊转换什么的,ts仅仅是让构造函数Test包裹在一个闭包中。
而我们在js中创建的对象或者构造函数创建的对象,其属性都是公有的,谁都可以获取到,谁都可以对其修改,显然在一些环境下可能并不是很符合我们的需要。
为此私有属性的需求就出现了,也可以称为私有成员,实现这个功能也很简单,上一章中我们讲到了函数,利用函数的作用域,我们就可以实现私有成员。
function Test() {
var name = "私有成员";
this.getName = function() {
return name;
}
}
//使用
var test = new Test();
console.log(test.name); //undefined
console.log(test.getName()); //私有成员
如你所见,JavaScript创建一个私有成员是非常容易的,我们只需要将其放置在函数的作用域下即可。
特权方法
所谓的特权方法,其实就是可以获取私有属性的方法统称而已,因为它可以获取到私有成员,所以它有特权,被称为特权方法。
上面我们的getName
就是一个特权方法
私有成员的失效
当我们使用私有成员时,有时也不意味着它的绝对安全,在一些情况下,私有成员也可能会被外部所修改。
function Test() {
var data = {
name: "私有成员对象",
age: 16,
address: "北京",
};
this.getData = function() {
return data;
};
}
//使用
var test = new Test();
var testData = test.getData();
testData.name = "我已经被外部修改了";
console.log(test.getData()); //{name:"我已经被外部修改了",...}
因为对象是引用类型,我们对外抛出了它的引用,导致外部可以直接修改到私有成员。
为此,我们可以在传递数据时使用数据的副本,比如使用深度克隆和浅克隆生成副本;甚至于我们还可以使用另一种思路:最低授权原则,永远不要给出超出需求的东西,一个人需要牙刷,就不要再给他牙膏,因为他没有说需要。
对象字面量的私有成员
上面我们都是构造函数的私有成员,我们的字面量也可以创建私有成员。
var test = (function() {
var name = "私有成员";
return {
getName: function() {
return name;
},
};
})();
//使用
console.log(test.getName()); //私有成员
原型与私有成员
上面使用构造函数创建的私有成员有一个弊端,就是每次new去构建一个新对象时,都会在内存中生成新的私有成员,虽然他们的内容都是一样的,为了避免这些重复的劳动,节省内存,我们可以将它们公共的内容都添加到构造函数的prototype
原型上,这样的话,就不用每次都创建了,而是通过原型链关联。
function Test() {}
Test.prototype = (function() {
var name = "原型上的私有成员";
return {
getName() {
return name;
},
};
})();
var test = new Test();
console.log(test.getName());
将私有函数暴露为公有方法
当我们有一些私有方法,在设计之初可能外部并不会用到,但是随着业务的变化,这些方法在外部使用可能会带来一些良好的效果,但是如果直接将这些方法公开,就会使得原有结构的破坏,有可能被公开的方法被外部无意或者恶意修改了,从而导致从前的逻辑发生错误。
在ECMAScript5中,我们可以选择冻结一个对象,从而防止它被修改,但是在之前的版本这种方法并不能使用,于是有了一种暴露模块模式的模式。
var myarray;
(function() {
var astr = "[object Array]",
toString = Object.prototype.toString;
function isArray(a) {
return toString.call(a) === astr;
}
function indexOf(haystack, needle) {
var i = 0,
max = haystack.length;
for (; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}
return− 1;
}
myarray = {
isArray: isArray,
indexOf: indexOf,
inArray: indexOf
};
}());
myarray变量用于承载私有函数,而且由于它本身与原本逻辑是完全不相干的,所以当外部修改myarray
的时候,并不会影响到原本的私有函数。
myarray.indexOf = null;
myarray.inArray(["a", "b", "z"], "z"); // 2
沙箱模式
沙箱模式主要着眼于命名空间模式的短处,即:
- 依赖一个全局变量成为应用的全局命名空间。在命名空间模式中,没有办法在同一个页面中运行同一个应用或者类库的不同版本,因为它们都会需要同一个全局变量名,比如
MYAPP
。 - 代码中以点分隔的名字比较长,无论写代码还是解析都需要处理这个很长的名字,比如
MYAPP.utilities.array
。
顾名思义,沙箱模式为模块提供了一个环境,模块在这个环境中的任何行为都不会影响其它的模块和其它模块的沙箱。
全局构造函数
在命名空间模式中 ,有一个全局对象,而在沙箱模式中,唯一的全局变量是一个构造函数,我们把它命名为Sandbox()
。我们使用这个构造函数来创建对象,同时也要传入一个回调函数,这个函数会成为代码运行的独立空间。
使用沙箱模式是像这样:
Sandbox(function(box) {
// 你的代码……
});
box
对象和命名空间模式中的MYAPP
类似,它包含了所有你的代码需要用到的功能。简单来说你需要的功能或者其他模块,都会被挂载到该对象上。
我们要多做两件事情:
- 防止没有被new构建
- 让
Sandbox()
构造函数可以接受一个(或多个)额外的配置参数,用于指定这个对象需要用到的模块名字。我们希望代码是模块化的,因此绝大部分Sandbox()
提供的功能都会被包含在模块中。
假设我们会这么使用:
//传入数组配置
Sandbox(['ajax', 'event'], function(box) {
// console.log(box);
});
//单个传入
Sandbox('ajax', 'dom', function(box) {
// console.log(box);
});
//获取全部功能
Sandbox('*', function(box) {
// console.log(box);
});
//事实上如果不传入任何参数,应该要和传入*一样的效果
Sandbox(function(box) {
// console.log(box);
});
添加模块
我们给Sandbox
增加一个modules
静态属性,用于存放我们的模块代码
Sandbox.modules = {};
Sandbox.modules.dom = function(box) {
box.getElement = function() {};
box.getStyle = function() {};
box.foo = "bar";
};
Sandbox.modules.event = function(box) {
// 如果有需要的话可以访问Sandbox的原型
// box.constructor.prototype.m = "mmm";
box.attachEvent = function() {};
box.dettachEvent = function() {};
};
Sandbox.modules.ajax = function(box) {
box.makeRequest = function() {};
box.getResponse = function() {};
};
每个模块都接受一个box参数,然后将所有功能都挂载到该参数上。
实现构造函数
function Sandbox() {
var args = Array.prototype.slice.call(arguments),
callback = args.pop(), //最后一个参数为回调函数,也就是用户的业务逻辑函数
//如果第一个参数是数组就使用这个数组,反之使用所有参数的数组
modules = args[0] && args[0] instanceof Array ? args[0] : args;
//保证函数是被new使用
if (!this instanceof Sandbox) {
return new Sandbox(modules, callback);
}
//根据modules给this添加模块
if (!modules || modules[0] === "*") {
//需要全部
modules = [];
for (var key in Sandbox) {
if (Sandbox.hasOwnProperty(key)) {
modules.push(key);
}
}
}
//挂载模块
for (var i = 0, len = modules.length; i < len; i++) {
//拿到模块的注册方法,传入this挂载
Sandbox[modules[i]](this);
}
//调用用户业务逻辑函数,并将this传给它
callback(this);
}
静态成员
在JavaScript中没有专门用于静态成员的语法。但通过给构造函数添加属性的方法,可以拥有和基于类的语言一样的使用语法。之所以可以这样做是因为构造函数和其它的函数一样,也是对象,可以拥有属性。
function Test() {
//这是实例成员
this.getName = function() {};
}
//这是静态成员
Test.getAge = function() {};
静态成员只能通过构造函数调用,而实例成员只能通过实例去调用,因为成员被赋值的地方不同,回想一下new操作做了什么?
有时候让静态方法也能用在实例上会很方便。我们可以通过在原型上加一个新方法来很容易地做到这点,这个新方法作为原来的静态方法的一个包装:
function Test() {}
//这是静态成员
Test.getAge = function() {};
//包装
Test.prototype.getAge = Test.getAge;
在这种情况下我们需要很小心的处理静态方法内的this,因为不同的调用者会导致this的指向不同,我们还可以在静态方法里做一下区分,是构造函数调用还是实例调用。
function Test() {}
//这是静态成员
Test.getAge = function() {
if (this instanceof Test) {
console.log("我是实例调用");
} else {
console.log("我是构造函数调用");
}
};
//包装
Test.prototype.getAge = Test.getAge;
var test = new Test();
test.getAge(); //我是实例调用
Test.getAge(); //我是构造函数调用
当构造函数去调用时,this指向的是Test
构造函数本身,它的prototype原型链上不可能含有自己,所以instanceof
判断为false
。
私有静态成员
目前静态成员都是公开的,我们可以通过构造函数访问任意静态成员,有没有可能让静态成员成为一个私有属性,无法直接背外部访问。
其实原理私有属性一样,通过函数的作用域。
var Test = (function() {
var name = "私有的静态成员";
return function() {
console.log(name);
};
})();
静态属性(包括私有和公有)有时候会非常方便,它们可以包含和具体实例无关的方法和数据,而不用在每次实例中再创建一次。
利用静态属性我们还可以实现设计模式的单例模式,这个后面再说。
对象常量
在一些比较现代的环境中可能会提供const
来创建常量,但在其它的环境中,JavaScript是没有常量的。
一种常用的解决办法是通过命名规范,让不应该变化的变量使用全大写。这个规范实际上也用在JavaScript原生对象中:
Math.PI; // 3.141592653589793
Math.SQRT2; // 1.4142135623730951
Number.MAX_VALUE; // 1.7976931348623157e+308
我们自己的代码也可以通过大写的命名规范来创建常量,但是这只是一种约定,这个值还是可以被修改的,而常量的定义是在程序生命周期里不会发生变化的。
或许我们真的会需要一个不会被改变的值,比如我们通过一个私有属性,然后通过配置来控制值是否可以被修改,通过指定的方法获取值,设置值。
var constant = (function() {
//私有属性
var constants = {},
//是否存在该属性
hasProp = Object.prototype.hasOwnProperty,
//控制允许设置值的类型
allowed = {
string: 1,
number: 1,
boolean: 1,
};
return {
//是否有值
isDefined: function(name) {
return hasProp.call(constants, name);
},
//获取
get: function(name) {
if (this.isDefined(name)) {
return constants[name];
}
return null;
},
//设置
set: function(name, value) {
//有值就不允许设置了
if (this.isDefined(name)) {
return false;
}
//允许设置的类型
if (!hasProp.call(allowed, typeof value)) {
return false;
}
//设置
constants[name] = value;
//成功
return true;
},
};
})();
链式调用
使用链式调用模式可以让你在一对个象上连续调用多个方法,不需要将前一个方法的返回值赋给变量,也不需要将多个方法调用分散在多行:
myobj.method1("hello").method2().method3("world").method4();
当你创建了一个没有有意义的返回值的方法时,你可以让它返回this
,也就是这些方法所属的对象。这使得对象的使用者可以将下一个方法的调用和前一次调用链起来。
链式调用模式的利弊
使用链式调用模式的一个好处就是可以节省代码量,使得代码更加简洁和易读,读起来就像在读句子一样。
另外一个好处就是帮助你思考如何拆分你的函数,创建更小、更有针对性的函数,而不是一个什么都做的函数。长时间来看,这会提升代码的可维护性。
一个弊端是调试这样写的代码会更困难。你可能知道一个错误出现在某一行,但这一行要做很多的事情。当链式调用的方法中的某一个出现问题而又没报错时,你无法知晓到底是哪一个出问题了。《代码整洁之道》的作者Robert Martion甚至叫这种模式为“train wreck”模式。(译注:直译为“火车事故”,指负面影响比较大。)
不管怎样,认识这种模式总是好的,当你写的方法没有明显的有意义的返回值时,你就可以返回this
。这个模式应用得很广泛,比如jQuery库。
method()方法
这节的内容我个人更倾向于了解,因为现实中很少会这么用,但是它确实提供了一个不错的解决方式,可以当做思路的扩展。
我们在构造函数中通过this
添加的实例属性,其实并不高效,最终这些属性会在每个实例中创建一次,这样会花费更多的内存,这也是为什么可重用的方法和属性应该放到prototype
原型上的原因。
但是对于很多开发者来说,往往会忽略这个,在ES6后class
的引入使得这种情况得到了改善。
给语言添加一个使用起来更方便的方法一般叫作“语法糖”。
我们可以添加一个method
语法糖用于改善这个情况
if (typeof Function.prototype.method !== "function") {
Function.prototype.method = function(name, implementation) {
this.prototype[name] = implementation; //注意,挂载在原型上了
return this;
};
}
使用时:
method()
方法接受两个参数:
- 新方法的名字
- 新方法的实现
var Person = function(name) {
this.name = name;
}.
method('getName', function() {
return this.name;
}).
method('setName', function(name) {
this.name = name;
return this;
});
当我们把函数拆出来后,它就不存在于构造函数的作用域,它有了一个可以复用引用地址,每次Person
被new出来时,其实都是调用的同一个引用地址的函数,那么这就可以避免每次实例化都在内存中创建重复内容。
而且因为new的时候,构造函数先运行,此时我们method其实是被实例运行的,method被预先挂载在了Function
构造函数原型上,运行method后会将方法挂载在实例的原型上。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据