(第二天)原型、继承
原型
引入
我们依然定义一个Person类
1 function person(age,name){ 2 this.age = age; 3 this.name = name; 4 this.information = function(){ 5 return "年龄"+ this.age+","+"名字"+ this.name; 6 } 7 }
下面我们实例化对象并调用方法输出相关信息
1 var p1 = new Person(12,'小黑'); 2 console.log(p1.information);
如上能正常输出,现在有这样一个需求,若有多个对象都共有一个方法该如何进行操作呢?那简单多了,直接在person类里面建一个方法对象用时直接调用这个不就得了!请看
1 var p1 = new person(1, "1"); 2 var p2 = new person(2, "2"); 3 console.log(p1.information == p2.information); 4 console.log(p1.information === p2.information); 5 6 /* 7 即使不用严格意义上的运算符也打印出false,因为方法地址赋给不同的对象 8 */
所以这就相当于每个person的实例都是有各自的方法而未共享方法,这样就造成不必要的内存消耗!对此我们引入 prototype(原型对象) 来实现共享对象实例的方法或者属性。
概念:
每一个JavaScript对象(null除外)都和另一个原型对象关联,并且每一个对象都从中继承属性。通过对象直接量创建的对象可以通过Object.prototype获得原型对象的引用。通过关键字new和构造函数调用创建的对象的原型就是构造函数的prototype属性的值。同样,JavaScript内置对象如Array,Date通过new Array(),new Date()创建的对象的原型就是Array.prototype以及Date.prototype。【注】Object.prototype无原型对象并且不继承任何属性。
所以综上所述我们可以给person类通过prototype定义一个共享方法common即
1 person.prototype.common = function() { 2 return "所有人晚上一起去ktv"; 3 }
实例化对象p1和p2并进行打印 console.log(p1.common === p2.common); 返回true,说明两个对象共享了common方法。
下面通过两种方法来更加深入的学习使用原型上的属性和方法
(1)手动编写构造函数定义对象的“类”调用原型
1 /* 2 通过原型继承创建新对象 3 */ 4 5 function inherit(p){ 6 function f(){}; 7 f.prototype = p; 8 return new f(); 9 }
下面编写一个求表示值范围的类
1 /* 2 通过原型继承创建新对象 3 */ 4 function inherit(p) { 5 function f() {}; 6 f.prototype = p; 7 return new f(); 8 } 9 10 function range(from, to) { 11 var r = inherit(range.methods); 12 r.from = from; 13 r.to = to; 14 return r; 15 } 16 range.methods = { 17 include: function(x) { 18 return this.from <= x && x <= this.to; 19 }, 20 foreach: function() { 21 var arr = []; 22 for (var x = Math.ceil(this.from); x <= this.to; x++) { 23 arr.push(x); 24 } 25 return arr; 26 }, 27 toString: function() { 28 return "(" + this.from + "..." + this.to + ")"; 29 } 30 }; 31 var r = range(1, 3); //创建一个范围对象 32 console.log(r.include(2)) // =>true:2 在此范围内 33 console.log(r.foreach()); //输出1,2,3 34 console.log(r); //输出(1...3)
我们创建新对象通过 inherit 函数继承函数range中定义的方法 methods ,这样通过函数range得到的对象都能共享此方法,而且在此基础上还定义了两个不可继承的属性from和to,所以这两个属性是不可共享的!但是在methods方法中定义那些可共享的、可继承的方法都用到了from和to属性,而且使用了this关键字,用this关键字来指定调用这个方法的对象,所以任何类的方法都可以通过this的方法来读取对象的基本属性。因此此种方法通过创建对象和使用prototype的方式不建议使用。
(2)通过构造函数上的prototype属性来获取原型上的属性和方法
调用构造函数的重要特征:构造函数的prototype属性被用做新对象的原型。也就意味着通过同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一个类的成员。
下面我们通过改造上面的代码使用构造函数来获得原型对象的属性和方法
1 function Range(from, to) { 2 this.from = from; 3 this.to = to; 4 } 5 Range.prototype = { 6 include: function(x) { 7 return this.from <= x && x <= this.to; 8 }, 9 foreach: function(f) { 10 for (var x = Math.ceil(this.from); x <= this.to; x++) 11 f(x); 12 }, 13 toString: function() { 14 return "(" + this.from + "..." + this.to + ")"; 15 } 16 }; 17 var r = new Range(1, 9); 18 r.include(2); 19 r.foreach(console.log()); 20 console.log(r);
【注】遵循的编程约定: 定义构造函数既是定义类,并且类名首字母要大写。而普通的 函数和方法都是首字母小写。
在此代码中只需用new关键字来创建对象而无需用上述中的inherit来手动创建从而添加原型对象的引用!在调用构造函数之前就创建了新对象,通过this关键字可以获取到这个对象。Range只不过是初始化this而已,构造函数不必返回这个新创建的对象,构造函数会自动创建,然后将构造函数作为这个对象的方法来调用一次,最后返回这个新对象。上述这段话听起来似乎很拗口或者说不太容易理解,个人理解加上用C#面向对象中创建类的思想这样认为(若有不对,恳请园友们批评和指正,我也可以更好的学习和理解):
(1)开辟堆空间
(2)创建对象
(3)调用构造函数,并将创建的对象用做其调用上下文,所以可以使用this关键字来引用创建的对象
(4)最后为该对象指定属性或者方法,然后返回该新创建的对象
补充
constructor属性
每个JavaScript函数都自动拥有一个prototype属性。这个属性的值是一个对象,这个对象包含唯一一个不可枚举属性constructor。它的值是一个函数对象
1 var F = function(){}; 2 var p = F.prototype; 3 var c = p.constructor; 4 console.log(c === F) 5 6 /* 7 打印出true:对于任意的函数F.prototype.constructor == F 8 */
可以看到构造函数的原型存在预先定义好的constructor属性,这意味着对象通常继承的constructor均指代它们的构造函数,由于构造函数是类的“公共标识”,因此这个constructor属性为对象提供了类。
通过理解上面的原型可以更好的理解继承
继承
假设要查询对象o的属性x,如果o中不存在x,那么将会在o的原型对象中查询属性x。如果原型对象中也没有x,但这个对象也有原型,那么继续在这个原型对象的原型上执行查询,直到找到x或者查找到一个原型是null的对象为止。可以看到,对象的原型属性构成了一个“链”即原型链,来实现属性的继承。
1 var o = {}; 2 o.x = 1; 3 4 /* 5 o从Object.prototype 继承对象的方法,并且给o定义一个属性x 6 */
只有通过查询操作才能充分体会到继承的存在,下面巨举例说明
1 function inherit(p){ 2 function f(){}; 3 f.prototype = p; 4 return new f(); 5 } 6 7 var obj = { r : 1 }; 8 var c = inherit(obj); 9 c.x = 1; c.y = 1; 10 c.r = 2; 11 cosole.log(obj.r); 12 13 /* 14 c定义两个属性x,y并且继承了属性r,c.r = 2试图覆盖继承来的属性,结果打印出为1,原型对象没有被修改 15 */
现假设给对象o的属性x赋值,如果o中已经有属性x(这个属性不是继承来的),那么这个赋值操作值只改变这个已有属性的值。如果o中不存在属性x,那么赋值操作给o添加一个新属性x。如果之前o继承自属性x,那么这个继承的属性就被新创建的同名属性覆盖了。【注】属性赋值操作首先检查原型链,以此判定是否允许赋值操作,如果o继承一个只读属性x,那么赋值操作是不允许的,例如对上述对象obj属性r设为只读 Object.defineProperty(obj, "x", { writeable: false }); !
下面继续用例子更深入的举例学习继承
1 function parent(age,name){ 2 this.age = age; 3 this.name = name; 4 } 5 6 parent.prototype.information = function(){ 7 8 console.log("今晚一起嗨"); 9 } 10 11 children.prototype = parent.prototype; //继承父类原型对象 12 13 function children(age,name){ 14 parent.call(this, age, name); //call属性指代当前正在执行的函数,继承父类的属性 15 }
现在实例化类children并获取属性值,调用方法。
1 var child = new Children(12,"小黑"); 2 console.log(child.age); 3 console.log(child.name); 4 console.log(child.information());
如上,似乎一切水到渠成,接下来我们为子类原型定义一个方法
children.prototype.childPrivateMethod = function(){ console.log("子类私有方法"); }
初始化父类,并尝试调用方法试试看
1 var parent = new Person(12,'父亲'); 2 console.log(parent.childPrivateMethod());
什么情况???父类竟然能调用子类的私有方法,这也完全不符合C#的继承特性啊,还越狱了罪行十恶不赦,其实不然是我的代码让它有机可乘!
children.prototype = parent.prototype;此时父类与子类的原型完全相等那父类对象实例能访问到其方法也是有理有据的了!关键是怎样让父类无法调用子类的方法呢?幸好在之前看过手动用类创建对象,前面inherit可知,实例化父类的对象试试,修改如下
children.prototype = new parent();
事实证明是正确的,只是把父类的实例给了子类,子类自然而然能得到父类的一切,而父类永远只能在自己一方土上为王!