放弃class,一步一图彻底理解Javascript的原型链

来源:互联网 发布:java实现双向链表 编辑:程序博客网 时间:2024/05/23 00:44

引言

  最近看了下ES6的class语法糖,越看越感觉这丫的就是个巨大的败笔:外表包装的像个类,底层就是原型链,但是却不严格遵守原型链,语法十分混乱,有人说这对初学JS的新手很友好,我觉得这反而让新手陷入误解的深渊,作为一名专注Java的后端程序员,都实在是看不下去了,于是连夜攻破了原型链,感觉原型和原型链比class清晰多了,并且更加强大和灵活,配合闭包,有什么是不能完成的,况且要面试的时候,考的肯定还是原型链,所以这个class的存在感极低,想要简化封装和继承,结果引入的问题其实更多,JS设计之初就根本没有class的概念,如今非要逆天而行,扬汤止沸,还顺便洒了自己一身。
  然而原型链和继承真的困难到需要包一层这么个蹩脚的语法糖吗?
  本文讲解自己对原型链的理解,个人习惯梳理知识点的细节,越详细越好,这样心中才有底气。特记录下个人对原型链的理解过程,方便日后参考。

__proto__, prototype和constructor

下面这三个属性的定义非常重要,始终贯穿在原型中。
  prototype:此属性只有构造函数才有,它指向的是当前构造函数的原型对象
  __proto__:此属性是任何对象在创建时都会有的一个属性,它指向了产生当前对象的构造函数的原型对象,由于并非标准规定属性,不要随便去更改这个属性的值,以免破坏原型链,但是可以借助这个属性来学习,所谓的原型链就是由__proto__连接而成的链。
  constructor:此属性只有原型对象才有,它默认指回prototype属性所在的构造函数

一步一图

  
在开始之前,一定要记住一句话,JS中不管是什么类型的对象,都一定有构造函数,包括构造函数本身。

function Computer(){    this.name = 'computer';}var c = new Computer();

我们现在来把原型链图示一步一步地画出来。

根据之前三个属性的定义,实例c的__proto__是和Computer的prototype指向同一个对象,即Computer的原型对象,同时Computer的原型对象有constructor属性,指回构造函数Computer,如下:
这里写图片描述

验证:

console.log(Computer.prototype === c.__proto__);            //trueconsole.log(Computer.prototype);                            //Object{...}console.log(Computer.prototype.constructor === Computer);   //true

现在我们想继续看下Computer的__proto__指向什么,那我们要思维转换一下,把Computer当成是一个实例对象,那么应该有一个更底层的构造函数来产生Computer这个构造函数,Computer的__proto__就应该指向那个更底层构造函数的原型,所以我们推测应该还有一个构造函数,现在输出来看下:

console.log(Computer.__proto__);            //function()console.log(Computer.__proto__.constructor);//function Function()console.log(Computer.__proto__.constructor === Function);//trueconsole.log(Computer.__proto__ === Function.prototype);//true

借助原型对象的constructor可以看到那个底层构造函数是Function,所以继续画:
这里写图片描述

再来看Function的__proto__,把Function当成一个实例,有一个更底层构造函数来产生Function,但是看Function这名字,它本来就是用来产生函数的,所以猜测应该没有什么更底层的构造函数了,它自己产生自己:

console.log(Function.__proto__);                //function ()console.log(Function.__proto__.constructor);    //function Function()console.log(Function.__proto__.constructor == Function);//true

这里写图片描述

再来看Function.prototype.__proto__,原型对象就是个普通对象,它肯定有自己的构造函数,都知道JS是面向对象,Object差不多也该出现了:

console.log(Function.prototype.__proto__);                      //Object{...}console.log(Function.prototype.__proto__.constructor);          //function Object()console.log(Function.prototype.__proto__.constructor === Object);//trueconsole.log(Computer.prototype.__proto__);                      //Object{...}console.log(Computer.prototype.__proto__.constructor);          //function Object()console.log(Computer.prototype.__proto__.constructor === Object);//trueconsole.log(Function.prototype.__proto__ === Computer.prototype.__proto__);//true

所以:
这里写图片描述

还剩最后一个,Object的__proto__指向什么,同样得找Object的构造函数。之前已经发现Function似乎充当了任何对象的构造函数,那么猜测Object的__proto__应该是指向Function的原型:

console.log(Object.__proto__);                          //function ()console.log(Object.__proto__ === Function.prototype);   //trueconsole.log(Object.__proto__.constructor);              //function Function()console.log(Object.__proto__.constructor === Function); //true

再次证明就是这么一回事:
这里写图片描述
(Object的原型对象的__proto__到这里已经到尽头了,为null,所以没有画出来)
这里面主要是Function和Object比较特殊,它们都是由Function作为构造函数产生,那么__proto__都指向Function的原型,同时Function的原型的原型又是Object的原型,于是就出现了Function是Object,Object也是Function这种奇妙现象:

//instanceof的作用是判断一个对象是否在另一个对象的原型链上console.log(Object instanceof Function);    //trueconsole.log(Function instanceof Object);    //true

前面说了,原型链就是__proto__连接的链,从上面的图中可以看到,如果选择不同的起点,会有不同的原型链,比如下面蓝色箭头
这里写图片描述

//以c为起点c -> Computer.prototype -> Object.prototype -> null//以Computer为起点Computer -> Function.prototype -> Object.prototype -> null//以Function为起点Function -> Function.prototype -> Object.prototype -> null//以Object为起点Object -> Function.prototype -> Object.prototype -> null

感觉__proto__属性这前后两根下划线还挺形象。

原型链继承

就用上面的图来继承一个,分析一下:

function Laptop(){    this.brand = 'acer';}Laptop.prototype = c;//也可以使用一个中间空对象,如果你不想继承父对象的实例成员的话Laptop.prototype.constructor = Laptop;//这里先不画var l = new Laptop();

这里写图片描述

Laptop的构造函数为Function,Laptop的__proto__指向构造函数Function的原型,那么继承之后,蓝色的原型链为:

l -> Laptop.prototype(也就是c) -> Computer.prototype -> Object.prototype -> null

注意了,现在Laptop.prototype(也就是c)还没有constructor属性,前面说了,原型对象一定要有一个constructor属性,指回构造函数,但是Laptop.prototype是Computer的实例c,实例对象是没有constructor属性的,此时输出c.constructor有结果,但是其实引用的是原型链上找到的Computer.prototype.constructor,这指向的是Computer:

console.log(c.constructor);             //function Computer()console.log(c.constructor === Laptop.prototype.constructor);//true

所以我们要给Laptop的原型对象上加一个constructor属性,指向Laptop:

Laptop.prototype.constructor = Laptop;

这里可能有疑问了,刚刚还说这里的Laptop原型(即实例c)的constructor是在原型链上引用的Computer.prototype.constructor,那把constructor改了,难道不会影响Computer的原型吗?之前也是一直在这里纠结很久,最后在《Javascript权威指南》的6.2.2节上找到原因,下面是原文:

Now suppose you assign to the property x of the object o. If o already has an own(noninherited) property named x, then the assignment simply changes the value of thisexisting property. Otherwise, the assignment creates a new property named x on theobject o. If o previously inherited the property x, that inherited property is now hidden by the newly created own property with the same name.Property assignment examines the prototype chain to determine whether the assignmentis allowed. If o inherits a read-only property named x, for example, then theassignment is not allowed.If the assignment is allowed, however, it always creates orsets a property in the original object and never modifies the prototype chain. The fact that inheritance occurs when querying properties but not when setting them is a key feature of JavaScript because it allows us to selectively override inherited properties

翻译一下:
现在假设你给o这个对象的x属性赋值,如果o已经有了一个非继承而来的属性x,那么赋值将仅仅是改变x的值。如果o本身并没有x这个属性,那么赋值操作将在o对象上添加一个属性x。如果o之前继承了属性x,那么此时被继承的属性x将被新添加的同名属性屏蔽。
属性赋值操作会检查原型链以确定是否允许赋值。比如,如果o对象继承了一个只读的属性x,那么赋值是禁止的。但是如果允许赋值,也总是只会在当前对象上添加或者修改属性值,不会去改动原型链。继承效果只会显现在查询属性值而非修改属性值的时候,此为Javascript的特点,这让我们可以有选择性的覆写继承而来的属性。

意思就是说在原型对象上,属性的查找是沿着原型链一级一级向上找的,但是属性的赋值就只发生在当前对象上。
所以上面那句代码让Laptop.prototype拥有了自己的constructor,最终图为:
这里写图片描述

总结

1、prototype只有构造函数才有,指向构造函数的原型。
2、__proto__任何对象都有,指向产生当前对象的构造函数的原型。
3、constructor只有原型对象才有,默认指回prototype属性所在的构造函数,使用原型链继承之后,要给新的原型对象添加constructor属性并指向构造函数。
4、任何对象都有产生自己的构造函数,包括构造函数自己。

原创粉丝点击