《JavaScript高级程序设计》一共提到了7种创建对象的方式:
- 工厂模式
- 构造函数模式
- 原型模式
- 构造函数和原型组合模式
- 动态原型模式
- 寄生构造模式
- 稳妥构造模式
工厂模式
// 工厂模式
function Person(){
var o = new Object();
o.name = 'xiaopao';
o.say = function(){
alert(this.name)
};
return o;
}
var person1 = Person();
console.log(person1);
console.log(person1 instanceof Person); // false 无法得知实例的类别
console.log(person1 instanceof Object); // true
console.log(person1.__proto__ === Object.prototype); // true
优点: 完成了返回一个对象的要求。
缺点:
- Person ---> Person.prototype ; person1 = o ; o.proto--->Object.prototype
无法通过constructor识别对象,以为都是来自Object , 无法得知来自Person - 每次通过Person创建对象的时候,所有的say方法都是一样的,却存储了多次,浪费内存资源。
构造函数模式
// 构造函数模式
function Person(name){
this.name = name;
this.say = function(){
console.log(this.name)
}
}
var person1 = new Person('xiaopao');
console.log(person1);
person1.say(); // xiaopao
console.log(person1 instanceof Person); // true 可以的是实例的类别
console.log(person1.__proto__ === Person.prototype); // true
Person('xiaopao');
console.log(name); // 添加了一个全局name属性
优点:
1.可以通过constructor或者instanceof可以识别实例的类型
2.可以通过new 关键字来创建对象实例,更像OO语言中创建对象实例
缺点:
1.多个实例的say方法都是实现一样的效果,但却存储了很多次(存放地址不同)
构造函数模式优化
// 构造函数模式优化
function Person(name){
this.name = name;
this.say = getName;
}
function getName(){ // 这样添加了全局方法,这就没所谓的封装性了
console.log(this.name);
}
var person1 = new Person('xiaopao');
console.log(person1);
person1.say(); // xiaopao
优点: 解决了每个方法都要被重新创建的问题
缺点:给全局添加方法,无封装性可言。。
原型模式
// 原型模式
function Person(){};
Person.prototype.name = 'xiaopao';
Person.prototype.say = function(){
console.log(this.name);
}
var person1 = new Person();
console.log(person1);
console.log(person1.name); //xiaopao
person1.say(); // xiaopao
优点:方法不会重新创建
缺点:
1.所有属性和方法都共享
2.不能初始化参数(不能传参)
原型模式优化
// 原型模式优化
function Person(){};
Person.prototype = {
name: 'xiaopao',
say: function(){
console.log(this.name);
}
}
var person1 = new Person();
console.log(person1.name); // xiaopao
person1.say(); // xiaopao
console.log(Person.prototype.constructor); // ƒ Object() { [native code] }
优点:封装性好一点
缺点:重写了原型,丢失了constructor属性
原型模式优化
// 原型模式优化
// function Person(){};
// Person.prototype = {
// constructor: Person,
// name: 'xiaopao',
// say: function(){
// console.log(this.name);
// }
// }
// var person1 = new Person();
// console.log(person1.name); // xiaopao
// person1.say(); // xiaopao
// console.log(Person.prototype.constructor); // Person(){}
优点:实例可以通过constructor属性找到所属构造函数
缺点: 原型模式该有的缺点还是有
原型模式缺点
// 原型模式缺点 属性是引用类型时
function Person(){};
Person.prototype.name = 'xiaopao';
Person.prototype.say = function(){
console.log(this.name);
}
Person.prototype.friends = ['ailse']; // friends属性存储的是内存地址,指向的是同一个数组(引用类型)对象, 当其中一个实例修改数组的值,其他实例访问该属性时值都已改变
var person1 = new Person();
var person2 = new Person();
console.log(person1.friends,person2.friends); // ["ailse"] ["ailse"]
person1.friends.push('lulu');
console.log(person1.friends,person2.friends); // ["ailse", "lulu"] ["ailse", "lulu"]
组合模式
构造函数与原型模式组合
// 组合模式
function Person(name){
this.name = name;
this.friends = ['ailse'];
}
Person.prototype = {
say: function(){
console.log(this.name);
}
}
var person1 = new Person('xiaopao');
var person2 = new Person('jianjian');
console.log(person1.name); // xiaopao
console.log(person2.name); // jianjian
person1.say(); // xiaopao
person2.say(); // jianjian
person1.friends.push('lulu');
console.log(person1.friends); // ["ailse", "lulu"]
console.log(person2.friends); // ["ailse"]
优点:
1.解决了原型模式对于引用对象的缺点
2.解决了原型模式没有办法传递参数的缺点
3.解决了构造函数模式不能共享方法的缺点
缺点:如果构造函数和原型对象放在一起,封装性强再强一点
(先new一个对象,再对原型对象通过对面字面量进行赋值的情况)
直接通过对象字面量给 Person.prototype进行赋值的时候会导致constructor改变,所以需要手动的设置
直接通过对象字面量给Person.prototype进行赋值,会无法作用在之前创建的对象实例上。
function Person(name){
this.name = name;
this.friends = ['ailse'];
}
var person1 = new Person('xiaopao'); // 报错 Uncaught TypeError: person1.say is not a function
// Person.prototype.say = function(){
// console.log(this.name);
// }
Person.prototype.getName = function(){
console.log(this.name);
}
Person.prototype = { // 用对象字面量的形式必须在new实例之前定义,
constructor: Person,
say: function(){
console.log(this.name);
}
}
var person2 = new Person('jianjian');
person1.getName(); // xiaopao
// person1.say(); // 报错
person2.say(); // jianjian
这是因为对象实例和对象原型直接是通过一个指针链接的,这个指针是一个内部属性[[Prototype]], 可以通过proto访问。我们通过对象字面量修改Person.prototype指向的地址,然而对象实例的proto,并没有跟着一起更新,所以这就导致,实例还是访问着原来的Person.prototype, 所以建议不要通过这种方式去改变 Person.prototype属性
动态原型模式
JavaScript除了function Person, 还有一个Person.prototype,被定义成了两部分。所以,JavaScript对于封装性还是不够完美,而动态原型模式正是致力于要解决这个问题,它把所有的信息都封装在了构造函数中,通过在构造函数中初始化原型,既很好的体现了封装性,又保持了组合使用构造函数和原型模式的特点,一举两得,非常完美。
// 动态原型模式 把原型对象属性也放在构造函数当中(封装性强一点),第一次new实例进行原型对象赋值, 之后不重复赋值, 但是不能通过对象字面量进行赋值
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
console.log(typeof this.sayName);
if(typeof this.sayName != 'function'){
Person.prototype.sayName = function(){
console.log(this.name);
};
Person.prototype.sayJob = function(){
console.log(this.job);
};
}
}
var p1 = new Person('张三', 18, 'JavaScript');//sayName不存在,添加到原型
var p2 = new Person('李四', 20, 'Java');//sayName已经存在,不会再向原型添加
p1.sayName();//张三
p2.sayName();//李四
为什么动态原型模式不能通过对象字面量赋值原型
第一次创建实例对象时,先new, 然后执行构造函数,重写原型,那么此时实例的 proto指向的还是原来的原型,不是重写后的原型。 第二次创建实例,因为新原型已经创建好了,所以实例的 proto指向的就是重写的这个原型。使用给原型添加属性的方式操作的一直是同一个原型,所以也就不存在先后的问题。
优点:封装性更优秀
小缺点: 不能使用对象字面量的形式初始化原型
// 动态原型模式优化
function Person(name) {
this.name = name;
if (typeof this.getName != "function") {
Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
}
return new Person(name);
}
}
var person1 = new Person('kevin');
var person2 = new Person('daisy');
person1.getName(); // kevin
person2.getName(); // daisy
寄生构造函数模式
// 寄生构造函数模式
function Person(name){
var o = new Object();
o.name = name;
o.say = function(){
console.log(this.name);
}
return o;
}
var p1 = new Person('xiaopao');
console.log(p1);
console.log(p1 instanceof Person); // false
console.log(p1 instanceof Object); // true
优点: 和工厂模式基本一样,除了多了个new操作符
缺点: 和工厂模式一样,不能区分实例的类别
这种方法可以在特殊情况下使用。 比如我们想创建一个具有额外方法的特殊数组,但是又不想直接修改Array构造函数,可以这样写:
function SpecialArray() {
var values = new Array();
for (var i = 0, len = arguments.length; i < len; i++) {
values.push(arguments[i]);
}
values.toPipedString = function () {
return this.join("|");
};
return values;
}
var colors = new SpecialArray('red', 'blue', 'green');
var colors2 = SpecialArray('red2', 'blue2', 'green2');
console.log(colors);
console.log(colors.toPipedString()); // red|blue|green
console.log(colors2);
console.log(colors2.toPipedString()); // red2|blue2|green2
其实所谓的寄生构造函数模式就是比工厂模式在创建对象的时候,多使用了一个new, 实例上两者的结果是一样的。
但是作者可能是希望能想使用普通Array一样使用SpecialArray, 虽然把SpecialArray当成函数也一样能用,但是这并不是作者的本意,也变得不优雅。
在可以使用其他模式的情况下,不要使用这种模式
稳妥构造函数模式
// 稳妥构造函数模式
function Person(name){
var o = new Object();
o.name = name;
o.say = function(){
console.log(name);
}
return o;
}
var p1 = Person('xiaopao');
console.log(p1.name); // xiaopao
p1.say(); // xiaopao
p1.name = 'lulu';
p1.say(); // xiaopao
console.log(p1.name) // lulu
优点:安全, name好像成了私有变量,只能通过say方法去访问
缺点: 不能区分实例的类别
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this对象。
与计生构造函数模式有两点不同:
- 新创建的实例方法不引用this
- 不使用new操作符调用构造函数
稳妥对象最适合在一些安全的环境中。
https://juejin.im/post/5bdf1cf06fb9a049cd53a3a1