一篇文章教你搞懂javaScript原型链

    JavaScript是被称为零入门的语言,但凡学习过编程的同学,入门起JavaScript这门语言都会觉得特别容易,但是深入之后也会有一些很基本却很容易被忽略的点,这篇文章教你重新认识JavaScript。

    首先明确一点,JavaScript并不是一门完全面向对象的语言,这也是老生常谈的一个问题,这里就不多讲。只说一点我的理解,既然不是一门完全面向对象的语言,那么它肯定会有面向对象语言的特性,也有非面向对象语言的特性。而针对原型链,我们暂时将它认为是一门完全面向对象的语言。

    完全面向对象的语言,经典代表是java,我们经常说java语言中,万物万事皆对象。那么对于JavaScript,我们如果认为它是完全面向对象的,也可以说它万物万事皆对象,那么首先搞清楚最基本的对象。

    最基本的对象

    var obj = Object.create(null);

    这个obj是一个最纯净的对象,它内部什么属性都没有。

    请注意!请注意!请注意!(重要的话说三遍),这里的obj与下面这种写法初始化后的对象不同。

    var obj1  = {};

    这样写法的得出的obj内部并不是空的。在js中,为了简化语法 这种写法其实是下面这种写法的简写: 

    var obj1 = new Object();

    两种写法区别主要在于obj内部没有任何属性和方法,obj1内部有一个属性__proto__指向一个Object定义的对象,这个之后再谈。

    最基本的传递(Delegation)

    所谓一生二,二生三,三生万物,有了这个最基本的 obj 我们就用它来构造整个JavaScript对象模型。首先是描述最基本的属性传递规则,也就是原型链的传递规则。Delegation是指当从一个对象中获取一个属性失败时,JS会自动再尝试去其__proto__链接指向的对象中去查找,如果还找不到,就沿着__proto__一路找上去,直至找到null。这个规则告诉我们一个很重要的事情,就是每个对象都必须有一个属性叫__proto__!按照这个规则,我们的最基本的对象可以描述成如下样子:

    {
       __proto__:undefined
    }

    还是请大家揣摩这个对象。

    构造一个新对象 同时保留老对象的所有特点 

    有了上面那条最基本的规则,我们现在来构造一个新的对象,并且这个心对象有老对象的所有特性,基本的算法应该这么描述:

    1.copy最原始对象得到一个副本

    2.再次copy原始对象得到另一个副本

    3.在第二个副本中添加新的属性或者方法

    4.将第二个副本中__proto__指向第一个副本

    更一般的,假设我们不是通过最原始的对象来构造一个新对象,则算法变为

    1.copy父级对象得到一个副本

    2.再copy原始对象得到另一个原始对象副本

    3.在原始对象副本中添加新的属性或者方法

    4.将原始对象副本中__proto__指向父对象副本

    以上就是构造出一个新对象的过程,并且是继承的过程。这个过程用代码描述 假设a是已有的对象,现在要构造新对象b。

    var copyA = copy(a);
    var b = copy({});
    b.newProperty = "我是新的";
    b.__proto__ = copyA;
    //大家体会这个过程。

    JavaScript中的function 和 new关键字

    大家对js的function应该不陌生,因为刚入门的时候我们就会使用它来封装我们的程序代码模块等等。

    其实function也是一个对象,但不同于上文中的那种对象(我们称上文中的对象为普通对象)。

    function 这种对象是可以被new关键字激活并且返回一个普通对象,也就是说function是一种可以生产对象的对象,我们一般把这种对象叫做类,或者叫做类类型。

    这种类类型有一个属性叫prototype,这个属性指向一个普通对象。而new关键字的作用就是

    1.创造一个纯净的对象 obj,

    2.执行fun.apply(obj,arguments);

    3.将新对象obj的__proto__指向 fun的prototype

    4.返回此对象obj

    举例说明:

    var fun = function(){
                this.aaa="222";
              };
    fun.prototype={
        property1:"111"
    }
    var funObj = new fun();
    //funObj 对象包含 
    //{
    //   aaa:'222',
    //   __proto__:{
    //       property1:"111"    
    //   }
    //}

    而当调用funObj.property1时 按照delegation原则会找到value "111"

    由上推断,js中普通对象和function使用new关键字联系在一起,普通对象的__proto__和function的prototype关系密切,其他更深层次的联系读者自己体会。

    还有一点需要指明,当function内部有返回值时,new关键字会失效。

    最基本的类类型

    JavaScript中内置了一些类类型,上文中所说的纯净的对象在js中是不存在的,因为js在出生的那一刻就给我们提供了一些基类的类型,注意这里说的是类类型,也就是说接下来说的几种都是function

    1. Object 

    Object 是js中生产基础对象的类类型,使用new关键字创建的Object对象是js中最简单最基础的对象,是js中一切对象的源,表现为Object的对象的__proto__的__proto__指向null。注意这里用了两个__proto__,分析一下原因:

    var obj = new Object();
    //分析一下new的过程
    //    obj=Object.create(null);    创造一个纯净对象
    //    obj.__proto__ = Object.prototype;
    //    我们知道Object的prototype为 "{ __proto__:undefined,... }"
    //    所以最终我们得到的对象obj.__proto__.__proto__指向undefined

    2.Function

    Function 是js中生产function的类类型,相比Object,Function似乎更难理解一点,因为Function本身是一个function,同时它的prototype也是一个function,当用new关键字激活Function时,生产的对象也是一个function,但生成的这个funtion的prototype不再是function而是Object对象。

    这点和上文中的规则有点相悖,但其实并不是相悖的。下面举例来说明Function内部大概的思路:

    如下的实现模仿Function是错误的

    var fun1 = function(){}
    fun1.prototype=function(){}
    var fun2 = new fun1();

    fun1的prototype是function(){} 此时用new关键字激活fun1返回一个对象,此时fun2的type应该是object而并非一个function,这是因为fun1并没有返回值,此时new关键字激活后会将生产的新对象返回,而这个对象是由fun1构造的普通对象,其实是Object派生出来的一个对象。

    如下的实现模仿Function是类似的

    var fun11 = function(){
        return function(){}
    }
    fun11.prototype=function(){}
    var fun12 = new fun11();

    此时fun12是一个function,也就是一个类类型,可以继续使用new关键字激活,但是要注意的是,这个fun12和fun11没有继承派生关系,fun12指向的是fun11内部生成的一个function对象,此时的fun11就类似于一个生产function的工厂,作用就和Function类似了。

    可以看出来,此时的new关键字已经失效了,因为不使用new关键字得到的fun12也是一样的。这点是js函数式的特点,返回值可以是函数。

    再来看原型链继承

    根据javascript delegation的特性,再去理解一般的继承写法就不难理解了。

    var fun1 = function(){
        this.a=234
    }
    fun1.prototype = {
        a:123,
        b:321
    }
    
    var fun2 = function(){
        this.c=111;
    }
    fun2.prototype=new fun1();
    var obj1 = new fun1();
    var obj2 = new fun2();
    
    //解析obj1内部结构如下
    // obj1 = {
    //	a:234,
    //	__proto__:{
    //    	    a:123,
    //          b:321
    //	}
    //}
    //obj1.a==234 true;
    //obj1.b==321 true;
    
    //解析obj2内部结构如下
    //obj2={
    	c:111,
    	__proto__:{
    //	    a:234,
    //	    __proto__:{
    //		a:123,
    //		b:321
    //	    }
    //	}
    //}
    //obj2.a==234
    //obj2.b==321
    //obj2.c==111

    这是一般的继承写法,当然这表面上看似乎没有问题,其实却有一点问题,这里简单说明一下

    还是以上的写法

    var fun1 = function(){
        this.a=234
    }
    fun1.prototype = {
        a:123,
        b:321
    }
    
    var fun2 = function(){
        this.c=111;
    }
    fun2.prototype=new fun1();
    
    var obj1 = new fun1();
    var obj2 = new fun2();
    
    //此时改变一下fun1的prototype
    fun1.prototype.b=322;
    //再去看一下obj2的值
    //obj2.b==322 !!!

    以上可以看出,obj2已经生成,按照一般的逻辑,此时改变类本身,不应该影响obj2,但显然现在不是这种情况。所以应该做如下处理:

    var fun1 = function(){
        this.a=234
    }
    fun1.prototype = {
        a:123,
        b:321
    }
    
    var fun2 = function(){
        this.c=111;
    }
    
    function copy(obj){
        var objnew = {};
        for(var key in obj){
            if(typeof obj[key] == "object"){
                objnew[key]=copy(obj[key]);
            }else{
                objnew[key]=obj[key];
            }
        }
        return objnew;
    }
    //这里多了一个copy方法
    fun2.prototype=copy(new fun1());
    
    var obj1 = new fun1();
    var obj2 = new fun2();

    但是这样问题就结束了么?当然没有,现在会出现一个新的问题,不太好描述,直接看下面的代码:

    var fun1 = function(){
        this.a=234
    }
    fun1.prototype = {
        a:123,
        b:321
    }
    
    fun1.prototype.b=fun1.prototype;
    //这里真的是神来之笔啊!!!!
    
    
    var fun2 = function(){
        this.c=111;
    }
    
    function copy(obj){
        var objnew = {};
        for(var key in obj){
            if(typeof obj[key] == "object"){
                objnew[key]=copy(obj[key]);
            }else{
                objnew[key]=obj[key];
            }
        }
        return objnew;
    }
    //因为fun1的变态的prototype结构 下面的函数会递归调用直到栈溢出,直接崩掉了!
    fun2.prototype=copy(new fun1());
    
    var obj1 = new fun1();
    var obj2 = new fun2();

    这个问题似乎比刚才那个问题更严峻,直接屏蔽掉一种prototype结构的合法性。

    由此可以看出单纯从代码规则上不能屏蔽掉这个bug问题,所以我们必须遵循一个比较严格的代码规范,即:

    类的定义必须在实例化对象之前,对象实例化之后禁止修改类本身。这个规范并非说代码层次的强制禁止,只是告诉大家如果不遵循这个规范有可能会埋下不小的bug风险,其危害也是显而易见的。除非你有特别的产品需求需要这样做,否则建议千万别这么做。

    这个问题的出现归根结底是因为js是解释型语言,在任何时候都可以对类本身做出调整,也没有内置的访问权限控制,即使可以使用闭包伪造私有域,但无论如何都不能杜绝对一个类的prototype的访问。

    无论如何刚才的继承写法都有点太low的感觉,要直接控制子类的prototype,如果有很多很多类,出了问题排查都不容易!有更好的写法么?

    接下来的文章会提供一套js继承系统,探究一下js框架代码中,是如何处理继承这个问题的。敬请期待!

    转载请注明出处:http://gagalulu.wang/blog/detail/7 您的支持是我最大的动力!