原型 , 构造模式 , 原型模式 ,组合模式
1.理解原型
在JS中,只要声明一个函数A , 就会对应的产生一个对象 , 这个对象就叫原型对象
试试看
function A() {
}
console.log(A.prototype);
console.log(A.prototype.constructor);
返回值如下:
定义了一个函数 A , 没有添加任何的属性和方法 , A 的两未定义个属性 却可以返回两个值?
prototype
属性返回了一个对象 , constructor
属性返回了函数A
prototype
是 A 里面的属性 , 指向一个对象,这个对象就叫原型对象 ,
而这个原型对象里 默认情况 储存着 constructor
属性和从Object继承而来,这里暂且不表;
constructor
是一个属性 , 存在原型对象里 , 指向了这个函数
比如俩人吵架 , 都用手指头指着对方骂;
而这两个属性就是手指头,构造函数和原型对象就是吵架的人;
手指头是人的一部分,就像这两个属性存在对象的内部是一样的
2.与原型相关的属性和方法
constructor
属性
存在于原型对象中,指向构造函数;
如果重写原型对象则会导致constructor
属性不再指向构造函数;
如果需要constructor
依旧指向构造函数,可以在重写的时候加上以下代码
Person.prototype = {
constructor : Person //让constructor重新指向Person函数
}
[[prototype]]
属性
用构造函数创建一个实例对象后,这个对象中会有一个不可访问的属性[[prototype]]
,这个属性就指向了构造函数的原型对象
Chrome浏览器和火狐浏览器提供这个对这个属性的访问
使用__proto__
方法可以访问到原型对象(左右各两个下划线),这个属性存在于被构造函数创建的实例对象中;一般不建议使用
hasOwnProperty()
方法
判断一个属性是否来自对象自身 ;
返回 true
表示为对象自身属性;返回false
可以判断出存在原型中或属性不存在,所以要如何确定一个属性存在原型里呢?
in
操作符
in
操作符会从对象的本身开始 , 查找是否有对应的属性 , 在对象自身中没有找到 , 就会沿着原型链开始查找 , 直到找到返回true
,反之,则返回false
3. 两个模型--构造函数模型 和 原型模型
3.1构造函数模型
理论上任何函数都可以作为构造函数,但是一般约定,构造函数的函数名以大写字母开头
比如:
构造函数 :
function Person(){ }
当把一个函数作为构造函数,并利用new创建的一个新的实例对象
利用构造函数创建实例 : var p1 = new Person( );
3.1.1构造过程
相关:
实例与构造函数的关系 :
实例对象p1被构造函数Person创建后,P1和Person是没有任何的关系了;
实例与原型 :
- 实例对象中有一个[[prototype]]属性指向原型对象;
- 使用new Person()创建多个对象,则多个对象都会同时指向原型对象。
- 可以手动给这个原型对象添加属性和方法,那么p1,p2,p3...都会共享这些属性和方法
- 属性和方法的查找会从实例开始 , 沿着原型链查找 , 直到找到属性或方法;
- 所以给实例对象添加和原型中同名的属性 , 会优先访问实例中的属性
- 通过实例只能访问到原型的属性或方法,不能修改;
//这段代码将会抛出错误
function Person(name,age) {
}
Person.prototype.sex = "男"
let p1 = new Person();
p1.prototype.sex = "女";
console.log(p1.sex);
3.1.2构造函数的优势
可以传入参数,适用于 每个实例都有的同名但是值不相同的属性
function Person(name,age) {
this.name = name;
this.age = age;
this.speakName = function (){
console.log(this.name)
}
}
let p1 = new Person("shark",3);
let p2 = new Person("dd",4)
console.log(p1.name,p1);
console.log(p1.age)
//输出结果
shark 3
dd 4
内存模型:
目前来说,每个实例都有了自己的name和age , 但是新的问题也随之而来了
3.1.3构造函数的缺陷
观察内存模型 , 就会发现每个实例中都有一个相同的方法 , 浪费了内存
这不是很OK , 不是我们想要的结果,所以一种新的模型站了出来 - -原型模型
3.2原型模型
3.2.1原型模式的优势
针对以上的问题 , 对代码做出了以下修改
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.speakName = function (){
console.log(this.name)
}
let p1 = new Person("shark",3);
let p2 = new Person("dd",4)
console.log(p1.name,p1);
console.log(p1.age)
利用原型模式的优势 ---共享所有的方法
可以让我们共享speakName
这个方法;
这正好解决了构造函数的缺陷 , 每个实例都有自己的名字 , 方法在不浪费内存和性能的情况下共用;
3.14 原型模式的缺陷
那假如使用原型模式添加属性呢?
function Person() {
}
Person.prototype.country = "China";
Person.prototype.name = "小李";
var p1 = new Person();
var p2 = new Person();
场景:
现在有假如两个中国人p1,p2 ,他们的国籍一样,使用原型模式,使得两个人的国籍一致
结合上一个例子说明了:
原型中适合存储大家共有的属性和方法;
不过原型也存在缺陷 :
名字没有办法做到每个人都独一无二 , 不过在之前提到的构造函数模型中 , 正好解决了给不同的实例的相同name
赋不同值的情况;
3.4 总结
原型模式适合封装方法和共享的属性,构造方法模式适合封装值不同的属性
如果把这两个模式结合起来,就有了组合模式;
4.组合使用两种模型
组合构造是基于两种模式互补的一种新的构造方法;
总结一下两种模式的优缺点:
优势 | 缺陷 | |
---|---|---|
构造函数模式 | 属性在对象中都独有一份 | 对于方法来说,没有必要一人一份 |
原型模式 | 方法可以共享 | 一般属性的值是不同的,不适合共享 |
不难看出,两者的正好是互补的,所以组合起来使用是最佳的方法;
4.1 组合模式
//在构造方法内部封装属性
function Person(name, age) {
this.name = name;
this.age = age;
}
//在原型对象内封装方法
Person.prototype.eat = function (food) {
alert(this.name + "爱吃" + food);
}
Person.prototype.play = function (playName) {
alert(this.name + "爱玩" + playName);
}
var p1 = new Person("李四", 20);
var p2 = new Person("张三", 30);
p1.eat("苹果");
p2.eat("香蕉");
p1.play("志玲");
p2.play("凤姐");
虽然完美解决了种模式的缺陷,但是还不够完美
因为 , 代码还不够优雅
4.2 动态原型模式
动态原型模式把所有的属性和方法都封装在构造方法中,而仅仅在需要的时候才去在构造方法中初始化原型,又保持了同时使用构造函数和原型的优点。
<script type="text/javascript">
//构造方法内部封装属性
function Person(name, age) {
//每个对象都添加自己的属性
this.name = name;
this.age = age;
/*
判断this.eat这个属性是不是function,如果不是function则证明是第一次创建对象,
则把这个funcion添加到原型中。
如果是function,则代表原型中已经有了这个方法,则不需要再添加。
perfect!完美解决了性能和代码的封装问题。
*/
if(typeof this.eat !== "function"){
Person.prototype.eat = function () {
alert(this.name + " 在吃");
}
}
}
var p1 = new Person("志玲", 40);
p1.eat();
</script>
看起来优美多了..但是还是差一丝优美...
4.3 优化
function Person(opt) {
this._init(opt)
}
//方法和属性都被封装到了一起,但是属性实际上还是属于构造模式创建的
//体现了封装性,但是重写Prototype带来了一个小问题
Person.prototype = {
//新的原型对象不存在constructor属性,故补齐
constructor:Person
//初始化属性
_init: function (opt) {
this.name = opt.name;
this.age = opt.age;
},
eat: function () {
return "名字:" + this.name
},
howOld: function () {
return "年龄:" + this.age
},
};
var p1 = new Person({
name: "李四",
age: 99
});
前端新人一枚 , 欢迎批评指正~