面向对象的 JavaScript

JavaScript 函数式脚本言语特性以及其看似随便的编写作风,致使长期以来人们对这一门言语的误会,即以为 JavaScript 不是一门面向对象的言语,或许只是部份具有一些面向对象的特性。本文将回归面向对象本意,从对言语感悟的角度论述为何 JavaScript 是一门完整的面向对象的言语,以及怎样正确地运用这一特性。

媒介

现今 JavaScript 大行其道,种种运用对其依托日深。web 顺序员已逐步习气运用种种优异的 JavaScript 框架疾速开辟 Web 运用,从而疏忽了对原生 JavaScript 的进修和深切明白。所以,经常涌现的状况是,许多做了多年 JS 开辟的顺序员对闭包、函数式编程、原型老是说不清道不明,纵然运用了框架,其代码组织也异常蹩脚。这都是对原生 JavaScript 言语特性明白不够的表现。要控制好 JavaScript,起首一点是必需摒弃一些其他高等言语如 Java、C# 等类式面向对象头脑的滋扰,周全地从函数式言语的角度明白 JavaScript 原型式面向对象的特性。把握好这一点以后,才有可以进一步运用好这门言语。本文适宜群体:运用过 JS 框架但对 JS 言语实质缺少明白的顺序员,具有 Java、C++ 等言语开辟履历,预备进修并运用 JavaScript 的顺序员,以及一直对 JavaScript 是不是面向对象含糊其词,但愿望晓得原形的 JS 爱好者。

重新认识面向对象

为了申明 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 便相似与宇宙的看法。

清单 1. 对象的上下文依托

 <script> 
 var str = "我是一个 String 对象 , 我声明在这里 , 但我不是自力存在的!"
 var obj = { des: "我是一个 Object 对象 , 我声明在这里,我也不是自力存在的。" }; 
 var fun = function() { 
    console.log( "我是一个 Function 对象!谁挪用我,我属于谁:", this ); 
 }; 

 obj.fun = fun; 

 console.log( this === window );     // 打印 true 
 console.log( window.str === str );  // 打印 true 
 console.log( window.obj === obj );  // 打印 true 
 console.log( window.fun === fun );  // 打印 true 
 fun();                              // 打印 我是一个 Function 对象!谁挪用我,我属于谁:window 
 obj.fun();                          // 打印 我是一个 Function 对象!谁挪用我,我属于谁:obj 
 fun.apply(str);                   // 打印 我是一个 Function 对象!谁挪用我,我属于谁:str 
 </script>

在接受了面向对象存在一种叫做基于原型完成的体式格局的现实以后,下面我们便可以来深切议论 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 划定,当要给某个对象的属性赋值时,诠释器会查找该对象原型链中第一个含有该属性的对象(注:原型本身就是一个对象,那末原型链即为一组对象的链。对象的原型链中的第一个对象是该对象本身)举行赋值。反之,假如要猎取某个对象属性的值,诠释器天然是返回该对象原型链中起首具有该属性的对象属性值。

明白了原型链,那末将异常随意马虎明白 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 是怎样做到这一点,进而完成了由原型继承体式格局到类式继承体式格局的转换的。

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 项目中发挥她的威力。

    原文作者:木头人
    原文地址: https://segmentfault.com/a/1190000004039288
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞