对象:即是一系列属性和方法的集合。
- 1.字面量方式
- 2.工厂模式
- 3.构造函数
- 4.原型模式
- 5.组合使用构造函数和原型模式
- 6.动态原型模式
以上六种方式是javascript创建对象的方法,每种方法都有自己的特点,下面是对每种方式的具体介绍。
1.字面量方式
var obj = {};
obj.name = "tom";
obj.age = 18;
obj.say = function(){
console.log("my name is "+this.name);
};
上面这种字面量创建对象的方式,是我们在编写Javascript
代码时,经常用来创建对象的方式。它和以下代码是等价的。
var obj = new Object();
obj.name = "tom";
obj.age = 18;
obj.say = function(){
console.log("my name is "+this.name);
};
使用字面量方式创建对象,显示很方便,当我们需要时,随时创建即可。但是它们只适合创建少量的对象。当我们需要创建很多对象时,显然这种方式是不实用的。
2.工厂模式
当我们需要创建出多个对象的时候,通过字面量方式创建对象的方法显然是不合理的。我们希望有这样一个容器,当我们需要创建对象时,我们只需将数据扔进去,返回给我们的就是一个个对象,虽然对象的属性值不尽相同,但是他们都拥有共同的属性。工厂模式就是为我们提供这个服务的。
function Factory(name,age){
var obj = {};
obj.name = name;
obj.age = age;
obj.say = function(){
console.log("my age is "+this.age+"old");
}
return obj;//创建的对象以返回值的方式返回给我们。
}
当我们需要创建对象的时候,我们将对象的私有数据扔进Factory
这个机器工厂即可
。
代码示例:
var person1 = Factory("jack",18);
var person2 = Factory("mark",22);
工厂方式的问题:使用工厂模式能够创建一个包含所有信息的对象,可以无数次的调用的这个函数。虽然其解决了创建多个相似对象的问题,但却没有解决对象识别的问题,因为创建出来的对象始终是个Object实例(即如何得知一个对象的类型)。
console.log(person1.constructor);//object()
console.log(person1 instanceof Factory)//false
对象的
constructor
属性,指向创建该对象的构造函数。
instanceof是用来判断一个对象是否为一个构造函数的实例。
3.构造函数
在上文中,我们可以通过 new Object()
来实例化对象,Object()
和 Array()
、Date()
都是Javascript为我们提供的原生构造函数,我们可以通过这些构造函数来创建出不同类型的对象。
var arr = new Array();//创建一个数组对象
var date= new Date();//创建一个时间对象
除了JS为我们提供的原生构造函数,我们也可以自己创建一个构造函数。
function Friend(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.introduce = function(){
console.log("my name is "+this.name+",my job is "+this.job);
};
};
var friend1 = new Friend("Bob",17,"student");
var friend2 = new Friend("Alens",25,"teacher");
console.log(friend1.introduce==friend2.introduce)//false
构造函数的特点
- 1.函数名首字母大写,从而区别与普通的函数。
- 2.在构造函数中使用this关键字
- 3.创建对象的时候使用new关键字
当我们通过new+构造函数
实例化一个对象的时候,实际上代码会执行以下操作。
var friend1 = new Friend("Bob",17,"student");;
//1.首先会先创建一个实例化对象friend1
//2.然后将构造函数Friend的作用域赋给该对象(this即指向该对象),也就相当于为该对象添加以下属性
friend1.name = "Bob";
friend1.age = 17;
firend1.job = "student";
//3.最后将该对象返回
通过构造函数创建出来的对象,一眼看上去,感觉和工厂模式创建对象并没有什么区别,但是通过这种方式创建出来的对象,我们可以明确的判断创建该对象的构造函数。
console.log(friend1.constructor);//function Friend()
console.log(friend1 instanceof Friend)//true
通过构造函数创建对象的方式,既解决了字面量
方式创建对象的局限性(无法适应创建多个对象
),也解决了工厂模式
创建出来对象的不可识别类型的欠缺性。但是,当我们在通过构造函数创建多个对象的时候,虽然他们的属性是不同的,但是这些对象却拥有共同的方法introduce
,这样的话,创建多少个对象,就会有多少个方法。这样的话,显得有些多余,并且对计算机内存也不利。
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。——《JavaScript高级程序设计(第3版)》
我们可以将这些对象共有的方法,定义在构造函数外面,有一个变量来引用该方法。这样的话,创建出来的每一个对象,都可以享用该方法。
function Friend(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.introduce = introduce;
}
function introduce(){
console.log("my name is "+this.name+",my job is "+this.job)
}
var person1 = new Friend("Bob",17,"student");
var person2 = new Friend("Alens",25,"teacher");
console.log(person1.introduce === person2.introduce); //true
虽然这种做法,可以解决内存消耗,实现多个对象共享一个方法,但是显然这种做法是不利于整体代码的阅读性。并且,构造函数也被硬生生的拆开了。
4.原型模式
原型模式,是Javascript中的一种设计模式
。指的是每一个构造函数,都有自己的一个原型对象prototype
,而通过该构造函数创建出来的对象,都有一个属性__proto__
,该属性指向创建该对象的构造函数的原型对象。即__proto__
指向构造函数的prototype
对象。正是这一连接赋予了JS对象属性的动态搜索特性:如果在对象本身找不到某个属性,那么就会通过这个连接到其原型对象中去找。
示例代码:
//先声明一个无参数、无内容的“空”构造函数
function Person() {
}
//使用对象字面量语法重写Person的原型对象
Person.prototype = {
name: 'jack',
age: '25',
job: 'front-end web developer',
sayName: function () {
console.log(this.name);
}
};
//因为上面使用对象字面量的方式完全重写了原型对象,
//导致初始原型对象(Person.prototype)与构造函数(Person)之间的联系(constructor)被切断,
//因此需要手动进行连接
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
});
//测试
var person1 = new Person();
var person2 = new Person();
person2.name = 'Faker';//将person2对象的名字改为自身的名字
console.log(person1.name); //'jack'
console.log(person2.name); //'Faker'
console.log(person1.sayName()); //'jack'
console.log(person2.sayName()); //'Faker'
console.log(person1.sayName === person2.sayName); //true
可以看到,通过原型模式,我们同样可以轻松地创建对象,而且可以像“继承”一般得到我们在原型对象中定义的默认属性,在此基础上,我们也可以对该对象随意地添加或修改属性及值。此外,通过上面最后一句测试代码还可以看出,其函数实现了完美的引用共享,从这一点上来说,原型模式真正解决了构造函数模式不能共享内部方法引用的问题。
原型模式看起来不错,不过它也不是没有缺点。第一,它不像构造函数模式那样,初始化时即提供参数,这使得所有新创建的实例在一开始时长得一模一样;第二,封装性欠佳;第三,对于包含引用类型值的属性,会导致不应该出现的属性共享。
对于第三个缺点,用代码更能说明问题:
function Person() {
}
Person.prototype = {
constructor: Person, //这样恢复连接会导致该属性的[[Enumerable]]特性变为true。上面的Object.defineProperty()才是完美写法。
name: 'Chuck',
age: '25',
job: 'Software Engineer',
friends: ['Frank', 'Lambert'],
sayName: function () {
console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push('Lily');
console.log(person1.friends); //["Frank", "Lambert", "Lily"]
console.log(person2.friends); //["Frank", "Lambert", "Lily"]
一般而言,我们都希望各个对象各有各的属性和值,相互没有影响。可像上面示例一样,原型模式共享了不应该共享的属性,这绝对不会是我们想要的结果
5.组合使用构造函数和原型模式
通过对上面几种创建对象方法的分析,我们希望是否有一种方式,能够在快速创建多个对象的同时,让这些对象既拥有自己的私有属性,也拥有一些共有的方法。同时,也不破坏构造函数创建对象的纯粹性。
代码示例:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Frank', 'Lambert'];
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name)
}
};
var person1 = new Person('tom', 25, 'Software Engineer');
var person2 = new Person('cindy', 18, 'doctor');
person1.friends.push('Lily');
console.log(person1.friends); //["Frank", "Lambert", "Lily"]
console.log(person2.friends); //["Frank", "Lambert"]
console.log(person1.sayName === person2.sayName); //true
通过这种方式创建出来的对象,使得对象实例拥有自己可完全支配的全部属性,同时还共享了方法引用以节省内存开销。
6.动态原型模式
到上一步的“组合模式”为止,就功能和性能上而言可以说已经达到我们的要求了,现在我们考虑是否可以对代码进一步优化,毕竟“组合模式”有两段代码,起码封装性看起来不够好。
我们把需要共享的函数引用通过原型封装在构造函数中,在调用构造函数初始化对象实例的同时将该函数追加到原型对象中。当然,为了避免重复定义,需要加一个if判断。代码如下:
function Person(name, age, job, friends){
this.name = name;
this.age = age;
this.job = job;
this.friends = friends;
if (typeof Person.prototype.sayName !== 'function') {
Person.prototype.sayName = function(){
return this.name;
}
}
}
var person1 = new Person('chuck', 25, 'Software Engineer', ['A','B','C']);
console.log(person1.sayName()); //'chuck'
以上六种创建对象的方式,是我们在编写代码时,经常用到的方法,具体要用哪中方法,还是要根据实际情况决定的。