JavaScript中的原型和继承详解(图文)


Posted in Javascript onJuly 18, 2014

请在此暂时忘记之前学到的面向对象的一切知识。这里只需要考虑赛车的情况。是的,就是赛车。

最近我正在观看 24 Hours of Le Mans ,这是法国流行的一项赛事。最快的车被称为 Le Mans 原型车。这些车虽然是由“奥迪”或“标致”这些厂商制造的,可它们并不是你在街上或速公路上所见到的那类汽车。它们是专为参加高速耐力赛事而制造出来的。

厂家投入巨额资金,用于研发、设计、制造这些原型车,而工程师们总是努力尝试将这项工程做到极致。他们在合金、生物燃料、制动技术、轮胎的化合物成分和安全特性上进行了各种实验。随着时间的推移,这些实验中的某些技术经过反复改进,随之进入到车辆的主流产品线中。你所驾驶车辆的某些技术,有可能是在赛车原型上第一次亮相的。

你也可以说,这些主流车辆继承了来自赛车的技术原型

到现在,我们就有讨论 JavaScript 中的原型和继承问题的基础了。它虽然并不像你在 C++、Java 或 C# 中了解的经典继承模式一样,但这种方式同样强大,并且有可能会更加灵活。

 有关对象和类

JavaScript 中全是对象,这指的是传统意义上的对象,也就是“一个包含了状态和行为的单一实体”。例如,JavaScript 中的数组是含有数个值,并且包含 push、reverse 和 pop 方法的对象。

var myArray = [1, 2];
myArray.push(3);
myArray.reverse();
myArray.pop();
var length = myArray.length;

现在问题是,push 这样的方法是从何而来的呢?我们前面提到的那些静态语言使用“类语法”来定义对象的结构,但是 JavaScript 是一个没有“类语法”的语言,无法用 Array“类”的语法来定义每个数组对象。而因为 JavaScript 是动态语言,我们可以在实际需要的情况下,将方法任意放置到对象上。例如下面的代码,就在二维空间中,定义了用来表示一个点的点对象,同时还定义了一个 add 方法。

var point = {
  x : 10,
  y : 5,
  add: function(otherPoint) {
    this.x += otherPoint.x;
    this.y += otherPoint.y;
  }
};

但是上面的做法可扩展性并不好。我们需要确保每一个点对象都含有一个 add 方法,同时也希望所有点对象都共享同一个 add 方法的实现,而不是这个方法手工添加每一个点对象上。这就是原型发挥它作用的地方。

 有关原型

在 JavaScript 中,每个对象都保持着一块隐藏的状态 —— 一个对另一个对象的引用,也被称作原型。我们之前创建的数组引用了一个原型对象,我们自行创建的点对象也是如此。上面说原型引用是隐藏的,但也有 ECMAScript(JavaScript 的正式名称)的实现可以通过一个对象的__proto__属性(例如谷歌浏览器)访问到这个原型引用。从概念上讲,我们可以将对象当作类似于 图1 所表示的对象 —— 原型的关系。

 JavaScript中的原型和继承详解(图文)

1

展望未来,开发者将能够使用 Object.getPrototypeOf 函数,代替__proto__属性,取得对象原型的引用。在本文写出的时候,已经可以在 Google Chrome,FIrefox 和 IE9 浏览器中使用 Object.getPrototypeOf 函数。更多浏览器在未来会实现此功能,因为它已经是 ECMAScript 标准的一部分了。我们可以使用下面的代码,来证明我们建立的 myArray 和点对象引用的是两个不同的原型对象。

  1. Object.getPrototypeOf(point) != Object.getPrototypeOf(myArray);

对于本文的其余部分,我将交叉使用 __proto__和Object.getPrototypeOf 函数,主要是因为 __proto__ 在图和句子中更容易识别。需要记住的是它(__proto__)不是标准,而 Object.getPrototypeOf 函数才是查看对象原型的推荐方法。

是什么让原型如此特别?

我们还没有回答这个问题:数组中 push 这样的方法是从何而来的呢?答案是:它来源于 myArray 原型对象。图 2 是 Chrome 浏览器中脚本调试器的屏幕截图。我们已经调用 Object.getPrototypeOf 方法查看 myArray 的原型对象。

 JavaScript中的原型和继承详解(图文)

2

注意 myArray 的原型对象中有许多方法,包括那些在代码示例中调用的 push、pop 和 reverse 方法。因此,原型对象中的确包括 push 方法,但是 myArray 方法如何引用到呢?

myArray.push(3);

了解其工作原理的第一步,是要认识到原型并不是特别的。原型只是普通的对象。可以给原型添加方法,属性,并把他们当作其他 JavaScript 对象一样看待。然而,套用乔治·奥威尔的小说《动物农场》中“猪”的说法 —— 所有的对象应当是平等的,但有些对象(遵守规则的)比其他人更加平等。

JavaScript 中的原型对象的确是特殊的,因为他们遵从以下规则。当我们告诉 JavaScript 我们要调用一个对象的 push 方法,或读取对象的 x 属性时,运行时会首先查找对象本身。如果运行时找不到想要的东西,它就会循着 __proto__ 引用和对象原型寻找该成员。当我们  调用 myArray 的 push 方法时,JavaScript 并没有在 myArray 对象上发现 push 方法,而是在 myArray 的原型对象上找到了,于是 JavaScript 调用此方法(见图 3)。

JavaScript中的原型和继承详解(图文)

3

上面所描述的行为是指一个对象本身继承了原型上的任何方法或属性。JavaScript 中其实不需要使用类语法也能实现继承。就像从赛车原型上继承了相应的技术的车,一个 JavaScript 对象也可以从原型对象上继承功能特性。

图 3 还展示了每个数组对象同时也可以维护自身的状态和成员。在请求得到 myArray 的 length 属性的情况下,JavaScript 会取得 myArray 中 length 属性的值,而不会去读取原型中的对应值。我们可以通过向对象上添加 push 这样的方法来“重写”push 方法。这样就会有效地隐藏原型中的 push 方法实现。

 共享原型

JavaScript 中原型的真正神奇之处是多个对象如何维持对同一个原型对象的引用。例如,如果我们创建了这样的两个数组:

var myArray = [1, 2];
var yourArray = [4, 5, 6];

那么这两个数组将共享同一个原型对象,而下面的代码计算结果为 true:

Object.getPrototypeOf(myArray) === Object.getPrototypeOf(yourArray);

如果我们引用两个数组对象上的 push 方法,JavaScript 会去寻找原型上共享的 push 方法。

JavaScript中的原型和继承详解(图文)

4

JavaScript 中的原型对象提供继承功能,同时也就实现了该方法实现的共享。原型也是链式的。换句话说,因为原型对象只是一个对象,所以一个原型对象可以维持到另一个原型对象的引用。如果你重新审视图 2 便可以看到,原型的 __proto__ 属性是一个指向另一个原型的非空值。当 JavaScript 查找像 push 方法这样的成员时,它会循着原型引用链检查每一个对象,直到找到该成员,或者抵达原型链的末端。原型链为继承和共享开辟了一条灵活的途径。

你可能会问的下一个问题是:我该如何设置那些自定义对象的原型引用呢?例如前面所使用的点对象,如何才能将 add 方法添加到原型对象中,并从多个点对象中继承方法呢?在回答这个问题之前,我们需要看看函数。

 有关函数

JavaScript 中的函数也是对象。这样的表述带来了几个重要的结果,而我们并不会在本文中涉及所有的事项。这其中,能将一个函数赋值给一个变量,并且将一个函数作为参数传递给另一个函数的能力构成了现代 JavaScript 编程表达的基本范式。

我们需要关注的是,函数本身就是对象,因此函数可以有自身的方法,属性,并且引用一个原型对象。让我们来讨论下面的代码的含义。

// 这将返回 true:
typeof (Array) === "function"
// 这样的表达式也是:
Object.getPrototypeOf(Array) === Object.getPrototypeOf(function () { })
// 这样的表达式同样:
Array.prototype != null

代码中的第一行证明, JavaScript 中的数组是函数。稍后我们将看到如何调用 Array 函数创建一个新的数组对象。下一行代码,证明了 Array 对象使用与任何其他函数对象相同的原型,就像我们看到数组对象间共享相同的原型一样。最后一行代码证明了 Array 函数都有一个 prototype 属性,而这个 prototype 属性指向一个有效的对象。这个 prototype 属性十分重要。

JavaScript 中的每一个函数对象都有 prototype 属性。千万不要混淆这个 prototype 属性的 __proto__ 属性。他们用途并不相同,也不是指向同一个对象。

// 返回 true
Object.getPrototypeOf(Array) != Array.prototype

Array.__proto__ 提供的是 数组原型 ? 请把它当作 Array 函数所继承的对象。

而 Array.protoype,提供的的是 所有数组的原型对象。也就是说,它提供的是像 myArray 这样数组对象的原型对象,也包含了所有数组将会继承的方法。我们可以写一些代码来证明这个事实。

// true
Array.prototype == Object.getPrototypeOf(myArray)
// 也是 true
Array.prototype == Object.getPrototypeOf(yourArray);

我们也可以使用这项新知识重绘之前的示意图。

JavaScript中的原型和继承详解(图文)

5

基于所知道的知识,请想象创建一个新的对象,并让新对象表现地像数组的过程。一种方法是使用下面的代码。

// 创建一个新的空对象
var o = {};
// 继承自同一个原型,一个数组对象
o.__proto__ = Array.prototype;
// 现在我们可以调用数组的任何方法...
o.push(3);

虽然这段代码很有趣,也能工作,可问题在于,并不是每一个 JavaScript 环境都支持可写的 __proto__ 对象属性。幸运的是,JavaScript 确实有一个创建对象内建的标准机制,只需要一个操作符,就可以创建新对象,并且设置新对象的 __proto__ 引用 ? 那就是“new”操作符。

var o = new Array();
o.push(3);

JavaScript 中的 new 操作符有三个基本任务。首先,它创建新的空对象。接下来,它将设置新对象的 __proto__ 属性,以匹配所调用函数的原型属性。最后,操作符调用函数,将新对象作为“this”引用传递。如果要扩展最后两行代码,就会变成如下情况:

var o = {};
o.__proto__ = Array.prototype;
Array.call(o);
o.push(3);

函数的 call 方法允许你在调用函数的情况下在函数内部指定“this”所引用的对象。当然,函数的作者在这种情况下需要实现这样的函数。一旦作者创建了这样的函数,就可以将其称之为构造函数。

构造函数

构造函数和普通的函数一样,但是具有以下两个特殊性质。

  1. 通常构造函数的首字母是大写的(让识别构造函数变得更容易)。
  2. 构造函数通常要和 new 操作符结合,用来构造新对象。

Array 就是一个构造函数的例子。Array 函数需要和 new 操作符一起使用,而且 Array 的首字母是大写的。JavaScript 将 Array 作为内置函数包括在内,而任何人都可以写出自己的构造函数。事实上,我们最后可以为先前创建的点对象编写出构造函数。

var Point = function (x, y) {
  this.x = x;
  this.y = y;
  this.add = function (otherPoint) {
    this.x += otherPoint.x;
    this.y += otherPoint.y;
  }
}
var p1 = new Point(3, 4);
var p2 = new Point(8, 6);
p1.add(p2);

在上面的代码中,我们使用了 new 操作符和 Point 函数来构造点对象,这个对象带有 x 属性和 y 属性和一个 add 方法。你可以将最后的结果想象成图 6 的样子。

JavaScript中的原型和继承详解(图文)

6

现在的问题是我们的每个点对象中仍然有单独的 add 方法。使用我们学到的原型和继承的知识,我们更希望将点对象的 add 方法从每个点实例中转移到 Point.prototype 中。要达到继承 add 方法的效果,我们所需要做的,就是修改 Point.prototype 对象。

var Point = function (x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype.add = function (otherPoint) {
  this.x += otherPoint.x;
  this.y += otherPoint.y;
}
var p1 = new Point(3, 4);
var p2 = new Point(8, 6);
p1.add(p2);

大功告成!我们刚刚在 JavaScript 中完成原型式的继承模式!

JavaScript中的原型和继承详解(图文)

7

总结

我希望这篇文章能够帮助你揭开 JavaScript 原型概念的神秘面纱。开始看到的是原型怎样让一个对象从其他对象中继承功能,然后看到怎样结合 new 操作符和构造函数来构建对象。这里所提到的,只是开启对象原型力量和灵活性的第一步。本文鼓励你自己发现学习有关原型和 JavaScript 语言的新信息。

同时,请小心驾驶。你永远不会知道这些行驶在路上的车辆会从他们的原型继承到什么(有缺陷)的技术。

原文链接: Script Junkie   翻译: 伯乐在线 - 埃姆杰

Javascript 相关文章推荐
一段实现页面上的图片延时加载的js代码
Feb 11 Javascript
JavaScript 程序编码规范
Nov 23 Javascript
js键盘事件的keyCode
Jul 29 Javascript
一款由jquery实现的整屏切换特效
Sep 15 Javascript
jQuery bt气泡实现悬停显示及移开隐藏功能的方法
Jul 12 Javascript
Jquery获取当前城市的天气信息
Aug 05 Javascript
js 中获取制定的cook信息实现方法
Nov 19 Javascript
JavaScript 栈的详解及实例代码
Jan 22 Javascript
vue2.0嵌套路由实现豆瓣电影分页功能(附demo)
Mar 13 Javascript
vue-router 中router-view不能渲染的解决方法
May 23 Javascript
js核心基础之闭包的应用实例分析
May 11 Javascript
浅析Vue中拆分视图层代码的5点建议
Aug 15 Javascript
JavaScript中伪协议 javascript:使用探讨
Jul 18 #Javascript
js清空表单数据的两种方式(遍历+reset)
Jul 18 #Javascript
js使用正则实现ReplaceAll全部替换的方法
Jul 18 #Javascript
javascript原生和jquery库实现iframe自适应高度和宽度
Jul 18 #Javascript
关于javaScript注册click事件传递参数的不成功问题
Jul 18 #Javascript
Javascript验证上传图片大小[前台处理]
Jul 18 #Javascript
IE中JS跳转丢失referrer问题的2个解决方法
Jul 18 #Javascript
You might like
详细介绍:Apache+PHP+MySQL配置攻略
2006/09/05 PHP
PHP回调函数与匿名函数实例详解
2017/08/16 PHP
Thinkphp5.0框架使用模型Model的获取器、修改器、软删除数据操作示例
2019/10/11 PHP
jquery ajax提交表单数据的两种方式
2009/11/24 Javascript
jQuery1.6 正式版发布并提供下载
2011/05/05 Javascript
解析Jquery的LigerUI如何实现文件上传
2013/07/09 Javascript
利用javascript实现禁用网页上所有文本框,下拉菜单,多行文本域
2013/12/14 Javascript
jQuery循环滚动新闻列表示例代码
2014/06/17 Javascript
JavaScript设计模式之单例模式实例
2014/09/24 Javascript
js弹出对话框方式小结
2015/11/17 Javascript
基于jquery实现百度新闻导航菜单滑动动画
2016/03/15 Javascript
js常用的继承--组合式继承
2017/03/06 Javascript
jQuery插件HighCharts实现的2D对数饼图效果示例【附demo源码下载】
2017/03/09 Javascript
vue使用自定义icon图标的方法
2018/05/14 Javascript
微信小程序 (地址选择1)--选取搜索地点并显示效果
2019/12/17 Javascript
python将人民币转换大写的脚本代码
2013/02/10 Python
python条件和循环的使用方法
2013/11/01 Python
python判断给定的字符串是否是有效日期的方法
2015/05/13 Python
python实现二叉树的遍历
2017/12/11 Python
对pycharm代码整体左移和右移缩进快捷键的介绍
2018/07/16 Python
对sklearn的使用之数据集的拆分与训练详解(python3.6)
2018/12/14 Python
Form表单及django的form表单的补充
2019/07/25 Python
Golang GBK转UTF-8的例子
2019/08/26 Python
网购亚洲时装、美容产品和生活百货:YesStyle
2016/09/15 全球购物
Clarins娇韵诗美国官网:法国天然护肤品牌
2016/09/26 全球购物
.NET面试问题集
2015/12/08 面试题
网络体系结构及协议的定义
2014/03/13 面试题
电子信息毕业生自荐信
2013/11/16 职场文书
电子商务专业推荐信范文
2013/12/02 职场文书
军人违纪检讨书
2014/02/04 职场文书
校园文明标语
2014/06/13 职场文书
2014大学生批评与自我批评思想汇报
2014/09/21 职场文书
2015年征兵工作总结
2015/07/23 职场文书
【海涛dota解说】海涛小满开黑4v5被破两路翻盘潮汐第一视角解说
2022/04/01 DOTA
Python开发五子棋小游戏
2022/04/28 Python
Nginx静态压缩和代码压缩提高访问速度详解
2022/05/30 Servers