木灵鱼儿
阅读:598
第四章 对象的创建
命名空间
在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后会将方法挂载在实例的原型上。
版权申明
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿 - 有梦就能远航站点。未经许可,禁止转载。
相关推荐
第五章 代码复用模式
代码复用是一个既重要又有趣的话题。如果你面对自己或者别人已经写好的代码,而这些代码又是经过测试的、可维护的、可扩展的、有文档的,这时候你只想写尽量少且可以被复用的代码就是一个再自然不过的想法。当我们说代码复用的时候,第一件想到方式就是继承,为此你可能看到JavaScript很多方式用于实现“继承”。什么是类?如果有一个过程,这个过程能产生一个实例,实例是对象,那这个过程就是类。JavaScript是基于原型面向对象程序设计的,那么它的继承方式也是基于原型实现的,而JavaScript是没有类的,只有构造函数,且支持new用法,所以看上去就和类的用法相似,但是本质还是一个函数,而继承也是一...
第一章 基本技巧
尽量少用全局变量全局变量污染是一个老生常谈的问题,在es5时代常见的做法就是使用函数作用域隔离,es6时const、let出现,我们还可以直接在块级作用域中声明。相对于使用,我们通过一些代码来回顾一下全局变量污染所带来的问题。全局作用域中是存在this的,this就等于window,只是浏览器为了方便,用window来表示全局对象本身。function sum(x, y) { result = x + y; return result; } console.log(result); //Uncaught ReferenceError: result is not def...
对象扁平化
前言后端返回给前端的数据,有时候会是一个多层级对象,但是我们前端使用的时候,for循环遍历渲染时,多层级对象往往需要进行单独处理,因为还需要判断这个key值是否存在,否则会报错。强行让后端改变数据结构又好像不现实,无奈,只有自己处理了。掘金看到一位大佬文章《【算法】JS 实现对象的扁平化》感觉很合适,逻辑清晰。要求将对象中的层级扁平化,改成如下格式:// 实现一个 flatten 函数,实现如下的转换功能 const obj = { a: 1, b: [1, 2, { c: true }], c: { e: 2, f: 3 }, g: null, }; // 转换为 l...
JSON 格式化对象,对象中存在函数的解决办法,JSON格式化函数
json在格式化对象是,如果是普通的键值对对象还是没有问题的,但是如果他的值是一个函数,在格式化之后,函数就会被剔除。const a = { name: '我爱你', fn: function(){ console.log("我也爱你!") } }; const b = JSON.stringify(a);当你输出变量b的时候,你得到的会是:"{\"name\":\"我爱你\"}"fn不见了!引用JSON.stringify()方法描述中的一段解释:undefined、任意的函数以及 s...
Object 对象的扩展
属性的简写es6允许对属性进行简写,可以直接使用变量,变量名直接成为了属性名。var a = "hello"; var b = {a}; //等同于 var b = { a:a }属性里的方法也可以简写var a = { b(){...} } //等同于 var a = { b: function(){ ... } }在CommonJS模块输出变量时,也就是node模块导出时,这种简写就显得十分方便,我们直接导致一个对象,对象里面使用简写。var a = {}; var b = {}; module.exports = {a,b}而属性的赋...
vuex namespaced 命名空间
我们有时候在使用vuex模块的时候,在其中加了一个键值对为:namespaced : trueexport default { namespaced: true, state { zoom: 15 }, getters:{}, mutations:{}, actions:{}, }namespaced为true的作用是告诉vuex,该模块所有的state 、getters、mutations、actions里面的东西调用时都需要加上命名空间,这个命名空间就是该模块被improt时命名的名字。假设这个模块的名字为m...
Ajax 表单序列化
什么是表单序列化呢?将所有表单的提交通过一个标准化的方法去获取并且提交出去,那就是序列化,也就是说不同的表单,如注册啊,登录啊,修改资料啊,这些东西可以通过一个通用的方法去处理它。那么表单序列化有几个要求:不能发送禁用的表单字段;只发送勾选的复选框和单选按钮;不发送type是reset、submit、file、button以及字段集;多选选择框中的每个选中的值单独一个条目;对于select元素,如果有value值,就指定value作为发送的字段,如果没有,就指定text值;已经将ajax的代码作为单独的一个文件保存,而调用则使用ajax()的方法,之前也做了一个表单提交的方法,在所有条件...
location.serach查询字符串参数
location.serach获取到链接地址中?后包括?的内容!此时,假设我们的地址是:https://www.mulingyuer.com/测试/?id=5&search=ok通过location.serach可以获得?id=5&search=ok这部分内容,如果我要分别拿到里面对应的内容,就需要使用字符操作了。首先,?问好是第一个要被排除的对象,我们可以利用slice、substring、substr三个区域选择符进行操作。slice,好处是负数是通过总字符数+负数得到需要的区域!substring,好处是:当只有一个负数的时候,不裁剪区域,全出输出,当其中一个数为负数的时候,默认识...
对象与原型的继承组合模式中仿冒和继承不会冲突吗?
代码部分:function Box(name) { this.name = name; this.famil = ['爷爷','奶奶','爸爸','妈妈']; }; Box.prototype.run = function() { return this.name + this.famil; }; function Desk(name) { Box.call(this,name); }; Desk.prototype = new Box(); 通过对象仿冒,将Box的属性仿冒到Desk中,这样就可以传入参数,也可以算是继承了,但是后面我们又通过Des...