js原型详解

前言

在讲原型之前,先讲一下原型是为了解决什么问题而存在的,不希望一起来就讲原型,这样的话会有点生硬。下面会一步步递进,从对象的创建、构造函数的使用、再到原型的引入,一步步的进阶。

对象的创建

在开始讲原型之前,先来看看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"]
总结

本文从对象的创建为起点,使用最原始的方法创建对象,到使用工厂模式创建对象来达到减少代码的冗余,进而使用构造函数来实现能创建自定义的对象。根据构造函数创建对象存在的问题引出了原型,步步递进到最后的原型链。如有遗漏或错误,欢迎补充、指定。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值