JavaScript面向对象编程-创建对象
1. 工厂模式:
考虑到em5无法创建类(我也不知道em6那个玩意儿算不算类),开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节;
【注】工厂模式解决不了对象识别的问题(即怎样知道一个对象的类型)
//工厂模式
function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.greeting = function(){
console.log(`Hi,I am ${this.name}`);
};
return o;
}
var person1 = new Person("xhy", 21, "student");
var person2 = new Person("yhs", 28, "rapper");
person1.greeting();//Hi,I am xhy
2. 构造函数模式:
借鉴了其它OO语言,我们可以创建一个 名字首字母大写的自定义构造函数,从而定义自定义对象类型的属性和方法:
//构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.greeting = function(){
console.log(`Hi,I am ${this.name}`);
};
}
var person1 = new Person("xhy", 21, "student");
var person2 = new Person("yhs", 28, "rapper");
person1.greeting();//Hi,I am xhy
要创建Person的实例,需要经历以下四个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
(3) 执行构造函数中的代码(为新对象添加属性);
(4) 返回新对象;
在本例中,person1和person2 这两个对象都有一个constructor(构造函数)属性,该属性指向Person:
console.log(person1.constructor === Person);//true
console.log(person2.constructor === Person);//true
【注】构造函数模式也有其问题所在,每当某一个实例调用构造函数中定义的方法时,每个方法都要被重新创建一遍;而创建方法函数function时,实际上也是实例化了一个对象,即
// this.name = funciton{...} 等价于 this.name = new Function(...);
// 毕竟 JS 里面万物皆可为对象
因此,不同实例上的同名函数其实是不相等甚至没有联系的,例如本例中:
console.log(person1.greeting == person2.greeting);//false
以这种方式创建函数,会导致不同的作用域链和标识符解析,莫名感觉浪费 ~
3. 原型模式
在上例代码上,当我把person1直接输出到控制台时,呈现的结果是这样的:
上边那几个属性,方法都很好理解,但最后一排这个__proto__又是什么呢
【注】无论何时,当一个新函数被创建出来时,就会为该函数自动创建一个prototype属性,这个属性指向函数的原型对象;而调用构造函数创建一个新实例后,该实例内部将产生一个名为 [[ Prototype ]] 的指针——指向构造函数的原型对象;
虽然在脚本中没有标准的方式可以去访问 [[ Prototype ]] 指针,但Firefox,Safari和Chrome在每个对象上都支持__proto__属性;
构造函数的原型对象默认只会取得constructor属性
而我们则可以将创建对象所需要的所有信息直接添加到原型对象中,这样就不必在构造函数里定义对象实例的信息了,并且我们可以让所有对象实例共享它所包含的属性和方法:
//原型模式
function Person(){}
Person.prototype.name = 'xhy';
Person.prototype.age = 21;
Person.prototype.job = 'student';
Person.prototype.greeting = function(){
console.log(`Hi,I am ${this.name}`);
}
var person1 = new Person();
var person2 = new Person();
console.log(person1.name);//xhy
person1.greeting();//Hi,I am xhy
console.log(person1);
此时的__proto__:
(就都在里面惹~)
下图展示了本例中,各个对象之间的关系:
具体实现过程中,例如,此时要访问 person1.name 的值,解析器就会先从person1实例中寻找name,发现找不到之后,再根据指针去找person1的原型,并将原型内部的name值进行返回;
显然,当我们在实例化对象后,再给person1实例赋予一个name值时,新的name值将被优先返回;
console.log(person1.name);//xhy —— 来自原型
person1.name = 'yhs';
console.log(person1.name);//yhs —— 来自实例
【注】:为对象实例添加一个属性,这个属性只会屏蔽原型对象中保存的属性,并不会修改;
———————————————————————————
上边例子中每添加一个属性和方法都要敲一遍 Person.prototype,我们可以按照如下方式简化代码:
Person.prototype = {
name : 'xhy',
age : 21,
job : 'student',
greeting : function(){
console.log(`Hi,I am ${this.name}`);
}
};
【注】:当采用此种写法时,constructor属性就不再指向Person了;原因在于我们在这里的写法本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person。
如果constructor值真的很重要,我们可以在上例创建对象的开头写入:
constructor : Person,
———————————————————————————
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此,如:
var fakeFriend = new Person();
Person.prototype.showFeeling = function(){
console.log('我一直一直把你看的很重要很重要');
}
fakeFriend.showFeeling();//我一直一直把你看的很重要很重要
从这里就可以很明显地反映出,实例与原型之间的连接只不过是一个指针,而非一个副本,因此 fakeFriend 就可以在原型中找到新的 showFeeling() 方法并返回;
【注】尽管可以很方便的为原型添加属性方法并快速地在实例中反映出来,但如果是重写整个原型对象,情况就很糟糕了:
function Person(){}
var fakeFriend = new Person();
Person.prototype = {
constructor : Person;
name : 'xhy',
age : 21,
job : 'student',
greeting : function(){
console.log(`Hi,I am ${this.name}`);
},
showFeeling : function(){
console.log('我一直一直把你看的很重要很重要');
}
};
fakeFriend.showFeeling();//fakeFriend.showFeeling is not a function
直接报错~ (果然,一个不真诚的人 在其他人眼里的形象完全颠覆之时,ta再怎么 晓之以情都是不好使的嘿嘿嘿)
嘛,出现这个问题的原因说来也简单,当重写整个原型对象时,相当于切断了构造函数和最初的原型对象之间的联系(在本例中,因为重写时赋予了constructor指向,它仍然会指向新的Person),而实例呢,它仍然指向的是初始的原型对象,其中自然不包含重写时候顺带添加上去的新方法。调用该方法时自然就报错了。
———————————————————————————
原型对象有缺点吗? 毋庸置疑,而且问题还不小;
首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值,这在一定程度上给操作带了不方便;
其次,也是最大的问题: 原型中所有属性是被很多实例共享的,这种共享对于函数极为合适,对于普通数据类型的属性倒也说的过去(前边提到过的覆盖方法),但对于引用数据类型来说,麻烦就很大了,例:
function Person(){}
Person.prototype = {
name : 'xhy',
age : 21,
job : 'student',
greeting : function(){
console.log(`Hi,I am ${this.name}`);
},
friend : ['yhs', 'zy', 'jonyJ']
};
var person1 = new Person();//(4) ["yhs", "zy", "jonyJ", "PGone"]
var person2 = new Person();//(4) ["yhs", "zy", "jonyJ", "PGone"]
当我们通过person1实例修改friend数组值时,person2中的数组值不自觉的也受到了影响(背后的原因可以去看笔者以前的一篇博客JavaScript数组引用问题),这样实例之间的引用数据类型的区别就体现不出来了。
为了解决此类问题,我们可以 组合使用构造函数模式与原型模式。
4. 组合使用构造函数模式和原型模式
//组合使用模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friend = ['yhs', 'zy', 'jonyJ'];
}
Person.prototype = {
constructor : Person ,
greeting : function(){
console.log(`Hi,I am ${this.name}`);
}
}
var person1 = new Person('xhy',21,'student');
var person2 = new Person('yhs',28,'rapper');
person1.friend.push('PGone');
console.log(person1.friend);//(4) ["yhs", "zy", "jonyJ", "PGone"]
console.log(person2.friend);//(3) ["yhs", "zy", "jonyJ"]
console.log(person1.friend === person2.friend);//false
console.log(person1.greeting() === person2.greeting());//true
问题完美解决有没有~
这种构造函数和原型混成的模式,是目前em中使用最为广泛,认同度最高的一种创建自定义类型的方法,可以说这是用来定义引用类型的一种默认模式;
5. 其它
其它的构造对象的模式还包括:
(1) 动态原型模式;
(2) 寄生构造函数模式;
(3) 稳妥构造函数模式;
感兴趣的小伙伴可以去搜集资料查阅一下~ 在此就不整理啦(其实只是笔者自己也没太搞懂~)
——————————————————————————————————————END——————————-——————