JavaScript 基础:Babel 转译 class 过程窥探
source link: https://hijiangtao.github.io/2018/11/16/What-Babel-Did-Behind-Your-Code/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
零、前言
虽然在 JavaScript 中对象无处不在,但这门语言并不使用经典的基于类的继承方式,而是依赖原型,至少在 ES6 之前是这样的。当时,假设我们要定义一个可以设置 id 与坐标的类,我们会这样写:
// Shape 类 function Shape(id, x, y) { this.id = id; this.setLocation(x, y); } // 设置坐标的原型方法 Shape.prototype.setLocation = function(x, y) { this.x = x; this.y = y; };
上面是类定义,下面是用于设置坐标的原型方法。从 ECMAScript 2015 开始,语法糖 class
被引入,开发者可以通过 class
关键字来定义类。我们可以直接定义类、在类中写静态方法或继承类等。上例便可改写为:
class Shape { constructor(id, x, y) { // 构造函数语法糖 this.id = id; this.setLocation(x, y); } setLocation(x, y) { // 原型方法 this.x = x; this.y = y; } }
一个更符合“传统语言”的写法。语法糖写法的优势在于当类中充满各类静态方法与继承关系时,class 这种对象模版写法的简洁性会更加突出,且不易出错。但不可否认时至今日,我们还需要为某些用户兼容我们的 ES6+ 代码,class 就是 TodoList 上的一项:
作为当下最流行的 JavaScript 编译器,Babel 替我们转译 ECMAScript 语法,而我们不用再担心如何进行向后兼容。
本地安装 Babel 或者利用 Babel CLI 工具,看看我们的 Shape 类会有哪些变化。可惜的是,你会发现代码体积由现在的219字节激增到2.1KB,即便算上代码压缩(未混淆)代码也有1.1KB。转译后输出的代码长这样:
"use strict";var _createClass=function(){function a(a,b){for(var c,d=0;d<b.length;d++)c=b[d],c.enumerable=c.enumerable||!1,c.configurable=!0,"value"in c&&(c.writable=!0),Object.defineProperty(a,c.key,c)}return function(b,c,d){return c&&a(b.prototype,c),d&&a(b,d),b}}();function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var Shape=function(){function a(b,c,d){_classCallCheck(this,a),this.id=b,this.setLocation(c,d)}return _createClass(a,[{key:"setLocation",value:function c(a,b){this.x=a,this.y=b}}]),a}();
Babel 仅仅是把我们定义的 Shape 还原成一个 ES5 函数与对应的原型方法么?
一、揭秘
好像没那么简单,为了摸清实际转译流程,我们先将上述类定义代码简化为一个只有14字节的空类:
class Shape {}
首先,当访问器走到类声明阶段,需要补充严格模式:
"use strict"; class Shape {}
而进入变量声明与标识符阶段时则需补充 let 关键字并转为 var:
"use strict"; var Shape = class Shape {};
到这个时候代码的变化都不太大。接下来是进入函数表达式阶段,多出来几行函数:
"use strict"; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Shape = function Shape() { _classCallCheck(this, Shape); };
该阶段不仅替换了 class,还在类中调用了叫做 _classCallCheck
的方法。这是什么呢?
这个函数的作用在于确保构造方法永远不会作为函数被调用,它会评估函数的上下文是否为 Shape 对象的实例,以此确定是否需要抛出异常。接下来,则轮到 babel-plugin-minify-simplify 上场,这个插件做的事情在于通过简化语句为表达式、并使表达式尽可能统一来精简代码。运行后的输出是这样的:
"use strict"; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function"); } var Shape = function Shape() { _classCallCheck(this, Shape); };
可以看到 if 语句中由于只有一行代码,于是花括号被去掉。接下来上场的便是内置的 Block Hoist ,该插件通过遍历参数排序然后替换,Babel 输出结果为:
"use strict"; function _classCallCheck(a, b) { if (!(a instanceof b)) throw new TypeError("Cannot call a class as a function"); } var Shape = function a() { _classCallCheck(this, a); };
最后一步,minify 一下,代码体积由最初的14字节增为338字节:
"use strict";function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var Shape=function a(){_classCallCheck(this,a)};
二、再说一些
这是一个什么都没干的类声明,但现实中任何类都会有自己的方法,而此时 Babel 必定会引入更多的插件来帮助它完成代码的转译工作。直接在刚刚的空类中定义一个方法吧。
class Shape { render() { console.log("Hi"); } }
我们用 Babel 转译一下,会发现代码中包含如下这段:
var _createClass = function () { function a(a, b) { for (var c, d = 0; d < b.length; d++) c = b[d], c.enumerable = c.enumerable || !1, c.configurable = !0, "value" in c && (c.writable = !0), Object.defineProperty(a, c.key, c); } return function (b, c, d) { return c && a(b.prototype, c), d && a(b, d), b; }; }();
类似前面我们遇到的 _classCallCheck
,这里又多出一个 _createClass
,这是做什么的呢?我们稍微把代码状态往前挪一挪,来到 babel-plugin-minify-builtins 处理阶段(该插件的作用在于缩减内置对象代码体积,但我们主要关注点在于这个阶段的 _createClass
函数是基本可读的),此时 _classCallCheck
长成这样:
var _createClass = function() { 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); } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; } ();
可以看出 _createClass
用于处理创建对象属性,函数支持传入构造函数与需定义的键值对属性数组。函数判断传入的参数(普通方法/静态方法)是否为空对应到不同的处理流程上。而 defineProperties
方法做的事情便是遍历传入的属性数组,然后分别调用 Object.defineProperty
以更新构造函数。而在 Shape 中,由于我们定义的不是静态方法,我们便这样调用:
_createClass(Shape, [{ key: "render", value: function render() { console.log("Hi"); } }]);
T.J. Crowder 在 How does Babel.js create compile a class declaration into ES2015? 中谈到 Babel 是如何将 class 转化为 ES5 兼容代码时谈到了几点,大意为:
-
constructor
会成为构造方法数; - 所有非构造方法、非静态方法会成为原型方法;
- 静态方法会被赋值到构造函数的属性上,其他属性保持不变;
- 派生构造函数上的原型属性是通过
Object.create(Base.prototype)
构造的对象,而不是new Base()
; -
constructor
调用构造器基类是第一步操作; - ES5 中对应
super
方法的写法是Base.prototype.baseMethod.call(this);
,这种操作不仅繁琐而且容易出错;
这些概述大致总结了类定义在两个 ES 版本中的一些差异,其他很多方面比如 extends
——继承关键字,它的使用则会使 Babel 在转译结果加上 _inherits
与 _possibleConstructorReturn
两个函数。篇幅所限,此处不再展开详述。
三、最后
语法糖 class
给我们带来了很多写法上的便利,但可能会使我们在代码体积上的优化努力“付诸东流”。
另一方面,如果你是一名 React 应用开发者,你是否已经在想将代码中的所有 class 写法换为 function 呢?那样做的话,代码体积无疑会减少很多,但你一定也知道 PureComponent 相比 Component 的好处。所以虽然 function 给你的代码体积减负了,但他在哪里又给你无形增加负担了呢?
因此,真的不推荐开发者用 class
这种写法么,你觉得呢?
四、更多阅读
Recommend
-
73
米饭是中国、日本和韩国等东亚地区最主要的粮食,米饭可与五味调配,几乎可以供给全身所需营养。留有胚与糊粉层的大米饭含有人体90%的必需营养元素,且各种营养素十分均衡。不过,用普通的锅烹制米饭,需要耗费较长的时间,而且稍不注意看守就会造成糊锅现象,耗时...
-
68
原标题:你在看支付宝账单,芝麻信用也许在看你的隐私今日,蚂蚁金服旗下支付宝2017年全民账单走心文案刷屏朋友圈:“时间总是偷偷流逝,打开账单,这一年是不是过得不太一样。”打开账单,个人2017年网购总支出、比去年多花多少钱、总出行次数、选择
-
49
-
22
窥探原理:手写一个 JavaScript 打包器
-
21
Webpack 转译 Typescript 现有方案 1. awesome-typescript-loader 这个 npm 包好久不更新了,而且类型检查的时...
-
35
我们先看一下使用 useState hooks写的todoList组件,里面我们需要层层传递回调函数。 import React, { useState } from "react"; const AddTodoBtn = ({ onAddTodo }) => ( <div className="action-ad...
-
8
Webpack 原理系列八:产物转译打包逻辑全文 6000 字,我们来聊聊打包闭环,欢迎点赞关注转发。回顾一下,在之前的文章《
-
6
导读:我们生活在一个多样的世界:丰富多样的操作系统、丰富多样的编程语言、丰富多样的技术栈,如此丰富多样的技术栈为软件提供商带来了的挑战:如何快速覆盖这些系统/技术栈以满足不同背景的用户的需求?本文基于网易云信的落地...
-
4
tars2node是用来将 tars协议文件 转换为Nodejs客户端/服务端代码文件的工具,仓库地址:https://github.com/tars-node/tars2node 我要说话 下面是主要内...
-
7
MacOS 13 支持转译 Linux x86_64 应用程序,使用条件苛刻-51CTO.COM
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK