创建对象
原型模式与构造模式组合使用
function Person(name,age,salary){ this.name = name; this.age = age; this.salary = salary; this.people = ["people1","people2"]; } Person.prototype = { constructor:Person, sayName:function(){ console.log(this.name); } } var p1 = new Person("name1","20","5000"); var p2 = new Person("name2","22","8000"); p1.people.push("people3"); console.log(p1.people); console.log(p2.people);
运行结果:
Array [ "people1", "people2", "people3" ] Array [ "people", "people" ]
这是创建对象最常用也是认同度最高的方式,把可共享的方法和属性放在prototype原型对象里面,把实例属性放在构造函数中。
动态原型模式
这种创建对象的模式,只写一个构造函数,而把原型放在构造函数中作为初始化用,这种结构就有点像java创建类一样,也是一种很好的创建对象的模式,我们来看一下:
function Person(name,age,salary){ this.name = name; this.age = age; this.salary = salary; if(typeof this.sayName != "function"){ Person.prototype.sayName = function(){ console.log("Hello, " + this.name); } Person.prototype.saySalary = function(){ console.log("My salary is " + this.salary); } } } var p1 = new Person("name1","20","8000"); var p2 = new Person("name2","21","7000"); p1.sayName(); p1.saySalary(); p2.sayName(); p2.saySalary();
运行结果:
Hello, name1 My salary is 8000 Hello, name2 My salary is 7000
这种模式不能用字面量来重写原型。如果你尝试使用下面的代码来编写:
function Person(name,age,salary){ this.name = name; this.age = age; this.salary = salary; if(typeof this.sayName != "function"){ Person.prototype = { constructor : Person, sayName : function(){ console.log("Hello, " + this.name); }, saySalary : function(){ console.log("My salary is " + this.salary); } } } } var p1 = new Person("name1","20","8000"); var p2 = new Person("name2","21","7000"); p1.sayName(); p1.saySalary(); p2.sayName(); p2.saySalary();
运行结果:
TypeError: p1.sayName is not a function
因为原型的重写是在创建了对象之后进行的,重写后的原型实际上是一个新的原型对象(重新创建一个,原来的还在,它们的constructor都指向Person),而实例的prototype指针还是指向原来的原型对象(里面没有其他属性和方法,因为属性和方法写在新的原型对象上),所以运行的时候,在实例对象中找不到sayName方法,在原型对象中也找不到sayName方法,即sayName未定义,所以在调用p1.sayName()方法时就报错,说sayName不是一个方法。
我们可以看下面的例子来深入理解一下:
function Person(){}; // 在之前创建 var p1 = new Person(); Person.prototype = { constructor:Person, name:"name1", age:"20", sayName:function(){ console.log(this.name); } }; // 在之后创建 var p2 = new Person(); p2.sayName(); //name1 p1.sayName(); //error
基本封装方法
请看下面的例子:
var Person = function(name,age){ this.name = name; this.age = age || "未填写"; this.hobbys = []; } Person.prototype = { sayName:function(){ console.log(this.name); }, sayAge:function(){ console.log(this.age); }, addHobby:function(hobbys){ this.hobbys = this.hobbys.concat(hobbys); } } var person1 = new Person("Jane","20"); var person2 = new Person("TabWeng","21"); person1.addHobby(['sing','drawing']); person2.addHobby(['football','study','running']); person1.sayName(); console.log(person1.hobbys.toString()); person2.sayName(); console.log(person2.hobbys.toString());
运行结果:
Jane sing,drawing TabWeng football,study,running
JavaScript创建对象,可以共用的属性和方法写在原型上,需要每个实例各自都有的副本的属性和方法放在构造函数中。
现在有个问题,名称的输入不能有数字,要怎么解决呢?解决的方法可以写一个检查名称的函数,这个函数写在原型上。
var Person = function(name,age){ //校验名称 if(this.checkName(name)){ throw new Error("名字 "+name+" 不能存在数字"); } this.name = name; this.age = age || "未填写"; this.hobbys = []; } Person.prototype = { //校验函数 checkName:function(name){ re = /\d/; return re.test(name); }, sayName:function(){ console.log(this.name); }, sayAge:function(){ console.log(this.age); }, addHobby:function(hobbys){ this.hobbys = this.hobbys.concat(hobbys); } } var person1 = new Person("Helen666","20"); var person2 = new Person("TabWeng","21"); person1.addHobby(['sing','drawing']); person2.addHobby(['football','study','running']); person1.sayName(); console.log(person1.hobbys.toString()); person2.sayName(); console.log(person2.hobbys.toString());
这段代码中,我们写了一个checkName()
函数,来校验名称,暂且只是校验不能有数字吧,然后再构造函数里的第一行代码中进行校验,若校验不通过,则抛出异常。
这里我传入一个名称Helen666,结果抛出如下异常:
Error: 名字 Helen666 不能存在数字
这样就做到了一个基本的封装,实现内部校验。
但是又有个问题,我们还可以这样来定义名称:
var person1 = new Person("Helen","20"); person1.name = "Helen666"; person1.sayName(); //Helen666
这样名称还是可以修改为不合法的名称,于是我们想到用get方法 和set方法来做控制,只能通过set方法来赋值,同时通过set方法进行校验,而通过get方法来获得值。现在的代码修改如下:
// Interface var People = new Interface("People",["setName","getName","setAge","getAge","addHobby","getHobby","sayName","sayAge"]); var Person = function(name,age){ //implement People this.setName(name); this.setAge(age); this._hobbys = []; } Person.prototype = { //校验函数 checkName:function(name){ re = /\d/; return re.test(name); }, sayName:function(){ console.log(this._name); }, sayAge:function(){ console.log(this._age); }, addHobby:function(hobbys){ this._hobbys = this._hobbys.concat(hobbys); }, getHobby:function(){ return this._hobbys; }, setName:function(name){ if(this.checkName(name)){ throw new Error("名字 "+name+" 不能含有数字"); } this._name = name; }, getName:function(){ return this._name; }, setAge:function(age){ this._age = age || "未设置"; }, getAge:function(){ return this._age; } } var person1 = new Person("Helen","20"); person1.addHobby(['sing','drawing']); function record(person){ Interface.ensureImplements(person,People); person.sayName(); console.log(person.getHobby().toString()); } record(person1);
运行结果:
Helen sing,drawing
首先,这段代码我们使用了接口,定义了People接口,而person来实现这个接口,注意注释的内容。(关于接口,请看这篇 JavaScript使用接口)
其次,我们使用了get方法 和 set方法来取值和赋值,我们可以约定程序员只能通过set来赋值,而在set方法里面我们对所赋予的值进行了校验,以确保准确。但是这仅仅是一种约定,程序员依然可以通过 person1.name = "123"
来赋值,修改内部属性。
为了规范和起到提醒作用,我们把内部属性的命名进行规范,在这些属性前面加上“_”,比如 **_name** 、**_age** ,这样如果程序员要直接修改属性,那么他就必须这样写person1._name = "123"
,这明显是一种故意的做法,一般程序员不会这么做,起到规范和提醒的作用。
尽管如此,这种仅仅是用规定进行约束,还是无法阻止通过person1._name
进行修改,下面的方法可以做到把内部属性真正做到私有化。
通过闭包进行封装
// Interface var People = new Interface("People",["setName","getName","setAge","getAge","addHobby","getHobby","sayName","sayAge"]); var Person = function(name,age){ //implement People // 私有变量 var _name,_age,_hobbys = []; this.addHobby = function(hobbys){ _hobbys = _hobbys.concat(hobbys); }, this.getHobby = function(){ return _hobbys; }, this.setName = function(name){ if(this.checkName(name)){ throw new Error("名字 "+name+" 不能含有数字"); } _name = name; }, this.getName = function(){ return _name; }, this.setAge = function(age){ _age = age || "未设置"; }, this.getAge = function(){ return _age; } this.setName(name); this.setAge(age); } Person.prototype = { checkName:function(name){ re = /\d/; return re.test(name); }, sayName:function(){ console.log(this.getName()); }, sayAge:function(){ console.log(this.getAge()); } } var person1 = new Person("Helen","20"); person1.addHobby(['sing','drawing']); function record(person){ Interface.ensureImplements(person,People); person.sayName(); console.log(person.getHobby().toString()); } record(person1);
在构造函数中,属性不使用this
,外部也就无法访问到这个属性,而闭包通过作用域链可以访问到这个属性,那么我们就通过闭包设置了为属性赋值的唯一入口,从而起到了严格校验这些属性的作用。
尽管如此,在构造函数中定义方法很多时候是没必要的,因为这样每创建一个实例,就会产生一个方法的副本,这是需要内存支持的,所以在使用的过程中,如果能用上面的基本封装方法,尽量用,除非对于私有属性有非常严格的校验要求才用闭包这种方法。