基于类的面向对象和基于原型的面向对象方式比较
在基于类的面向对象方式中(比如JAVA),对象依靠类来产生。而在基于原型的面向对象方式中(比如JavaScript),对象则是依靠构造器(constructor)利用 原型(prototype)构造出来的。
举个客观世界的例子来说明二种方式认知的差异。例如工厂造一辆车,一方面,基于类的车间,工人必须参照一张工程图纸,设计规定这辆车应该如何制造。这里的工程图纸就好比是语言中的类,而车就是按照这个类制造出来的;另一方面,基于原型的车间,工人和机器 ( 相当于 constructor) 利用各种零部件如发动机,轮胎,方向盘 ( 相当于 prototype 的各个属性 ) 将汽车构造出来。
事实上关于这两种方式谁更为彻底地表达了面向对象的思想,目前尚有争论。但我认为原型式面向对象是一种更为彻底的面向对象方式,理由是:客观世界中的对象的产生都是其它实物对象构造的结果,而抽象的“图纸”是不能产生“汽车”的,也就是说,类是一个抽象概念而并非实体(类本身并不是一个对象);原型方式中的构造器和原型本身确是由其他对象通过原型方式构造出来的对象,恰恰符合一切事物皆对象;
从零开始构建JavaScript对象
javascript中一个函数,也是对象,为了方便与基于类的编程语言相比较,我们暂且称之为类
function People(name){
this.name = name;
this.printName = function(){
console.log(name);
};
}
p1是People类new出来的对象,我们称之为实例
var p1 = new People('Byron');
类是砖的模具,实例就是根据模具印出来的砖块。一个模具可以印出(实例化)多个实例,每个实例都符合类的特征。在Java中类不能称之为对象,是对对象的抽象,如同‘人类’是一个概念、规则的集合,但是在JavaScript中,本身没有类的概念,我们需要用对象模拟出类,然后用类去创建对象,类似于开篇中提到的:用零件拼凑一辆车
所以我们需要创建一个对象。在JavaScript中使用对象很简单,使用new操作符执行object()函数就可以构建一个最基本的对象,通过.
来为对象添加属性和方法
var obj = new Object();
obj.name = 'feng';
obj.printName = function(){
console.log(obj.name);
}
我们称通过关键字 new 调用的函数为构造函数,构造函数和普通函数区别仅仅在于是否使用了new
来调用,它们的返回值也会不同。所谓“构造函数”,就是专门用来生成“对象”的函数。它提供模板,作为对象的基本结构。一个构造函数,可以生成多个对象,这些对象都有相同的结构
PS:在实际工作中,我们更多的会用‘字面量’的形式来创建一个对象,下面的写法和上面可以达到相同的效果:
var obj1 = {
nick: 'feng',
age: 25,
printName: function(){
console.log(obj1.nick);
}
}
var obj2 = {
nick: 'bling',
age: 25,
printName: function(){
console.log(obj2.nick);
}
}
以上的构造方式有两个明显问题
- 太麻烦了,每次构建一个对象都是复制一遍代码
- 如果想个性化,只能通过手工赋值,使用者必需了解对象详细
为了解决这个问题,我们通过创建一个函数来实现自动创建对象的过程,至于个性化通过参数实现,开发者不必关注细节,只需要传入指定参数即可
function createObj(nick, age){
var obj = {
nick: nick,
age: age,
printName: function(){
console.log(this.nick);
}
};
return obj;
}
var obj3 = createObj('feng', 24);
obj3.printName();//feng
这种方法解决了构造过程复杂,需要了解实现细节的问题。但是构造出来的对象类型都是Object,没有识别度。那怎样调用构造函数才可以返回类型为funcation的name的对象呢?怎样让function接受参数,根据参数来创建相同类型且不同值的对象呢?这一切都可以通过 利用new操作符调用构造函数打成
new
我们就开始介绍new操作符。new 运算符接受一个函数 F 及其参数:new F(arguments...)。这一过程分为三步:
- 创建类的实例。这步是把一个空的对象的 proto 属性设置为 F.prototype (下文有介绍)。
- 初始化实例。函数 F 被传入参数并调用,关键字 this 被设定为该实例。
- 返回实例。
我们改造一下创建对象的方式
function Person(nick, age){
this.nick = nick;
this.age = age;
this.printName = function(){
console.log(this.nick);
}
}
var p1 = new Person('feng','24');
构造函数在解决了上面所有问题,同时为实例带来了类型,但可以注意到每个实例的printName方法实际上作用一样,但是每个实例要重复一遍,大量对象存在的时候是浪费内存。我们希望把共享的属性和方法放置到一个共享的容器,而每个构造函数中保存那些具有各自特点的属性和方法,这些东西不共享,这时候我们需要使用prototype。
prototype
-
每个函数在生成的那一瞬间就自动添加一个名称为
prototype
属性,指向一个所谓的原型对象(prototype) -
该原型对象的constructor属性,指回该原型对象对应的构造函数
-
每个对象都有一个内部属性
__proto__
(规范中没有指定这个名称,但是浏览器都这么实现的) 指向其类型的prototype属性,类的实例也是对象,其proto属性指向“类”的prototype,如下所示
通过图示我们可以看出一些端倪,实例可以通过__prop__
访问到其类型的prototype属性,这就意味着类的prototype对象可以作为一个公共容器,供所有实例访问。有了这个公共容器,那么我们的问题就迎刃而解了
-
1.所有实例对象都会通过原型链引用到类型的prototype(原型对象)
-
2.prototype相当于特定类型所有实例都可以访问到的一个公共容器
-
3.所有公用的属性和函数放到原型中,共享这一份属性,私有的属性放到各自的构造函数中
function Person(nick, age){
this.nick = nick;
this.age = age;
}
Person.prototype.sayName = function(){
console.log(this.nick);
}
var p1 = new Person();
p1.sayName();
这时候我们对应的关系是这样的
至此,终于有个靠谱的构建对象的方式了
创建JavaScript对象的多种方法
上文,我们探讨了javascript中创建对象的方式,中间使用过很多方法。确实,灵活多变就是javascript的魅力。为了方便各位进行对比和分析,接下来,我将罗列出一些常用的创建对象的方法,供各位参考
1. 使用Object构造函数来创建一个对象:下面代码创建了一个person对象,并用两种方式打印出了Name的属性值(这种加括号的方式字段之间是可以有空格的如person["my age"])。
var person = new Object();
person.name="feng";
person.age=25;
console.log(person.name);
console.log(person["name"])
2.使用对象字面量创建一个对象,其实和上面是一样的
var person =
{
name:"feng",
age:25,
5:"Test"
};
console.log(person.name);
console.log(person["5"]);
3. 使用工厂模式创建对象:返回带有属性和方法的person对象。
function createPerson(name, age) {
var o = new Object();
o.name=name;
o.age=31;
o.sayName=function(){
alert(this.name);
};
return o;
}
createPerson("feng",25).sayName();
4. 使用自定义构造函数模式创建对象:这里注意命名规范,作为构造函数的函数首字母要大写,以区别其它函数。这种方式有个缺陷是sayName这个方法,它的每个实例都是指向不同的函数实例,而不是同一个。
function Person(name,age) {
this.name=name;
this.age=age;
this.sayName=function() {
alert(this.name);
};
}
var person = new Person("feng",25);
person.sayName();
5. 使用原型模式创建对象:解决了方法4中提到的缺陷,使不同的对象的函数(如sayName)指向了同一个函数。但它本身也有缺陷,就是实例共享了引用类型friends,从下面的代码执行结果可以看到,两个实例的friends的值是一样的,这可能不是我们所期望的。
function Person(){}
Person.prototype = {
constructor : Person, <span class="comment" style="margin: 0px; padding: 0px; border: none; color: rgb(0, 130, 0); background-color: rgb(248, 248, 248); font-family: Consolas, 'Courier New', Courier, mono, serif; line-height: 18px;">//我们重写了原型对象,重写prototype会切断现有原型与任何之前已经存在的对象实例之间的联系,所以此处将构造函数的原型重新指回构造函数</span><span style="margin: 0px; padding: 0px; border: none; background-color: rgb(248, 248, 248); font-family: Consolas, 'Courier New', Courier, mono, serif; line-height: 18px;"> </span>
name:"feng",
age:25,
friends:["MJ","Kobe"],
sayFriends:function() {
alert(this.friends);
}
};
var person1 = new Person();
person1.friends.push("Joe");
person1.sayFriends();//MJ,KoBe,Joe
var person2 = new Person();
person2.sayFriends();//MH,KoBe,Joe
6. 组合使用原型模式和构造函数创建对象,解决了方法5中提到的缺陷,而且这也是使用最广泛、认同度最高的创建对象的方法。
function Person(name,age) {
this.name=name;
this.age=age;
this.friends=["Jams","Martin"];
}
Person.prototype.sayFriends=function() {
console.log(this.friends);
};
var person1 = new Person("feng",25);
var person2 = new Person("feng2",30);
person1.friends.push("Joe");
person1.sayFriends();//Mj,KoBe,Joe
person2.sayFriends();//MJ,KoBe
7. 动态原型模式:这个模式的好处在于看起来更像传统的面向对象编程,具有更好的封装性,因为在构造函数里完成了对原型创建。这也是一个推荐的创建对象的方法。
function Person(name,age,job) {
//属性
this.name=name;
this.age=age;
this.friends=["MJ","KoBe"];
//方法
if(typeof this.sayName !="function") {
Person.prototype.sayName=function() {
console.log(this.name);
};
Person.prototype.sayFriends=function() {
alert(this.friends);
};
}
}
var person = new Person("feng",25);
person.sayName();
person.sayFriends();