这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战
一、创建对象的模式
在上一篇文章中我从一个更深的层次介绍了对象(注:建议阅读 再说js中的面向对象(一)),了解了对象属性的属性。那么今天我们开启第二个阶段,面向对象的封装。封装是真正意义上的面向对象实际功能的实现。
在上一篇文章中,我们说过面向对象最大的优势就在于降低代码的非必要重复,提高代码的可维护性,降低服务器的压力。
面向对象有很多种封装方式,这里我们只探讨常用的,具有代表性的。如单例模式、工厂模式、构造函数模式以及基于构造函数的原型模式。
1.1 单例模式
所谓单例模式就是把描述同一个事物(同一个效果或者方法)的属性和方法放在一个对象当中。
单例模式作用是起到了分组的作用,这样一来,不同事物之间的属性或者方法即使名字相同(属性名和方法名),也不会互相感染,种分组编写代码的方式称为单例模式。单例模式是项目开发过程中最为常用的开发方式,也是实现模块化开发的最简便的方式。我们来看看单例模式:
例如我们要实现一个编辑的方法:
var edit={
copy:function(source,target){},
delete:function(target){},
back:function(){}
};
这些方法只需要定义一次,当我们需要使用edit中的方法时,我们只需要edit.copy(a,b);edit.delte(c)…即可,我们不需要重复定义copy和delete方法,这样便降低了代码的耦合度,而且在Node和Angular中,自定义模块导出时也常用单例模式。单例模式结合js高阶编程技巧中的惰性思想可以十分方便的封装一些小的方法库; 这里简单示例:
例如我们要培养FE:
var tools=(function(){
var isStandarBrowser = "getComputedStyle" in window;
function jsonParse(jsonStr) {
return "JSON" in window ? JSON.parse(jsonStr) : eval("(" + jsonStr + ")");
}
function listToAry(likeAry) {
try {
return Array.prototype.slice.call(likeAry, 0);
} catch (e) {
var ary = [];
for (var i = 0; i < likeAry.length; i++) {
ary.push(likeAry[i]);
}
}
return ary;
}
function getRandom(n, m) {
n = Number(n);
m = Number(m);
if (isNaN(n) || isNaN(m)) {
return Math.random();
}
if (n > m) {
var tem = m;
m = n;
n = tem;
}
return Math.round(Math.random() * (m - n) + n);
}
return {
getRandom: getRandom,
listToAry: listToAry,
jsonParse:jsonParse
}
})();
1.2 工厂模式
单例模式虽然有了命名空间,但是他是一个写死的对象,如果我们要创建一个包含方法的单例,单例就做不到了。所以工厂模式应运而生。不同于单例模式,工厂模式是把实现一个事物的代码封装到一个函数中,每当我们需要实现这个事物时,只需执行这个函数即可;示例:
function developeFE(student,skill){
var objFE={};
objFE.name=student;
objFE.skill=skill;
return objFE;
};
frontEngineer=developeFE(Binary, angular);//将Binary培养成一个会angular的前端工程师
console.log(frontEngineer);//{name:Binary,skill:angular}
- 构造函数模式:工厂模式虽然解决了创建多个相似的对象的问题,但是却没有解决对象识别的问题,(即我们不知道对象是哪个类的实例);这时候构造函数模式应运而生;构造函数是自定义一个类,并且通过new 运算符创建这个自定义类的实例;并且实现了实例识别,通过new A创建的实例,是A类的实例,通过new B创建的实例是B类的实例;【这里有必要强调一下,js中的类都是函数数据类型的,只不过他通过new执行是成为了一个类,但是他是一个函数的本质从未改变】;示例
function DevelopFE(stu,skill){
this.name=stu;
this.skill=skill;
this.saySkill=function(){alert(this.skill)}
};
var FE=new DevelopFE(Binary,nodejs);//FE是DevelopFE的一个实例;
console.log(FE);//{name:Binary,skill:nodejs, saySkill: function(){alert(this.skill)};
1.3 构造函数中的注意事项
- 构造函数区别于工厂模式,首先我们不需要自己创建空对象,第二也不需要我们手动return,第三,向实例添加属性使用this关键字;通过this关键字添加的属性,默认都是enumerable(可枚举)的;
- 构造函数创建一个实例,必须使用new操作符;使用new操作符实际是经历了四步: 2.1 创建一个新对象; 2.2 将this指向这个新对象 2.3 执行构造函数中的代码 2.4 将新对象返回
- 构造函数中的this是当前构造函数的实例,这是实例识别的实现原理;
- 构造函数中许可以随意return,如果return一个基本数据类型的值,实例不会改变,如果return一个引用数据类型,实例将会被返回值所替代;
- 使用this关键字添加给实例的属性是实例的私有属性,是受命名空间的保护的,与其他实例(含当前类的实例)无关;检测私有属性的方法obj.hasOwnProperty(key);返回值是布尔值,true表示是该实例的私有属性,false则不是;
1.4 原型模式
原型模式:构造函数在工作中也很常用,构造函数很完美,也很方便,但是他也不是没有缺点的,比如说在前面构造函数的例子中,每一个FE都应该具备saySkill的方法,所以这应该是一个共用的方法,没有必要每个实例身上都有一份,这说明一个问题呢?说明构造函数模式虽然解决了工厂模式的实例识别的问题,但是并没有实现面向对象中公有方法共用;这时候基于构造函数的原型模式诞生了;其实,从逻辑上来讲,我们也应该能想到:既然要方法共用,那就不应该放在实例上,应该在构造函数身上找一个公共的地方来存放这些方法,而这个公共的地方需满足实例能访问,构造函数可以访问的要求;这个地方就是原型prototype,原型定义在构造函数身上,构造函数可以通过 . 访问,实例通过__proto__访问;
如果想理解原型模式及原型链,请将下面的段落熟读5遍; 每一个函数数据类型(普通函数、类),都有一个天生自带的属性prototype(原型,下称原型),并且他是一个对象数据类型的【公共的地方,定义在构造函数上】;
并且在prototype上天生自带constructor(构造函数)属性,constructor的值是当前原型的类本身;
每一个对象数据类型(原型,实例,普通对象)都有一个__proto__属性,该属性的值是一个指针,指向该实例(对象)所属类的prototype。【实例也可以找得到】 示例:
function DevelopFE(stu,skill){
this.name=stu;
this.skill=skill;
};
DevelopeFE.prototype.saySkill=function(){alert(this.skill)};
原型的逻辑就是这样,但是实例又是怎样访问原型的呢?这里就引入了“原型链”,原型链和作用域链很相似,都是链式查找的;
二、原型链机制
当我们调用实例的方法或者读取实例的属性时,机制会现在私有属性中查找,如果找到就读取调用;如果找不到,会读取 __proto__ 的属性值,通过 __proto__ 指向去实例所属类的原型上查找,找到就读取调用,如果还没找到,就通过这个类的__proto__指向的原型找,一直找到Object上,Objcet是所有类的基类,通俗说就是找到头了。因Object是基类,他没有__proto__。这里很多人可能很疑惑,类怎么也有__proto__,请大家不要忘记,在js中大到DOM、BOM小到key都是一个对象,所谓js中万物皆对象;
三、原型模式细节问题
-
在IE中为避免我们修改原型上的属性或者方法,
__proto__被禁用,如果我们想修改,只能用.prototype的方式来修改; -
constructor问题,在我们手动更换类的原型的时候,constructor 指向也可能改变,示例
var obj={
this.sayExprience=function(years){alert(years)}
}
console.log(DevelopeFE.prototype.contructor)// DevelopeFE
DevelopeFE. prototype=obj;
console.log(DevelopeFE.prototype.contructor)// Object
这并不是我们想要的,如果此时我们用 constructor 检测数据类型,这时结果就会出现偏差;所以如果更好原型,记得修改constructor指向。