一篇文章教你搞懂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 您的支持是我最大的动力!