javascript中的对象,原型,原型链和面向对象

一、javascript中的属性、方法  

  1.首先,关于javascript中的函数/“方法”,说明两点:

  1)如果访问的对象属性是一个函数,有些开发者容易认为该函数属于这个对象,因此把“属性访问”叫做“方法访问”,而实际上,函数永远不会属于一个对象,对象拥有的,只是函数的引用。确实,有些函数体内部使用到了this引用,有时候这些this确实会指向调用位置的对象引用,但是这种用法从本质上并没有把一个函数变成一个方法,只是发生了this绑定罢了。因此,如果属性访问返回的是一个函数,那它也并不是一个方法,属性访问返回的函数和其他函数并没有任何区别,只是有时候会发生隐式的this绑定罢了

  2)javascript中很难确定“复制”函数究竟意味着什么。事实上,在javascript中,函数无法(用标准、可靠的方法)真正地复制,能够复制的只是函数的引用。

  2.对象的原型[[prototype]]链与函数的原型

  原型链:对象[[prototype]]l链是一种机制,是对象中的一个内部链接引用另一个对象。

  原型:函数.prototype指向的就是一个对象,叫做函数的原型对象。

  A.对象原型链

  javascript对象有一个特殊的[[prototype]]内置属性,其实就是对于其他对象的引用,几乎所有的对象在创建时[[prototype]]的属性都会被赋予一个非空的值(除了object.create(null)).所有普通的[[prototype]]链最终都会指向内置的object.prototype。里面包含了很多常见的功能,诸如.toString(),.valueOf(),.hasOwnProperty(),.isPrototypeOf();

  原型链本质上是属性查找使用的。myObject.a不仅仅是在myObject中查找名字为a的属性。在语言规范中,myObject.a实际上是执行了[[Get]]操作。对象默认的[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就返回这个属性的值。如果没有找到,按照[[Get]]算法的定义会遍历对象的原型链(prototype链),直到找到匹配的属性名或者查找完整条[[prototype]]链,如果是后者的话,[[Get]]操作的返回值是undefined.

  在javascript中,给一个对象设置属性如myObject.foo='bar',不仅仅是添加一个新属性或者修改已有的属性,,完整的流程如下:

  1)如果对象中包含名为foo的普通属性,不论上层原型链存不存在,这条赋值语句会直接修改myObject已有的属性名,此时会发生屏蔽;

  2)如果对象myObject中不包含名为foo的属性,[[prototype]]链就会遍历,如果原型链上找不到foo,foo就会被直接添加到myObject上。

  3)如果myOjbect中不包含foo这个属性,但foo存在于原型链的上层,赋值语句myObjec.foo的行为会有些不同,具体如下:

    a.如果[[prototype]]链上层名为foo的普通数据访问属性没有被标记为只读(writable,false),那就会直接在myObject添加一个名为foo的新属性,它是屏蔽属性;

    b.如果[[prototype]]链上层存在foo,且被标记为只读,那么无法修改已有属性或者在myObject上面创建屏蔽属性,在费严格模式下,这条赋值语句会被忽略。

    c.如果[[prototype]]链上层存在foo并且它是一个setter,那么一定会调用这个setter,foo不会被添加到myObject上,也不会重新定义foo这个setter。

  可见只有在myObject中包含foo属性,或者myObject与原型链都不包含foo属性,或者myObject不包含foo属性,原型链的foo属性没有被标记为只读的情况下,才会发生屏蔽。如果在上面b,c两种情况下也希望屏蔽foo,不能使用=操作符,而是使用object.defineProperty来向myObject添加属性foo。

  B:函数原型

  对象原型链[[prototype]]与函数原型prototype,这两个是截然不同的事物,虽然二者存在一定的关联。对象的原型链[[prototype]]如上所述,是对象的一个隐藏属性,用于属性查找。在chrome等浏览器的实现中,可以通过_proto_访问到。

  函数作为对象,自然也有内置的隐藏属性[[prototype]],其原型链的终点指向Function.prototype,而Function.prototype又指向Object.prototype.

  此外,任何函数比如function Foo()默认都有一个特殊的显式属性prototype,(String,Number,Object,Function这些所谓的子类型说白了就是一些内置函数,因此都有prototype属性)。函数的prototype属性是显示的,它指向一个对象,这个对象通常称之为函数原型。

  那么函数原型这个对象究竟是什么呢?

  最直接的解释就是:这个对象时在调用a=new Foo()时创建的,执行这句话同时令新创建的对象a,其a.[[prototype]]链接到这个Foo.prototype所指向的原型对象。如果多次调用new Foo(),那么他们的[[prototype]]关联的是同一个对象,都是Foo.prototype所指向的对象。可见:1)函数作为对象,其Foo.[[prototype]]是不链接到Foo.prototype的,而是new 调用创建的对象连接到Foo.prototype指向的原型对象;2)new调用会在新创建的对象和函数原型之间创建关联,这个关联不存在于对象和构造函数之间,只是上述代码会同时为Foo.prototype添加construnctor属性,该属性指向“构造函数“Foo”(再次强调,javascript没有构造函数,只有函数的构造调用),对象通过原型链也能访问construnctor属性,不过这除了营造出类似“构造对象”的假象,貌似用处不大!!!

  实际上,new Foo()这个函数调用实际上并没有直接创建关联,这个关联只是一个意外的副作用。new Foo()只是间接完成了我们的目的:一个关联到其他对象的新对象。那么有没有更直接的方法做到这一点呢?当然,那就是Object.create(...)!

  var bar=Object.create(foo)会创建一个新对象(bar),并把其[[prototype]]关联到指定对象(foo)。这样就可以充分发挥[[prototype]]委托的威力而避免不必要的麻烦(比如使用new 构造函数会生成.prototype和.constuctor的引用)

  Object.create(null),会创建一个空[[prototype]]链的对象,这个对象无法进行委托。因此不会受到原型链的干扰,非常适合用于存储数据。

  在ES5之前的Object.create()的polyfill的代码:  

  

if(!Object.create){
    Object.create=function(obj){
        function Foo(){};
        Foo.prototype=obj;
        return new Foo();
    };   
}

一、javascript中所谓“类”

  类说白了只是一种设计模式(模板模式),在编程尤其是函数式编程中,类并不是必须的编程基础,只是一种可选的代码抽象。只不过在有些语言如java中,并不给你选择的机会,在c/c++中,会提供过程化和面向类这两种语法。类本身仅仅是一种抽象的表示,需要实例化才能对它进行操作。类通过复制操作被实例化为对象形式,继承操作也类似,子类会包含父类行为的复制后的原始副本,因此子类可以重写所有的继承行为或者新行为而不会影响到父类。可见,在面向类的语言中,类的继承,类的实例化本质上是复制。

  javascript属于哪一种呢?事实上,javascript拥有一些近似类的语法,但在近似类的表象之下,javascript的机制和类完全不同。在类的继承以及实例化时,javascript的对象机制并不会自动执行复制行为,而是通过原型链关联到实际的父类属性,看起来似乎其他语言“继承”的是方法的签名,而javascript继承的是实际的方法。

  记住:javascript中只有对象,并不存在可以实例化的“类”,一个对象并不会被复制到其他对象中,他们只会被“关联”起来

  同样,在面向类的语言中,继承、实例化着复制操作。就实例化来说,javascript不存在将类实例化为对象的说法,javascipt中本来就只有对象;就继承来说,javascript只会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。“委托”这个术语比“继承”更准确地描述Javascipt中对象的关联机制!

  不过,为了使得javascript表现出和其他语言类似的复制行为(不论继承还是实例化,本质都是复制),javscript开发者想出了很多方法来模拟类的复制行为:如混入,寄生继承和基于原型的继承。  

二、在javascript中模拟类的实现

  1.混入

  包括在jQuery源代码中,模拟其他语言的类复制行为,这种方法就是“混入”(mixin)。

  手动实现的mixin(在很多库中也叫做extends)代码如下:

function mixin(sourceObj,targetObj){
    for(var key in sourceObj){
        if(!(key in targetObj)){
            targetObj[key]=sourceObj[key];
        }
    }
}

  

  1)这不能够解决javascript中的显式伪多态问题,在javascript中调用类似父类中的同名方法没有super这样的用法(相对多态),只能使用绝对引用父类名.方法名.call,(显式伪多态)。这样的后果是,在支持多态的面向类的语言中,子类和父类的关系只需要在类定义的开头创建即可,因此只需要在这一个地方维护两个类的联系。然而javascript的显示伪多态会在所有需要使用多态的地方引入一个函数关联,因而增加了代码的维护成本。

  2)混入也无法完全模拟类的复制行为,因为对象(和函数)只能复制引用,无法复制被引用的对象或函数本身。

  2.寄生式继承

  寄生式继承的主要推广者是Doulgas Crockford.寄生式继承的主要思路是创建一个用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后返回这个对象。  

function SuperType(name){
    this.name=name;
}
SuperType.prototype.sayName=function(){
    return this.name;
};

//寄生式继承
function SubType(name,age){
    var instance=new SuperType(name);//实际上寄生式继承中,这里不一定非得是new,凡是返回一个对象的操作都可以
    var sayName=instance.sayName;
    instance.age=age;
    //方法重写
    instance.sayName=function(){    
        var name=sayName.call(this);
        console.log('welcome '+name);        
    };
    instance.sayAge=function(){
        console.log(this.age);
    }
    return instance;
}
 
//测试
//事实上,使用new时候会创建一个对象,但是由于我们返回了Instance这个对象,new创建的这个对象会被忽略,因此下面的代码加不加new实质上是一样的
var subInstance=new SubType('bobo',28);
subInstance.sayName();//输出welcome bobo
subInstance.sayAge();//输出28

  寄生式继承的主要问题之一是不能复用函数,在上面的例子中,如果调用两次SubType()[或者调用new SubType()达到的效果是一致的],那么返回的两个SubType“实例”各自拥有自己的一套函数。此外,引用父类的方法,需要首先将方法的引用赋值给对应变量,如上面的代码所示。

  3.基于原型的继承

   基于原型的继承,其思路是使用原型链实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承,这也是被普遍使用的一种方法。下面是一个案例: 

//基于原型的继承
function SuperType(name){
    this.name=name;
}
SuperType.prototype.getName=function(){
    return this.name;
};

function SubType(name,age){
    SuperType.call(this,name);
    this.age=age;
}
SubType.prototype=Object.create(SuperType.prototype);
SubType.prototype.sayName=function(){
    //调用this的方法
    var name=this.getName();
    console.log('welcome '+name);
}
SubType.prototype.sayAge=function(){
    console.log(this.age);
}

//测试
var subInstance=new SubType('bobo',28);
subInstance.sayName();//输出welcome bobo
subInstance.sayAge();//输出28

上面代码最核心的部分就是:

  SubType.prototype=Object.create(SuperType.prototype).这句话调用会创建一个新对象SubType.prototype,并将其内部的[[prototype]]关联到指定对象,本例中是SuperType.prototype.

  注意,有两种常见的错误,实际上他们都有一些问题:  

//达不到想要的效果
SubType.prototype=Foo.prototype;
//基本达到需求,但存在一些副作用
SubType.prototype=new SuperType();

第一种,SubType.prototype=SuperType.prototype并不会创建一个关联到Foo.prototype的新对象,而只是让SubType.prototype直接引用SuperType.prototype,因此执行类似SubType.prototype.xxx的赋值语句的时候,将直接修改Foo.prototype对象本身。实际上这样不是你想要的效果,因为此时根本不需要SubType对象,直接使用SuperType就可以了,代码还可以更简单。

第二种:SubType.prototype=new SuperType()的确会创建关联到SuperType.protoType的新对象。但使用了SuperType的构造函数调用,如果函数SuperType有一些副作用(比如写日志,注册到其他对象等等),就会影响到SubType()的后代,造成不可预知的后果。其次是调用了两次SuperType这个函数。第一次是在子类构造函数的内部,通过调用父类的SuperType为子类的对象添加name属性;第二次是在设置子类原型链prototype的地方,对SuperType实行了构造调用,这会导致子类的原型链中也存在一个name属性(并且值为undefined),只不过由于属性屏蔽,子类实例对象中的name属性屏蔽了其原型链中的name属性b罢了。

三、javascipt中的“类”关系(称为内省或者反射)

假设有对象a,如何寻找a委托的对象呢?

1)站在“类”的角度来判断:

   a instanceof Foo;

instanceof回答的问题是,在a的整条[[prototype]]链中是否有指向Foo.prototype的对象?其左操作符是一个对象,右操作符是一个函数。不能两个对象(比如a和b)是否通过[[prototype]]相互关联。

2)更简洁的判断[[prototype]]反射的方法

  Foo.prototype.isPrototypeof(a)

isPrototypeof同样能够回答上述问题:在a的整条[[prototype]]链中,是否出现过Foo.prototype?

同样的问题,同样的答案,但在第二种方法中并不需要访问函数Foo,只需要两个对象就可以判断他们之间的关系,

如:b.isPrototypeOf(c)

四、面向委托的设计

记住!javascript中只有对象,并不存在可以实例化的“类”,上述任何模拟类的方法都显得不伦不类。对象直接定义自己的行为即可!!!

我们不需要类来创建两个对象的关系,只需要通过委托来关联对象就足够了。从现在开始,尽量抛弃所有的function ,new (),.prototype的写法!!

出于各种原因,以“父类”,“子类”,“继承”,“多态”结束的属于(包括原型继承)和其他面向对象的术语都无法帮助理解javascript的真实机制!相比之下,“委托”是一个更合适的描述,委托行为意味着某些对象在找不到属性或者方法引用时会把这个请求委托给另一个对象,javascript对象之间的关系是委托而不是复制!

下面以具体案例为例,对比基于类的写法和基于委托的写法二者的不同:

1.基于类的写法

//基于类的写法
function SuperType(name){
    this.name=name;
}
SuperType.prototype.getName=function(){
    return this.name;
}

function SubType(name,age){
    SuperType.call(this,name);
    this.age=age;
}
SubType.prototype=Object.create(SuperType.prototype);
SubType.prototype.getAge=function(){
    return this.age;
};
SubType.prototype.sayHello=function(){
    var name=this.getName();
    return 'hello '+name;
};


//测试
var instance=new SubType('bobo',28);
console.log(instance.sayHello());//输出hello bobo
console.log(instance.getAge());//输出28

2.基于委托的写法

//采用基于委托的写法
var superObj={
    init:function(name){
        this.name=name;
    },
    getName:function(){
        return this.name;
    }
};
var subObj=Object.create(superObj);
//不能像下面这么写了
//对象字面量的写法会创建一个新对象赋值给subObj,原来的关联就不存在了
// subObj={
//     setup:function(name,age){
//         this.init(name);
//         this.age=age;
//     },
//     sayHello:function(){
//         var name=this.getName();
//         return 'hello '+name;
//     },
//     getAge:function(){
//         return this.age;
//     }

// };
 
//只能这么写
subObj.setup=function(name,age){
    this.init(name);
    this.age=age;
};
subObj.sayHello=function(){
    return 'hello '+this.name;
};
subObj.getAge=function(){
    return this.age;
};
var b1=Object.create(subObj);
b1.setup('bobo',28);
console.log(b1.sayHello()); //hello bobo
console.log(b1.getAge()); //28

var b2=Object.create(subObj);
b2.setup('leishao',27);
console.log(b2.sayHello()); //hello leishao
console.log(b2.getAge()); //27

对比两种写法:

1)基于委托的写法中不会出现function构造函数,new,prototype,有的只是对象和对象之间的关联;

2)基于类的写法,子类通常和父类取相同的方法名来实现重写的效果,而在基于委托的写法中,则需要尽量避免这种方法,避免重名在原型链查找中引起不可预测的后果。

3)基于类的写法中,属性一般在构造函数中声明,创建对象和对象初始化是在一次构造函数的调用中一次性完成的(var instance=new SuperType('bobo',28));然而在基于委托的写法中,创建对象和对象初始化分开,分成了两次(var b1=Object.crate(subObj);b1.setup('bobo',28)),属性一般也在初始化函数中定义。这样固然多谢了代码,但同时也增加了灵活性,可以根据需要让他们出现在不同的地方。