目录

  • 序言
  • class 是一个特殊的函数
  • class 的工作原理
  • class 继承的原型链关系
  • 参考

1.序言

ECMAScript 2015(ES6) 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法(class)不会为JavaScript引入新的面向对象的继承模型。

2.class 是一个特殊的函数

ES6 的 class 主要提供了更多方便的语法去创建老式的构造器函数。我们可以通过 typeof 得到其类型:

class People {
constructor(name) {
this.name = name;
}
} console.log(typeof People) // function

那 class 声明的类到底是一个什么样的函数呢?我们可以通过在线工具 ES6 to ES5 来分析 class 背后真正的实现。

3.class 的工作原理

下面通过多组代码对比,来解析 class 声明的类将转化成什么样的函数。

第一组:用 class 声明一个空类

ES6的语法:

class People {}

这里提出两个问题:

1.class 声明的类与函数声明不一样,不会提升(即使用必须在声明之后),这是为什么?

console.log(People) // ReferenceError

class People {}

在浏览器中运行报错,如下图:

2.不能直接像函数调用一样调用类People(),必须通过 new 调用类,如 new People(),这又是为什么?

class People {}

People() // TypeError

在浏览器中运行报错,如下图:

转化为ES5:

"use strict";

function _instanceof(left, right) {
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
} // 判断 Constructor.prototype 是否出现在 instance 实例对象的原型链上
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
} var People = function People() {
// 检查是否通过 new 调用
_classCallCheck(this, People);
};

针对上面提到的两个问题,我们都可以用转化后的 ES5 代码来解答:

对于问题1,我们可以看到 class 声明的类转化为的是一个函数表达式,并且用变量 People 保存函数表达式的值,而函数表达式只能在代码执行阶段创建而且不存在于变量对象中,所以如果在 class 声明类之前使用,就相当于在给变量 People 赋值之前使用,此时使用是没有意义的,因为其值为 undefined,直接使用反而会报错。所以 ES6 就规定了在类声明之前访问类会抛出 ReferenceError 错误(类没有定义)。

对于问题2,我们可以看到 People 函数表达式中,执行了 _classCallCheck 函数,其作用就是保证 People 函数必须通过 new 调用。如果直接调用 People(),由于是严格模式下执行,此时的 this 为 undefined,调用 _instanceof 函数检查继承关系其返回值必然为 false,所以必然会抛出 TypeError 错误。

补充:类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,getter和setter都在严格模式下执行。

第二组:给类添加公共字段和私有字段

ES6的语法:

class People {
#id = 1 // 私有字段,约定以单个的`#`字符为开头
name = 'Tom' // 公共字段
}

转化为ES5:

...

// 将类的公共字段映射为实例对象的属性
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
} var People = function People() {
_classCallCheck(this, People); // 初始化私有字段
_id.set(this, {
writable: true,
value: 1
}); // 将类的公共字段映射为实例对象的属性
_defineProperty(this, "name", 'Tom');
}; // 转化后的私有字段(会自动检查命名冲突)
var _id = new WeakMap();

对比转化前后的代码可以看出:

对于私有字段,在使用 class 声明私有字段时,约定是以字符 '#' 为开头,转化后则将标识符中的 '#' 替换为 '_',并且单独用一个 WeakMap 类型的变量来替代类的私有字段,声明在函数表达式后面(也会自动检查命名冲突),这样就保证了类的实例对象无法直接通过属性访问到私有字段(私有字段根本就没有在实例对象的属性中)。

对于公共字段,则是通过 _defineProperty 函数将类的公共字段映射为实例对象的属性,如果是对已有属性进行重载,则会通过 Object.defineProperty 函数来进行设置,设置属性的可枚举性(enumerable)、可配置性(configurable)、可写性(writable)。

第三组:给类添加构造函数与实例属性

ES6的语法:

class People {
#id = 1 // 私有字段,约定以单个的`#`字符为开头
name = 'Tom' // 公共字段 constructor(id, name, age) {
this.#id = id
this.name = name
this.age = age // 实例属性 age
}
}

转化为ES5:

...

// 设置(修改)类的私有字段
function _classPrivateFieldSet(receiver, privateMap, value) {
var descriptor = privateMap.get(receiver);
if (!descriptor) {
throw new TypeError("attempted to set private field on non-instance");
}
if (descriptor.set) {
descriptor.set.call(receiver, value);
} else {
if (!descriptor.writable) {
throw new TypeError("attempted to set read only private field");
}
descriptor.value = value;
}
return value;
} var People = function People(id, name, age) {
_classCallCheck(this, People); _id.set(this, {
writable: true,
value: 1
}); _defineProperty(this, "name", 'Tom'); // constructor 从这开始执行 _classPrivateFieldSet(this, _id, id); this.name = name;
this.age = age;
}; var _id = new WeakMap();

对比转化前后的代码可以看出:

类的构造函数(constructor)里面的代码的执行时机是在字段定义(字段映射为实例对象的属性)之后。而对私有字段的赋值(修改)是专门通过 _classPrivateFieldSet 函数来实现的。

第四组:给类添加原型方法和静态方法

ES6的语法:

class People {
#id = 1
name = 'Tom' constructor(id, name, age) {
this.#id = id
this.name = name
this.age = age
} // 原型方法
getName() { return this.name } // 静态方法
static sayHello() { console.log('hello') }
}

转化为ES5:

...

// 设置对象的属性
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
} // 将类的方法映射到构造函数的原型(Constructor.prototype)的属性上
// 将类的静态方法映射到构造函数(Constructor)的属性上
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
} var People = function () {
function People(id, name, age) {
// ...
} // 设置类的方法和静态方法
_createClass(People, [{
key: "getName",
value: function getName() {
return this.name;
}
}], [{
key: "sayHello",
value: function sayHello() {
console.log('hello');
}
}]); return People;
}(); var _id = new WeakMap();

对比一下第三组和第四组转化后的代码,可以明显发现:

  1. 类的字段通过 _defineProperty 函数映射到实例对象(this)的属性上。

  2. 类的方法则通过 _createClass 函数映射到构造函数的原型(Constructor.prototype)的属性上,

  3. 类的静态方也通过 _createClass 函数映射到构造函数(Constructor)的属性上。

第五组:类的继承

ES6的语法:

// 父类(superClass)
class People {} // 子类(subClass)继承父类
class Man extends People {}

转化为ES5:

...

var People = function People() {
_classCallCheck(this, People);
}; var Man = function (_People) {
// Man 继承 _People
_inherits(Man, _People); // 获取 Man 的父类的构造函数
var _super = _createSuper(Man); function Man() {
_classCallCheck(this, Man); // 实现了父类构造函数的调用, 子类的 this 继承父类的 this 上的属性
return _super.apply(this, arguments);
} return Man;
}(People);

在 _inherits 函数中,实现了原型链和静态属性的继承:

// 实现继承关系
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); }
// Object.create(proto, propertiesObject) 方法
// 创建一个新对象,使用 proto 来提供新创建的对象的__proto__
// 将 propertiesObject 的属性添加到新创建对象的不可枚举(默认)属性(即其自身定义的属性,而不是其原型链上的枚举属性)
subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } });
if (superClass) _setPrototypeOf(subClass, superClass);
} // 设置对象 o 的原型(即 __proto__ 属性)为 p
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; };
return _setPrototypeOf(o, p);
}

1.通过 Object.create 函数调用可知:

(1)subClass.prototype.__proto__ === superClass.prototype ,相当于实现了原型链的继承

(2)subClass.prototype.constructor === subClass ,表明 subClass 构造函数的显示原型对象(prototype)的 constructor 属性指向原构造函数

2.通过调用 _setPrototypeOf(subClass, superClass)可知:

(1)subClass.__proto__ === superClass,相当于实现了静态属性的继承

在 Man 构造函数中,通过调用其父类的构造函数(_super),实现了子类的 this 继承父类的 this 上的属性:

// 获得父类的构造函数
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function () {
var Super = _getPrototypeOf(Derived), result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
} // 判断 call 的类型,返回合适的 Constructor
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; }
return _assertThisInitialized(self);
} // 断言 selft 是否初始化
function _assertThisInitialized(self) {
if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); }
return self;
} // 判断是否能否使用 Reflect
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Date.prototype.toString.call(Reflect.construct(Date, [], function () {}));
return true;
} catch (e) {
return false;
}
} // 获取 o 对象的原型(__proto__)
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); };
return _getPrototypeOf(o);
}

从上述可知 class 继承的实现主要包含三部分:

  • 原型链的继承
  • 静态属性的继承
  • 通过调用父类的构造函数,获得父类的构造函数 this 上的属性

4.class 继承的原型链关系

实例代码:

class People {
constructor(name) {
this.name = name
}
} class Man extends People {
constructor(name, sex) {
super(name)
this.sex = sex
}
} var man = new Man('Tom', 'M')

根据上面分析所知道的类(class)的继承的实现原理,并结合 深入理解JS中的对象(一):原型、原型链和构造函数 中所提到的构造函数的原型链关系,可得示例代码的完整原型链关系如下图:

5.参考

类- JavaScript | MDN

exploring-es6 - class

为什么说ES6的class是语法糖?

深入理解JavaScript系列(15):函数(Functions)

class继承做了什么呢?

深入理解JS中的对象(三):class 的工作原理的更多相关文章

  1. 深入理解JS中的对象(二):new 的工作原理

    目录 序言 不同返回值的构造函数 深入 new 调用函数原理 总结 参考 1.序言 在 深入理解JS中的对象(一):原型.原型链和构造函数 中,我们分析了JS中是否一切皆对象以及对象的原型.原型链和构 ...

  2. 深入理解JS中的对象(一)

    目录 一切皆是对象吗? 对象 原型与原型链 构造函数 参考 1.一切皆是对象吗? 首先,"在 JavaScript 中,一切皆是对象"这种表述是不完全正确的. JavaScript ...

  3. 【学习笔记】六:面向对象的程序设计——理解JS中的对象属性、创建对象、JS中的继承

    ES中没有类的概念,这也使其对象和其他语言中的对象有所不同,ES中定义对象为:“无序属性的集合,其属性包含基本值.对象或者函数”.现在常用的创建单个对象的方法为对象字面量形式.在常见多个对象时,使用工 ...

  4. 和我一起理解js中的事件对象

    我们知道在JS中常用的事件有: 页面事件:load: 焦点事件:focus,blur: 鼠标事件:click,mouseout,mouseover,mousemove等: 键盘事件:keydown,k ...

  5. 怎么理解js中的事件委托

    怎么理解js中的事件委托 时间 2015-01-15 00:59:59  SegmentFault 原文  http://segmentfault.com/blog/sunchengli/119000 ...

  6. 浅解析js中的对象

    浅解析js中的对象 原文网址:http://www.cnblogs.com/foodoir/p/5971686.html,转载请注明出处. 前面的话: 说到对象,我首先想到的是每到过年过节见长辈的时候 ...

  7. 前端之JavaScript:JS之DOM对象三

    js之DOM对象三   一.JS中for循环遍历测试 for循环遍历有两种 第一种:是有条件的那种,例如    for(var i = 0;i<ele.length;i++){} 第二种:for ...

  8. 图文结合深入理解 JS 中的 this 值

    图文结合深入理解 JS 中的 this 值 在 JS 中最常见的莫过于函数了,在函数(方法)中 this 的出现频率特别高,那么 this 到底是什么呢,今天就和大家一起学习总结一下 JS 中的 th ...

  9. JavaScript学习12 JS中定义对象的几种方式

    JavaScript学习12 JS中定义对象的几种方式 JavaScript中没有类的概念,只有对象. 在JavaScript中定义对象可以采用以下几种方式: 1.基于已有对象扩充其属性和方法 2.工 ...

随机推荐

  1. 2019-2020-1 20199308《Linux内核原理与分析》第六周作业

    <Linux内核分析> 第五章 系统调用的三层机制(下) 5.1 给MenuOS增加命令 强制删除当前menu目录,用get clone重新克隆一个新版本的menu,运行make root ...

  2. chcp437 转换英语,在西班牙语系统中无效

    https://social.technet.microsoft.com/Forums/en-US/9c772011-5094-4df0-bf73-7140bf91673b/chcp-command- ...

  3. Zabbix3.0安装部署最佳实践

    Zabbix介绍 1.1zabbix 简介 Zabbix 是一个高度集成的网络监控解决方案,可以提供企业级的开源分布式监控解决方案,由一个国外的团队持续维护更新,软件可以自由下载使用,运作团队靠提供收 ...

  4. 《JavaScript 模式》读书笔记(6)— 代码复用模式3

    我们之前聊了聊基本的继承的概念,也聊了很多在JavaScript中模拟类的方法.这篇文章,我们主要来学习一下现代继承的一些方法. 九.原型继承 下面我们开始讨论一种称之为原型继承(prototype ...

  5. Scala教程之:scala的参数

    文章目录 默认参数值 命名参数 scala的参数有两大特点: 默认参数值 命名参数 默认参数值 在Scala中,可以给参数提供默认值,这样在调用的时候可以忽略这些具有默认值的参数. def log(m ...

  6. Docker安装MySql完整教程、实操

    docker:官网 docker:镜像官网:        镜像官网可以所有应用,选择安装环境:会给出安装命令,例如:docker pull redis 默认拉取最新的版本(指定版本:docker p ...

  7. nginx日志、nginx日志切割、静态文件不记录日志和过期时间

    2019独角兽企业重金招聘Python工程师标准>>> 12.10 Nginx访问日志 日志格式 vim /usr/local/nginx/conf/nginx.conf //搜索l ...

  8. 实战-MySQL定时增量备份(2)

    概要 引言 增量备份 恢复增量备份 定时备份 引言 在产品上线之后,我们的数据是相当重要的,容不得半点闪失,应该做好万全的准备,搞不好哪一天被黑客入侵或者恶意删除,那就 gg 了.所以要对我们的线上数 ...

  9. P1495 CRT,P4777 EXCRT

    updata on 2020.4.11 修正了 excrt 的一处笔误 CRT 求解方程: \[\begin{cases} x \equiv a_1 \pmod {m_1}\\ x \equiv a_ ...

  10. MySQL 数据库赋权

    1.进入数据库,查看数据库账户 # 进入数据库 mysql –u root –p ---> 输入密码... # 使用 mysql 库 use mysql; # 展示 mysql 库中所有表 sh ...