《JS高级程序设计》第6章的读书笔记
- 创建对象(一)工场模式和构造函数模式
- 创建对象(二)原型模式和组合模式
- 创建对象(三)再探原型
- 对象继承(一)原型链
- 对象继承 (二)借用构造函数和组合继承
- 对象继承(三)原型式继承和寄生式继承
- 对象继承(四)寄生组合式继承
1 前言
JS的OO(面向对象)之所以与一般OO语言不同,一个关键原因在于JS多了原型
,也就是prototype
这个概念
在这篇文章之前,你应该先读下面这篇文章:
《JS高级程序设计》第6章读书笔记:创建对象(一)之工场模式和构造函数模式
本文的示例代码我放在github上:这个仓库
2 原型模式
实际上,当我们创建函数时,函数都有一个prototype的属性,指向一个对象,这个对象被称为原型对象,这个对象的用途是:”包含可以由特定类型的所有实例共享的属性和方法“。
所以根据其用途可知可以让所有对象实例共享它所有它所包含的属性和方法,不必在构造函数中定义对象实例的信息。
让我们看原型模式的基本代码:
function Person() {}
Person.prototype.name = 'achao';
Person.prototype.getName = function() {
return this.name;
}
var p1 = new Person();
var p2 = new Person();
console.log(p1.getName());//'achao'
console.log(p2.getName()); //'achao'
console.log(p1.getName == p2.getName); //true
我们可以看到p1,p2共享了getName
这个方法,当然还有name
这个基本类型的变量。
还记得吗?我们之前所说:构造函数没有彻底解决对象实例共享属性和方法
的问题。
而原型模式解决了这个问题。
3 理解原型对象
首先让我们看一张图:
上图展示了Person构造函数,Person的原型属性,以及Person现有的两个实例p1,p2之间的关系。
Person.prototype指向了原型对象,而Person.prototype.constructor指向了Person。
实例对象p1,p2都有各自属性[[Prototype]]
指向Person.prototype。虽然这个属性是访问不到的,但是可以通过isPrototypeOf()方法确认原型对象和实例对象的关系。
console.log(Person.prototype.isPrototypeOf(p1)); //true
console.log(Person.prototype.isPrototypeOf(p2)); //true
ES5的新方法:Object.getPrototypeOf()
可以返回[[Prototype]]
的值,也可以通过这个方法判定原型和实例对象之间的关系。
console.log(Object.getPrototypeOf(p1) == Person.prototype);// true
Q&A:
Q:如何读取实例对象的属性。
A:首先搜索实例对象,然后搜索原型对象,直至找到模板属性
Q:在对象实例中创建同名属性
A:不会重写原型对象中的同名属性属性,但是会屏蔽它,代码如下
p2.name = 'hhha';
console.log(p1.name);//achao
console.log(p2.name);//hhha
delete p2.name;
通过delete删除实例属性
p2.name = 'hhha';
console.log(p1.name);//achao
console.log(p2.name);//hhha
delete p2.name;
console.log(p2.name);//hhha
hasOwnProperty()
(从Object继承而来)方法可以检测一个属性是存在于实例中,还是存在于原型。当且仅当给定属性存在于实例对象中,才会返回true
p2.name = 'hhha';
console.log(p1.hasOwnProperty('name'));//false ,存在于原型对象
console.log(p2.hasOwnProperty('name'));//true,存在于实例
delete p2.name
4 更简单的原型语法
可以看到,前面例子中每添加一个属性和方法,都要敲一遍Person.prototype,为减少不必要的输入,可以通过对象字面量的方式重写整个原型对象
function Person() {}
Person.prototype = {
name: 'achao',
getName: function() {
return this.name;
}
}
var p3 = new Person();
但是这样的有一个问题:重写改变了实例的constructor属性
console.log(p3.constructor == Person);//false
console.log(p3.constructor == Object);//true
当然instanceof操作符仍然works
console.log(p3 instanceof Person);
如果constructor操作符很重要,可以设置回适当的值
function Person() {}
Person.prototype = {
constructor: Person,
name: 'achao',
getName: function() {
return this.name;
}
};
var p3 = new Person();
console.log(p3.constructor == Person);//true
但是这样有一个从问题:导致constructor
属性可枚举,而改写前[[Enumerable]]
特性是false
可以枚举的意思是:可以通过for-in访问到
for (var prop in Person.prototype) {
console.log(prop);//constructor,name,getName
}
解决方法是:Object.defineProperty
function Person() {}
Person.prototype = {
name: 'achao',
getName: function() {
return this.name;
}
};
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
})
for (var prop in Person.prototype) {
console.log(prop); //name,getName
}
如此就不可枚举了,但是此方法要求环境兼容ES5
5 原型对象的问题
原型模式省略了为构造函数传递初始化参数这一环节,造成实例在默认情况下取得相同的属性值。
对于基本数据类型的属性,这不是什么问题,只需在实例上添加一个同名属性,即可屏蔽原型属性,但是对于引用类型的属性,这是个大问题。看以下代码
function Person() {}
Person.prototype = {
constructor: Person,
name: 'achao',
friends: ['lilei', 'hanmeimei'],
getName: function() {
return this.name;
}
};
var p1 = new Person();
var p2 = new Person();
p1.friends =[];
p1.friends.push('xiaoming');
console.log(p1.friends);//['lilei','hanmeimei','xiaoming']
console.log(p2.friends);//['lilei','hanmeimei','xiaoming']
看出来了吗?p1,p2共享了friends
这个原型属性。从语义上讲,即使他们的确有共同的朋友,但是也有各自的朋友,这个属性是不适合共享的。这个问题造成原型模式很少被单独使用
回想一下:构造函数模式的问题是什么呢?属性和方法不能共享。
原型模式的问题在于:不该共享的属性共享。
那么,能够尝试把构造函数模式原型模式结合起来呢?
这就是接下来要讲的组合模式。
6 组合使用构造函数模式和原型模式
在这种模式下:构造函数模式用于定义实例属性,而原型模式用于定于方法和共享属性,结合了两者的长处。
直接看代码
function Person(name, friends) {
this.name = name;
this.friends = friends;
}
Person.prototype = {
constructor: Person,
getName: function() {
return this.name;
}
};
var p1 = new Person('achao', ['lilei', 'hanmeimei']);
var p2 = new Person('hhha', ['xiaoming', 'xiaohong']);
p1.friends.push('xiaogang');
console.log(p1.friends);// ['lilei', 'hanmeimei','xiaogang']
console.log(p2.friends);//['xiaoming', 'xiaohong']
console.log(p1.getName == p2.getName);//true
共享属性和方法被共享,实例属性没有共享
这种模式是ES5中使用最广泛,认同度最高的创建自定义类型的方法。