javascript设计模式
在javascript中,在全局作用域中定义变量得非常小心,因为一不注意,就会重复命令相同变量名,造成变量的覆盖,并且很难察觉。
但是函数是函数,变量是变量。怎么函数也是变量?且看下面的函数:
functioncheck(){ }
以上函数再变化一下:
var check = function(){ }
通过以上代码可以发现,函数可以通过赋值操作把自己保存在变量名check中,而且在js中,函数是第一公民,函数声明和变量声明一样会被提升,编译阶段就会出现错误。所以函数从声明上,函数也是变量。
对象有属性和方法,可以把全局函数挂载到对象中。
varcheckObject = { checkVue:function(){ }, checkReact:function(){ }}
把函数都保存在同一对象中,通过调用对象的方法来访问相应的函数,不把函数暴露在全局作用域中,避免了函数冲突。
在项目协作中,一个人往往只是负责其中一个模块的开发,但是如果某个人的函数方法需要复用,为避免重复定义以及提高开发效率,这需要开发人员把自己代码中的函数方法尽可能不依赖外部变量独立封装起来,以便他人复用。
但是以上代码中的checkObject对象是没办法复用的,通俗来讲没办法用new关键字创建新的对象。代码如下:
varcheckObject = {checkVue:function(){vara =1;console.log(a); },checkReact:function(){ }}varobj =newcheckObject();console.log(obj)
输出错误:
varobj1 =newcheckObject(); ^TypeError: checkObjectisnotaconstructor
因为checkObject不是函数,不能被当做构造函数使用new关键字实例化赋值。
解决办法1:因为里面的checkVue、checkReact是函数啊,可以当做构造函数。代码如下:
varcheckObject = {checkVue:function(){vara =1;console.log(a); },checkReact:function(){ }}varobj =newcheckObject.checkReact();console.log(obj);
但是在es5中没有类的概念,一般属于封装类的函数都保存在对象中被封装起来,封装对象按功能、组件进行命名并分类保存。所以一般想调用这个封装对象的函数方法个数肯定不止一个,以上代码需要一个一个调用,很麻烦。
解决办法2:有人会说可以直接把checkOject复制给另外一个对象,反正对象都是引用值。但是这样子做有很大的缺点。代码如下:
varcheckObject = {checkVue:function(){vara =1;console.log(a); },checkReact:function(){ }}varobj = checkObject;console.log(obj);
对象的确是引用过来了,但是:
1.obj对象和checkObject对象是同一个引用地址,在obj对象做的任何修改,都会对checkObject上产生一样的影响,这显然是不能允许的。某人写好了这个对象里的方法,任何人都可以去修改,这违反了分工原则。
2.想在obj复用checkObject的基础上,通过引用再添加自己的一些方法,也非常难。
综上所诉,这种引用复用不利于团队的代码管理,会有极大的开销。
要进行更好的复用,现在解决的难点有几个:
1.checkObject是对象,没办法进行实例化,也就是new操作;
2.实例化的对象可以自定义方法与属性,但又不能影响与它对应的构造函数;
先从第一点开始,checkObject是对象不是函数,可以把它变为函数吗?答案是可以的,还记得函数在命名上也是变量这个定义吧?代码如下:
varcheckObject =function(){ }
可以通过匿名函数的方式赋值给checkObject,使对象本身变为一个函数对象,就可以利用函数的特性进行实例化。不过这又产生了另外一个问题,匿名函数里不能直接以json的形式书写属性,会报语法错误。代码如下:
varcheckObject =function(){ checkVue:function(){ },checkReact:function(){ }}varobj = checkObject();console.log(obj);
报错如下:
C:\Users\Administrator\Desktop\udpp\object.js:2 checkVue:function(){ ^SyntaxError: Unexpected token (
先看看这一种解决办法,只要是函数就都可以设置返回值,不设置默认为空。以上代码修改如下:
varcheckObject =function(){return{checkVue:function(){ },checkReact:function(){ }}}varobj = checkObject();console.log(obj);
输出:
{checkVue: [Function: checkVue], checkReact: [Function: checkReact] }
以上代码用return给匿名函数返回一个json对象,并且赋值给checkObject。虽然这里解决了函数不能直接书写json对象问题,但是最后实际上对象赋值给对象,还是不能用new实例化,需要多写很多个obj.XXXX形式的调用。
另外obj对象和checkObject没有任何关系,是一个新的对象。
return这个表达式先放一边,后面有用。再梳理一下思路,要函数对象才能实例化。
给出解决方法前,先简单介绍一下js中this的指向。
作为对象的方法调用
当函数作为对象的方法被调用时,this指向该对象:
varobj = {a:1,b:function(){console.log(this=== obj );//输出:trueconsole.log(this.a );//输出: 1}};obj.b();
作为普通函数调用
当函数不作为对象的属性被调用时,即被普通函数调用时,此时this总是全局对象
name ='globalName';vargetName =function(){returnthis.name;};console.log( getName() );//输出:globalName
有时候会遇到一些特殊情况,比如在div节点的事件函数内部,有一个局部的callback方法,callback被作为普通函数调用时,callback内部的this指向了window,但我们往往是想让它指向该div节点,代码如下:
我是一个divwindow.id ='window';document.getElementById('div1').onclick =function(){ alert (this.id );//输出:'div1'varcallback =function(){ alert (this.id );//输出:'window'} callback(); };
有一种简单的解决方案,可以用一个变量保存div节点的引用:
document.getElementById('div1').onclick =function(){varthat =this;//保存div的引用varcallback =function(){ alert ( that.id );//输出:'div1'} callback();};
在es5中,this已经被规定为不会指向全局对象,而是undefined:
functionfunc(){ "use strict"alert (this);//输出:undefined}func();
构造函数调用
当用new运算符调用函数时,该函数总会返回一个对象,通常情况下,构造函数的this就指向返回的这个对象。
varMyClass =function(){this.name ='sven';};varobj =newMyClass();alert ( obj.name );//输出:sven
以上代码new调用时返回对象MyClass,thisMyClass。指向并赋值给obj对象,所以obj.name调用相当于MyClass.name。
但用new调用构造器时,还要注意一个问题,如果构造函数显式地返回了一个object类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的this:
varMyClass =function(){this.name ='sven';return{//显式地返回一个对象name:'anne'}};varobj =newMyClass();alert ( obj.name );//输出:anne
如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题
varMyClass =function(){this.name ='sven'return'anne';//返回string类型};varobj =newMyClass();alert ( obj.name );//输出:sven
要想解决非函数对象实例化的问题,就要用到this,代码如下:
varcheckObject =function(){this.checkVue =function(){console.log('checkVue'); },this.checkReact =function(){ }}varobj =newcheckObject();obj.checkVue();//输出:checkVue
以上代码中的checkObject的匿名赋值函数里没有添加return语句,没有返回对象,巧妙运用this使匿名赋值函数通过赋值保存到了对象checkObject中,成功让checkObject对象实例化了。
好了,我们已经解决了前文中“1.checkObject是对象,没办法进行实例化,也就是new操作;”的问题。
这种方式,需要注意的是,实例化的对象会对构造函数this上的属性进行复制,在内存中会保存两份同样的方法,很浪费内存空间。测试如下:
```
var checkObject = function(){
this.checkVue = function(){
console.log('checkVue');
},
this.checkReact = function(){
}
}
var obj = new checkObject();
console.log(obj);
console.log(checkObject);
console.log(obj == checkObject);
```
输出:
```
checkObject{checkVue: ƒ, checkReact: ƒ}checkReact: ƒ ()checkVue: ƒ ()__proto__: Object
ƒ (){
this.checkVue = function(){
console.log('checkVue');
},
this.checkReact = function(){
}
}
false
```
从以上可以看到,通过这种方法实例化的函数和构造函数是不同的,还可以观察到,构造函数实际上是以对象的形式保存下来的,实例化的函数才是一个函数。
**好了,又发现一个新问题,那就是如何解决内存浪费的问题?**
##### 这里需要用到原型对象prototype,先简单介绍一下prototype。
在JavaScript中,每一个对象都从原型继承属性,除了null除外。几乎所有的对象都有一个原型对象,我们可以通过__proto__(首尾都是双下划线)来获取实例的原型对象。可以通过Object.prototype获取原型对象的引用。通过构造函数实例化的函数原型就是构造函数prototype属性的值。
当创建出一个新函数时,假设这个函数为Person,会自动为Person生成一个prototype属性,指向原型对象__proto__,在__proto__原型对象上,有contructor属性,person.prototype.constructor又指回了Person。并且除了contructor属性之外,还有一些自定义的属性和方法,可以直接在Person.prototype上进行添加,因为都是引用类型,相当于在__proto__上添加了。实例对象Person1和Person2并没有属性和方法,但是可以通过原型对象__proto__来进行查找,__proto__相当于一条连接实例对象和构造函数的通道。
需要注意的是,函数(Function)才有prototype属性,对象(除Object)拥有__proto__。
**prototype和__prototype__的区别**
1.prototype是函数才有的属性
2.__proto__是每个对象都有的属性
3.但是__proto__不是一个规范属性,只是部分浏览器实现了此属性,对应的标准属性是[[Prototype]]
需要注意的是,大多数情况下,__proto__可以理解为“构造器的原型”,即:__proto__ === constructor.prototype;(通过Object.create()创建的对象不适用此等式)
区别代码如下:
```
var a = {};
console.log(a.prototype); //undefined
console.log(a.__proto__); //Object {}
var b = function(){}
console.log(b.prototype); //b {}
console.log(b.__proto__); //function() {}
```
**__proto__属性指向谁?**
__proto__的指向取决于对象创建时的实现方式。三种常见方式创建对象后,__proto__指向如下:
1.字面量方式
```
var = {};
```
对象a的__proto__指向内置函数Object {}的prototype,因为两者都属于对象,按址引用,调用时会指向类似于函数的实际值。需要注意的是,js通过内部设置构造函数区分了引用类型,Object就是其中一种引用类型
2.构造函数
```
var A = function(){};
var a = new A();
```
这里对象a的__proto__指向的是构造函数A的prototype
3.Object.create方式
```
var a1 = {};
var a2 = Object.create(a1);
```
注意,这里对象a2的__proto__指向内置函数Object{},而不是它的prototype
代码如下:
```
/*1、字面量方式*/
var a = {};
console.log(a.__proto__); //Object {}
console.log(a.__proto__ === a.constructor.prototype); //true
/*2、构造器方式*/
var A = function(){};
var a = new A();
console.log(a.__proto__); //A {}
console.log(a.__proto__ === a.constructor.prototype); //true
/*3、Object.create()方式*/
var a1 = {a:1}
var a2 = Object.create(a1);
console.log(a2.__proto__); //Object {a: 1}
console.log(a.__proto__ === a.constructor.prototype); //false
```
**理解原型链**
由于__prototype__是任何对象都有的属性,而js里万物基于对象,所以会形成一条__proto__连起来的链条,递归访问__proto__必须到头,并且值是null
当js引擎查找对象的属性时,先查找对象本身是否存在该属性,如果不存在,会在原型链上查找,但不会查找自身的prototype。
```
var
```
**好了,在此打住prototype介绍。**
通过上面的简单的介绍,我们知道了在函数的prototype上添加函数,创建出对象实例时,不会在内存中在保存一份和checkObject相同的方法和属性了,通过对象实例调用这些方法,会在原型链上进行依次寻找。
代码如下:
```
var checkObject = function(){};
checkObject.prototype.checkVue = function(){
},
checkObject.prototype.checkReact = function(){
}
```
#### 小技巧:实例对象的链式调用
以上代码的方法比较多,要调用得每个都写一遍,可以使用链式调用来稍微简化一下这个繁琐的操作。
如果XX构造函数里的有this,那么当xx通过new 调用被赋值给YY时,this指向的是XX。这种方式就给链式调用带来的可能。
代码如下:
```
var checkObject = function(){
};
checkObject.prototype = {
checkVue:function(){
return this;
},
checkReact:{
return this;
}
};
var a = new checkobject();
a.checkVue().checkReact();
```
在js中,有Function这种源生对象,即js中内置的构造函数。我们声明的每一个函数都是此构造函数的实例。对Function进行设置,可以对函数进行扩展。
```
Function.prototype.checkVue = function(){
console.log("checkVuje");
}
var f = function(){};
f.checkVue();//checkVue
```
以上代码虽然是对原生对象Function添加了一个checkVue方法,不过由于污染了原生对象Function,所以在项目协作中,别人创建的函数也会污染。
可以封装一个统一的添加方法的方法。代码如下:
```
Function.prototype.addMethod = function(name, fn){
this[name] = fn;
}
var methods = function(){};
methods.addMethod('checkVue', function(){
console.log("checkVue");
});
methods.addMethod('checkReact', function(){
console.log("checkReact");
});
methods.checkVue();//checkVue
methods.checkReact();checkReact
```
再加入前文的链式调用方式:
```
Function.prototype.addMethod = function(name, fn){
this[name] = fn;
return this;
}
var methods = function(){};
methods.addMethod('checkVue', function(){
console.log("checkVue");
return this;
}).addMethod('checkReact', function(){
console.log("checkReact");
return this;
});
methods.checkVue().checkReact();//checkVue checkReact
```
## 面向对象
**面向对象解决的问题:**
在全局作用域中,定义太多函数容易冲突,且不利于项目协作中他人重复使用,如果别人提前使用这些函数,还不能轻易去修改这些函数,不利于代码的维护。
虽然在es5中没有强类型那种通过class等关键字实现的类的封装方式。但是可以通过模仿把需求抽象成一个对象,把需要的功能封装在这个对象。
#### 模拟一个类
**这里有两种方式:**
1.在函数内部的this上添加属性对象和方法
```
var Book = function(id, bookname, price){
this.id = id;
this.bookname= bookname;
this.price = price;
}
```
2.在函数的原型上挂载属性和方法
```
var Book = function(){};
Book.prototype.display = function(){
//
};
```
两者的区别:
通过this添加的属性、方法是在当前对象上添加的,然而JavaScript是一种基于原型prototype的语言,所以每创建一个对象时(当然在JavaScript中函数也是一种对象),它都有一个原型prototype用于指向其继承的属性、方法。这样通过prototype继承的方法并不是对象自身的,所以在使用这些方法时,需要通过prototype一级一级查找来得到。所以我们每次通过类创建一个新对象时,this指向的属性和方法都会得到相应的创建,而通过prototype继承的属性或者方法是每个对象通过 prototype 访问到,所以我们每次通过类创建一个新对象时这些属性和方法不会再次创建。
#### 运用面向对象解决一些属性或方法的暴露与隐藏
属性或方法的隐藏一般只能在局部作用域中。在es5中,局部作用域只有通过函数去生成,换句话说,函数就是一个局部作用域。面向对象一方面可以通过函数(这里指代类)封装该函数一些公有方法与属性在此函数的局部作用域里,避免暴露在全局引起冲突,且更利于维护;另一方面也可以封装一些不被外部访问到的私有属性与方法。
在es5中,在函数内部声明的变量以及方法是外界访问不到的,这样可以创建私有方法与属性。另外,在函数内部,挂载到this上的属性与方法,在外部可以通过new XXX访问到,这种可以称为公有属性和公有方法。最后在this还可以挂载另外一种方法,外界可以通过该方法,访问到私有属性与私有方法,称为特权方法。这些特权方法可以初始化实例对象的一些属性,称为构造器。所有代码如下:
```
var Student = function(num,name,grade){
//私有属性
var girlStudents = 19;
//私有方法
function checkName(){
}
//特权方法
this.getName = function(){};
this.getGrade = function(){};
this.setName = function(){};
this.setGrade = function(){};
//对象公有属性
this.num = num;
//对象公有方法
this.copy = function(){};
//构造器
this.setName(name);
this.setGrade(grade);
}
```
在函数(类)外面,追加属性和方法,通过Student.xxx的形式,之后的实例化对象是无法访问到这些属性和方法的,称为静态公有属性(实例对象不能访问)和静态公有方法(实例对象不能访问)
```
var Student = function(num,name,grade){
//私有属性
var girlStudents = 19;
//私有方法
function checkName(){
}
//特权方法
this.getName = function(){console.log(girlStudents)};
this.getGrade = function(){};
this.setName = function(){console.log("setName")};
this.setGrade = function(){};
//对象公有属性
this.num = num;
//对象公有方法
this.copy = function(){};
//构造器
this.setName(name);
this.setGrade(grade);
}
//静态公有属性(实例对象不能访问)
Student.isSchool = true;
//静态公有方法(实例对象不能访问)
Student.countGrade = function(){
console.log("countGrade");
};
Student.prototype = {
//公有属性
isStudent:true,
//公有方法
whatName:function(){console.log("whatName")}
}
var a = new Student();
a.countGrade();
```
(未完待续)