javascript中对象的属性是可以任意扩展的,所以可以在创建的实例中添加任何属性
var p = new Object();
p.name = "tom";
p.age = 15;
还可以使用对象直接量简化上述的代码
var p = {name: "tom", age: 15}
但是如果要频繁的去创建类似的对象,就避免不了重复去写创建对象的逻辑细节,比如,继续创建几个类似的对象。
var p2 = new Object();
p2.name = "jerry";
p2.age = 16;
var p3 = new Object();
p3.name = "mike";
p3.age = 17;
工厂模式可以把创建对象的细节封装起来,避免了我们创建对象的重复的逻辑。
function createPerson(name, age){
var o = new Object();
o.name = name;
o.age = age;
return o;
}
var p1 = createPerson("tom", 15);
var p2 = createPerson("jerry", 16);
工厂模式解决了创建多个类似的对象的问题,但是没有解决如何识别对象类型的问题,比如我们在使用工厂模式再创建其他类型的对象
function createDog(name, age){
var o = new Object();
o.name = name;
o.age = age;
return o;
}
var d1 = createDog("lucky", 2);
我们无法识别实例p1和d1的类型是Person还是Dog,因为它们本质上都是Object的实例。
构造函数的出现解决了这个问题
function Person(name, age){
this.name = name;
this.age = age;
}
function Dog(name, age){
this.name = name;
this.age = age;
}
var p1 = new Person("tom", 15);
var d1 = new Dog("lucky", 1);
p1 instanceof Person; //=> true
p1 instanceof Dog; //=> false
d1 instanceof Dog; //=> true
d1 instanceof Person; //=> false
通过构造函数解决了识别对象类型的问题。但它依然不是完美的,构造函数的主要问题是每个方法都要在实例中重新创建一遍,比如:
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = function(){
return this.name;
}
}
var p1 = new Person("tom", 15);
var p2 = new Person("jerry", 16);
p1.sayName == p2.sayName; //=> false
p1和p2都是Person类型的实例 ,但是方法sayName却不是同一个。导致这个问题的原因,是因为EcmaScript中的定义的函数都是对象,它们都是Function的实例,上述构造函数可以等价成下面的形式
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = new Function("return this.name");
}
所以每次调用构造函数Person,方法sayName都重新创建了一个新的实例。
把函数定义放在构造函数外面可以解决这个问题
function sayName(){
return "I am " + this.name;
}
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = sayName;
}
var p1 = new Person("tom", 15);
var p2 = new Person("jerry", 16);
p1.sayName == p2.sayName; //=> true
但是这种方式依然存在问题,很显然sayName方法本意只是想作为Person实例的一个方法,但是把它移到了函数外,成了全局对象的属性了。这样每添加一个方法,就要定义一个全局函数。污染了全局对象不说,定义的类型也谈不上封装性了。
原型模式就是来解决上面的问题的
function Person(){}
Person.prototype.name = "tom";
Person.prototype.age = 15;
Person.prototype.sayName = function(){
return this.name;
}
var p1 = new Person();
var p2 = new Person();
p1.sayName == p2.sayName; //=> true
p1.name; //=> tom
p2.name; //=> tom
通过原型模式创建对象的问题在于,每个实例都共享了原型对象的属性。比如上述p1.name和p2.name都是相同的。
更好的方式是组合使用构造函数和原型模式,去其糟粕取其精华。
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayName = function(){
return this.name;
}
var p1 = new Person("tom", 15);
var p2 = new Person("jerry", 16);
p1.sayName == p2.sayName; //=> true
p1.name; //=> tom
p2.name; //=> jerry
这种方式没有什么问题,非要鸡蛋里挑骨头,就是相对于其他面向对象语言,这种构造函数和原型独立书写的方式还是挺另类的。
动态原型模式就是为了解决这种另类
function Person(name, age){
this.name = name;
this.age = age;
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
return this.name;
}
}
}
var p1 = new Person("tom", 15);
在构造函数中,只在方法sayName不存在的情况下,才会将它添加到原型中。只有在初次调用构造函数才会执行代码。
ES6 Class
在es6出现之前,最友好的方式是使用上面提到过的动态原型模式,它为了解决构造函数和原型独立定义的问题。但是平心而论它只是把构造函数和原型放在一起了,相对于其他面向对象语言的语法,还是存在很大的差异。为了解决这种差异。es6引入了关键字Class,提供了更接近面向对象的语法。
Class Person {
constructor(name, age){
this.name = name;
this.age = age;
}
sayName = function(){
return this.name;
}
sayAge = function(){
return this.age;
}
}
var p = new Person("tom", 15);
p.name; //=> tom
p.sayName(); //=> tom