【读书笔记】之Javascript prototype(JS原型)深入理解

最近比较疑惑这个问题,看过博客、论坛、知乎等关于JS的prototype各种讨论,也仔细阅读了手上几本书关于prototype的介绍,包括《Javascript语言精髓与编程实践》、《Javascript面向对象编程指南》、《Javascript权威指南》、《Javascript高级程序设计》、《悟透Javascript》等等。

个人觉得还是《Javascript语言精髓与编程实践》介绍的最为到位,《面向对象指南》介绍得有些含糊:

在处理原型问题时,我们需要特别注意一下两种行为。
◆ 当我们对原型对象执行完全替换时,可能会触发原型链中某种异常(exception)。
◆ prototype.constructor属性是不可靠的。

首先,是否是“异常”有待探讨(个人觉得这才是正确的原型继承),其次如果你真正理解了prototype并正确的使用它,constructor属性是不可靠的吗?

下面,来深入介绍一下JS的prototype:

一、 什么是原型?

(见《语言精髓》中“原型继承的基本性质”)
在JavaScript 的语言和对象系统的实现来讲,对象(Object Instance)并没有原型,而是构造器(Constructor)有原型。对象只有“构造自某个原型”的问题,并不存在“持有(或拥有)某个原型”的问题。
原型其实也是一个对象实例。原型的含义是指,如果构造器有一个原型对象A,则由该构造器创建的实例( Instance)都必然复制自A。

这段话,句句经典,如果真的看懂了,会解决你很多的疑惑(什么才有原型?原型继承为什么会触发“某种异常”?…)

二、 原型“复制”:

这里的“复制”就存在了多种可能性,由此引申出了动态绑定和静态绑定等等问题。但我们先不考虑“复制”如何被实现,而只需先认识到:由于实例复制自对象A,所以实例必然继承了A的所有属性、方法和其它性质。“原型也是对象实例”是一个最关键的性质,这是它与“类继承体系”在本质上不同。

构造复制?写时复制?还是读遍历?那么JS的prototype继承属于哪种“复制”?

构造复制: 每构造一个实例,都从原型中复制出一个实例来,新的实例与原型占用了相同的内存空间。这虽然使得实例与它们的原型“完全一致”,但也非常地不经济—— 内存空间的消耗会急速增加。
写时复制:在系统中指明实例等同于它们的原型,这样在读取的时候,只需要顺着指示去读原型即可。当需要写实例对象的属性时, 我们就复制一个原型的映象出来,并使以后的操作指向该映象就行了。优点是只在第一次写的时候会用一些代码来分配内存,并带来一些代码和内存上的开销。但此后这种开销就不再有了,因为访问映象与访问原型的效率是一致的。不过对于经常写操作的系统来说,这种法子并不比上一种法子经济。
读遍历: 把写复制的粒度从原型变成了成员。这种方法的特点是:仅当写某个实例的成员时,将成员的信息复制到实例的映象中。这样一来,在初始构该对象时的局面仍与写时复制一样。但当需要写对象属性(例如obj.value=10 )时,会产生一个名为value 的属性值,放在实例对象的成员列表中。
这张表是否与原型一致并不重要,只需要:
● 规则1:保证在读取时被首先访问到即可。
● 规则2:如果在对象中没有指定属性,则尝试遍历对象的整个原型链,直到原型为空(null) 或找到该属性。

很显然,JS采用了第三种方案“读遍历”。

三、 Prototype 继承:

// 构造器声明
function MyObject() {}
function MyObjectEx() {} 

//原型链
MyObjectEx.prototype = new MyObject(); 

//创建对象实例
obj1 = new MyObjectEx();
obj2 = new MyObjectEx();

上面代码是我们常用到的继承方法,而且文章开头讲到的“异常(exception)”也出于此:

输出obj1.constructor的结果是什么?MyObject


的确,你没看错通过MyObjectEx实例化构造的对象的constructor竟然指向MyObject,正应证了我们上面介绍的原型“复制”。
先来看看MyObject的prototype是什么

MyObject.prototype是一个constructor属性指向MyObject本身的对象。所以就有我们常听说的“构造器的原型的constructor指向构造器本身”这样一个说法。 那么为什么MyObject.prototype.constructor要等于MyObject呢?

原型就相当于一个模板,通过构造器的加工,来产生所需的实例。所以实例就和模板长的很像,在模板中定义了constructor为构造器本身,那么,从模板“复制”出来的实例的constructor就也是构造器本身了,这不正是constructor本意么。

MyObject().prototype.constructor=MyObject;  //原型constructor为MyObject
Obj=new MyObject();  //构造产生实例
Obj.constructor; 输出: MyObject();  //实例的constructor也就为MyObject

这样“输出obj1.constructor的结果是什么?MyObject!”这个“异常”就很正常了,
MyObjectEx.prototype = new MyObject ();

MyObjectEx.prototype是MyObject的实例,自然MyObjectEx.prototype中的constructor也是从MyObject.prototype“复制”过来的,理所当然的就是MyObject。

所以通过MyObjectEx的“模板”产生的实例obj1的constructor就是MyObject,这是正确的原型“复制”,但这并不是我们想要的,因为obj是从MyObjectEx构造来的constructor应该是MyObjectEx,不然大家伙都不知道孩子的亲爹到底是谁了,MyObject是它爷爷好不!

那么如何解决这个异常呢?

很多人说,这简单,不是说“构造器的原型的constructor指向构造器本身”么,那我就重置回去就好了。原型继承的代码就变成下面这种了:

● 法一:

原型继承要求的“复制行为”其实已经正确的发生了,问题是出在我们给MyObjectEx 的原型时,应该“修正” 该原型的构造器值。为此,一般的建议(例如《JavaScript 权威指南》)是这样处理:

MyObjectEx.prototype = new MyObject();
MyObjectEx.prototype.constructor = MyObjectEx;  //告诉大家MyObjectEx才是孩儿他爹

//创建对象实例 
obj1 = new MyObjectEx(); 
obj1.constructor输出: MyObjectEx  //孩子终于知道他爹是谁了

但是这么一来,通过obj1 就没法访问到MyObject的原型了。这就是我们所说的原型链“断裂”(oh no! 族谱没上文了!祖宗找不到了!这子孙真是不孝。)

可能你会问,既然原型链“断裂”了,那为什么obj1里还能输出从原型中“复制”来的属性呢?这就是靠系统自己维护的另一条原型链(内部原型链)了FF和chrome中可以通过“__proto__”访问到,IE中不能通过这个访问到。(记住,是内部原型链,别和我说既然有这个内部原型链,那就没断裂啊,,,政府专用食堂,你能随便进去吃饭么?所以在外部是不能这么访问的)

于是乎你承认了,政府的专用食堂平民还进不去,再于是,通过obj1就没法修改MyObject的原型了(祖宗啊,子孙真的没法把你写入族谱了)。 当然,通常情况下,我们也用不着通过obj1修改MyObject,只需要在MyObjectEx的prototype中增删改就可以了,那么通过MyObjectEx构造的实例依然有我们改过的东西。这种情况下,法一就够你用的了。

● 法二:

保持原型的构造器属性,而在子类构造器函数内初始化实例的构造器属性——这看起来有点令人发晕,但代码不过如下:

//方法二:正确维护constructor ,以便回溯原型链
function MyObject() {}
function MyObjectEx() {
 	this.constructor = arguments.callee;
    // or, this.constructor = MyObject Ex; 
}
 
//原型链
MyObjectEx.prototype = new MyObject();
Obj1=new MyObject();  //构造产生实例

原型的constructor 则指向MyObject()。虽然你会发现它的效率较低—— 因为每次构造实例时都要重写constructor 属性—— 但是它是唯一有效的方法。

通过法二,我们看到obj1.constructor属性正常了,而且我们也可以通过obj1访问到MyObject的原型了(终于可以认祖归宗了)。


● 法三:

再来看看《Javascript面向对象编程指南》中讲到的一种对法一改造的方法:通过往实例中添加uber属性来维持原型链的完整性。

function extend(Child, Parent){
	var F=function(){};
    F.prototype=Parent.prototype;
    Child.prototype=new F(); //这个地方为什么不用Child.prototype=new Parent();呢?
    Child.prototype.constructor=Child;
    Child.uber=Parent.prototype;
}

//那么创建实例就需要变成这样了,
extend(MyObjectEx, MyObject);
extend(obj1, MyObjectEx);

自然,可以看出,法三通过uber属性指向Parent.prototype实现了原型链的完整。Obj1就可以通过uber访问修改Parent的prototype了。

关于Child.prototype=new F();这个地方为什么不用Child.prototype=new Parent();而是去构建一个prototype等于Parent.prototype的构造器,应该是为了只继承Parent.prototype的属性,而不受Parent构造器的影响。比如如下:

function Parent(){
   this.name= "二毛"
}
Parent.prototype.name="大毛";

function Child(){}
Child.prototype=new Parent();
obj1=new Child();
->obj1.name 输出”二毛”;

(皇阿玛呀,“大毛”这名字是太后老佛爷给起的,你又给改成“二毛”了,您还是给儿臣改回来吧,,,),结果皇阿玛不理你!好吧,只好自己改回去了,于是不得不又重复添加一条语句:

obj1.name="大毛";

当然,这种方法也需要综合考虑,如果obj1不想受到父级构造器的影响,那就用法三,如果需要原封的继承自父级,那么就用法二。

自此,JS的prototype就讲解完成了。

 

四、 Prototype 的一些相关问题:

下面讲解一些大家经常碰到的问题的疑惑,

一、__proto__到底是什么?

前面已经讲到过prototype继承有两条原型链:“构造器原型链”、“内部原型链”。

从表象来看,维护原型链似乎全是构造器的事情,因为只有构造器(函数对象)有一个名为prototype 的成员。但事实上并非完全如此。从更前面的讨论来看,我们知道一个实例(注意是实例)至少应该拥有指向原型的__proto__(指向其构造器的原型) 属性,这是JavaScript中的对象系统的基础。不过这个属性是不可见的,我们称之为“内部原型链”,以便和构造器的prototype 所组成的“构造器原型链”(亦即是我们通常所说的“原型链”)区别开来。

构造器通过了显式的prototype 属性构建了一个原型链,而对象实例也通过内部属性__proto__构建了一个原型链。由于obj1. __proto__是一个不可访问的内部属性所以没有办法从obj(指代所有MyObjectEx 的实例)开始访问整个原型链。解决这个问题的法子是通过obj.constructor。

接下来会有人问:既然用户代码只需要正确维护constructor 属性就可以回溯原型链,那么实例的内部属性__proto__有什么价值呢?换而言之,内部原型链有什么价值呢?

这个问题与原型继承的实质有关,也与面向对象的实质有关。面向对象的继承性约定:子类与父类具有相似性。在原型继承中,这种相似性是在构造时决定的,也就是由new()运算内部的那个“复制”操作决定的。

如果我们改变了“继承”的定义,说:子类可以与父类不具备相似性。那么我们就违背了对象系统的基本特性。因此,你会发现子类实例有一个特性:不能用delete 删除从父类继承来的成员。也就是说,子类必须具有父类的特性。

即使你可以重写成员,改变它的实现,但在界面(Interfaces)上必然与父类一致。为了达成这种一致性,且保证它不被修改。JavaScript 使对象实例在内部执有这样一个proto 属性,且不允许用户访问。这样,用户可以出于任何目的来修改constructor 属性,而不用担心实例与父类的一致性。

简单地说, 内部原型链是JavaScript 的原型继承机制所需的。而通过constructor 与prototype 所维护的构造器原型链,则是用户代码需要回溯时才需要的。如果用户无需回溯,那么不维护这个“原型链”,也没有关系——例如上一小节中用于修正constructor 属性的“方法一”。

二、再来看第二个问题(摘自知乎)

Javascript 继承代码中,B.prototype = new A(); 的含义是什么?
function A() {this.name = "A"} 
function B() {this.name = "B"}

//这个时候要使 B 继承 A,用
B.prototype = new A(); 
B.prototype.constructor = B;
我想知道,这里为啥是 B.prototype = new A(); 而不是 B.prototype = A or B.prototype = A.prototype?

这个问题,知乎上有两个回答,但对于“而不是 B.prototype = A”目测都没有回答正确。先不要看下面的,你能回答出来吗?


看出什么毛病来了没有?

obj.name竟然是A,为嘛不是“大毛”呢?而且为嘛obj.email是undefined呢? “B.prototype = A”,当你设置B.prototype = A时,A是什么?一个函数!B的prototype是一个函数,如果没有在B中重设name,当访问obj.name时,返回的就是A的函数名称“A” 而不是“大毛”。

同样,访问obj.email时,根本不是访问的A.prototype.email,而是A这个函数的email属性,话说函数有email属性么?函数对外只有个函数名而已,自然就是undefined了。

关于“为什么不是B.prototype = A.prototype”,看看下图就知道了:

通过A和B构造的实例name值竟然都是“二毛”,为嘛A的实例不是叫“大毛”呢? 那是因为当设置B.prototype = A.prototype,那么着两个对象的引用是完全一致了,这样的话,修改B的原型就会直接把A的原型给污染掉!

所以:必须用B.prototype = new A();这个方法,B.prototype = new A();创建了一个新的对象{},并且继承了A的原型,这是一个新对象,不是和A同一引用,所以不会污染A。

三、为什么typeof Function.prototype的结果是function?(来自知乎)

先来看看Stackoverflow上被采纳的回答:

This seems to be defined in ECMAScript 5:

15.3.4 Properties of the Function Prototype Object
The Function prototype object is itself a Function object (its [[Class]] is "Function") that, when invoked, accepts any arguments and returns undefined.

在ECMAScript 5中定义的:

15.3.4 Function 原型对象的属性

Function 原型对象本身就是一个Function对象(它的[[Class]] 是“Function”),这意味着,当被调用时,接受传递的任何参数并且返回undefined

Function is the constructor of function objects

Function.prototype is the prototype from which all function objects inherit - it might contain properties like call and apply which are common to all Function instances; the implementations you checked were consistent in that it is implemented as a function object itself (as some pointed out, the ECMA specification requires this)

Funcion是所有函数对象的构造器

Function.prototype,所有函数对象都继承与它,它可能包含一些属性,如所有Function实例中所常见的call和apply。它被实现为一个函数对象本身(正如一些人士指出的,ECMA规范要求这样),这和你检查的结果是一致的。

其实,从最开始的原型“复制”的概念,也可以简单的理解出来,通过prototype为函数象的Function构造器,才能最简单,最直观的构造出函数实例。参考问题二的图片:



关于JS继承方式,后续会再写到。







阅读本文后,您的心情是:
 
恶心
愤怒
强赞
感动
路过
无聊
雷囧
关注
知识共享许可协议
评论(0) 浏览(17916) 引用(0)
引用地址:http://blog.baiwand.com/tb.php?sc=f39048&id=209
Tags:
« 【站点更新】站点已整体迁移至PHPCloud开发者云服务 【经验总结】复制内容到剪贴板完美解决方案-Zero Clipboard »

Blogger

  • blogger
  • 天之骄子
  • 职位:研发工程师
    铭言:
    阳光与欢乐同在,
    与我同在
    主页:
    blog.baiwand.com

分类目录

日志归档

主题标签

数据统计

  • 日志:151篇
  • 评论:45条
  • 碎语:264条
  • 引用:0条

链接表

随机日志 »

最新日志 »

最新评论 »

标签云 »

订阅Rss
sitemap