发布于 

全面理解面向对象的 JavaScript (转载整理)

重新认识面向对象

为了说明 JavaScript 是一门彻底的面向对象的语言,首先有必要从面向对象的概念着手 , 探讨一下面向对象中的几个概念:

  • 一切事物皆对象
  • 对象具有封装和继承特性
  • 对象与对象之间使用消息通信,各自存在信息隐藏
    以这三点做为依据,C++ 是半面向对象半面向过程语言,因为,虽然他实现了类的封装、继承和多态,但存在非对象性质的全局函数和变量。Java、C# 是完全的面向对象语言,它们通过类的形式组织函数和变量,使之不能脱离对象存在。但这里函数本身是一个过程,只是依附在某个类上。

然而,面向对象仅仅是一个概念或者编程思想而已,它不应该依赖于某个语言存在。比如 Java 采用面向对象思想构造其语言,它实现了类、继承、派生、多态、接口等机制。但是这些机制,只是实现面向对象编程的一种手段,而非必须。换言之,一门语言可以根据其自身特性选择合适的方式来实现面向对象。所以,由于大多数程序员首先学习或者使用的是类似 Java、C++ 等高级编译型语言(Java 虽然是半编译半解释,但一般做为编译型来讲解),因而先入为主地接受了“类”这个面向对象实现方式,从而在学习脚本语言的时候,习惯性地用类式面向对象语言中的概念来判断该语言是否是面向对象语言,或者是否具备面向对象特性。这也是阻碍程序员深入学习并掌握 JavaScript 的重要原因之一。

实际上,JavaScript 语言是通过一种叫做原型(prototype的方式来实现面向对象编程的。下面就来讨论基于类的(class-based)面向对象基于原型的 (prototype-based) 面向对象这两种方式在构造客观世界的方式上的差别。



 

基于类的面向对象和基于原型的面向对象方式比较

在基于类的面向对象方式中,对象(object依靠类(class来产生。而在基于原型的面向对象方式中,对象(object则是依靠构造器(constructor利用原型(prototype构造出来的。举个客观世界的例子来说明二种方式认知的差异。例如工厂造一辆车,一方面,工人必须参照一张工程图纸,设计规定这辆车应该如何制造。这里的工程图纸就好比是语言中的类 (class),而车就是按照这个类(class)制造出来的;另一方面,工人和机器 ( 相当于 constructor) 利用各种零部件如发动机,轮胎,方向盘 ( 相当于 prototype 的各个属性 ) 将汽车构造出来。·

事实上关于这两种方式谁更为彻底地表达了面向对象的思想,目前尚有争论。但笔者认为原型式面向对象是一种更为彻底的面向对象方式,理由如下:

首先,客观世界中的对象的产生都是其它实物对象构造的结果,而抽象的“图纸”是不能产生“汽车”的,也就是说,类是一个抽象概念而并非实体,而对象的产生是一个实体的产生;

其次,按照一切事物皆对象这个最基本的面向对象的法则来看,类 (class) 本身并不是一个对象,然而原型方式中的构造器 (constructor) 和原型 (prototype) 本身也是其他对象通过原型方式构造出来的对象。

再次,在类式面向对象语言中,对象的状态 (state) 由对象实例 (instance) 所持有,对象的行为方法 (method) 则由声明该对象的类所持有,并且只有对象的结构和方法能够被继承;而在原型式面向对象语言中,对象的行为、状态都属于对象本身,并且能够一起被继承(参考资源),这也更贴近客观实际。

最后,类式面向对象语言比如 Java,为了弥补无法使用面向过程语言中全局函数和变量的不便,允许在类中声明静态 (static) 属性和静态方法。而实际上,客观世界不存在所谓静态概念,因为一切事物皆对象!而在原型式面向对象语言中,除内建对象 (build-in object) 外,不允许全局对象、方法或者属性的存在,也没有静态概念。所有语言元素 (primitive) 必须依赖对象存在。但由于函数式语言的特点,语言元素所依赖的对象是随着运行时 (runtime) 上下文 (context) 变化而变化的,具体体现在 this 指针的变化。正是这种特点更贴近 “万物皆有所属,宇宙乃万物生存之根本”的自然观点。在程序清单 1中window便类似与宇宙的概念。


彻底理解原型链 (prototype chain)

在 ECMAScript 中,每个由构造器创建的对象拥有一个指向构造器 prototype 属性值的隐式引用(implicit reference,这个引用称之为原型(prototype。进一步,每个原型可以拥有指向自己原型的隐式引用(即该原型的原型),如此下去,这就是所谓的原型链(prototype chain(参考资源)。在具体的语言实现中,每个对象都有一个**proto 属性来实现对原型的隐式引用**。 程序清单4说明了这一点。

清单 4. 对象的 proto 属性和隐式引用

function Person( name ) { this.name = name; }

var p = new Person();

// 对象的隐式引用指向了构造器的 prototype 属性,所以此处打印 true

console.log( p.__proto__ === Person.prototype );

// 原型本身是一个 Object 对象,所以他的隐式引用指向了

// Object 构造器的 prototype 属性 , 故而打印 true

console.log( Person.prototype.__proto__ === Object.prototype );

// 构造器 Person 本身是一个函数对象,所以此处打印 true

console.log( Person.__proto__ === Function.prototype );

有了原型链,便可以定义一种所谓的属性隐藏机制,并通过这种机制实现继承。ECMAScript 规定,当要给某个对象的属性赋值时,解释器会查找该对象原型链中第一个含有该属性的对象(注:原型本身就是一个对象,那么原型链即为一组对象的链。对象的原型链中的第一个对象是该对象本身)进行赋值。反之,如果要获取某个对象属性的值,解释器自然是返回该对象原型链中首先具有该属性的对象属性值。

图 1. 原型链中的属性隐藏机制
image001

在图 1 中,object1->prototype1->prototype2 构成了对象object1 的原型链,根据上述属性隐藏机制,可以清楚地看到prototype1 对象中的 property4 属性和 prototype2 对象中的 property3 属性皆被隐藏。理解了原型链,那么将非常容易理解 JS 中基于原型的继承实现原理,

程序清单5.是利用原型链实现继承的简单例子。
清单 5. 利用原型链 Horse->Mammal->Animal 实现继承

// 声明 Animal 对象构造器 
function Animal() { } //将 Animal 的 prototype 属性指向一个对象,
// 亦可直接理解为指定 Animal 对象的原型 
Animal.prototype = {
    name: "animal",
    weight: 0, eat:
    function () { 
        alert("Animal is eating!"); 
    }
} // 声明 Mammal 对象构造器 
function Mammal() { // 指定 Mammal 对象的原型为一个 Animal 对象。 
    this.name = "mammal";
} 
// 实际上此处便是在创建 Mammal 对象和 Animal 对象之间的原型链 
Mammal.prototype = new Animal(); // 声明 Horse 对象构造器 
function Horse(height, weight) {
    this.name = "horse";
    this.height = height;
    this.weight = weight;
}
// 将 Horse 对象的原型指定为一个 Mamal 对象,继续构建 Horse 与 Mammal 之间的原型链 
Horse.prototype = new Mammal(); // 重新指定 eat 方法 , 此方法将覆盖从 Animal 原型继承过来的 eat 方法 
Horse.prototype.eat = function () { alert("Horse is eating grass!"); } // 验证并理解原型链 
var horse = new Horse(100, 300);
console.log(horse.__proto__ === Horse.prototype);
console.log(Horse.prototype.__proto__ === Mammal.prototype);
console.log(Mammal.prototype.__proto__ === Animal.prototype);

理解清单 5中对象原型继承逻辑实现的关键在于
Horse.prototype = new Mammal()Mammal.prototype = new Animal() 这两句代码。
首先,等式右边的结果是构造出一个临时对象,然后将这个对象赋值给等式左边对象的 prototype 属性。也就是说将右边新建的对象作为左边对象的原型。读者可以将这两个等式替换到相应的程序清单 5 代码最后两行的等式中自行领悟。


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本站由 @shyiuanchen 创建,使用 Stellar 作为主题。