前言
在讲原型之前,先讲一下原型是为了解决什么问题而存在的,不希望一起来就讲原型,这样的话会有点生硬。下面会一步步递进,从对象的创建、构造函数的使用、再到原型的引入,一步步的进阶。
对象的创建
在开始讲原型之前,先来看看js中创建一个对象的过程。但是通过这种方式去创建对象的话会至少存在一下需要改进的地方。
1、如果需要创建多个对象的话,那么需要执行多次重复的代码,会造成代码的冗余。
2、所有创建出来的对象都是Object类的实例,如果要创建一个Person实例的话,这种方法无法实现。
let obj = new Object();
obj.name = 'star';
obj.age='10';
console.log(obj.name);
工厂模式
为了解决上面问题1,从而引入了工厂模式,通过工厂模式,避免了手动去创建对象的操作,而是把对象的属性和方法传递给对应的“工厂”,通过工厂的加工之后再返回相应的实例。但是工厂模式的话只是实现了封装,如果想要创建一个Person对象实例的话,还是无法实现,进而引出了构造函数来创建对象。
function personFactory(name, age) {
let obj = new Object();
obj.name = name;
obj.age = age;
return obj;
}
构造函数
构造函数其实也是一种特殊的工厂模式,只是通过构造函数可以创建特定的对象实例。通过构造函数来创建对象,与普通的工厂模式存在以下区别
1、不需要显示调用new Object去创建一个实例,构造函数默认会自己去创建一个实例。
2、所有的属性和方法都挂载到this上,并且默认返回this,也就是调用构造函数创建的对象实例。例如jack。
function Person(name, age) {
this.name = name;
this.age = age;
this.getName = () => {
return this.name;
}
// return this; 默认返回this
}
let jack = new Person('jack', 10);
console.log(jack.name); // 由于上面返回的this,所以这里才能调用对应的属性,返回相应的值。
console.log(jack.getName());
通过构造函数来创建对象,解决了创建特定对象。但是通过构造函数创建对象也存在问题。在讲存在的问题之前先来看js中函数定义的本质,在js中函数也是特殊的对象,定义函数的时候js会调用new Function创建一个Function对象,由于函数也是一个对象,所以也会有自己的内存空间,所以如果在构造函数中声明方法的时候会存在下面的问题
function Person(name, age) {
this.name = name;
this.age = age;
this.getName = () => { // new Function("return this.name");
return this.name;
}
}
let person1 = new Person('jack1', 10);
let person2 = new Person('jack2', 11);
person1.getName == person2.getName;// false
1、在构造函数中定义方法,在通过new Person创建对象的时候,每一个实例都会执行构造函数中的逻辑,将属性和方法都挂载到实例上,所以每创建一个实例的时候,都会创建一个函数对象,并且将实例的getName属性指向该函数对象。虽然getName函数对象每一个实例来说,执行的逻辑都是一样的,但是每一个实例中都会保存一个相同的函数对象,这会造成内存浪费。解决方案就是将getName函数提取出来,不在构造函数中定义。
function Person(name, age) {
this.name = name;
this.age = age;
this.getName = getName;
}
function getName() {
return this.name; // 对象实例调用该方法,所以this指向是对象实例
}
console.log(new Person('jack', 10).getName());
但是这种方法会破坏构造函数的封装性,为了解决这个问题,于是引入了原型。
原型
原型可以解决多个对象实例之间资源共用的问题,原型可以理解成是一块共享内存块,在这个内存块中存储着一份数据,所有的实例需要的时候,只需要访问该内存块即可,从而达到共用共享的问题。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
return this.name;
}
let person1 = new Person('jack', 10);
let person2 = new Person('jack1', 11)
console.log(person1.getName());
console.log(person1.getName == person2.getName);// true
console.log(person1.constructor == Person);// true
1、对于原型的理解
原型中最重要的就是prototype属性,在定义构造函数的时候,其实也就是创建一个Function函数对象实例,这个实例与构造函数本质上没关联,通过new创建一个Person对象实例的时候,默认会将__prop__属性指向该函数对象实例,函数对象实例中的construct属性又会指向构造函数Person,在构造函数内部又会通过构造函数的prototype属性指向该函数对象实例,所以,构造函数、构造函数实例、对象实例三者之间就形成了关联。有一种说法就是实例对象的__prop__属性指向构造函数的prototype属性,其实这种说法不太准备,本质来将,两者是没有什么关联性的,只是两者都指向了同一个构造函数实例,这个下面会做相应的解释。
上面的图诠释了三者之间的关系。上图中name、age属性没有挂载在prototype上,所以应该是在对象实例上,不应该在原型对象上。Person的prototype很关键的一环,因为它指向了原型对象,其实就是一块共享内存构造函数实例。当在Person的对象实例中调用getName方法的时候,由于对象实例中并没有这个方法,所以会通过__prop__到原型中去查找,从而实现调用。所以通过原型解决了共享和封装的问题。
2、变异
如果改变Person的prototype,直接将其指向一个普通的Object对象的话,又会是怎样?
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
getName: function() {
console.log(this.name);
},
getAge: function() {
console.log(this.age);
}
} // 这里会调用Object创建一个对象
let person1 = new Person('Jack', 10);
console.log(person1.constructor == Person);// false
console.log(person1.constructor);// Object
上面将prototype指向一个对象,从原来的指向构造函数实例指向了该Object对象实例,这种方法也可以使用原型,只是此时person1.constructor的构造函数由原来的Person变成了Object,本质其实是此次prototype指向了Object实例,该实例的constructor默认是指向构造函数的,由于该实例是通过Object构造函数创建的,自然constructor也就指向Object,上面的constructor指向Person也是一个道理,prototype指向的Person构造函数的实例对象。
这种方法也能实现原型,只是constructor的指向发生了改变,但是不影响调用。如果需要用到constructor的话可以手动将其指向Person即可。
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
3、__prop__与prototype之间的关系
开始prototype指向Person构造函数实例,后面改变prototype,将其指向一个Object的对象实例,此时person1.getAge()调用会报错,这也就说明了一个问题,对象实例的__prop__与prototype之间没有直接的联系。改变prototype指向之后创建实例person2,执行person2.getAge()没有报错,说明了对象实例的__prop__是在创建对象的时候,构造函数根据当前prototype的指向,也将__prop__指向该原型,后面prototype指向其他的原型,对于已经创建的对象实例的__prop__没有影响。所以__prop__与prototype之间之间其实没有强关联。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
console.log(this.name);
}
let person1 = new Person('Jack', 10);
console.log(person1.getName());// Jack
Person.prototype = {
getAge: function() {
console.log(this.age);
}
}
// console.log(person1.getAge());// error
let person2 = new Person('Mark', 11);
console.log(person2.getAge());// 11
原型链和继承
原型链的本质就是将一个构造函数Student的prototype指向另一个构造函数Person的实例。当创建Student的实例,调用对应方法的时候,会通过__prop__到构造函数Student的prototype指向的原型中去查找,此时构造函数Student的prototype指向了构造函数Person的实例,也就是会通过构造函数Person的实例调用该方法,找不到的话就会到构造函数Person的prototype指向的原型中去查找,从而找到该方法进行调用,输出jack,所以原型链就是在原型的基础上进行一层层的调用,实现一条链式的操作。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function(){
console.log(this.name);
}
function Student(clazz, no) {
this.clazz = clazz;
this.no = no;
}
Student.prototype = new Person('jack', 10);
let student = new Student('1', 1);
console.log(student.getName());// jack
继承
继承就是利用原型链来实现的,通过原型链将两个构造函数之间实现了绑定,实现链式。但是上面的继承存在问题。
1、如果Person中存在引用属性的话,那么通过Student的实例去修改的时候会造成所以实例对应的该属性都修改,造成联动效果。
2、由于Student.prototype的指向了Person的实例,所以后面创建Student对象的时候,无法动态向Person的构造函数传递参数。
function Person(name, age) {
this.name = name;
this.age = age;
this.phone = ['iphone6', 'iphone10'];
}
Person.prototype.getName = function(){
console.log(this.name);
}
Person.prototype.getPhone = function(){
console.log(this.phone);
}
Person.prototype.setPhone = function(phone){
this.phone.push(phone);
}
function Student(clazz, no) {
this.clazz = clazz;
this.no = no;
}
Student.prototype = new Person('jack', 10);
let student1 = new Student('1', 1);
let student2 = new Student('1', 2);
console.log(student1.getName());// jack
student1.setPhone('iphone7');
student1.getPhone(); // iphone6, iphone10, iphone7
student2.getPhone();// iphone6, iphone10, iphone7
联动问题的关键在于将Student.prototype指向了Person的实例,所有的Student的实例都共用此对象,所以会造成联动修改。
解决办法就是手动调用父构造函数,将父构造函数中的属性复制一份到子构造函数中,覆盖原来的共用属性。
function Person(name, age) {
this.name = name;
this.age = age;
this.phone = ['iphone6', 'iphone10'];
}
function Student(name, age, clazz, no) {
Person.call(this, name, age);// 伪继承,手动调用构造函数,将构造函数中的属性都复制一个副本到student中,因为此时已经指定了Person的this对象为Student的实例对象
this.clazz = clazz;
this.no = no;
}
let student1 = new Student('jack1', 10, '1', 1);
let student2 = new Student('jack2', 11, '1', 2);
student1.phone.push('iphone7');
student1.phone;// ["iphone6", "iphone10", "iphone7"]
student2.phone;// ["iphone6", "iphone10"]
虽然上面的方案也能解决向父构造函数传递参数的问题,但是此时又引发了另一个问题,父构造函数中如果有方法的话,由于直接复制给子构造函数,所以无法实现复用,又是造成每一个子构造函数对象实例中保存一份方法的实例对象。
最终方案
将伪构造函数与将prototype的指向了父构造函数的实例两种方式进行结合。
function Person(name, age) {
this.name = name;
this.age = age;
this.phone = ['iphone6', 'iphone10'];
}
Person.prototype.getName = function(){
console.log(this.name);
}
Person.prototype.getPhone = function(){
console.log(this.phone);
}
Person.prototype.setPhone = function(phone){
this.phone.push(phone);
}
function Student(name, age, clazz, no) {
Person.call(this, name, age);// 伪构造
this.clazz = clazz;
this.no = no;
}
Student.prototype = new Person('jack', 10);// 改变prototype指向
let student1 = new Student('jack1', 10, '1', 1);
let student2 = new Student('jack2', 11, '1', 2);
console.log(student1.getName());// jack
student1.setPhone('iphone7');
student1.getPhone(); // ["iphone6", "iphone10", "iphone7"]
student2.getPhone();// ["iphone6", "iphone10"]
总结
本文从对象的创建为起点,使用最原始的方法创建对象,到使用工厂模式创建对象来达到减少代码的冗余,进而使用构造函数来实现能创建自定义的对象。根据构造函数创建对象存在的问题引出了原型,步步递进到最后的原型链。如有遗漏或错误,欢迎补充、指定。