0

0

总结Javascript的继承和多态

零下一度

零下一度

发布时间:2017-05-17 09:17:47

|

1297人浏览过

|

来源于php中文网

原创

本文先对es6发布之前javascript各种继承实现方式进行深入的分析比较,然后再介绍es6中对类继承的支持以及优缺点讨论。最后介绍了javascript面向对象编程中很少被涉及的“多态”,并提供了“运算符重载”的思路。本文假设你已经知道或了解了js中原型、原型链的概念。

es6之前,javascript本质上不能算是一门面向对象的编程语言,因为它对于封装、继承、多态这些面向对象语言的特点并没有在语言层面上提供原生的支持。但是,它引入了原型(prototype)的概念,可以让我们以另一种方式模仿类,并通过原型链的方式实现了父类子类之间共享属性的继承以及身份确认机制。其实,面向对象的概念本质上来讲不是指某种语言特性,而是一种设计思想。如果你深谙面向对象的编程思想,即使用c这种面向过程的语言也能写出面向对象的代码(典型的代表就是windows nt 内核实现),而javascript亦是如此!正是由于javascript本身对面向对象编程没有一个语言上的支持标准,所以才有了五花八门、令人眼花缭乱的“类继承”的代码。所幸,es6增加了classextendsstatic等关键字用以在语言层面支持面向对象,但是,还是有些保守!我们先列举出es6之前常见的几种继承方案,然后再来一探es6的类继承机制,最后再讨论下javascript多态。

ES6之前的继承

原型赋值方式

简而言之,就是直接将父类的一个实例赋给子类的原型。如下示例:

function Person(name){
 this.name=name;
 this.className="person" 
}
Person.prototype.getClassName=function(){
 console.log(this.className)
}

function Man(){
}

Man.prototype=new Person();//1
//Man.prototype=new Person("Davin");//2
var man=new Man;
>man.getClassName()
>"person"
>man instanceof Person
>true

如代码中1处所示,这种方法是直接new 了一个父类的实例,然后赋给子类的原型。这样也就相当于直接将父类原型中的方法属性以及挂在this上的各种方法属性全赋给了子类的原型,简单粗暴!我们再来看看man,它是Man的一个实例,因为man本身没有getClassName方法,那么就会去原型链上去找,找到的是person的getClassName。这种继承方式下,所有的子类实例会共享一个父类对象的实例,这种方案最大问题就是 子类无法通过父类创建私有属性 。比如每一个Person都有一个名字,我们在初始化每个Man的时候要指定一个不同名字,然后子类将这个名字传递给父类,对于每个man来说,保存在相应person中的name应该是不同的,但是这种方式根本做不到。所以,这种继承方式,实战中基本不用!

调用构造函数方式

function Person(name){
 this.name=name;
 this.className="person" 
}
Person.prototype.getName=function(){
 console.log(this.name)
}
function Man(name){
  Person.apply(this,arguments)
}
var man1=new Man("Davin");
var man2=new Man("Jack");
>man1.name
>"Davin"
>man2.name
>"Jack"
>man1.getName() //1 报错
>man1 instanceof Person
>true

这里在子类的在构造函数里用子类实例的this去调用父类的构造函数,从而达到继承父类属性的效果。这样一来,每new一个子类的实例,构造函数执行完后,都会有自己的一份资源(name)。但是这种办法只能继承父类构造函数中声明的实例属性,并没有继承父类原型的属性和方法,所以就找不到getName方法,所以1处会报错。为了同时继承父类原型,从而诞生了组合继承的方式:

组合继承

function Person(name){
 this.name=name||"default name"; //1
 this.className="person" 
}
Person.prototype.getName=function(){
 console.log(this.name)
}
function Man(name){
  Person.apply(this,arguments)
}
//继承原型
Man.prototype = new Person();
var man1=new Man("Davin");
> man1.name
>"Davin"
> man1.getName()
>"Davin"

这个例子很简单,这样不仅会继承构造函数中的属性,也会复制父类原型链中的属性。但是,有个问题, Man.prototype = new Person(); 这句执行后,Man的原型如下:

立即学习Java免费学习笔记(深入)”;

> Man.prototype
> {name: "default name", className: "person"}

也就是说Man的原型中已经有了一个name属性,而之后创建man1时传给构造的函数的name则是通过this重新定义了一个name属性,相当于只是覆盖掉了原型的name属性(原型中的name依然还在),这样很不优雅。

分离组合继承

这是目前es5中主流的继承方式,有些人起了一个吊炸天的名字“寄生组合继承”。首先说明一下,两者是一回事。分离组合继承的名字是我起的,一来感觉不装逼会好点,二来,更确切。综上所述,其实我们可以将继承分为两步:构造函数属性继承和建立子类和父类原型的链接。所谓的分离就是分两步走;组合是指同时继承子类构造函数和原型中的属性。

function Person(name){
 this.name=name; //1
 this.className="person" 
}
Person.prototype.getName=function(){
 console.log(this.name)
}
function Man(name){
  Person.apply(this,arguments)
}
//注意此处
Man.prototype = Object.create(Person.prototype);
var man1=new Man("Davin");
> man1.name
>"Davin"
> man1.getName()
>"Davin"

这里用到了 Object.creat(obj) 方法,该方法会对传入的obj对象进行浅拷贝。和上面组合继承的主要区别就是:将父类的 原型 复制给了子类原型。这种做法很清晰:

  1. 构造函数中继承父类属性/方法,并初始化父类。

  2. 子类原型和父类原型建立联系。

还有一个问题,就是constructor属性,我们来看一下:

> Person.prototype.constructor
< Person(name){
   this.name=name; //1
   this.className="person" 
 }
> Man.prototype.constructor
< Person(name){
   this.name=name; //1
   this.className="person" 
  }

constructor是类的构造函数,我们发现,Person和Man实例的constructor指向都是Person,当然,这并不会改变instanceof的结果,但是对于需要用到construcor的场景,就会有问题。所以一般我们会加上这么一句:

Man.prototype.constructor = Man

综合来看,es5下,这种方式是首选,也是实际上最流行的。

行文至此,es5下的主要继承方式就介绍完了,在介绍es6继承之前,我们再往深的看,下面是独家干货,我们来看一下Neat.js中的一段简化源码(关于Neat.js,这里是传送门Neat.js官网,待会再安利):

//下面为Neat源码的简化
-------------------------
function Neat(){
  Array.call(this)
}
Neat.prototype=Object.create(Array.prototype)
Neat.prototype.constructor=Neat
-------------------------
//测试代码
var neat=new Neat;
>neat.push(1,2,3,4)
>neat.length //1
>neat[4]=5
>neat.length//2
>neat.concat([6,7,8])//3

现在提问,上面分割线包起来的代码块干了件什么事?

对,就是定义了一个继承自数组的Neat对象!下面再来看一下下面的测试代码,先猜猜1、2、3处执行的结果分别是什么?期望的结果应该是:

4
5
1,2,3,4,5,6,7,8

而实际上却是:

4
4
[[1,2,3,4],6,7,8]

呐尼!这不科学啊 !why ?

我曾在阮一峰的一篇文章中看到的解释如下:

因为子类无法获得原生构造函数的内部属性,通过 Array.apply() 或者分配给原型对象都不行。原生构造函数会忽略 apply 方法传入的 this ,也就是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。ES5是先新建子类的实例对象 this ,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性 [[DefineOwnProperty]] ,用来定义新属性时,更新 length 属性,这个内部属性无法在子类获取,导致子类的 length 属性行为不正常。

然而,事实并非如此!确切来说,并不是原生构造函数会忽略掉 apply 方法传入的this而导致属性无法绑定。要不然1处也不会输出4了。还有,neat依然可以正常调用push等方法,但继承之后原型上的方法有些也是有问题的,如neat.concat。其实可以看出,我们通过 Array.call(this) 也是有用的,比如length属性可用。但是,为什么会出问?根据症状,可以肯定的是最终的this肯定有问题,但具体是什么问题呢?难道是我们漏了什么地方导致有遗漏的属性没有正常初始化?或者就是浏览器初始化数组的过程比较特殊,和自定义对象不一样?首先我们看第一种可能,唯一漏掉的可能就是数组的静态方法(上面的所有继承方式都不会继承父类静态方法)。我们可以测试一下:

for(var i in  Array){
 console.log(i,"xx")
}

然而并没有一行输出,也就是说Array并没有静态方法。当然,这种方法只可以遍历可枚举的属性,如果存在不可枚举的属性呢?其实即使有,在浏览器看来也应该是数组私有的,浏览器不希望你去操作!所以第一种情况pass。那么只可能是第二种情况了,而事实,直到es6出来后,才找到了答案:

ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的 所有行为 都可以继承。

请注意我加粗的文字。“所有”,这个词很微妙,不是“没有”,那么言外之意就是说es5是部分了。根据我之前的测试(在es5下),下标操作和concat在chrome下是有问题的,而大多数函数都是正常的,当然,不同浏览器可能不一样,这应该也是jQuery每次操作后的结果集以一个新的扩展后的数组的形式返回而不是本身继承数组(然后再直接返回this的)的主要原因,毕竟jQuery要兼容各种浏览器。而Neat.js面临的问题并没有这么复杂,只需把有坑的地方绕过去就行。言归正传,在es5中,像数组一样的,浏览器不让我们愉快与之玩耍的对象还有:

Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

es6的继承方式

es6引入了class、extends、super、static(部分为ES2016标准)

class Person{
  //static sCount=0 //1
  constructor(name){
     this.name=name; 
     this.sCount++;
  }
  //实例方法 //2
  getName(){
   console.log(this.name)
  }
  static sTest(){
    console.log("static method test")
  }
}

class Man extends Person{
  constructor(name){
    super(name)//3
    this.sex="male"
  }
}
var man=new Man("Davin")
man.getName()
//man.sTest()
Man.sTest()//4
输出结果:
Davin
static method test

ES6明确规定,Class内部只有静态方法,没有静态属性,所以1处是有问题的,ES7有一个静态属性的 提案 ,目前Babel转码器支持。熟悉java的可能对上面的代码感觉很亲切,几乎是自解释的。我们大概解释一下,按照代码中标号对应:

Summarizer
Summarizer

基于 AI 的文本段落摘要生成器

下载
  1. constructor为构造函数,一个类有一个,相当于es5中构造函数标准化,负责一些初始化工作,如果没有定义,js vm会定义一个空的默认的构造函数。

  2. 实例方法,es6中可以不加"function"关键字, class内定义的所有函数都会置于该类的原型当中 ,所以,class本身只是一个语法糖。

  3. 构造函数中通过super()调用父类构造函数,如果有super方法,需要时构造函数中第一个执行的语句,this关键字在调用super之后才可用。

  4. 静态方法,在类定义的外部只能通过类名调用,内部可以通过this调用,并且静态函数是会被继承的。如示例中:sTest是在Person中定义的静函数,可以通过 Man.sTest() 直接调用。

es6和es5继承的区别

大多数浏览器的ES5实现之中,每一个对象都有 proto 属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和 proto 属性,因此同时存在两条继承链。

(1)子类的 proto 属性,表示构造函数的继承,总是指向父类。

(2)子类 prototype 属性的 proto 属性,表示方法的继承,总是指向父类的 prototype 属性。

class A {
}

class B extends A {
}

B.proto === A // true
B.prototype.proto === A.prototype // true

上面代码中,子类 B 的 proto 属性指向父类 A ,子类 B 的 prototype 属性的 proto 属性指向父类 A 的 prototype 属性。

这样的结果是因为,类的继承是按照下面的模式实现的:

class A {
}

class B {
}

// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B继承A的静态属性
Object.setPrototypeOf(B, A);

Object.setPrototypeOf的简单实现如下:

Object.setPrototypeOf = function (obj, proto) {
  obj.proto = proto;
  return obj;
}

因此,就得到了上面的结果。

Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.proto = A.prototype;

Object.setPrototypeOf(B, A);
// 等同于
B.proto = A;

这两条继承链,可以这样理解:作为一个对象,子类( B )的原型( proto 属性)是父类( A );作为一个构造函数,子类( B )的原型( prototype 属性)是父类的实例。

Object.create(A.prototype);
// 等同于
B.prototype.proto = A.prototype;

es6继承的不足

  1. 不支持静态属性(除函数)。

  2. class中不能定义私有变量和函数。class中定义的所有函数都会被放倒原型当中,都会被子类继承,而属性都会作为实例属性挂到this上。如果子类想定义一个私有的方法或定义一个private 变量,便不能直接在class花括号内定义,这真的很不方便!

总结一下,和es5相比,es6在语言层面上提供了面向对象的部分支持,虽然大多数时候只是一个语法糖,但使用起来更方便,语意化更强、更直观,同时也给javascript继承提供一个标准的方式。还有很重要的一点就是-es6支持原生对象继承。

更多es6类继承资料请移步:MDN Classess 。

多态

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。这是标准定义,在c++中实现多态的方式有虚函数、抽象类、模板,在java中更粗暴,所有函数都是“虚”的,子类都可以重写,当然java中没有虚函数的概念,我们暂且把相同签名的、子类和父类可以有不同实现的函数称之为虚函数,虚函数和模版(java中的范型)是支持多态的主要方式,因为javascript中没有模版,所以下面我们只讨论虚函数,下面先看一个例子:

function Person(name,age){
 this.name=name
 this.age=age
}
Person.prototype.toString=function(){
 return "I am a Person, my name is "+ this.name
}
function Man(name,age){
  Person.apply(this,arguments)
}
Man.prototype = Object.create(Person.prototype);
Man.prototype.toString=function(){
  return "I am a Man, my name is"+this.name;
}
var person=new Person("Neo",19)
var man1=new Man("Davin",18)
var man2=new Man("Jack",19)
> person+""
> "I am a Person, my name is Neo"
> man1+""
> "I am a Man, my name isDavin"
> man1<man2 //期望比较年龄大小 1
> false

上面例子中,我们分别在子类和父类实现了toString方法,其实,在js中上述代码原理很简单,对于同名函数,子类会覆父类的,这种特性其实就是虚函数,只不过js中不区分参数个数,也不区分参数类型,只看函数名称,如果名称相同就会覆盖。现在我们来看注释1,我们期望直接用比较运算符比较两个man的大小(按年龄),怎么实现?在c++中有运算符重载,但java和js中都没有,所幸的是,js可以用一种变通的方法来实现:

function Person(name,age){
 this.name=name
 this.age=age
}
Person.prototype.valueOf=function(){
 return this.age
}
function Man(name,age){
  Person.apply(this,arguments)
}

Man.prototype = Object.create(Person.prototype);
var person=new Person("Neo",19)
var man1=new Man("Davin",18)
var man2=new Man("Jack",19)
var man3=new Man("Joe",19)

>man1<19//1
>true
>person==19//2
>true
>man1<man2//3
>true
>man2==man3 //4 注意
>true
>person==man2//5
>false

其中1、2、3、5在所有js vm下结果都是确定的。但是4并不一定!javascript规定,对于比较运算符,如果一个值是对象,另一个值是数字时,会先尝试调用valueOf,如果valueOf未指定,就会调用toString;如果是字符串时,则先尝试调用toString,如果没指定,则尝试valueOf,如果两者都没指定,将抛出一个类型错误异常。如果比较的两个值都是对象时,则比较的时对象的引用地址,所以若是对象,只有自身===自身,其它情况都是false。现在我们回过头来看看示例代码,前三个都是标准的行为。而第四点取决于浏览器的实现,如果严格按照标准,这应该算是chrome的一个bug ,但是,我们的代码使用时双等号,并非严格相等判断,所以浏览器的相等规则也会放宽。值得一提的是5,虽然person和man2 age都是19,但是结果却是false。 总结一下,chrome对相同类的实例比较策略是先会尝试转化,然后再比较大小,而对非同类实例的比较,则会直接返回false,不会做任何转化。 所以我的建议是:如果数字和类实例比较,永远是安全的,可以放心玩,如果是同类实例之间,可以进行 非等 比较,这个结果是可以保证的,不要进行相等比较,结果是不能保证的,一般相等比较,变通的做法是:

var equal= !(ob1<ob2||ob1>ob2) 
//不小于也不大于,就是等于,前提是比较操作符两边的对象要实现valueOf或toString

当然类似toString、valueOf的还有toJson方法,但它和重载没有什么关系,故不冗述。

数学运算符

让对象支持数学运算符本质上和让对象支持比较运算符原理类似,底层也都是通过valueOf、toString来转化实现。 但是通过这种覆盖原始方法模拟的运算符重载有个比较大局限就是:返回值只能是数字!而c++中的运算符重载的结果可以是一个对象 。试想一下,如果我们现在要实现一个复数类的加法,复数包括实部与虚部,加法要同时应用到两个部分,而相加的结果(返回值)仍然是一个复数对象,这种情况下,javascript也就无能为力了。

【相关推荐】

1. 特别推荐“php程序员工具箱”V0.1版本下载

2. class0class1class2class3

3. class4class5class6

相关文章

java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

76

2026.03.13

Python异步编程与Asyncio高并发应用实践
Python异步编程与Asyncio高并发应用实践

本专题围绕 Python 异步编程模型展开,深入讲解 Asyncio 框架的核心原理与应用实践。内容包括事件循环机制、协程任务调度、异步 IO 处理以及并发任务管理策略。通过构建高并发网络请求与异步数据处理案例,帮助开发者掌握 Python 在高并发场景中的高效开发方法,并提升系统资源利用率与整体运行性能。

117

2026.03.12

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

350

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

63

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

109

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

108

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

243

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

684

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

179

2026.03.04

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 6.1万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 3.5万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号