天天看点

深入浅出JavaScript之原型链和继承

javascript语言的继承机制,它没有”子类”和”父类”的概念,也没有”类”(class)和”实例”(instance)的区分,全靠一种很奇特的”原型链”(prototype chain)模式,来实现继承。

这部分知识也是javascript里的核心重点之一,同时也是一个难点。我把学习笔记整理了一下,方便大家学习,同时自己也加深印象。这部分代码的细节很多,需要反复推敲。那我们就开始吧。

小试身手

原型链例子(要点写在注释里,可以把代码复制到浏览器里测试,下同)

function foo(){}              //通过function foo(){}定义一个函数对象 

foo.prototype.z = 3;          //函数默认带个prototype对象属性   (typeof foo.prototype;//"object") 

var obj =new foo();           //我们通过new foo()构造器的方式构造了一个新的对象 

obj.y = 2;                    //通过赋值添加两个属性给obj 

obj.x = 1;                    //通过这种方式构造对象,对象的原型会指向构造函数的prototype属性,也就是foo.prototype 

obj.x; // 1                 //当访问obj.x时,发现obj上有x属性,所以返回1 

obj.y; // 2                 //当访问obj.y时,发现obj上有y属性,所以返回2 

obj.z; // 3                 //当访问obj.z时,发现obj上没有z属性,那怎么办呢?它不会停止查找,它会查找它的原型,也就是foo.prototype,这时找到z了,所以返回3 

//我们用字面量创建的对象或者函数的默认prototype对象,实际上它也是有原型的,它的原型指向object.prototype,然后object.prototype也是有原型的,它的原型指向null。 

                                   //那这里的object.prototype有什么作用呢? 

typeof obj.tostring; // ‘function'   

//我们发现typeof obj.tostring是一个函数,但是不管在对象上还是对象的原型上都没有tostring方法,因为在它原型链的末端null之前都有个object.prototype方法, 

//而tostring正是object.prototype上面的方法。这也解释了为什么js基本上所有对象都有tostring方法 

'z' in obj; // true               //obj.z是从foo.prototype继承而来的,所以'z' in obj返回了true 

obj.hasownproperty('z'); // false   //但是obj.hasownproperty('z')返回了false,表示z不是obj直接对象上的,而是对象的原型链上面的属性。(hsaownproperty也是object.prototype上的方法) 

深入浅出JavaScript之原型链和继承

刚才我们访问x,y和z,分别通过原型链去查找,我们可以知道:当我们访问对象的某属性时,而该对象上没有相应属性时,那么它会通过原型链向上查找,一直找到null还没有话,就会返回undefined。

基于原型的继承

深入浅出JavaScript之原型链和继承

function foo(){ 

   this.y = 2;      

foo.prototype.x = 1; 

var obj3 = new foo();  //①当使用new去调用的时候,函数会作为构造器去调用②this会指向一个对象(这里是obj3),而这个对象的原型会指向构造器的prototype属性(这里是foo.prototype) 

obj3.y; //2  

obj3.x; //1    //可以看到y是对象上的,x是原型链上的原型(也就是foo.prototype上) 

prototype属性与原型

深入浅出JavaScript之原型链和继承
深入浅出JavaScript之原型链和继承

我们再来看看foo.prototype是什么样的结构,当我们用函数声明去创建一个空函数的时候,那么这个函数就有个prototype属性,并且它默认有两个属性,constructor和__proto__,

constructor属性会指向它本身foo,__proto__是在chrome中暴露的(不是一个标准属性,知道就行),那么foo.prototype的原型会指向object.prototype。因此object.prototype上

的一些方法tostring,valueof才会被每个一般的对象所使用。

function foo(){} 

typeof foo.prototype; // "object" 

var obj3 = new foo(); 

总结一下:我们这里有个foo函数,这个函数有个prototype的对象属性,它的作用就是当使用new foo()去构造实例的时候,这个构造器的prototype属性会用作new出来的这些对象的原型。

所以我们要搞清楚,prototype和原型是两回事,prototype是函数对象上的预设属性,原型通常是构造器上的prototype属性。

实现一个class继承另外一个class

function person(name, age) { 

   this.name = name;    //直接调用的话,this指向全局对象(this知识点整理) 

   this.age = age;      //使用new调用peoson的话,this会指向原型为person.prototype的空对象,通过this.name给空对象赋值,最后this作为return值 

person.prototype.hi = function() {   //通过person.prototype.hi创建所有person实例共享的方法,(可以参考上节的左图:对象的原型会指向构造器的prototype属性,所以想让obj1,obj2,obj3共享一些方法的话,只需在原型对象上一次性地添加属性和方法就可以了); 

   console.log('hi, my name is ' + this.name + ',i am ' + this.age + ' years old now.')//这里的this是全局对象 

}; 

person.prototype.legs_num = 2;   //再设置一些对person类的所有实例共享的数据 

person.prototype.arms_num = 2; 

person.prototype.walk = function() { 

  console.log(this.name + ' is walking...'); 

function student(name, age, classname) {  //每个学生都属于人 

  person.call(this, name, age);  //在student这个子类里面先调用一下父类 

  this.classname = classname; 

//下一步就是我们怎么去把student的实例继承person.prototype的一些方法 

student.prototype = object.create(person.prototype);    //object.create():创建一个空对象,并且这个对象的原型指向它的参数  //这样子我们可以在访问student.prototype的时候可以向上查找到person.prototype,又可以在不影响person的情况下,创建自己的方法 

student.prototype.constructor = student;  //保持一致性,不设置的话constructor会指向person 

student.prototype.hi = function() {    //通过student.prototype.hi这样子的赋值可以覆盖我们基类person.prototype.hi 

  console.log('hi, my name is ' + this.name + ',i am ' + this.age + ' years old now, and from ' + this.classname + '.'); 

student.prototype.learn = function(subject) {    //同时,我们又有自己的learn方法 

  console.log(this.name + 'is learning ' + subject + ' at' + this.classname + '.'); 

//test 

var yun = new student('yunyun', 22, 'class 3,grade 2'); 

yun.hi(); //hi,my name is yunyun,i'm 22 years old now,and from class 3, grade 2. 

console.log(yun.arms_num); // 2     //我们本身对象是没有的,对象的原型也就是student.prototype也没有,但是我们用了继承,继续向上查找,找到了person.prototype.arms_num,所以返回2 

yun.walk(); //yunyun is walking... 

yun.learn('math'); //yunyun is learning math at class 3,grade 2. 

深入浅出JavaScript之原型链和继承

结合图我们来倒过来分析一下上面代码:我们先通过new

student创建了一个student的实例yun,yun的原型指向构造器的prototype属性(这里就是student.prototype),

student.prototype上有hi方法和learn方法,student.prototype是通过object.create(person.prototype)构造的,所以这里的student.prototype是空对象,并且这个对象的原型指向person.prototype,接着我们在person.prototype上也设置了legs_num,arms_num属性以及hi,walk方法。然后我们直接定义了一个person函数,person.prototype就是一个预置的对象,它本身也会有它的原型,它的原型就是object.prototype,也正是因为这样,我们随便一个对象才会有hasownproperty,valueof,tostring这样些公共的函数,这些函数都是从object.prototype上来的。这样子就实现了基于原型链的继承。

那我们调用hi,walk,learn方法的时候发生了什么呢?比如我们调用hi方法的时候,我们首先看这个对象yun上有没有hi方法,但是在这个实例中没有所以会向上查找,查找到yun的原型也就是student.protoype上有这hi方法,所以最终调用的是student.prototype.hi,调用其他方法也是类似的。

改变prototype

我们知道javascript中的prototype原型不像java中的class,java中的class一旦写好就很难动态的去改变了,但是javascript中的原型实际上也是普通的对象,那就意味着在程序运行的阶段,我们也可以动态的给prototype添加或删除些属性。

深入浅出JavaScript之原型链和继承

在上述代码的基础上,我们已经有yun这个实例了,我们接着来进行实验:

tudent.prototype.x = 101;        //通过student.prototype.x把yun的原型动态地添加一个属性x 

yun.x;   //101                    //那我们发现所有的实例都会受到影响 

//接着我们做个有趣的实验 

student.prototype = {y:2};        //我们直接修改构造器的prototype属性,把它赋值为一个新的对象 

yun.y;  //undefined                

yun.x;  //101                     //所以我们得出:当我们修改student.prototype值的时候,并不能修改已经实例化的对象 

var tom = new student('tom',3,'class lol kengb');   

tom.x; //undefined                //但当我们创建一个新的实例时,这一次x就不见了, 

tom.y; //2                        //并且y是新的值 

所以说当动态修改prototype的时候,是会影响所有已创建或新创建的实例的,但是修改整个prototype赋值为新的对象的话,对已创建的实例是不会影响的,但是会影响后续的实例。

实现继承的方式

实现继承有多种方式,下面我们还是以person和student来分析

function person() { 

function student() { 

student.prototype = person.prototype; // 我们可不可用这种方式呢?这种方法是错误的:因为子类student有自己的一些方法 

//,如果通过这样子赋值,改变student的同时也改变了person。 

student.prototype = new person(); //这种方式是可以实现的,但是调用构造函数有时候也是有问题的,比如要传进person一个name和age 

//,这里的student是个类,还没实例化,这时候有些奇怪了,传什么都不是。 

student.prototype = object.create(person.prototype); //相对来说这中方式是比较理想的,这里我们创建了一个空的对象 

//,并且对象的原型指向person.prototype,这样我们既保证了继承了person.prototype上的方法,并且student.prototype又有自己空的对象。 

//但是object.create是es5以后才有的

作者:牧云云

来源:51cto