Javascript语言的继承机制,它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instance)的区分,全靠一种很奇特的"原型链"(prototype chain)模式,来实现继承。html
这部分知识也是JavaScript里的核心重点之一,同时也是一个难点。我把学习笔记整理了一下,方便你们学习,同时本身也加深印象。这部分代码的细节不少,须要反复推敲。那咱们就开始吧。chrome
原型链例子(要点写在注释里,能够把代码复制到浏览器里测试,下同)浏览器
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上的方法)
刚才咱们访问x,y和z,分别经过原型链去查找,咱们能够知道:当咱们访问对象的某属性时,而该对象上没有相应属性时,那么它会经过原型链向上查找,一直找到null尚未话,就会返回undefined。函数
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上)
咱们再来看看Foo.prototype是什么样的结构,当咱们用函数声明去建立一个空函数的时候,那么这个函数就有个prototype属性,而且它默认有两个属性,constructor和__proto__,学习
constructor属性会指向它自己Foo,__proto__是在chrome中暴露的(不是一个标准属性,知道就行),那么Foo.prototype的原型会指向Object.prototype。所以Object.prototype上测试
的一些方法toString,valueOf才会被每一个通常的对象所使用。this
function Foo(){} typeof Foo.prototype; // "object"
Foo.prototype.x = 1; var obj3 = new Foo();
总结一下:咱们这里有个Foo函数,这个函数有个prototype的对象属性,它的做用就是当使用new Foo()去构造实例的时候,这个构造器的prototype属性会用做new出来的这些对象的原型。spa
因此咱们要搞清楚,prototype和原型是两回事,prototype是函数对象上的预设属性,原型一般是构造器上的prototype属性。prototype
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.
结合图咱们来倒过来分析一下上面代码:咱们先经过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,调用其余方法也是相似的。code
咱们知道JavaScript中的prototype原型不像Java中的class,Java中的class一旦写好就很难动态的去改变了,可是JavaScript中的原型实际上也是普通的对象,那就意味着在程序运行的阶段,咱们也能够动态的给prototype添加或删除些属性。
在上述代码的基础上,咱们已经有yun这个实例了,咱们接着来进行实验:
Student.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之后才有的