快捷搜索:

金沙第一娱乐娱城官网

当前位置:金沙第一娱乐娱城官网 > 金沙第一娱乐娱城官网 > 大多数面向对象的编程语言都支持两种继承方式

大多数面向对象的编程语言都支持两种继承方式

来源:http://www.dlksamusic.com 作者:金沙第一娱乐娱城官网 时间:2020-01-10 23:49

接着在子类的原型链中添加showChildValue方法。然后创建了一个子类对象,这时的子类对象c既可以调用自己的方法,也可以调用继承自父类的方法。

References

  1. 详解Javascript中的Object对象
  2. new操作符
  3. JavaScript面向对象简介
  4. Object.create()
  5. 继承与原型链         

转自 

从上图我们可以看到,在创建子类的时候,子类的prototype属性是指向子类的原型对象的。当我们通过Child.prototype = new Parent();语句让子类的原型指向父类对象的时候,实际上是重写了子类的原型对象。此时在子类的原型对象中会有一个_proto_属性指向父类的原型对象。而原来的子类原型对象实际上已经没有用了。

借助原型模式定义对象的方法

我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向该函数的原型对象, 该对象包含了由特定类型的所有实例共享的属性和方法。也就是说,我们可以利用原型对象来让所有对象实例共享它所包含的属性和方法。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}

// 通过原型模式来添加所有实例共享的方法
// sayName() 方法将会被Person的所有实例共享,而避免了重复创建
Person.prototype.sayName = function () {
  console.log(this.name);
};

var person1 = new Person('Weiwei', 27, 'Student');
var person2 = new Person('Lily', 25, 'Doctor');

console.log(person1.sayName === person2.sayName); // true

person1.sayName(); // Weiwei
person2.sayName(); // Lily

正如上面的代码所示,通过原型模式定义的方法sayName()为所有的实例所共享。也就是, person1person2访问的是同一个sayName()函数。同样的,公共属性也可以使用原型模式进行定义。例如:

function Chinese (name) {
    this.name = name;
}

Chinese.prototype.country = 'China'; // 公共属性,所有实例共享

原型链虽然是否强大,可以实现继承,但是原型链也存在一些缺点。原型链继承的缺点主要有:

组合使用原型链和借用构造函数

通常,我们会组合使用原型链继承和借用构造函数来实现继承。也就是说,使用原型链实现对原型属性和方法的继承, 而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。 我们改造最初的例子如下:

// 父类构造函数
function Person (name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}

// 父类方法
Person.prototype.sayName = function () {
  console.log(this.name);
};

// --------------

// 子类构造函数
function Student (name, age, job, school) {
  // 继承父类的所有实例属性
  Person.call(this, name, age, job);
  this.school = school; // 添加新的子类属性
}

// 继承父类的原型方法
Student.prototype = new Person();

// 新增的子类方法
Student.prototype.saySchool = function () {
  console.log(this.school);
};

var person1 = new Person('Weiwei', 27, 'Student');
var student1 = new Student('Lily', 25, 'Doctor', "Southeast University");

console.log(person1.sayName === student1.sayName); // true

person1.sayName();  // Weiwei
student1.sayName(); // Lily
student1.saySchool(); // Southeast University

组合集成避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为了JavaScript中最常用的继承模式。 而且,instanceofisPropertyOf()也能够用于识别基于组合继承创建的对象。

使用原型链进行继承时的注意事项

借用构造函数继承

借用构造函数(constructor stealing)的基本思想如下:即在子类构造函数的内部调用超类型构造函数。

function Father (name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

function Child (name) {
  // 继承了Father,同时传递了参数
  Father.call(this, name);
}

var instance1 = new Child("weiwei");
instance1.colors.push('black');
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'black' ]
console.log(instance1.name); // weiwei

var instance2 = new Child("lily");
console.log(instance2.colors); // [ 'red', 'blue', 'green' ]
console.log(instance2.name); // lily

为了确保Father构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。

当我们创建了子类对象c之后,通过对象c调用了父类的showParentValue方法。对象c在自己的空间中没有找到这个方法,就会通过_proto_属性去子类的原型中查找,同样也没有找到这个方法,接着它有通过原型中的_proto_属性到父类的原型中去查找,这时,showParentValue方法被找到,并被正确的执行。

别忘了Object

所有的函数都默认原型都是Object的实例,因此默认原型都会包含一个内部指针[[Prototype]],指向Object.prototype。 这也正是所有自定义类型都会继承toString()valueOf()等默认方法的根本原因。所以, 我们说上面例子展示的原型链中还应该包括另外一个继承层次。关于Object的更多内容,可以参考这篇博客。

也就是说,Child继承了Father,而Father继承了Object。当调用了instance.toString()时, 实际上调用的是保存在Object.prototype中的那个方法。

方法的覆盖及原型链继承的缺点

继承

大多的面向对象语言都支持两种继承方式:接口继承和实现继承。ECMAScript只支持实现继承,而且其实现继承主要依靠原型链来实现。

/**覆盖父类中的showParentValue方法**/Child.prototype.showParentValue=function(){alert("OverrideParentmethod");}

类的静态方法:static

静态方法就是可以直接使用类名调用的方法,而无需对类进行实例化,当然实例化后的类也无法调用静态方法。 静态方法常被用于创建应用的工具函数。参考链接

原型链和原型在处理引用类型的值的时候存在同样的问题。我们在介绍原型的时候曾经举过一个使用引用类型的例子。在使用原型链时同样会有这个问题。来看下面的例子:

类构造器:constructor

constructor()方法是有一种特殊的和class一起用于创建和初始化对象的方法。注意,在ES6类中只能有一个名称为constructor金沙娱场app下载,的方法, 否则会报错。在constructor()方法中可以调用super关键字调用父类构造器。如果你没有指定一个构造器方法, 类会自动使用一个默认的构造器。参考链接

//创建父类functionParent(){this.parentValue="Parent";}//在父类的原型中添加方法Parent.prototype.showParentValue=function(){alert;}//创建子类functionChild(){this.childValue="Child";}//实现继承,让子类Child的原型链指向Parent对象Child.prototype=newParent();//在子类的原型中添加方法Child.prototype.showChildValue=function(){alert;}//创建子类对象varc=newChild();//子类对象调用继承自父类的方法c.showParentValue();//子类对象调用自己的方法c.showChildValue();

继承父类:extends

extends关键字可以用于继承父类。使用extends可以扩展一个内置的对象(如Date),也可以是自定义对象,或者是null

接着我们创建了一个子类Child,并通过让子类的原型链指向父类来实现继承。

面向对象的几个概念

在进入正题前,先了解传统的面向对象编程(例如Java)中常会涉及到的概念,大致可以包括:

  • 类:定义对象的特征。它是对象的属性和方法的模板定义。
  • 对象(或称实例):类的一个实例。
  • 属性:对象的特征,比如颜色、尺寸等。
  • 方法:对象的行为,比如行走、说话等。
  • 构造函数:对象初始化的瞬间被调用的方法。
  • 继承:子类可以继承父类的特征。例如,猫继承了动物的一般特性。
  • 封装:一种把数据和相关的方法绑定在一起使用的方法。
  • 抽象:结合复杂的继承、方法、属性的对象能够模拟现实的模型。
  • 多态:不同的类可以定义相同的方法或属性。

在JavaScript的面向对象编程中大体也包括这些。不过在称呼上可能稍有不同,例如,JavaScript中没有原生的“类”的概念, 而只有对象的概念。因此,随着你认识的深入,我们会混用对象、实例、构造函数等概念。

这样做的后果是实现继承之后,子类指向的是新的子类原型,而前面添加的方法是放置在原来的原型中的,所以在实现继承之后,子类对象将不再拥有这个方法,因为原来的原型现在已经没有作用了。

构造函数的问题

我们不建议在构造函数中直接定义方法,如果这样做的话,每个方法都要在每个实例上重新创建一遍,这将非常损耗性能。 ——不要忘了,ECMAScript中的函数是对象,每定义一个函数,也就实例化了一个对象。

幸运的是,在ECMAScript中,我们可以借助原型对象来解决这个问题。

functionParent(){this.parentValue="Parent";}Parent.prototype.showParentValue=function(){alert;}functionChild(){this.childValue="Child";}//在实现继承之前为子类在原型中添加方法Child.prototype.showChildValue=function(){alert;}//实现继承,让Child的原型链指向Parent对象Child.prototype=newParent();

更简单的原型语法

在上面的代码中,如果我们要添加原型属性和方法,就要重复的敲一遍Person.prototype。为了减少这个重复的过程, 更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。 参考资料。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}

Person.prototype = {

  // 这里务必要重新将构造函数指回Person构造函数,否则会指向这个新创建的对象
  constructor: Person, // Attention!

  sayName: function () {
    console.log(this.name);
  }
};

var person1 = new Person('Weiwei', 27, 'Student');
var person2 = new Person('Lily', 25, 'Doctor');

console.log(person1.sayName === person2.sayName); // true

person1.sayName();  // Weiwei
person2.sayName();  // Lily

在上面的代码中特意包含了一个constructor属性,并将它的值设置为Person,从而确保了通过该属性能够访问到适当的值。 注意,以这种方式重设constructor属性会导致它的[[Enumerable]]特性设置为true。默认情况下,原生的constructor属性是不可枚举的。 你可以使用Object.defineProperty()

// 重设构造函数,只适用于ES5兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person
});

如果我们需要实现子类的方法来覆盖父类的方法,只需要在子类的原型中添加与父类同名的方法即可。

Object.keys()

要取得对象上所有可枚举的实例属性,可以使用ES5中的Object.keys()方法。例如:

Object.keys(p1); // ["name", "age", "job"]

此外,如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyName()方法。

所有函数的默认原型都是Object,所以默认原型都会包含一个内部指针指向Object.prototype。在Object.prototype中会有内置的hasOwnPropertyisPrototypeOfpropertyEmunerabletoLocaleStringtoStringvalueOf方法,所以我们自定义的类型都会继承这些方法。

关键字:super

super关键字用于调用父对象上的函数。 super.propsuper[expr]表达式在类和对象字面量中的任何方法定义中都有效。

super([arguments]); // 调用父类构造器
super.functionOnParent([arguments]); // 调用父类中的方法

如果是在类的构造器中,需要在this关键字之前使用。参考链接

1、不能在设定了原型链之后再重新为原型链赋值。

对象(类)的创建

在JavaScript中,我们通常可以使用构造函数来创建特定类型的对象。诸如Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。 此外,我们也可以创建自定义的构造函数。例如:

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}

var person1 = new Person('Weiwei', 27, 'Student');
var person2 = new Person('Lily', 25, 'Doctor');

按照惯例,构造函数始终都应该以一个大写字母开头(和Java中定义的类一样),普通函数则小写字母开头。 要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

  1. 创建一个新对象(实例)
  2. 将构造函数的作用域赋给新对象(也就是重设了this的指向,this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

有关new操作符的更多内容请参考这篇文档。

在上面的例子中,我们创建了Person的两个实例person1person2。 这两个对象默认都有一个constructor属性,该属性指向它们的构造函数Person,也就是说:

console.log(person1.constructor == Person);  //true
console.log(person2.constructor == Person);  //true

返回javascript教程主目录>>

原型对象

现在我们来深入的理解一下什么是原型对象。

只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。 在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。 也就是说:Person.prototype.constructor指向Person构造函数。

创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。 当调用构造函数创建一个新实例后,该实例内部将包含一个指针(内部属性),指向构造函数的原型对象。ES5中称这个指针为[[Prototype]], 在Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__(目前已被废弃);而在其他实现中,这个属性对脚本则是完全不可见的。 要注意,这个链接存在于实例与构造函数的原型对象之间,而不是实例与构造函数之间。

这三者关系的示意图如下:

金沙娱场app下载 1

上图展示了Person构造函数、Person的原型对象以及Person现有的两个实例之间的关系。

  • Person.prototype指向了原型对象
  • Person.prototype.constructor又指回了Person构造函数
  • Person的每个实例person1person2都包含一个内部属性(通常为__proto__),person1.__proto__person2.__proto__指向了原型对象

基于原型链实现继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。在前面我们已经介绍了原型,构造函数和对象实例之间的关系,并详细的分析了它们的内存模型结构。我们通过下面的例子来分析JavaScript基于原型链实现继承的方法。

自定义对象的类型检测

我们可以使用instanceof操作符进行类型检测。我们创建的所有对象既是Object的实例,同时也是Person的实例。 因为所有的对象都继承自Object

console.log(person1 instanceof Object);  //true
console.log(person1 instanceof Person);  //true
console.log(person2 instanceof Object);  //true
console.log(person2 instanceof Person);  //true

面向对象的特征之一就是继承。大多数面向对象的编程语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于在JavaScript中函数没有签名,所以无法实现接口继承。在JavaScript中主要是通过原型链来实现继承。

面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式,主要包括模块化、多态、和封装几种技术。 对JavaScript而言,其核心是支持面向对象的,同时它也提供了强大灵活的基于原型的面向对象编程能力。 本文将会深入的探讨有关使用JavaScript进行面向对象编程的一些核心基础知识,包括对象的创建,继承机制, 最后还会简要的介绍如何借助ES6提供的新的类机制重写传统的JavaScript面向对象代码。

2、一定要在原型链赋值之后才能添加或者覆盖方法。

ES6中的面向对象语法

ES6中引入了一套新的关键字用来实现class。 JavaScript仍然是基于原型的,这些新的关键字包括class、 constructor、 static、 extends、 和super。

对前面的代码修改如下:

'use strict';

class Person {

  constructor (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
  }

  sayName () {
    console.log(this.name);
  }

}

class Student extends Person {

  constructor (name, age, school) {
    super(name, age, 'Student');
    this.school = school;
  }

  saySchool () {
    console.log(this.school);
  }

}

var stu1 = new Student('weiwei', 20, 'Southeast University');
var stu2 = new Student('lily', 22, 'Nanjing University');

stu1.sayName(); // weiwei
stu1.saySchool(); // Southeast University

stu2.sayName(); // lily
stu2.saySchool(); // Nanjing University

本文由金沙第一娱乐娱城官网发布于金沙第一娱乐娱城官网,转载请注明出处:大多数面向对象的编程语言都支持两种继承方式

关键词: