JS创建对象的几种模式及分析对比
工厂模式
function createPerson(name, age, job){
//创建对象
var o = new Object();
//添加属性
o.name = name;
o.age = age;
o.job = job;
//添加方法
o.sayName = function(){
console.log(this.name);
};
//返回对象
return o;
}
实例化:
var person1 = createPerson('smith', 25, 'coder');
缺点:没有解决对象的识别问题.
构造函数模式
function Person(name, age, job){ //Person也是普通的函数, 但便于与普通函数区分, 通常将构造函数首字母大写
//添加属性
this.name = name;
this.age = age;
this.job = job;
//添加方法
this.sayName = function(){
console.log(this.name);
};
}
通过对比工厂模式我们发现:
- 没有显示创建对象;
- 直接将属性和方法赋给了this对象,this就是new出来的对象;
- 没有return语句.
实例化:
var person1 = new Person('smith', 25, 'coder');
判断实例类型:
console.log(person1.constructor === Person); //true
console.log(person1 instanceof Person); //true
console.log(person1 instanceof Object); //true
优点: 创建自定义的构造函数可以将实例标识为一种特定的类型
缺点: 每个方法都要在每个实例上重新创建一遍, 无法代码共用.
原型模式
function Person(){}
//Person.prototype为原型对象,每个构造函数的protot属性指向其对应的原型对象,以下为原型对象添加属性和方法
Person.prototype.name = 'smith';
Person.prototype.age = 25;
Person.prototype.job = 'coder';
Person.prototype.sayName = function(){
console.log(this.name);
}
实例化:
var person1 = new Person();
获取原型对象:
console.log(Object.getPrototypeOf(person1) === Person.prototype); //true
判断原型对象:
console.log(Person.prototype isPrototypeOf(person1)) //true
若为实例添加的属性或方法时与原型对象里的属性或方法相同, 添加的属性或方法会屏蔽原型对象中的那个属性或方法
function Person(){}
Person.prototype.name = 'smith';
Person.prototype.age = 25;
Person.prototype.job = 'coder';
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = 'Lihua';
console.log(person1.name) //'Lihua'
console.log(person2.name) //'smith'
通过以上结果发现: person1.name
属性屏蔽了原型对象里的name
属性, 阻止了访问原型中的那个属性, 使用delete
操作符删除实例属性后, 能够重新访问原型中的属性
function Person(){}
Person.prototype.name= 'smith';
Person.prototype.age = '25';
Person.prototype.job = 'coder';
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = 'Lihua';
console.log(person1.sayName()); //Lihua
console.log(person2.sayName()); //smith
delete person1.name;
console.log(person1.sayName()); //smith
delete Object.getPrototypeOf(person1).name; //删除原型对象的name属性
console.log(person1.sayName()); //undefined
console.log(person2.sayName()); //undefined
判断属性是否存在于实例中:
person1.name = 'Lihua';
console.log(person1.hasOwnProperty('name')) //ture
原型的动态性
对原型对象所做的任何修改能够立即从实例上反映出来, 即使是先实例化后修改原型对象也是如此
var friend = new Person();
Person.prototype.sayHi = function(){
console.log('hi');
}
friend.sayHi() //依然能够执行
JS高程写到:
尽管对原型对象所做的任何修改能够立即在所有实例中反映出来, 但重写整个原型对象情况还是不一样的, 调用构造函数时会为实例添加一个指向最初原型的
[[prototype]]
指针, 把原型修改为另外一个对象就切断了构造函数与最初原型之间的联系
function Person(){}
var friend = new Person();
Person.prototype = {
constructor: Person,
name: 'Lihua',
job: 'student',
sayName: function(){
console.log(this.name);
}
};
friend.sayName(); //error
但觉得JS高程这种说法有点欠妥, 比如:
function Person(){}
Person.prototype.job = 'coder';
Person.prototype.sayJob = function(){
console.log(this.job);
}
var friend = new Person();
Person.prototype = {
constructor: Person,
name: 'Lihua',
job: 'student',
sayName: function(){
console.log(this.name);
}
};
var person = new Person();
person.sayName(); //Lihua
friend.sayJob(); //coder
// person.sayJob(); error
我的理解是:
- 原型是一种动态动态的关系,添加一个新的属性或方法到原型中时,该属性或方法会立即对所有基于该原型创建的对象可见,哪怕是修改原型之前创建的对象;
- 将原型重写为另外一个对象后, 会影响重写后实例得到的对象, 切断了构造函数与最初原型之间的联系; 对于重写之前已经实例化的对象没有影响, 因为这些对象里已经保存了一个
[[prototype]]
指针, 该指针指向重写之前的原型对象;
原型对象的问题
尽管可以在实例上添加一个同名属性从而隐藏那些不需要共享的属性, 然而对于包含引用类型的值的属性来说, 问题就比较突出.
function Person(){}
Person.prototype = {
constructor: Person,
name: 'smith',
job: 'coder',
friends: ['shelby', 'court'],
sayName: function(){
console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push('van');
console.log(person1.friends); //'shelby,court,van'
console.log(person2.friends); //'shelby,court,van'
console.log(person1.friends === person2.friends); //true
在这种情况下,person1
与person2
的friends
属性完全相同, 这不符合常识(一个人的朋友怎么可能和另外一个人的朋友完全相同呢?).
优点: 所有实例共享原型对象的属性和方法
缺点: 原型模式的最大问题正如以上所述, 无法屏蔽原型中数据类型为引用类型的属性
构造函数模式+原型模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['shelby', 'court'];
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person('smith', 25, 'coder');
var person2 = new Person('Lihua', 28, 'worker');
person1.friends.push('van');
console.log(person1.friends); //"shelby, court, van"
console.log(person2.friends); //"shelby, court"
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true
构造函数模式+原型模式结合了构造函数模式和原型模式的优点, 将需要实例单独拥有的属性放到构造函数里, 将需要实例共享的方法放到原型里, 这种模式是目前使用最广泛和认同度最高的一种创建自定义类型的方法.
动态原型模式
function Person(name, age, job){
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if(typeof this.sayName != 'function'){ //在sayName方法不存在的情况下,将其添加到原型中
Person.prototype.sayName = function(){
console.log(this.name);
};
}
}
var friend = new Person('smith', 25, 'coder');
friend.sayName(); //smith
动态原型模式把所有信息都封装到了构造函数中, 通过在构造函数中初始化原型(可选), 保持了同时使用构造函数和原型的优点.
寄生构造函数模式
function createPerson(name, age, job){
//创建对象
var o = new Object();
//添加属性
o.name = name;
o.age = age;
o.job = job;
//添加方法
o.sayName = function(){
console.log(this.name);
};
//返回对象
return o;
}
var person1 = new Person('smith', 25, 'coder');
friend.sayName(); //'smith
寄生构造函数模式除了调用方式与工厂模式不同外new Person('smith', 25, 'coder'),其他完全一样, 通过在构造函数末尾添加return语句, 重写了调用构造函数时返回的对象
寄生构造函数模式适合在特殊情况下为对象创建构造函数. 因为不推荐在产品化的程序中修改原生对象的原型, 但在某些场合下又的确需要创建一个具有额外方法的原生对象:
function SpecialArray(){
//创建数组
var values = new Array();
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipedString = function(){
return this.join('|');
}
//返回具有额外方法的数组
return values;
}
var colors = new SpecialArray('red', 'blue', 'green');
console.log(colors.toPipedString()) //'red|blue|green'
缺点: 返回的对象与构造函数或与构造函数的原型属性之间没有关系, 因为return
语句重写了调用构造函数时返回的对象, 也即是构造函数返回的对象与在构造函数外部创建的对象没有什么不同, 不能依赖instanceof
操作符确定对象类型
稳妥构造函数模式
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//这里定义私有变量和函数
//添加方法
o.sayName = function(){
console.log(name);
}
//返回对象
return o;
}