在接受了面向对象存在一种叫做基于原型实现的方式的事实之后,下面我们就可以来深入探讨 ECMAScript 是如何依据这一方式构造自己的语言的。最基本的面向对象
ECMAScript 是一门彻底的面向对象的编程语言(参考资源),JavaScript 是其中的一个变种 (variant)。它提供了 6 种基本数据类型,即 Boolean、Number、String、Null、Undefined、Object。为了实现面向对象,ECMAScript 设计出了一种非常成功的数据结构 - JSON(JavaScript Object Notation), 这一经典结构已经可以脱离语言而成为一种广泛应用的数据交互格式 (参考资源)。
应该说,具有基本数据类型和 JSON 构造语法的 ECMAScript 已经基本可以实现面向对象的编程了。开发者可以随意地用 字面式声明(literal notation)方式来构造一个对象,并对其不存在的属性直接赋值,或者用 delete 将属性删除 (注:JS 中的 delete 关键字用于删除对象属性,经常被误作为 C++ 中的 delete,而后者是用于释放不再使用的对象),如 程序清单 2。
清单 2. 字面式 (literal notation) 对象声明
- var person = {
- name: "张三",
- age: 26,
- gender: "男",
- eat: function( stuff ) {
- alert( "我在吃" + stuff );
- }
- };
- person.height = 176;
- delete person[ "age" ];
在实际开发过程中,大部分初学者或者对 JS 应用没有太高要求的开发者也基本上只用到 ECMAScript 定义的这一部分内容,就能满足基本的开发需求。然而,这样的代码复用性非常弱,与其他实现了继承、派生、多态等等的类式面向对象的强类型语言比较起来显得有些干瘪,不能满足复杂的 JS 应用开发。所以 ECMAScript 引入原型来解决对象继承问题。
使用函数构造器构造对象
除了 字面式声明(literal notation)方式之外,ECMAScript 允许通过 构造器(constructor)创建对象。每个构造器实际上是一个 函数(function) 对象, 该函数对象含有一个 "prototype" 属性用于实现 基于原型的继承(prototype-based inheritance)和 共享属性(shared properties)。对象可以由 "new 关键字 + 构造器调用" 的方式来创建,如 程序清单 3:
清单 3. 使用构造器 (constructor) 创建对象
- // 构造器 Person 本身是一个函数对象
- function Person() {
- // 此处可做一些初始化工作
- }
- // 它有一个名叫 prototype 的属性
- Person.prototype = {
- name: "张三",
- age: 26,
- gender: "男",
- eat: function( stuff ) {
- alert( "我在吃" + stuff );
- }
- }
- // 使用 new 关键字构造对象
- var p = new Person();
由于早期 JavaScript 的发明者为了使这门语言与大名鼎鼎的 Java 拉上关系 (虽然现在大家知道二者是雷锋和雷锋塔的关系),使用了 new 关键字来限定构造器调用并创建对象,以使其在语法上跟 Java 创建对象的方式看上去类似。但需要指出的是,这两门语言的 new 含义毫无关系,因为其对象构造的机理完全不同。也正是因为这里语法上的类似,众多习惯了类式面向对象语言中对象创建方式的程序员,难以透彻理解 JS 对象原型构造的方式,因为他们总是不明白在 JS 语言中,为什么 "函数名可以作为类名" 的现象。而实质上,JS 这里仅仅是借用了关键字 new,仅此而已;换句话说,ECMAScript 完全可以用其它 非 new 表达式来用调用构造器创建对象。
彻底理解原型链 (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 说名了这中隐藏机制:
图 1. 原型链中的属性隐藏机制
在图 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() {
- this.name = "mammal";
- }
- // 指定 Mammal 对象的原型为一个 Animal 对象。
- // 实际上此处便是在创建 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 代码最后两行的等式中自行领悟。
JavaScript 类式继承的实现方法
从代码清单 5 可以看出,基于原型的继承方式,虽然实现了代码复用,但其行文松散且不够流畅,可阅读性差,不利于实现扩展和对源代码进行有效地组织管理。不得不承认,类式继承方式在语言实现上更具健壮性,且在构建可复用代码和组织架构程序方面具有明显的优势。这使得程序员们希望寻找到一种能够在 JavaScript 中以类式继承风格进行编码的方法途径。从抽象的角度来讲,既然类式继承和原型继承都是为实现面向对象而设计的,并且他们各自实现的载体语言在计算能力上是等价的 (因为图灵机的计算能力与 Lambda 演算的计算能力是等价的),那么能不能找到一种变换,使得原型式继承语言通过该变换实现具有类式继承编码的风格呢?
目前一些主流的 JS 框架都提供了这种转换机制,也即类式声明方法,比如 Dojo.declare()、Ext.entend() 等等。用户使用这些框架,可以轻易而友好地组织自己的 JS 代码。其实,在众多框架出现之前,JavaScript 大师 Douglas Crockford 最早利用三个函数对 Function 对象进行扩展,实现了这种变换,关于它的实现细节可以(参考资源)。此外还有由 Dean Edwards 实现的著名的 Base.js(参考资源)。值得一提的是,jQuery 之父 John Resig 在搏众家之长之后,用不到 30 行代码便实现了自己的 Simple Inheritance。使用其提供的 extend 方法声明类非常简单。程序清单 6 是使用了 Simple Inheritance 库实现类的声明的例子。其中最后一句打印输出语句是对 Simple Inheritance 实现类式继承的最好说明。
清单 6. 使用 Simple Inheritance 实现类式继承
- // 声明 Person 类
- var Person = Class.extend( {
- _issleeping: true,
- init: function( name ) {
- this._name = name;
- },
- isSleeping: function() {
- return this._issleeping;
- }
- } );
- // 声明 Programmer 类,并继承 Person
- var Programmer = Person.extend( {
- init: function( name, issleeping ) {
- // 调用父类构造函数
- this._super( name );
- // 设置自己的状态
- this._issleeping = issleeping;
- }
- } );
- var person = new Person( "张三" );
- var diors = new Programmer( "张江男", false );
- // 打印 true
- console.log( person.isSleeping() );
- // 打印 false
- console.log( diors.isSleeping() );
- // 此处全为 true,故打印 true
- console.log( person instanceof Person && person instanceof Class
- && diors instanceof Programmer &&
- diors instanceof Person && diors instanceof Class );
如果您已对原型、函数构造器、闭包和基于上下文的 this 有了充分的理解,那么理解 Simple Inheritance 的实现原理也并非相当困难。从本质上讲,var Person = Class.extend(...) 该语句中,左边的 Person 实际上是获得了由 Class 调用 extend 方法返回的一个构造器,也即一个 function 对象的引用。顺着这个思路,我们继续介绍 Simple Inheritance 是如何做到这一点,进而实现了由原型继承方式到类式继承方式的转换的。图 2 是 Simple Inheritance 的源码及其附带注释。为了方便理解,用中文对代码逐行补充说明。图 2.Simple Inheritance 源码解析 抛开代码第二部分,整体连贯地考察第一和第三部分会发现,extend 函数的根本目的就是要构造一个具有新原型属性的新构造器。我们不禁感叹 John Resig 的大师手笔及其对 JS 语言本质把握的细腻程度。至于 John Resig 是如何想到这样精妙的实现方法,感兴趣的读者可以阅读本文 (参考资源),其中有详细介绍关于最初设计 Simple Inheritance 的思维过程。
JavaScript 私有成员实现
到此为止,如果您任然对 JavaScript 面向对象持怀疑态度,那么这个怀疑一定是,JavaScript 没有实现面向对象中的信息隐藏,即私有和公有。与其他类式面向对象那样显式地声明私有公有成员的方式不同,JavaScript 的信息隐藏就是靠闭包实现的。见 程序清单 7:
清单 7. 使用闭包实现信息隐藏
- // 声明 User 构造器
- function User( pwd ) {
- // 定义私有属性
- var password = pwd;
- // 定义私有方法
- function getPassword() {
- // 返回了闭包中的 password
- return password;
- }
- // 特权函数声明,用于该对象其他公有方法能通过该特权方法访问到私有成员
- this.passwordService = function() {
- return getPassword();
- }
- }
- // 公有成员声明
- User.prototype.checkPassword = function( pwd ) {
- return this.passwordService() === pwd;
- };
- // 验证隐藏性
- var u = new User( "123456" );
- // 打印 true
- console.log( u.checkPassword( "123456" ) );
- // 打印 undefined
- console.log( u.password );
- // 打印 true
- console.log( typeof u.gePassword === "undefined" );
JavaScript 必须依赖闭包实现信息隐藏,是由其函数式语言特性所决定的。本文不会对函数式语言和闭包这两个话题展开讨论,正如上文默认您理解 JavaScript 中基于上下文的 this 一样。关于 JavaScript 中实现信息隐藏,Douglas Crockford 在《 Private members in JavaScript 》(参考资源)一文中有更权威和详细的介绍。
结束语
JavaScript 被认为是世界上最受误解的编程语言,因为它身披 c 语言家族的外衣,表现的却是 LISP 风格的函数式语言特性;没有类,却实也彻底实现了面向对象。要对这门语言有透彻的理解,就必须扒开其 c 语言的外衣,从新回到函数式编程的角度,同时摒弃原有类的面向对象概念去学习领悟它。随着近些年来 web 应用的普及和 JS 语言自身的长足发展,特别是后台 JS 引擎的出现 (如基于 V8 的 NodeJS 等),可以预见,原来只是作为玩具编写页面效果的 JS 将获得更广阔发展天地。这样的发展趋势,也对 JS 程序员提出了更高要求。只有彻底领悟了这门语言,才有可能在大型的 JS 项目中发挥她的威力。
来源: http://www.phperz.com/article/17/0413/278545.html