全面理解面向对象的 JavaScript(来自ibm)

当今 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 规定,当要给某个对象的属性赋值时,解释器会查找该对象原型链中第一个含有该属性的对象(注:原型本身就是一个对象,那么原型链即为一组对象的链。对象的原型链中的第一个对象是该对象本身)进行赋值。反之,如果要获取某个对象属性的值,解释器自然是返回该对象原型链中首先具有该属性的对象属性值。图 1说名了这中隐藏机制:


图 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";

本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。
具体如何屏蔽更多的快捷键可以自行google搜索。 这里要说的是如何屏蔽后去执行自定义的事件。 这里为了方便使用的Kibo做例子,使用google搜索出来的结果一般都是javascript原生实现,很简单的,这里不做介绍。 这里是实现了在一个textarea中enter进行保存的例子,屏蔽掉了原来的回车事件。 代码如下: 复制代码 代码如下: //键盘监听 var areaKey = new Kibo($("#aac010")[0]); areaKey.down('enter',doSave); funct
在这之前我们比较值使用两等号 “==” 或 三等号“===”, 三等号更加严格,只要比较两方类型不同立即返回false。 另外,有且只有一个值不和自己相等,它是NaN 现在ES6又加了一个Object.is,让比较运算的江湖更加混乱。多数情况下Object.is等价于“===”,如下 1 === 1 // trueObject.is(1, 1) // true 'a' === 'a' // trueObject.is('a', 'a') // true true === true // trueObjec
复制代码 代码如下: ?php /* ?xml version="1.0" encoding="utf-8"? article item title name="t1"/title contentcontent1/content pubdate2009-10-11/pubdate /item item title name="t2"title2/title contentcontent2/content pubdate2009-11-11/pubdate /item /article */ /* 使用DO
本文实例讲述了JS+CSS实现模仿浏览器网页字符查找功能的方法。分享给大家供大家参考。具体实现方法如下: 复制代码 代码如下: html head titleJS+CSS模仿的网页字符查找功能/title style type=text/css BODY { FONT-SIZE: 12px; LETTER-SPACING: 1pt; LINE-HEIGHT: 22px; MARGIN-LEFT: 5pt; MARGIN-TOP: 5pt } #scontentmain { HEIGHT: 30px; PO
复制代码 代码如下: !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" html xmlns="http://www.w3.org/1999/xhtml" head meta http-equiv="Content-Type" content="text/html; charset=utf-8" / titlecrea
JavaScript数组的splice()方法改变数组的内容,增加了新的元素,同时消除旧元素。 语法 array.splice(index, howMany, [element1][, ..., elementN]); 下面是参数的详细信息: index : 在该索引开始改变的数组。 howMany : 整数,表示旧数组元素数去除。如果的howmany为0,没有元素被删除。 element1, ..., elementN : 元素添加到数组。如果不指定任何元素,拼接只需删除该数组中的元素。 返回值: 返回
福利彩票的双色球号码是由6个红球号码和1个篮球号码组成的,其中,6个红球号码是从01到33中随机抽出的6个数字,1个篮球号码是从01到16中随机抽出的1个数字。6个红球号码通常按照从小到大的顺序排列。下面是JavaScript中生成一注双色球号码的方法,供大家参考! var redBall = new Array();var redLen = redBall.length;while(redLen6){ var ball = ranNumber(1,33); var flag = true; for(va

nodejs简单实现中英文翻译 - 2015-07-01 11:07:36

帮以前同事解决一个需求,中文项目 翻译 英文项目~~~ 考虑到具体实现方面的问题,如果智能的话,肯定是要做中文的语法分析,不过感觉这个有难度。 所以最后的方案是遍历文件,将中文短语匹配出来,再进行人工翻译,将中文短语替换成翻译的内容。当然后期还是需要人工再检验下,毕竟代码中的中文,可能会影响到相关的程序。 这个问题,明显涉及到 多线程,文件读写,第一时间就想到的是 nodejs,虽然nodejs是一个主线程,但是异步文件读写,事件响应机制,肯定也是调用了线程,在实际编程的时候不需要考虑线程的相关的问题。
我们在进行表单设计时,可能要用到select下拉选项控件,遗憾的是,IE浏览器默认的select控件外观非常丑陋,而且不能用样式来控制,不能在选项中添加图片等信息。今天我将通过实例来讲解如何用CSS和jQuery来制作漂亮的下拉选项菜单。 XHTML div id="dropdown" p请选择城市/p ul lia href=#> 可以看出,我们使用div来替换下拉选项控件原生的select标签。 CSS #dropdown{width:186px; margin:80px auto; position

jquery 问答知识整理 - 2015-07-01 11:07:26

获取ID : $(this).attr("id"); :not用法 1. 列表用法 复制代码 代码如下: var notList = []; notList.push("#%=txtSuggest.ClientID %"); var textElements = $("input[type=text]:not(" + notList + ")"); var firstFocusItem = null; //遍历Type=Text的元素 textElements.each(function(i) { //TO