JS继承与原型链

来源:互联网 发布:青岛易亚网络骗局 编辑:程序博客网 时间:2024/06/05 04:24

原型和原型链

在JS中,原型和原型链是实现继承的继承。因此,先说说原型和原型链。

在JS中,每个函数都有一个原型属性 prototype 指向自身的原型对象。而该函数创建的对象都有一个属性 _proto_ 。该属性指向该原型。

同时该原型对象也有个 _proto_ 属性,指向自己的原型,即原型对象的原型。这样一层层的指向,直到 Object 对象的原型,就形成了一条原型链。obj._proto_._proto_

就像Java中所有的类都是Object 类的子类。下面这张图说明了这个关系,其中我把 Object 对象的原型 _proto_ 属性指向 null。这样容易理解。


打开控制台,可以发现 Object 对象的原型其实只有一些最基本的函数。


原型链继承

在ES6之前实现继承,都是使用原型链继承。

要点:将父类的实例或者原型作为子类的原型。 不过具体实现上,还是有点区别的。

例子一

先看代码

// ------Parent start----function Parent() {this.desc = 'Parent';}Parent.prototype.yell = function() {return 'I am Parent, and I am speaking.';};// ------Parent end----// ------Child start----function Child() {this.desc = 'Child';// Parent.call(this);// 调用父对象的构造函数,初始化。// 或者 调用apply(this, arguments);}// 设置对象的原型Child.prototype = new Parent();// 使用类名为构造函数命名Child.prototype.constructor = Child;// 实现具体函数Child.prototype.yell = function() {return 'I am Child, haha!';};// ------Child end----var par = new Parent();var ch = new Child();console.log(par.desc + ' : ' + par.yell())console.log(ch.desc + ' : ' + ch.yell())
上面代码很简单,创建一个父对象并赋值给 子类的原型。该方式继承结构如下:



从图上看有一定的优点:1:父类新增原型方法/原型属性,子类都能访问到。2:简单,易于实现。
当然也有缺点:

1:子类的实现必须要在Child.prototype = new Parent()这样的语句之后执行,不能放到构造器中。
2:另外,因为是把对象赋值给原型来实现继承,无法实现多继承
3:还有初始化,desc 覆写初始化造成重复的代码。

例子二

为了去除重复的初始化代码,现在对上面的继承进行改进

// 省略其他代码var Child = function(desc){// 调用父类构造函数完成初始化    Parent.apply(this, arguments);} ;Child.prototype = new Parent();
这个例子中,巧妙地在子类构造函数中调用了父类的构造函数完成初始化。

不过也有一个问题,就是父类的构造函数运行了两次。一次是初始化,一次是给子类原型赋值。

例子三

// 省略其他代码var Child = function(desc){// 调用父类构造函数完成初始化    Parent.apply(this, arguments);} ;// 把父类的原型直接赋值给子类的原型Child.prototype = Parent.prototype;
改例子把父类的原型直接赋值给子类的原型。 该继承结构如下:


显然,这样做会造成一个问题:父子类共用原型,如果对子类的原型做了修改,那么这个修改同时也会影响到父类的原型,进而影响父类对象。比如:

Child.prototype.goToSchool = function() {return 'I go to school';}console.log( (new Parent()).goToSchool() )
根据输出发现,父类是可以获取子类的实现的。

例子四

上面代码中,子类的实现之所以能影响到父类,是应为原型直接赋值造成的,因此可以选择使用一个临时的函数过渡一下。

// function Parent() {this.desc = 'Parent';}Parent.prototype.yell = function() {return 'I am Parent, and I am speaking.';};Parent.prototype.info = {age : 25,height : 180}Parent.prototype.job = 'Some job';var Child = function(desc){    Parent.apply(this,arguments) ;} ;var Temp = function(){} ;Temp.prototype = Parent.prototype ;Child.prototype = new Temp() ;var ch = new Child();// 更改实例值属性,父类不受影响。只影响当前实例。ch.job = 'Study';// 更改引用属性,父类也被改ch.info.age = 3;console.log(ch);var par = new Parent();console.log(par);
该例子中,先把父类原型赋值给 临时函数,然后利用临时函数创建一个对象,并作为子类的原型。这种做法其实就是上面两种方式的综合。继承结构如下:


运行上面的代码,发现其实还有个问题:更改了 引用属性,影响到了父类,父类对象的 info.age 也变成了 3。


这与我们的期待不一致,同时修改了 job 和 info.age 属性,父类的 job 属性没有受影响,info.age 属性受到了影响。

解释:

从图上看,很明显地 ch._proto_._proto_ 指向了 Parent  的原型。
当ch 对象修改 job 属性的时候并不会对原型中的 job 属性产生影响,只是给自身添加了一个 job 属性,覆盖了父类原型中的 job 属性。
当ch 对象修改 info.age 属性的时候,它先在 原型 ch._proto_ 上查找,发现没有,继续向上查找 ch._proto_._proto_,发现了有该属性存在,并修改了它。
因而影响到了父类的对象。可知,更改实例值属性,父类不受影响。只影响当前实例。更改引用属性,父类也会被改。

好的做法是把对象属性 copy 一份赋值给子类。

ES6继承

extends

值得高兴的是,ES6提供了一种新的类和构造函数实现方法,即使用 extends, class, constructor, supper实现继承。

先看例子:

// 使用Node时,要求严格模式,否则报错。// SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode'use strict'class Person {constructor(name, age){this.name = name;this.age = age;}get description() {return this.name + ' : ' + this.age + ' year old';}speak() {console.info("I am Person")}}class Worker extends Person {  constructor(name, age, job) {  super(name, age);  // 必须在子类中调用 supper(), 否则报错(ReferenceError: this is not defined)。ES2015规定。    this.job = job;  }  get description() {  return this.name + ' : ' + this.age + ' year old, job : ' + this.job;  }  get work() {  return "I am working!";  }}var wr = new Worker('Kaka', 24, 'programmer');console.log(wr);console.log(wr.description);console.log(wr.speak());  // 继承到了父类的方法console.log(wr instanceof Person);
这里有两点需要注意:

1. 必须要在严格模式下使用。

2. 必须在子类的 constructor 调用 super。 否则报错。据说是 ES6的规定。

// In a child class constructor,  this cannot be used until super is called.
// ES6 class constructors MUST call  super if they are subclasses, or they must explicitly return some object to take the place of the one that was not initialized.
不过使用extends 继承有个要求,即 父子类均要使用 class 定义。当我们在现有代码上进行改写时,可能不是很方便。

setPrototypeOf

ES6 提供了 setPrototypeOf() 方法可以 帮助我们解决这点:

下面是官方的代码:

'use strict'var Animal = {  speak() {    console.log(this.name + ' makes a noise.');  }};class Dog {  constructor(name) {    this.name = name;  }}Object.setPrototypeOf(Dog.prototype, Animal);// If you do not do this you will get a TypeError when you invoke speakvar d = new Dog('Mitzie');d.speak(); // Mitzie makes a noise.


原创粉丝点击