javascirpt的原型继承原理

要理解JavaScript到底是怎么实现面向对象、继续以及多态的原理,就要追溯到它本身的语言特点说起,JavaScript和Java等后端语言不同,它没有强制性地要求声明时就要知道某个数据的类型,也就是说,JavaScript是一门灵活多变的动态语言,也正是因为这样的特点,所以才让它具有原型继承的一系列优点,
从原型设计模式中,我们可以非常清楚的知道,编程范型这种设计是,有多么的好,实际上基于原型进行继承的语言,也不止有JavaScript一种,在很早之前就有了,比喻:Self 语言和 Smalltalk 语言,Io语言,
JavaScript就是参考了这几种语言的。下面我们以Io语言来举例:

Io 语言在 2002 年由 Steve Dekorte 发明。可以从http://iolanguage.com下载到 Io 语言的解释器,
安装好之后打开 Io 解释器,输入经典的“Hello World”程序。解释器打印出了 Hello World 的字
符串,这说明我们已经可以使用 Io 语言来编写一些小程序了,如图 1-1 所示。
图 1-1
作为一门基于原型的语言,Io 中同样没有类的概念,每一个对象都是基于另外一个对象的
克隆。
就像吸血鬼的故事里必然有一个吸血鬼祖先一样,既然每个对象都是由其他对象克隆而来
的,那么我们猜测 Io 语言本身至少要提供一个根对象,其他对象都发源于这个根对象。这个猜
测是正确的,在 Io 中,根对象名为 Object。
这一节我们依然拿动物世界的例子来讲解 Io 语言。在下面的代码中,通过克隆根对象 Object,
就可以得到另外一个对象 Animal。虽然 Animal 是以大写开头的,但是记住 Io 中没有类,Animal
跟所有的数据一样都是对象。
Animal := Object clone // 克隆动物对象
现在通过克隆根对象 Object 得到了一个新的 Animal 对象,所以 Object 就被称为 Animal 的原
型。目前 Animal 对象和它的原型 Object 对象一模一样,还没有任何属于它自己方法和能力。我
们假设在 Io 的世界里,所有的动物都会发出叫声,那么现在就给 Animal 对象添加 makeSound 方法
吧。代码如下:
Animal makeSound := method( "animal makeSound " print );
好了,现在所有的动物都能够发出叫声了,那么再来继续创建一个 Dog 对象。显而易见,Animal
对象可以作为 Dog 对象的原型,Dog 对象从 Animal 对象克隆而来:
Dog := Animal clone
可以确定,Dog 一定懂得怎么吃食物,所以接下来给 Dog 对象添加 eat 方法:
Dog eat = method( "dog eat " print );
现在已经完成了整个动物世界的构建,通过一次次克隆,Io 的对象世界里不再只有形单影只
的根对象 Object,而是多了两个新的对象:Animal 对象和 Dog 对象。其中 Dog 的原型是 Animal,
Animal 对象的原型是 Object。最后我们来测试 Animal 对象和 Dog 对象的功能。
先尝试调用 Animal 的 makeSound 方法,可以看到,动物顺利发出了叫声:
Animal makeSound // 输出:animal makeSound
然后再调用 Dog 的 eat 方法,同样我们也看到了预期的结果:
Dog eat // 输出:dog eat

我们看到了如何在 Io 语言中从无到有地创建一些对象。跟使用“类”
的语言不一样的地方是,Io 语言中最初只有一个根对象 Object,其他所有的对象都克隆自另外一
个对象。如果 A 对象是从 B 对象克隆而来的,那么 B 对象就是 A 对象的原型。
在上一小节的例子中,Object 是 Animal 的原型,而 Animal 是 Dog 的原型,它们之间形成了一
条原型链。这个原型链是很有用处的,当我们尝试调用 Dog 对象的某个方法时,而它本身却没有
这个方法,那么 Dog 对象会把这个请求委托给它的原型 Animal 对象,如果 Animal 对象也没有这
个属性,那么请求会顺着原型链继续被委托给 Animal 对象的原型 Object 对象,这样一来便能得
到继承的效果,看起来就像 Animal 是 Dog 的“父类”,Object 是 Animal 的“父类”。
这个机制并不复杂,却非常强大,Io 和 JavaScript 一样,基于原型链的委托机制就是原型继
承的本质。
我们来进行一些测试。在 Io 的解释器中执行 Dog makeSound 时,Dog 对象并没有 makeSound 方
法,于是把请求委托给了它的原型 Animal 对象 ,而 Animal 对象是有 makeSound 方法的,所以该条
语句可以顺利得到输出

在原型继承方面,JavaScript 的实现原理和 Io 语言非常相似,
JavaScript 也同样遵守这些原型编程的基本规则。
 所有的数据都是对象。
 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
 对象会记住它的原型。
 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型

1. 所有的数据都是对象

JavaScript 在设计的时候,模仿 Java 引入了两套类型机制:基本类型和对象类型。基本类型
包括 undefined、number、boolean、string、function、object。从现在看来,这并不是一个好的
想法。
按照 JavaScript 设计者的本意,除了 undefined 之外,一切都应是对象。为了实现这一目标,
number、boolean、string 这几种基本类型数据也可以通过“包装类”的方式变成对象类型数据来
处理。
我们不能说在 JavaScript 中所有的数据都是对象,但可以说绝大部分数据都是对象。那么相
信在 JavaScript 中也一定会有一个根对象存在,这些对象追根溯源都来源于这个根对象。
事实上,JavaScript 中的根对象是 Object.prototype 对象。Object.prototype 对象是一个空的
对象。我们在 JavaScript 遇到的每个对象,实际上都是从 Object.prototype 对象克隆而来的,
Object.prototype 对象就是它们的原型。比如下面的 obj1 对象和 obj2 对象:
var obj1 = new Object();
var obj2 = {};
可以利用 ECMAScript 5 提供的 Object.getPrototypeOf 来查看这两个对象的原型:
console.log( Object.getPrototypeOf( obj1 ) === Object.prototype ); // 输出:true
console.log( Object.getPrototypeOf( obj2 ) === Object.prototype ); // 输出:true

在这里插入图片描述
在这里插入图片描述
2. 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
JavaScript 语言里,我们并不需要关心克隆的细节,因为这是引擎内部负责实现的。我
们所需要做的只是显式地调用 var obj1 = new Object()或者 var obj2 = {}。此时,引擎内部会从
Object.prototype 上面克隆一个对象出来,我们最终得到的就是这个对象。
再来看看如何用 new 运算符从构造器中得到一个对象,下面的代码我们再熟悉不过了:
function Person( name ){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
var a = new Person( ‘sven’ )
console.log( a.name ); // 输出:sven
console.log( a.getName() ); // 输出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 输出:true
在 JavaScript 中没有类的概念,这句话我们已经重复过很多次了。但刚才不是明明调用了 new
Person()吗?
在这里 Person 并不是类,而是函数构造器,JavaScript 的函数既可以作为普通函数被调用,
也可以作为构造器被调用。当使用 new 运算符来调用函数时,此时的函数就是一个构造器。 用
new 运算符来创建对象的过程,实际上也只是先克隆 Object.prototype 对象,再进行一些其他额
外操作的过程。① 在 Chrome 和 Firefox 等向外暴露了对象__proto__属性的浏览器下,我们可以通过下面这段代
码来理解 new 运算的过程:
function Person( name ){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
var objectFactory = function(){
var obj = new Object(), // 从 Object.prototype 上克隆一个空的对象
Constructor = [].shift.call( arguments ); // 取得外部传入的构造器,此例是 Person
obj.proto = Constructor.prototype; // 指向正确的原型
var ret = Constructor.apply( obj, arguments ); // 借用外部传入的构造器给 obj 设置属性
return typeof ret === ‘object’ ? ret : obj; // 确保构造器总是会返回一个对象
};
var a = objectFactory( Person, ‘sven’ );
console.log( a.name ); // 输出:sven
console.log( a.getName() ); // 输出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 输出:true
我们看到,分别调用下面两句代码产生了一样的结果:
var a = objectFactory( A, ‘sven’ );
var a = new A( ‘sven’ );

在这里插入图片描述
在这里插入图片描述

3. 对象会记住它的原型

如果请求可以在一个链条中依次往后传递,那么每个节点都必须知道它的下一个节点。同理,
要完成 Io语言或者 JavaScript语言中的原型链查找机制,每个对象至少应该先记住它自己的原型。
目前我们一直在讨论“对象的原型”,就 JavaScript 的真正实现来说,其实并不能说对象有
原型,而只能说对象的构造器有原型。对于“对象把请求委托给它自己的原型”这句话,更好
的说法是对象把请求委托给它的构造器的原型。那么对象如何把请求顺利地转交给它的构造器
的原型呢?
JavaScript 给对象提供了一个名为__proto__的隐藏属性,某个对象的__proto__属性默认会指
向它的构造器的原型对象,即{Constructor}.prototype。在一些浏览器中,__proto__被公开出来,
我们可以在 Chrome 或者 Firefox 上用这段代码来验证:
var a = new Object();
console.log ( a.proto=== Object.prototype ); // 输出:true
实际上,__proto__就是对象跟“对象构造器的原型”联系起来的纽带。正因为对象要通过
__proto__属性来记住它的构造器的原型,所以我们用上一节的 objectFactory 函数来模拟用 new
创建对象时, 需要手动给 obj 对象设置正确的__proto__指向。
obj.proto = Constructor.prototype;
通过这句代码,我们让 obj.proto 指向 Person.prototype,而不是原来的 Object.prototype。

4. 如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型

在 JavaScript 中,每个对象都是从 Object.prototype 对象克隆而来的,如果是这样的话,
我们只能得到单一的继承关系,即每个对象都继承自 Object.prototype 对象,这样的对象系统显
然是非常受限的。
实际上,虽然 JavaScript 的对象最初都是由 Object.prototype 对象克隆而来的,但对象构造
器的原型并不仅限于 Object.prototype 上,而是可以动态指向其他对象。这样一来,当对象 a 需
要借用对象 b 的能力时,可以有选择性地把对象 a 的构造器的原型指向对象 b,从而达到继承的
效果。下面的代码是我们最常用的原型继承方式:
var obj = { name: ‘sven’ };
var A = function(){};
A.prototype = obj;
var a = new A();
console.log( a.name ); // 输出:sven
我们来看看执行这段代码的时候,引擎做了哪些事情。
 首先,尝试遍历对象 a 中的所有属性,但没有找到 name 这个属性。
 查找 name 属性的这个请求被委托给对象 a 的构造器的原型,它被 a.proto 记录着并且
指向 A.prototype,而 A.prototype 被设置为对象 obj。
 在对象 obj 中找到了 name 属性,并返回它的值。
当我们期望得到一个“类”继承自另外一个“类”的效果时,往往会用下面的代码来模拟实现:
var A = function(){};
A.prototype = { name: ‘sven’ };
var B = function(){};
B.prototype = new A();
var b = new B();
console.log( b.name ); // 输出:sven
再看这段代码执行的时候,引擎做了什么事情。
 首先,尝试遍历对象 b 中的所有属性,但没有找到 name 这个属性
 查找 name 属性的请求被委托给对象 b 的构造器的原型,它被 b.proto 记录着并且指向
B.prototype,而 B.prototype 被设置为一个通过 new A()创建出来的对象。
 在该对象中依然没有找到 name 属性,于是请求被继续委托给这个对象构造器的原型
A.prototype。  在 A.prototype 中找到了 name 属性,并返回它的值。
和把 B.prototype 直接指向一个字面量对象相比,通过 B.prototype = new A()形成的原型链比
之前多了一层。但二者之间没有本质上的区别,都是将对象构造器的原型指向另外一个对象,继
承总是发生在对象和对象之间。
最后还要留意一点,原型链并不是无限长的。现在我们尝试访问对象 a 的 address 属性。而
对象 b 和它构造器的原型上都没有 address 属性,那么这个请求会被最终传递到哪里呢?
实际上,当请求达到 A.prototype,并且在 A.prototype 中也没有找到 address 属性的时候,
请求会被传递给 A.prototype 的构造器原型 Object.prototype,显然 Object.prototype 中也没有
address 属性,但 Object.prototype 的原型是 null,说明这时候原型链的后面已经没有别的节点了。
所以该次请求就到此打住,a.address 返回 undefined。
a.address // 输出:undefined

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

春风得意之时

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值