原型对象
什么是构造函数
函数是什么
在我看来,函数是就是一个工厂,我们向其加入原材料(参数),工厂进行操作与产出(return)
普通函数和构造函数的差别
- 构造函数和普通函数并不存在本质差别。
- 对于任意函数,使用new操作符调用,那么它就是构造函数;不使用new操作符调用,那么它就是普通函数。
构造函数----手工工厂
我们先来探讨一下手工工厂的特点。
- 定制化:可以根据不同需求轻易改变操作过程,达到定制化。比如我做一个手工机器人,但一个客户想让机器人的胳膊长一点,我在制作时就可以轻易达成。而流水线工厂因为模板固定无法轻易达成。
- 重复且劳累:手工工厂的缺点就是太累了,每做一遍都要在脑子里想一遍,即使是相同的部件也要再想一遍做一遍。而流水线工厂因为模板固定可以轻易达成。
接下来我们看一下构造函数:
function ManualRobot(){
this.name=name;
this.age=age;
this.sayHello=function(){
console.log('Hello')
}
}
构造函数是一个手工工厂。
为什么这样说呢?
- 定制化:构造函数的好处在于随着不同的传参会有个性化的属性
- 重复且劳累:在构造函数中即使是一个重复利用的东西,比如这里的sayHello,每个机器人的sayHello都是一样的,但会随着我每次的实例化对象而占用不同的空间,就是我实例化200个对象,同样的sayHello就重复占用了两百个不同的空间。
也就是说构造函数会造成内存的浪费,我们能不能把sayHello写在一个空间里,让这200多个对象指向它呢?
当然是可以的,那就是原型对象。(将函数写成全局函数再调用,存在封装不好的缺点,这里不进行讨论。)
什么是原型对象----流水线工厂
众所周知,随着构造函数的产生,会伴随生成一个原型对象,什么是原型对象呢?
- 简单点来说,就是实例对象的原型
- 官方一点说就是一个包含所有实例对象共享属性和方法的对象
- 按照我们的例子,原型对象是流水线工厂的模板。
举个简单的例子,我有一个全自动化工厂,它会由固定的模板生产出完全相同的机器人。
这个固定的模板是这样的:
function AutoRobot(){}
AutoRobot.prototype.name='小机器人'
AutoRobot.prototype.age=1
AutoRobot.prototype.sayHello=function(){
console.log('Hello')
}
原型对象的特点
现在,有一个人从我这里买了一个机器人
那么他这个购买的行为也就是:
var r1=new AutoRobot ( )
这个时候可以输出这个机器人的属性
console.log(r1.name) //小机器人
console.log(r1.age) //1
r1.sayHello() //Hello
然后,我的顾客嫌弃机器人太小了,不够成熟
她首先改了这个机器人的名字
r1.name='大机器人'
然后改了机器人的年龄
r1.age=18
这个时候实例r1的名字和年龄就改变了
也就是说:
当我们需要读取对象的某个属性时,都会执行一次搜索。首先在该对象中查找该属性,若找到,返回该属性值;否则,到[[prototype]]指向的原型对象中继续查找。
由此我们也可以看出另外一层意思:如果对象实例中包含和原型对象中同名的属性或方法,则对象实例中的该同名属性或方法会屏蔽原型对象中的同名属性或方法。原因就是“首先在该对象中查找该属性,若找到,返回该属性值;”
以上呢就是利用原型对象创造实例对象,就是原型对象是实例对象的模板。
构造函数,原型对象,实例对象的关系(原理)
现在大家对原型对象有了一个基本的认识,接下来我们看一下构造函数,原型对象,实例对象的关系
function AutoRobot(){}
AutoRobot.prototype.name='小机器人'
AutoRobot.prototype.age=1
AutoRobot.prototype.sayHello=function(){
console.log('Hello')
}
在刚开始构造函数的protype属性指向原型对象,原型对象的constructor属性指向构造函数,并且我向原型对象中添加了一些东西。为下图:
然后我们new了一个实例,实例r1的[[Prototype]]属性指向原型对象
var r1=new AutoRobot ( )
console.log(r1.name) //小机器人
console.log(r1.age) //1
r1.sayHello() //Hello
接着我们对实例的属性进行修改
r1.name='大机器人'
r1.age=18
console.log(r1.name) //大机器人
console.log(r1.age) //18
这是因为当我们需要读取对象的某个属性时,都会执行一次搜索。首先在该对象中查找该属性,若找到,返回该属性值;否则,到[[prototype]]指向的原型对象中继续查找。
构造函数和原型对象的比较
现在我们来总结一下构造函数和原型对象的优缺点
构造函数:
- 定制化:可以根据不同需求轻易改变操作过程,达到定制化。
- 重复且劳累:相同的内容会占用不同的空间,造成内存浪费。
原型对象
- 不累:所有对象的属性指向同一个地址,不会占用无效内存。
- 没有个性化:因为是流水线工厂,模板一样,做出来的东西一摸一样。
构造函数和原型对象的结合使用
组合模式
构造函数的优点是个性化,原型对象的优点是共享属性。
但是我们在实例化对象的时候呢,往往希望他们的属性是个性的,但方法是共享的。
比如小明叫小明,小红叫小红,郭越叫郭越,但是我们都会吃饭学习记笔记。
这个时候我们可不可以将构造函数和原型对象结合使用呢?
只要我们将属性写在构造函数中,将方法写在原型对象中:
function Student(name,age){
this.name = name
this.age = age
}
Student.prototype.sayHello=function(){
console.log('Hello')
}
Student.prototype.study=function(){
console.log('我要学习了')
}
var xiaoming = new Student('小明',18)
var xiaohong = new Student('小红',18)
var guoyue = new Student('郭越',18)
但这样封装性并不好,有没有更好的方法?
动态原型模式
我们在构造函数中加入一个判断,判断某个方法的属性是不是函数。
- 如果不是,说明此时是第一次创建对象,我们将这个方法写入原型对象。
- 如果是,说明这个方法已经写入原型对象,我们不需要其他操作。
function Student(name, age) {
this.name = name;
this.age = age;
if(typeof this.studay !== "function"){
Student.prototype.study = function(){
console.log('我要学习了')
}
}
}
var guoyue = new Student("郭越", 18)
guoyue.studay() //我要学习了
class类
或者直接用ES6中的class关键字创造类
在class类中
- 实例属性和实例方法:写在constructor中
- 原型方法:constructor外的方法自动转为原型方法
- 原型属性:通过类名.prototype添加
class Robot{
constructor(name,age){
this.name = name
this.age = age
this.myName = function(){
console.log('Hello,i am '+this.name)
}
}
sayBye(){
console.log('Bye!')
}
}
Robot.prototype.factory = '郭越机器厂'
//实例化
var r1 = new Robot('机器人1号',18)
console.log(r1.factory)
Robot.prototype.sayBye()
Robot.prototype.myName()
执行结果:
可见sayBye是原型方法,myName不是