我对对象的理解是一种对一系列无序数据的封装,这些数据都有对应的作为键的属性名,即key,最后构成了一组组名值对,在调用对象的属性或者方法时,通过属性名(key)找到对应的值(value)。
javascript中的对象和其他OO语言中的对象有所不同,他并非基于常见于其他语言的class,而是基于prototype原型对象。刚开始学习js的面向对象时常常感到非常困惑,今天来梳理一下目前创建对象的各种模式,同时涉及一些关于new和prototype的内容。既是对自己近期学习成果的总结,也是和js的初学者分享一下经验,可能会包含一些错误,欢迎批评和讨论。
创建对象的方式:
在Js中,我们可以直接创建对象的实例,比如
var o = new Object();
或者
var o = {
name: “cauzinc”
code:”123456”
}
但是由于用同一个接口直接创建大量实例会产生大量重复代码,因此设计了各种创建对象的模式来减少代码冗余。
1、工厂模式
这种模式将创建对象的过程封装到函数中,并且允许在创建对象时,直接通过参数写入对象的属性。代码如下:
1 function Person(name,code,job){ 2 var o = new Object(); 3 o.name=name; 4 o.code=code; 5 o.job=job; 6 o.sayName = function(){ 7 return o.name; 8 } 9 return o; 10 } 11 12 console.log(typeof (new Person("a",123,"c")));
可以从代码中看出,每次调用这个函数就返回一个对象,这个对象包含了三个属性一个方法。但是这个模式有一个严重的缺点,就是返回的对象都是object类型,这样就无法识别这些对象了。
console.log( (new Person(“a”,123,”c”) instanceof Person));
返回false。
2、构造函数模式
为了解决这个问题,我们可以使用构造函数模式。构造函数是用于构造对象的一种特殊的函数。构造函数模式的代码如下:
1 function Person(name,age,job){ 2 this.name = name; 3 this.age = age; 4 this.job = job; 5 this.sayName = function(){ 6 return this.name; 7 } 8 }
我们可以看出这段代码和刚才的工厂模式代码有所不同:首先,在函数的内部并没有创建新的对象,而是用把属性赋给了this对象;其次,这段代码也没有return语句来返回对象。
那么问题就来了,这种函数如何创建对象?函数中的this指向谁?这个模式是如何解决辨识对象类型的问题的?
为了解决这些问题,我们必须注意在调用函数的时候我们使用了new关键字。在使用new关键字的时候,程序一般会经历以下的步骤:
1、创建一个新的object对象;
2、将Person的原型对象的指针传递给object对象的_proto_,在执行instanceof的时候,就会将object对象辨认为Person的实例,而不是Object的实例;
3、在object对象的执行环境中调用Person方法,这样一来this就指向了object对象,并将属性的值赋给object;
4、返回object对象。
可以用以下代码来理解:
//new 关键字: new Person("a",123,"b") { var obj={}; obj.__proto__ = Person.prototype; var result = Person.call(obj,"a",123,"b"); return result; }
反之,如果我们不用new关键字的话,而直接调用person()函数的话,那么person()函数就会在window对象中创建变量和方法,因为他的执行环境是window对象。
但是构造函数模式也会产生新的问题,就是每次创建函数都会实例化一次对象的函数方法,由于函数本身也是一种对象,也就是每个实例对象被创建的同时,都会创建一个新的函数实例,但是实际上,我们并没有必要创建这么多功能相同的Function实例。
3、原型模式
为了在创建函数对象时,可以共享同一个函数方法,我们可以使用原型模式。所谓的原型(prototype)对象是所有函数中都包含的一个属性,这个属性中包含的是由这个函数创建的所有实例都可以共享的属性和方法。原型模式代码如下:
1 function Person(){ 2 3 } 4 Person.prototype.name = "cauzinc"; 5 Person.prototype.age = 123; 6 Person.prototype.job = "student"; 7 Person.prototype.sayName = function(){ 8 return this.name; 9 } 10 var person1 = new Person(); 11 var person2 = new Person(); 12 console.log(person1.name); 13 console.log(person1.sayName()); 14 console.log(person1 instanceof Person); 15 console.log(person1.sayName === person2.sayName);
以上代码中,我们创建了一个空函数Person(),之后直接在Person的prototype中设置对象的各种属性,通过验证,我们可以发现,person1和person2两个实例中的sayName方法指向同一个函数,就是Person原型中的sayName。
对象属性的搜索机制:当程序要读取一个实例的属性时,解析器会先搜索对象实例本身的属性,如果没有找到,则继续搜索指针指向的原型对象,在原型对象中继续查找目标属性。同理,我们每次调用实例的方法,如果实例本身不存在同名的方法,那么他们调用的都是原型中的函数。
虽然我们可以通过对象实例来访问原型中的值,但是我们不能通过对象重写这个值。加入我们想要使person1.name=”grey”,那么对象实例的person1会自己创建一个新的name属性,解析器在搜索name时会屏蔽原型中的属性(并不是覆盖,原型中的name属性依旧没有改变)。
但是这样就出现了新的问题,由于每次调用原型中的属性都需要解析器进行重新搜索,因此对原型属性的修改会直接影响代码中所有实例的属性。如下图代码所示:
Person.prototype.name = "amanda"; console.log(person1.name); //amanda
虽然person1是在Person原型修改前创建的对象,但是修改之后,person1的name属性也立即被修改。这是因为person1是通过指针而不是属性的副本得到数据的。
那么这样一来,用这种方式创建的对象实例都拥有相同的属性了,假如我们需要某项属性是对象独有的,那么这种模式就会变得很麻烦。
拓展:
可以直接重写原型:
Function Animal(){
}
Animal.prototype = {
constructor = Animal,
name:”cat”,
}
此时要注意,由于重写了原型,constructor指针会将不再指向构造函数,可以在构造的时候重新定义该属性。
一些相关方法和操作符:
Isprototypeof() 判断原型是否是某实例的原型
Object.getprototypeof(obj)
Object.getOwnPropertyNames() 得到对象中所有的属性
Object.keys() 得到对象中所有的可枚举属性
delete操作符
hasOwnProperty(property)和in操作符
4、构造函数模式加原型模式
组合模式结合了以上所述的两种模式的优点,在构造函数中定义属性,在原型中定义方法和一些共有的属性,这样每个实例对象就拥有了共同的方法和独有的属性。
这种方式目前运用的最为广泛。
1 function Person(name,age,job){ 2 this.name = name; 3 this.age = age; 4 this.job = job; 5 } 6 Person.prototype.sayName = function(){ 7 return this.name; 8 } 9 var person = new Person("a",12,"c");
5、动态原型模式
当开发时,程序员如果不知道原型中是否已经定义了某个方法,那么就先进行检测,在根据结果判断是否给原型增加方法。
function Person(name,age){ this.name = name; this.age = age; if(typeof this.sayName != “function”){ Person.prototype.sayName = function(){ return this.name; } } }
6、寄生构造函数模式
寄生构造函数模式是一种在特殊情况下会用到的模式,先来看一下代码的例子:
1 function SpecialArray(){ 2 var values = new Array(); 3 values.push.apply(values,arguments); 4 5 values.toPipedString = function(){ 6 return this.join("|"); 7 } 8 return values; 9 } 10 var friends = SpecialArray("cauzinc","Bob","Java"); 11 console.log(friends.toPipedString()); //cauzinc|Bob|Java
在这段代码中,我们可以发现这种模式的本质还是一种工厂模式,即直接在函数内部显性地创造对象,因此这里不用new操作符也可以得到一样的结果。
这种方式的特殊性在于,在不修改已有的其他对象原型的同时,在其它构造函数的基础上增加了新的方法。如上述例子,虽然Array是原始数据类型,但是依然给Array增加了独有的方法。