欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Javascript中类式继承和原型式继承的实现方法和区别之处

程序员文章站 2023-10-26 17:38:10
在所有面向对象的编程中,继承是一个重要的话题。一般说来,在设计类的时候,我们希望能减少重复性的代码,并且尽量弱化对象间的耦合(让一个类继承另一个类可能会导致二者产生强耦合)...

在所有面向对象的编程中,继承是一个重要的话题。一般说来,在设计类的时候,我们希望能减少重复性的代码,并且尽量弱化对象间的耦合(让一个类继承另一个类可能会导致二者产生强耦合)。关于“解耦”是程序设计中另一个重要的话题,本篇重点来看看在javascript如何实现继承。

其它的面向对象程序设计语言都是通过关键字来解决继承的问题(比如extend或inherit等方式)。但是javascript中并没有定义这种实现的机制,如果一个类需要继承另一个类,这个继承过程需要程序员自己通过编码来实现。

一、类式继承的实现

1、创建一个类的方式:

//定义类的构造函数
function person(name) {
  this.name = name || '默认姓名';
}
//定义该类所有实例的公共方法
person.prototype.getname = function() {
  return this.name;
}
var smith = new person('smith');
var jacky = new person('jacky');
console.log( smith.getname(), jacky.getname() ); //smith jacky

2、继承这个类:

这需要分两个步骤来实现,第1步是继承父类构造函数中定义的属性,第2步是继承父类的prototype属性

//定义类的构造函数
function person(name) {
  this.name = name || '默认姓名';
}
//定义该类所有实例的公共方法
person.prototype.getname = function() {
  return this.name;
}
function author(name, books) {
  //继承父类构造函数中定义的属性
  //通过改变父类构造函数的执行上下文来继承
  person.call(this, name);
  this.books = books;
}
//继承父类对应的方法
author.prototype = new person(); //author.prototype.constructor === person
author.prototype.constructor = author; //修正修改原型链时造成的constructor丢失
author.prototype.getbooks = function() {
  return this.books;
};
//测试
var smith = new person('smith');
var jacky = new author('jacky', ['booka', 'bookb']);
console.log(smith.getname()); //smith
console.log(jacky.getname()); //jacky
console.log(jacky.getbooks().join(', ')); //booka, bookb
console.log(smith.getbooks().join(', ')); //uncaught typeerror: smith.getbooks is not a function

从测试的结果中可以看出,author正确继承了person,而且修改author的原型时,并不会对person产生影响。这其中的关键一句就是 author.prototype = new person(),要与author.prototype = person.prototype区分开来。前者产生了一个实例,这个实例有person.prototype的副本(这里先这么理解,后面有更详细的解析)。后者是指将两者的prototype指向同一个原型对象。

那么,这也意味着每次继承都将产生一个父类的副本,肯定对内存产生消耗,但为了类式继承这个内存开销必须得支付,但还可以做得更节省一点:author.prototype = new person()这一句其实多执行了构造函数一次(而这一次其实只需在子类构造函数中执行即可),尤其是在父类的构造函数很庞大时很耗时和内存。修改一下继承的方式,如下:

author.prototype = (function() {
  function f() {}
  f.prototype = person.prototype;
  return new f();
})();

如上所示的代码,new时,去掉了对父类的构造函数的调用,节省了一次调用的开销。

3、类式继承显著的特点是每一次实例化对象时,子类都将执行一次父类的构造函数。如果e继承了d,d继承了c,c继承了b,b继承了a,在实例化一个e时,一共要经过几次构造函数的调用呢?

/*继承方法的函数*/
function extend(son, father) {
  function f() {}
  f.prototype = father.prototype;
  son.prototype = new f();
  son.prototype.constructor = son;
}
//a类
function a() {
  console.log('a()');
}
a.prototype.hello = function() {
  console.log('hello, world.');
}
//b类
function b() {
  a.call(this);
  console.log('b()');
}
extend(b, a);
//c类
function c() {
  b.call(this);
  console.log('c()');
}
extend(c, b);
//d类
function d() {
  c.call(this);
  console.log('d()');
}
extend(d, c);
//e类
function e() {
  d.call(this);
  console.log('e()');
}
extend(e, d);
//创建一个e的实例
var e = new e(); //a() b() c() d() e()
e.hello(); //hello, world.

5次,这还只是实例化一个e时调用的次数。所以,我们应该尽可能的减少继承的级别。但这并不是说不要使用这种类式继承,而是应该根据自己的应用场合决定采用什么方法。

二、原型式继承

1、先来看一段代码:我们先将之前类式继承中的继承prototype那一段改成另一个函数clone,然后通过字面量创建一个person,最后让author变成person的克隆体。

//这个函数可以理解为克隆一个对象
function clone(object) {
  function f() {}
  f.prototype = object;
  return new f();
}
var person = {
  name: 'default name';
  getname: function() {
    return this.name;
  }
}
//接下来让author变为person的克隆体
var author = clone(person);

问一个问题:clone函数里的new f()为这个实例开辟内存空间来存储object的副本了吗?

按我之前的理解,回答是肯定的。但是,当我继续将代码写下去的时候,奇怪的事情发生了,代码如下:

//接下来让author变为person的克隆体
var author = clone(person);
author.books = [];
author.getbooks = function() {
  return this.books.join(', ');
}
//增加一个作者smith
var smith = clone(author);
console.log(smith.getname(), smith.getbooks()); //default name
smith.name = 'smith';
smith.books.push('<<book a>>', '<<book b>>'); //作者写了两本书
console.log(smith.getname(), smith.getbooks()); //smith <<book a>>, <<book b>>
//再增加一个作者jacky
var jacky = clone(author);
console.log(jacky.getname(), jacky.getbooks()); // default name <<book a>>, <<book b>>

当我们继续增加作者jacky时,奇怪的现象发生了!!jacky的名字依然是default name,但是他居然也写两本与smith一样的书?jacky的书都还没push呢。到了这里,我想到了引用对象的情况(引用一个对象时,引用的是该对象的内存地址),发生这样的现象,问题肯定出在clone()函数中的new f()这里。

事实上,这个clone中的new f()确实返回了一个新的对象,该对象拥有被克隆对象的所有属性。但这些属性保留的是对被克隆对象中相应属性的引用,而非一个完全独立的属性副本。换句话说,新对象的属性 与 被克隆的对象的属性指向同一个内存地址(学过c语言的同学应该明白指针类型,这里意义差不多)。

那么为什么上面的代码中,jacky的书与smith的书相同了,为什么jacky的名字却不是smith而是default name呢?这就是javascript中继承的机制所在,当smith刚刚继承自author时,他的属性保留了对author的属性的引用,一旦我们显示的对smith的属性重新赋值时,javascritp引擎就会给smith的该属性重新划分内存空间来存储相应的值,由于重新划分了内址地址,那么对smith.name的改写就不会影响到author.name去了。这就很好的解释了前面的那个问题——为什么jacky的名字却不是smith而是default name。

2、基于原型继承

通过前面的情况分析,可以看出基于原型继承的方式更能节约内存(只有在需要时候才开辟新的内存空间)。但要注意:基于原型继承时,对象的属性一定要重新赋值后(重新划分内存)再去引用该属性。对于对象的方法,如果有不同的处理方式,我们只需重新定义即可。

下面将前一段代码做一个完整、正确的范例出来,以说明原型继承的特点和使用方式:

//这个函数可以理解为克隆一个对象
function clone(object) {
  function f() {}
  f.prototype = object;
  return new f();
}
var person = {
  name: 'default name',
  getname: function() {
    return this.name;
  }
}
//接下来让author变为person的克隆体
var author = clone(person);
author.books = [];
author.getbooks = function() {
  return this.books.join(', ');
}
//增加一个作者smith
var smith = clone(author);
smith.name = 'smith';
smith.books = [];
smith.books.push('<<book a>>', '<<book b>>'); //作者写了两本书
console.log(smith.getname(), smith.getbooks()); //smith <<book a>>, <<book b>>
//再增加一个作者jacky
var jacky = clone(author);
jacky.name = 'jacky';
jacky.books = [];
jacky.books.push('<<book c>>', '<<book d>>');
console.log(jacky.getname(), jacky.getbooks()); // jacky <<book c>>, <<book d>>

三、类式继承与原型式继承的区别与相式之处

1、类式继承中:使用构造函数初始化对象的属性,通过调用父类的构造函数来继承这些属性。通过new 父类的prototype来继承方法。

2、原型式继承中:去掉了构造函数,但需要将对象的属性和方法写一个{}里申明。准确的说,原型式继承就是类式继承中继承父类的prototype方法。

以上所述是小编给大家介绍的javascript中类式继承和原型式继承的实现方法和区别,希望对大家有所帮助