《王者归来》读书笔记 ── JavaScript 面向对象编程
跳过 JS 核心(语言结构、数据类型、函数、对象、集合、字符串等)以及 BOM、DOM 部分,这个系列的笔记主要总结一下《王者归来》面向对象编程部分的知识点,以便梳理和查阅。JavaScript 究竟是不是一种面向对象的语言呢?
“面向对象不是只有类模型一种,prototype-based(基于原型)是 class-based(基于类)的简化版,是一种 class-less 的面向对象。对应的,prototype 继承是 class 继承的简化版(例如省略了多重继承、基类构造函数、忽略了引用属性的继承等),但不能因为它不支持这些特性就不承认它是一种完整的继承。是否为继承添加额外的特性,开发者可以自由选择,但在不需要这些额外特性的时候,还是有理由尽量用 prototype-based 继承。
总而言之,prototype-based 认为语言本身可能不需要过分多的 reuse 能力,它牺牲了一些特性来保持语言的简洁,这没有错,prototype-based 虽然比 class-based 简单,但它依然是真正意义上的 object-oriented。”
I. 公有和私有 ── 属性的封装
JS中,函数是绝对的“第一型”,JS 的对象和闭包都是通过函数实现的。利用闭包的概念,JS 中可以有不逊色于其他各种面向对象语言的公有和私有特性:
function List(){
var m_elements = []; //私有成员,对象外无法访问
m_elements = Array.apply(m_elements, arguments);
//公有属性,可以通过“.”运算符或下标访问
this.length = {
valueOf : function(){return m_elements.length;},
toString : function(){return m_elements.length;}
}
this.toString = function(){
return m_elements.toString();
}
this.add = function(){
m_elements.push.apply(m_elements, arguments);
}
}这个例子中 this.length, this.toString, this.add 是公有成员,其中 this.length 是私有成员 m_elements 的 length 属性的 getter,外部我们可以通过“.”运算符对这些属性进行访问。
对象的 getter 是一种特殊的属性,它形式上像是变量或者对象属性,但它的值随着传入参数的改变而改变。在不支持 getter 的语言中,我们通常用 get<Name> 方法来替代,其中 <Name> 是 getter 的实际名字,其效果与 getter 等价。ECMAScript v3 不支持 getter,但可以用上面这种构造带有 valueOf 和 toString 方法的对象来模拟 getter。
对象的 setter 是另一个相对应的属性,它的作用是通过类似赋值的方式改变对象的某些参数或者状态,遗憾的是,ECMAScript v3 不支持 setter,并且目前为止也没什么好的方法可以在 JS 中模拟 setter。要实现,只有通过定义 set<Name> 方法来实现:
II. 属性和方法的类型
在 JS 里,对象的属性和方法支持 4 种不同的类型,下面通过一个例子来说明:function myClass(){
var p = 100;//private property; 1. 私有属性
this.x = 10;//dynamic public property 2. 动态公有属性
}
myClass.prototype.y = 20;//static public property or prototype property 3. 静态公有属性或称原型属性
myClass.z = 30;//static property 4. 静态属性或称类属性
下面说下他们的特点和区别:
1. 私有属性上面已经提到,它的特点是对外界不开放,只能通过特定的 getter 和 setter 访问。实例化 myClass() 后,如果通过“.”运算符直接访问 p 会得到 undefined;
2. 动态公有属性的特点是外界可以访问,而且每一个对象实例持有一个副本,他们之间不会相互影响;
3. 原型属性的特点是每个对象实例共享唯一副本,对它的改写会相互影响;
4. 类属性的特点是作为类型的属性而不是对象实例的属性,也就是说不能通过对象实例的“.”运算符访问,那样会得到 undefined。上例中通过 myClass.z 直接访问即可。
有关 prototype 的知识点要放到下几篇,毕竟是 JS 面向对象编程的重点所在,内容比较多。简单预告一下第 2 篇笔记,包括 prototype 的使用技巧、实质及其范例。之后几篇会涉及继承和多态、构造和析构、包装对象、元类,类模板等。
本篇结尾以一个例子简述一下 prototype,“prototype 是在 IE 4 及其以后版本引入的一个针对于某一类的对象的方法,而且特殊点在于:它是一个给类的对象添加方法的方法”:
为本地对象 Number 添加数字阶乘方法:Number.fact()
实现:
Number.prototype.fact = function(){
var num = Math.floor(this);
if(num<0) return NaN;
else if(num==0 || num==1) return 1;
else return (num*(num-1).fact());
}
alert((10).fact());//3628800
《王者归来》读书笔记 ── JavaScript 面向对象编程(2)
I. 什么是 prototypeJS 中对象的 prototype 属性可以返回对象类型原型的引用(确实拗口),让我们分开来理解。对象的类(Class)和对象实例(Instance)之间是一种“创建”关系,所以类(Class)是对象的一个类型(Type)。在面向对象领域里,实例和类型不是唯一的一对可描述的抽象关系。在 JS 里还有另外一个更高层次的抽象关系:类型(Type)与原型(prototype),它恰好和类型与实例的抽象关系构成了一个三层的链。
在生活中有个习语“照猫画虎”,这里的猫就是原型,而虎就是类型,用 JS 的 prototype 表示为:“虎.prototype = 某只猫”或“虎.prototype = new 猫()”。当然这只是个比喻。
要注意的是,原型模式要求一个类型在一个时刻只能有一个原型,这里有两层含义:
1. 每个具体的 JS 类型有且仅有一个原型(prototype),在默认情况下该原型是一个 Object 对象(不是 Object 类型);
2. 这个类型的实例的所有类型,必须是满足原型关系的类型联。看个例子:
function ClassA(){...}
ClassA.prototype = new Object();//默认值,可以省略
function ClassB(){...}
ClassB.prototype = new ClassA();
function ClassC(){...}
ClassC.prototype = new ClassB();
var obj = new ClassC();
alert(obj instanceof ClassC);//ture;
alert(obj instanceof ClassB);//ture;
alert(obj instanceof ClassA);//ture;
alert(obj instanceof Object);//ture;
简单描述一下原型关系的类型链:
object <─ ClassA <- objectA <─ ClassB <- objectB <─ ClassC <- objectC
有意思的是,JS 并没有规定一个类型的原型的类型,因此可以是任何类型,但通常是某种对象,这样,对象 - 类型 - 原型(对象)就可能构成一个环形结构,或其他有意思的拓扑结构。
II. prototype 使用技巧
JS 的对象是动态的,prototype 也不例外,给 prototype 增减属性会改变这个类型的原型,以及由这个原型所创建的对象上。
function Point(x, y){
if(x) this.x = x;
if(y) this.y = y;
}
//设定 Point 对象的 x, y 默认值并动态的添加一个属性 z
Point.prototype.x = 0;
Point.prototype.y = 0;
Point.prototype.z = 0;
var p1 = new Point;
var p2 = new Point(1, 2);
alert(p1.x + ', ' + p1.y + ', ' + p1.z);
//0, 0, 0 p1 为默认(0, 0)的对象,加上 z 的值也为 0,所以是 0, 0, 0
alert(p2.x + ', ' + p2.y + ', ' + p2.z);
//1, 2, 0 原型属性与对象属性重名,对象属性会覆盖原型属性,所以为1, 2
如果我们用 delete 运算符删除 p2 的 x 属性,那么 p2.x 会恢复 prototype.x 的默认值 0:
delete p2.x;
alert(p2.x)//0
关于用 delete 操作还原默认值还有一个例子:
function ClassA(){
this.x = 10;
this.y = 20;
this.z = 30;
}
ClassA.prototype = new ClassA;//将 x, y, z 同时设为 ClassA 的默认值
//下面这个方法会将自身的非原型属性删除,达到 reset 的效果
ClassA.prototype.reset = function(){
for(var each in this){
delete this;
}
}
var a = new ClassA();
a.x *= 2;
a.y *= 2;
a.x *= 2;
alert(a.x + ', ' + a.y + ', ' + a.z)//20, 40, 60
//调用 reset 方法回复对象的默认值
a.reset();
alert(a.x + ', ' + a.y + ', ' + a.z)//10, 20, 30
我们还可以利用 prototype 为对象属性设置一个可读的 getter,如果忘记了 getter,可以再回顾下笔记(1)。实际上,将一个对象设置为一个类型的原型,相当于通过实例化这个类型,为对象创建只读副本:
//定义一个多边形类型
function Polygon(){
//存放多个多边形的定点
var m_points = [];
m_points = Array.apply(m_points, arguments);
//利用上面提到的方法为第一个顶点创建只读副本
function GETTER(){};
GETTER.prototype = m_points;
this.firstPoint = new GETTER();
//公有属性
this.length = {
valueOf : function(){return m_points.length},
toString : function(){return m_points.length}
}
//添加一个或多个顶点
this.add = function(){
m_points.push.apply(m_points, arguments);
}
//取得序号为 idx 的顶点
this.getPoint = function(idx){
return m_points;
}
//设置一个特定位置的顶点
this.setPoint = function(idx, point){
if(m_points == null){
m_points == point;
}
else{
m_points.x = point.x;
m_points.y = point.y;
}
}
}
//构建一个三角形 p
var p = new Polygon{(x:0, y:1), (x:3, y:1), (x:0, y:4))}
p.firstPoint.x = 100;//假如我们为第一个定点重新设定 x 值
alert(p.getPoint(0).x)// 0,私有成员的值并未受到影响
delete p.firstPoint.x//恢复默认值
alert(p.firstPoint.x);//0
p.setPoint(0, {x:3, y:4});//通过 setter 改写了私有成员
alert(p.firstPoint.x);//3,getter 的值发生了改变
上面的例子还说明了,用 prototype 可以快速创建对象的一个或多个副本,以一个对象为原型来创建大量的新对象,这正是 prototype pattern 的本质:
var p1 = new Point(1, 2);
var points = [];
var PointPrototype = function(){};
PointPrototype.prototype = p1;
for(var i = 0, i < 10000, i++){
points = new PointPrototype();//由于 PointPrototype 是个空函数,所以它的构造要比直接构造 //p1 快得多
}
除了以上作用,prototype 更常见的用处是声明对象的方法,这样避免了在构造函数中每次对方法进行重新赋值,节省了时间和空间。所以应尽量采用 prototype 定义对象方法,除非该方法要访问对象的私有成员或者返回某些引用了构造函数上下文的闭包。
习惯上,我们把采用 prototype 定义的属性和方法称为静态属性和静态方法,或者原型属性原型方法,把用 this 定义的属性和方法称为公有属性和公有方法。
III. prototype 的实质、价值和局限性
上面已经说了 prototype 的作用,现在来透过规律揭示 prototype 的实质。prototype 的行为类似于 C++ 中的静态域,将一个属性添加为 prototype 的属性,这个属性将被该类型所创建的所有实例所共享,但这种共享是只读的。在任何一个实例中只能够用自己的同名属性覆盖这个属性,而不能够改变它。看个例子:
function Point2D(x, y){
this.x = x;
this.y = y;
}
Point2D.prototype.x = 0;
Point2D.prototype.y = 0;
function ColorPoint2D(x, y, c){
this.x = x;
this.y = y;
}
ColorPoint2D.prototype = new Point2D();
ColorPoint2D.prototype.x = 1;
ColorPoint2D.prototype.y = 1;
var cp = new ColorPoint2D(10, 20, red);
alert(cp.x)//10,先查找 cp 自身属性
delete cp.x;
alert(cp.x)//1,被删除后查找上层原型属性
delete ColorPoint2D.prototype.x;
alert(cp.x)//0,删除后继续查找更上层原型链上的属性
以一个对象为实例,安全地创建大量的实例,这就是 prototype 的真正含义,也是它的价值所在。
但由于 prototype 仅仅是以对象为原型给类型构建副本,因此也具有很大局限性,比如改变某个原型上引用类型的属性的属性值,将会彻底影响到这个类型创建的每一个实例。
总之,prototype 是一种面向对象的机制,它通过原型来管理类型与对象之间的关系,prototype 的特点是能够以某个类型为原型构造大量的对象。
页:
[1]