使用TypeScript或者ES2015+标准中的extends关键字是很容易实现继承的,但这不是本文的重点。JS使用了基于原型(prototype-based)的继承方式,extends只是语法糖,本文重点在于不使用extends来自己实现继承,以进一步理解JS中的继承,实际工作中肯定还是要优先考虑使用extends关键字的。

原型 & 原型链

原型用于对象属性的查找。画出下面代码中的原型链图示:

class Person {
private _name: string;

constructor(name: string) {
this._name = name;
}

get getName(): string {
return this._name;
}
}

let person = new Person("xfh");

图中,__proto__表示实例的原型对象,prototype表示构造函数的原型对象。不再推荐使用__proto__,将来可能会被废弃,可使用Object.getPrototypeOf()来获取对象的原型。

关于原型/链,记住以下几点:

  • 原型链的终点是null,从这个角度,可以将null看作所有Object的基类

  • 实例的原型对象和它构造函数的原型对象是同一个对象(比较拗口)

  • 所有的函数(包括构造函数及Function自身)都是Function的实例

  • 函数是普通的对象,只是具备了可调用(callable)功能 ,想到了Python中的类装饰器,也是具备了可调用功能的普通类

  • 所有的对象终归是Object的实例,即Object位于所有对象的原型链上

// 原型链的终点是null
Object.getPrototypeOf(Object.prototype)===null // true
Object.prototype instanceof Object // false
// 实例和构造函数的原型对象是同一个对象
Object.getPrototypeOf(Function)===Function.prototype // true
// 所有的函数(包括构造函数及Function自身)都是Function的实例
Function instanceof Function // true,Function是自己的实例
Object instanceof Function // true,构造函数是Function的实例
// 所有的对象终归是Object的实例,即Object位于所有对象的原型链上
Function.prototype instanceof Object // true
Function instanceof Object // true
Object instanceof Object // true

typeof操作符与instanceof`关键字的区别如下:

Keep in mind the only valuable purpose of typeof operator usage is checking the Data Type. If we wish to check any Structural Type derived from Object it is pointless to use typeof for that, as we will always receive "object". The indeed proper way to check what sort of Object we are using is instanceof keyword. But even in that case there might be misconceptions.

实现继承

JS中对象成员分为三类:实例、静态、原型。实例成员绑定到具体实例上(通常是this上),静态成员绑定到构造函数上,原型成员就存在原型对象上:

/**
* 从基类继承成员
* @param child 子类构造函数或实例
* @param base 基类构造函数或实例
*/
function inheritMembers(child, base) {
let ignorePropertyNames = ["name", "caller", "prototype", "__proto__", "length", "arguments"];
let propertyNames = Object.getOwnPropertyNames(base);
for (let propertyName of propertyNames) {
if (ignorePropertyNames.includes(propertyName)) {
continue;
}
let descriptor = Object.getOwnPropertyDescriptor(base, propertyName);
if (!descriptor) {
continue;
}
Object.defineProperty(child, propertyName, descriptor);
}
}
/**
* 从基类继承原型及静态成员
* @param thisCtor 子类构造函数
* @param baseCtor 基类构造函数
*/
function inheritSharedMembers(thisCtor, baseCtor) {
if (typeof thisCtor !== "function" || typeof baseCtor !== "function") {
throw TypeError("参数必须是函数:thisCtor,baseCtor");
}
// 继承原型成员
thisCtor.prototype = Object.create(baseCtor.prototype);
thisCtor.prototype.constructor = thisCtor;
// 继承静态成员
inheritMembers(thisCtor, baseCtor);
}
/**
* 调用子类及父类构造函数创建子类实例,并继承父类实例成员(这也是调用父类构造函数的原因)
* @param thisInstance 子类实例
* @param baseInstance 父类实例
*/
function createInstance(thisInstance, baseInstance) {
inheritMembers(thisInstance, baseInstance);
return thisInstance;
}

// 构造函数
function Animal(tag) {
// 实例属性
this.tag = tag;
}
// 静态方法,需通过构造函数来调用
Animal.bark = function () {
console.log("static func, this= " + this + ", typeof this=" + typeof this);
};
// 原型方法,需通过实例来调用
Animal.prototype.getInfo = function () {
console.log("property func, tag:" + this.tag);
};

function Dog(name = null) {
this.name = name ?? "default";
}
// 添加子类原型方法
Dog.prototype.dogBark = function () {
console.log("dog bark");
};
// 继承父类原型及静态成员
inheritSharedMembers(Dog, Animal);

var animal = new Animal("animal");
Animal.bark();
// TypeError: animal.bark is not a function
// animal.bark();
animal.getInfo();
// property getInfo not exist on type 'typeof Animal'
// Animal.getInfo();


let dog = createInstance(new Dog("dog"), new Animal("dog"));

dog.getInfo();
dog.dogBark();
Dog.bark();
console.log(dog.name);

最后使用v4.1.3版本的TS,编译为ES5版本的JS,看看TS背后是如何实现继承的:

class Person {
name: string;
age: number;
constructor(name: string, age: number) {
// 只能在构造函数中使用this关键字
this.name = name;
this.age = age;
}
// 静态方法中调用本类中的另一个静态方法时,可以使用this.methodName的形式
// 在外部调用时只能类名.方法名的形式,所以此时方法内部,this是指向构造函数的
// 即,this.methodName等价于类名.方法名
static static_method() {
// 这里this指向Person类,typeof this=function
// 可以看出class Person本质上是构造函数,class只是语法糖
console.log(`static method, this=${this}, typeof this=${typeof this}`);
}
}

// 使用extends继承
class Chinese extends Person {
constructor(name: string, age: number) {
// 必须调用父类构造函数,且需要在子类构造函数使用this关键字之前调用,否则会产生错误:
// A 'super' call must be the first statement in the constructor when a class contains initialized properties or has parameter properties.
super(name, age);
}

sayHello() {
console.log(`I'm ${this.name}, I'm ${this.age} years old.`)
}
}


let cn = new Chinese('xfh', 26);

cn.sayHello();
Chinese.static_method();

编译后代码如下:

"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Person = /** @class */ (function () {
function Person(name, age) {
// 只能在构造函数中使用this关键字
this.name = name;
this.age = age;
}
// 静态方法中调用本类中的另一个静态方法时,可以使用this.methodName的形式
// 在外部调用时只能类名.方法名的形式,所以此时方法内部,this是指向构造函数的
// 即,this.methodName等价于类名.方法名
Person.static_method = function () {
// 这里this指向Person类,typeof this=function
// 可以看出class Person本质上是构造函数,class只是语法糖
console.log("static method, this=" + this + ", typeof this=" + typeof this);
};
return Person;
}());
// 使用extends继承
var Chinese = /** @class */ (function (_super) {
__extends(Chinese, _super);
function Chinese(name, age) {
// 必须调用父类构造函数,且需要在子类构造函数使用this关键字之前调用,否则会产生错误:
// A 'super' call must be the first statement in the constructor when a class contains initialized properties or has parameter properties.
return _super.call(this, name, age) || this;
}
Chinese.prototype.sayHello = function () {
console.log("I'm " + this.name + ", I'm " + this.age + " years old.");
};
return Chinese;
}(Person));
var cn = new Chinese('xfh', 26);
cn.sayHello();
Chinese.static_method();

推荐阅读

JavaScript data types and data structures

Object.prototype.__proto__

Object prototypes

实现JavaScript继承的更多相关文章

  1. javascript继承的三种模式

    javascript继承一般有三种模式:组合继承,原型式继承和寄生式继承: 1组合继承:javascript最为广泛的继承方式通过原型链实现对原型属性和方法的继承,通过构造函数实现对实例属性的继承,同 ...

  2. javascript继承机制的设计思想(ryf)

    我一直很难理解Javascript语言的继承机制. 它没有"子类"和"父类"的概念,也没有"类"(class)和"实例" ...

  3. 【读书笔记】javascript 继承

    在JavaScript中继承不像C#那么直接,C#中子类继承父类之后马上获得了父类的属性和方法,但JavaScript需要分步进行. 让Brid 继承 Animal,并扩展自己fly的方法. func ...

  4. 图解JavaScript 继承

    JavaScript作为一个面向对象语言,可以实现继承是必不可少的,但是由于本身并没有类的概念(不知道这样说是否严谨,但在js中一切都类皆是对象模拟)所以在JavaScript中的继承也区别于其他的面 ...

  5. JavaScript强化教程——Cocos2d-JS中JavaScript继承

    javaScript语言本身没有提供类,没有其它语言的类继承机制,它的继承是通过对象的原型实现的,但这不能满足Cocos2d-JS引擎的要求.由于Cocos2d-JS引擎是从Cocos2d-x演变而来 ...

  6. [原创]JavaScript继承详解

    原文链接:http://www.cnblogs.com/sanshi/archive/2009/07/08/1519036.html 面向对象与基于对象 几乎每个开发人员都有面向对象语言(比如C++. ...

  7. javascript继承(六)—实现多继承

    在上一篇javascript继承—prototype最优两种继承(空函数和循环拷贝)(3) ,介绍了js较完美继承的两种实现方案,那么下面来探讨一下js里是否有多继承,如何实现多继承.在这里可以看看j ...

  8. javascript继承(五)—prototype最优两种继承(空函数和循环拷贝)

    一.利用空函数实现继承 参考了文章javascript继承—prototype属性介绍(2) 中叶小钗的评论,对这篇文章中的方案二利用一个空函数进行修改,可以解决创建子类对象时,父类实例化的过程中特权 ...

  9. javascript继承(四)—prototype属性介绍

    js里每一个function都有一个prototype属性,而每一个实例都有constructor属性,并且每一个function的prototype都有一个constructor属性,这个属性会指向 ...

  10. 【JavaScript】重温Javascript继承机制

    上段时间,团队内部有过好几次给力的分享,这里对西风师傅分享的继承机制稍作整理一下,适当加了些口语化的描述,留作备案. 一.讲个故事吧 澄清在先,Java和Javascript是雷锋和雷峰塔的关系.Ja ...

随机推荐

  1. spring框架使用c3po链接数据库

    编辑工具:idea 1.配置pom.xml文件(创建模板时软件自动创建) 导入spring的核心架包 全部架包官网:https://mvnrepository.com/ 1 <dependenc ...

  2. PyQt(Python+Qt)学习随笔:QTreeWidget中获取指定位置项的itemAt方法

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 QTreeWidget的itemAt方法通过视口内的坐标点获取对应坐标位置的项,相关调用方法如下: ...

  3. PyQt(Python+Qt)学习随笔:Qt Designer中部件的快捷菜单策略(contextMenuPolicy)取值及含义

    在Qt Designer中可以设置部件的快捷菜单策略,快捷菜单通过在部件上点击鼠标右键触发. 快捷菜单策略通过枚举类型Qt.ContextMenuPolicy来定义,对应枚举类型取值及含义如下: 通过 ...

  4. RSA简单实践

    RSA公钥文件解密密文的原理分析 前言 最近在学习 RSA 加解密过程中遇到一个这样的难题:假设已知 publickey 公钥文件和加密后的密文 flag ,如何对其密文进行解密,转换成明文~~ 分析 ...

  5. SELECT 1,2,3...的含义及其在SQL注入中的用法

    首先,select 之后可以接一串数字:1,2,3-只是一个例子,这串数字并不一定要按从小到大排列,也不一定从1开始,这串数字的值和顺序是任意的,甚至可以是重复的,如:11,465,7461,35 或 ...

  6. python安装Scrapy框架

    看到自己写的惨不忍睹的爬虫,觉得还是学一下Scrapy框架,停止一直造轮子的行为 我这里是windows10平台,python2和python3共存,这里就写python2.7安装配置Scrapy框架 ...

  7. Java并发编程的艺术(五)——线程和线程的状态

    线程 什么是线程 操作系统调度的最小单元就是线程,也叫轻量级进程. 为什么要使用多线程 多线程程序能够更有效率地利用多处理器核心. 用户响应时间更快. 方便程序员将程序模型映射到Java提供的多线程编 ...

  8. Vue项目上线环境部署,项目优化策略,生成打包报告,及上线相关配置

    Node.js简介 Node.js是一个基于Chrome V8引擎的JavaScript运行环境,用来方便快速地搭建易于扩展的网络应用.Node.js使用了一个事件驱动.非阻塞式I/O的模型,使其轻量 ...

  9. Typora+图床详解(小白都能学得会)

    Typora+图床详解(小白都能学得会) 1 了解工作 博客中用的笔记软件--Typora(Markdown语法) 博客中用的图床--阿里云对象存储(Object Storage Service,简称 ...

  10. Docker修改默认的网段

    一,问题 docker安装后默认的网段是172.17网段的,和真实环境网段冲突导致本机电脑无法连接docker机器. 二,解决办法 修改docker默认网段 1,先把docker停止 systemct ...