ECMAScript 6+ vs TypeScript
- четверг, 25 января 2024 г. в 00:00:14
Минули те времена, когда разработчики писали Frontend на «чистом» JavaScript (вплоть до ECMAScript 5). Все изменилось с выходом в свет версии ECMAScript 6 в 2015-м году. Это событие стало, по истине значимым в мировой Frontend разработке. Предыдущие 6 лет до этого, язык практически не менялся. Годом ранее, в 2014-м, компания Microsoft опубликовала TypeScript 1.0 и предоставила встроенную поддержку языка в своей IDE VisualStudio 2013. На самом деле, официально, TypeScript был выпущен еще в 2012 (версия 0.8), однако, популярностью он не пользовался в виду практически полного отсутствия поддержки со стороны существующих, на тот момент, IDE.
С тех прошло много времени. Оба языка развивались параллельно. В чем‑то они схожи, в чем‑то кардинально различаются. Каждый разработчик и каждая команда вольна самостоятельно решать, какой из них использовать. В это статье попробуем найти точки, как пересечения, так и расхождения этих двух языков и сравним их подходы.
Типизацию TypeScript рассматривать в этой статье не будем, т.к. очевидно, что в ECMAScript её нет, и сравнивать тут нечего.
Для чистоты эксперимента код будем транспилировать в старый добрый ECMAScript 5. TypeScript, для удобства, возьмем версии 4.8.4 (этой версии, для целей статьи достаточно) и будем компилировать его родным tsc
компилятором. Для ECMAScript воспользуемся инструментарием Babel
.
Начнем с самого простого, с переменных. В этой части, оба языка имеют идентичный синтаксис.
let a = "";
const b = "";
var c = "";
// == babel ==
var a = "";
var b = "";
var c = "";
let a = "";
const b = "";
var c = "";
// == tsc ==
var a = "";
var b = "";
var c = "";
В обоих случаях, после транспиляции, переменные приводятся к единственному возможному варианту декларирования переменных в ECMAScript 5, посредством var.
Однако, мы знаем, что let и const отличаются от var тем, что область их видимости ограничена LexicalEnvironment.
Попробуем обернуть переменную let в простейший LexicalEnvironment — Block.
{
let a = "1";
}
a = "2";
// == babel ==
{
var _a = "1"; // babel добавил префикс "_" к переменной, таким образом,
// отделив её от глобальной переменной "a" ниже
}
a = "2";
{
let a = "1";
}
a = "2";
// == tsc ==
{
var a = "1"; // tsc не переименовал переменную и никаким другим образом
// не изолировал её от переопределения ниже.
}
a = "2";
Как видимо из примера, TypeScript никак не позаботился об изоляции переменной let в runtime. В этой части компилятор полностью полагается на compile time исключение. Значит ли это, что TypeScript не безопасен в runtime в принципе?
Давайте взглянем на следующий пример:
{
let a = "1";
setTimeout(() => {
console.log(a);
}, 0);
}
a = "2";
// == babel ==
{
var _a = "1";
setTimeout(function () {
console.log(_a);
}, 0);
}
a = "2";
С точки зрения ECMAScript переменная по прежнему «изолирована» и переопределения не произойдет.
{
let a = "1";
setTimeout(() => {
console.log(a);
}, 0);
}
a = "2";
// == tsc ==
{
var a_1 = "1";
setTimeout(function () {
console.log(a_1);
}, 0);
}
a = "2";
В этом случае, TypeScript учел факт того, что к переменной есть обращение в макротаске и позаботился о её изоляции, аналогично ECMAScript. С точки зрения разработчика, изоляция обеспечена, однако вопрос злонамеренных действий из вне, всё ещё остается открыт. По крайней мере, это чуть менее безопасно, чем в случае с ECMAScript.
C let разобрались, давайте теперь посмотрим на const.
const b = "1";
b = "2";
// == babel ==
function _readOnlyError(name) {
throw new TypeError('"' + name + '" is read-only');
}
var b = "1";
"2", _readOnlyError("b");
Babel не позволил переопределить константу и вернул исключение в runtime.
const b = "1";
b = "2";
// == tsc ==
const b = "1";
b = "2";
TypeScript, как и в случае с let, никак не позаботился о защите const в runtime, полагаясь на ошибку компиляции.
Еще одним нововведением ES6, стали стрелочные функции. Они отличаются от обычных отсутствием собственного контекста и ссылки на arguments.
const foo = () => {
this.a = "1"
}
// == babel ==
var _this = this;
var foo = function foo() {
_this.a = "1";
};
Здесь мы видим, что Babel заменил ссылку на контекст функции, перенаправив обращение в глобальный контекст.
const foo = () => {
this.a = "1"
}
// == tsc ==
var _this = this;
var foo = function foo() {
_this.a = "1";
};
Абсолютно идентичным образом поступил и tsc.
А как обстоят дела с массивом arguments функции?
const foo = () => {
console.log(arguments.length);
}
// == babel ==
var _arguments = typeof arguments === "undefined" ? void 0 : arguments;
var foo = function foo() {
console.log(_arguments.length);
};
Babel предусмотрительно проверил, нет ли переменной с таким именем в глобальном контексте, и если вдруг есть, он вернет ссылку на него. В противном случае массив будет не определен.
const foo = () => {
console.log(arguments.length);
}
// == tsc ==
var foo = function () {
console.log(arguments.length);
};
В случае с TypeScript, ссылка на arguments будет вести на реальный массив аргументов функции, исключение случится только в compile time.
Одним из важнейших нововведение ES6 были классы. Взглянем на простейшую реализацию класса.
class A {}
// == babel ==
function _typeof(o) {
"@babel/helpers - typeof";
return (
(_typeof =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (o) {
return typeof o;
}
: function (o) {
return o &&
"function" == typeof Symbol &&
o.constructor === Symbol &&
o !== Symbol.prototype
? "symbol"
: typeof o;
}),
_typeof(o)
);
}
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, _toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", { writable: false });
return Constructor;
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : String(i);
}
function _toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var A = /*#__PURE__*/ _createClass(function A() {
_classCallCheck(this, A);
});
Простое объявление класса, после трансипляции средствами Babel, приводит к довольно громоздкому коду в виде набора фабрик, чтобы обеспечить необходимый уровень безопасности.
class A {}
// == tsc ==
var A = /** @class */ (function () {
function A() {
}
return A;
}());
Та же самая инструкция на TypeScript создает всего несколько строк кода.
Дополним наш класс полями и методами.
class A {
propA = "1";
#probB = "2";
methodC() {}
#methodD() {}
}
// == babel ==
function _typeof(o) {
"@babel/helpers - typeof";
return (
(_typeof =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (o) {
return typeof o;
}
: function (o) {
return o &&
"function" == typeof Symbol &&
o.constructor === Symbol &&
o !== Symbol.prototype
? "symbol"
: typeof o;
}),
_typeof(o)
);
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a 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, _toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", { writable: false });
return Constructor;
}
function _classPrivateMethodInitSpec(obj, privateSet) {
_checkPrivateRedeclaration(obj, privateSet);
privateSet.add(obj);
}
function _classPrivateFieldInitSpec(obj, privateMap, value) {
_checkPrivateRedeclaration(obj, privateMap);
privateMap.set(obj, value);
}
function _checkPrivateRedeclaration(obj, privateCollection) {
if (privateCollection.has(obj)) {
throw new TypeError(
"Cannot initialize the same private elements twice on an object"
);
}
}
function _defineProperty(obj, key, value) {
key = _toPropertyKey(key);
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : String(i);
}
function _toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
var _propB = /*#__PURE__*/ new WeakMap();
var _methodD = /*#__PURE__*/ new WeakSet();
var A = /*#__PURE__*/ (function () {
function A() {
_classCallCheck(this, A);
_classPrivateMethodInitSpec(this, _methodD);
_defineProperty(this, "propA", "1");
_classPrivateFieldInitSpec(this, _propB, {
writable: true,
value: "2"
});
}
_createClass(A, [
{
key: "methodC",
value:
// private
function methodC() {}
// private
}
]);
return A;
})();
function _methodD2() {}
В данном классе присутствуют публичное свойство, приватное свойство, публичный метод и приватный метод. Приватные свойства и метода Bebel поместил в Weak‑коллекции. Публичныей же — настроил посредством Object.defineProperty
.
class A {
public propA = "1";
protected propB = "2";
private propC = "3";
public methodD() {}
protected methodE() {}
private methodF() {}
}
// == tsc ==
var A = /** @class */ (function () {
function A() {
this.propA = "1";
this.propB = "2";
this.propC = "3";
}
A.prototype.methodD = function () { };
A.prototype.methodE = function () { };
A.prototype.methodF = function () { };
return A;
}());
Не смотря на то, что TypeScript имеет чуть более гибкую и более традиционную объектную модель, итоговый код весьма примитивен. Свойства просто помещаются в функциональный контекст, а методы — в прототип. Никаких проверок и настроек свойств и методов тут не происходит, что позволяет нам в runtime, например, обратиться к protected свойству или вызвать private‑метод из класса‑наследника.
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) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var A = /** @class */ (function () {
function A() {
this.propA = "1";
this.propB = "2";
this.propC = "3";
}
A.prototype.methodD = function () { };
A.prototype.methodE = function () { };
A.prototype.methodF = function () { };
return A;
}());
var B = /** @class */ (function (_super) {
__extends(B, _super);
function B() {
return _super !== null && _super.apply(this, arguments) || this;
}
B.prototype.methodG = function () {
this.methodF();
};
return B;
}(A));
В примере выше, класс B, фактически, имеет доступ к приватному методу родителя через свою ссылку на контекст.
В версии ES7 появились асинхронные операторы await и async. Посмотрим, как они устроены.
async function foo() {
await Promise.resolve();
}
// == babel ==
function _typeof(o) {
"@babel/helpers - typeof";
return (
(_typeof =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (o) {
return typeof o;
}
: function (o) {
return o &&
"function" == typeof Symbol &&
o.constructor === Symbol &&
o !== Symbol.prototype
? "symbol"
: typeof o;
}),
_typeof(o)
);
}
function _regeneratorRuntime() {
"use strict";
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime =
function _regeneratorRuntime() {
return e;
};
var t,
e = {},
r = Object.prototype,
n = r.hasOwnProperty,
o =
Object.defineProperty ||
function (t, e, r) {
t[e] = r.value;
},
i = "function" == typeof Symbol ? Symbol : {},
a = i.iterator || "@@iterator",
c = i.asyncIterator || "@@asyncIterator",
u = i.toStringTag || "@@toStringTag";
function define(t, e, r) {
return (
Object.defineProperty(t, e, {
value: r,
enumerable: !0,
configurable: !0,
writable: !0
}),
t[e]
);
}
try {
define({}, "");
} catch (t) {
define = function define(t, e, r) {
return (t[e] = r);
};
}
function wrap(t, e, r, n) {
var i = e && e.prototype instanceof Generator ? e : Generator,
a = Object.create(i.prototype),
c = new Context(n || []);
return o(a, "_invoke", { value: makeInvokeMethod(t, r, c) }), a;
}
function tryCatch(t, e, r) {
try {
return { type: "normal", arg: t.call(e, r) };
} catch (t) {
return { type: "throw", arg: t };
}
}
e.wrap = wrap;
var h = "suspendedStart",
l = "suspendedYield",
f = "executing",
s = "completed",
y = {};
function Generator() {}
function GeneratorFunction() {}
function GeneratorFunctionPrototype() {}
var p = {};
define(p, a, function () {
return this;
});
var d = Object.getPrototypeOf,
v = d && d(d(values([])));
v && v !== r && n.call(v, a) && (p = v);
var g =
(GeneratorFunctionPrototype.prototype =
Generator.prototype =
Object.create(p));
function defineIteratorMethods(t) {
["next", "throw", "return"].forEach(function (e) {
define(t, e, function (t) {
return this._invoke(e, t);
});
});
}
function AsyncIterator(t, e) {
function invoke(r, o, i, a) {
var c = tryCatch(t[r], t, o);
if ("throw" !== c.type) {
var u = c.arg,
h = u.value;
return h && "object" == _typeof(h) && n.call(h, "__await")
? e.resolve(h.__await).then(
function (t) {
invoke("next", t, i, a);
},
function (t) {
invoke("throw", t, i, a);
}
)
: e.resolve(h).then(
function (t) {
(u.value = t), i(u);
},
function (t) {
return invoke("throw", t, i, a);
}
);
}
a(c.arg);
}
var r;
o(this, "_invoke", {
value: function value(t, n) {
function callInvokeWithMethodAndArg() {
return new e(function (e, r) {
invoke(t, n, e, r);
});
}
return (r = r
? r.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg)
: callInvokeWithMethodAndArg());
}
});
}
function makeInvokeMethod(e, r, n) {
var o = h;
return function (i, a) {
if (o === f) throw new Error("Generator is already running");
if (o === s) {
if ("throw" === i) throw a;
return { value: t, done: !0 };
}
for (n.method = i, n.arg = a; ; ) {
var c = n.delegate;
if (c) {
var u = maybeInvokeDelegate(c, n);
if (u) {
if (u === y) continue;
return u;
}
}
if ("next" === n.method) n.sent = n._sent = n.arg;
else if ("throw" === n.method) {
if (o === h) throw ((o = s), n.arg);
n.dispatchException(n.arg);
} else "return" === n.method && n.abrupt("return", n.arg);
o = f;
var p = tryCatch(e, r, n);
if ("normal" === p.type) {
if (((o = n.done ? s : l), p.arg === y)) continue;
return { value: p.arg, done: n.done };
}
"throw" === p.type && ((o = s), (n.method = "throw"), (n.arg = p.arg));
}
};
}
function maybeInvokeDelegate(e, r) {
var n = r.method,
o = e.iterator[n];
if (o === t)
return (
(r.delegate = null),
("throw" === n &&
e.iterator.return &&
((r.method = "return"),
(r.arg = t),
maybeInvokeDelegate(e, r),
"throw" === r.method)) ||
("return" !== n &&
((r.method = "throw"),
(r.arg = new TypeError(
"The iterator does not provide a '" + n + "' method"
)))),
y
);
var i = tryCatch(o, e.iterator, r.arg);
if ("throw" === i.type)
return (r.method = "throw"), (r.arg = i.arg), (r.delegate = null), y;
var a = i.arg;
return a
? a.done
? ((r[e.resultName] = a.value),
(r.next = e.nextLoc),
"return" !== r.method && ((r.method = "next"), (r.arg = t)),
(r.delegate = null),
y)
: a
: ((r.method = "throw"),
(r.arg = new TypeError("iterator result is not an object")),
(r.delegate = null),
y);
}
function pushTryEntry(t) {
var e = { tryLoc: t[0] };
1 in t && (e.catchLoc = t[1]),
2 in t && ((e.finallyLoc = t[2]), (e.afterLoc = t[3])),
this.tryEntries.push(e);
}
function resetTryEntry(t) {
var e = t.completion || {};
(e.type = "normal"), delete e.arg, (t.completion = e);
}
function Context(t) {
(this.tryEntries = [{ tryLoc: "root" }]),
t.forEach(pushTryEntry, this),
this.reset(!0);
}
function values(e) {
if (e || "" === e) {
var r = e[a];
if (r) return r.call(e);
if ("function" == typeof e.next) return e;
if (!isNaN(e.length)) {
var o = -1,
i = function next() {
for (; ++o < e.length; )
if (n.call(e, o))
return (next.value = e[o]), (next.done = !1), next;
return (next.value = t), (next.done = !0), next;
};
return (i.next = i);
}
}
throw new TypeError(_typeof(e) + " is not iterable");
}
return (
(GeneratorFunction.prototype = GeneratorFunctionPrototype),
o(g, "constructor", {
value: GeneratorFunctionPrototype,
configurable: !0
}),
o(GeneratorFunctionPrototype, "constructor", {
value: GeneratorFunction,
configurable: !0
}),
(GeneratorFunction.displayName = define(
GeneratorFunctionPrototype,
u,
"GeneratorFunction"
)),
(e.isGeneratorFunction = function (t) {
var e = "function" == typeof t && t.constructor;
return (
!!e &&
(e === GeneratorFunction ||
"GeneratorFunction" === (e.displayName || e.name))
);
}),
(e.mark = function (t) {
return (
Object.setPrototypeOf
? Object.setPrototypeOf(t, GeneratorFunctionPrototype)
: ((t.__proto__ = GeneratorFunctionPrototype),
define(t, u, "GeneratorFunction")),
(t.prototype = Object.create(g)),
t
);
}),
(e.awrap = function (t) {
return { __await: t };
}),
defineIteratorMethods(AsyncIterator.prototype),
define(AsyncIterator.prototype, c, function () {
return this;
}),
(e.AsyncIterator = AsyncIterator),
(e.async = function (t, r, n, o, i) {
void 0 === i && (i = Promise);
var a = new AsyncIterator(wrap(t, r, n, o), i);
return e.isGeneratorFunction(r)
? a
: a.next().then(function (t) {
return t.done ? t.value : a.next();
});
}),
defineIteratorMethods(g),
define(g, u, "Generator"),
define(g, a, function () {
return this;
}),
define(g, "toString", function () {
return "[object Generator]";
}),
(e.keys = function (t) {
var e = Object(t),
r = [];
for (var n in e) r.push(n);
return (
r.reverse(),
function next() {
for (; r.length; ) {
var t = r.pop();
if (t in e) return (next.value = t), (next.done = !1), next;
}
return (next.done = !0), next;
}
);
}),
(e.values = values),
(Context.prototype = {
constructor: Context,
reset: function reset(e) {
if (
((this.prev = 0),
(this.next = 0),
(this.sent = this._sent = t),
(this.done = !1),
(this.delegate = null),
(this.method = "next"),
(this.arg = t),
this.tryEntries.forEach(resetTryEntry),
!e)
)
for (var r in this)
"t" === r.charAt(0) &&
n.call(this, r) &&
!isNaN(+r.slice(1)) &&
(this[r] = t);
},
stop: function stop() {
this.done = !0;
var t = this.tryEntries[0].completion;
if ("throw" === t.type) throw t.arg;
return this.rval;
},
dispatchException: function dispatchException(e) {
if (this.done) throw e;
var r = this;
function handle(n, o) {
return (
(a.type = "throw"),
(a.arg = e),
(r.next = n),
o && ((r.method = "next"), (r.arg = t)),
!!o
);
}
for (var o = this.tryEntries.length - 1; o >= 0; --o) {
var i = this.tryEntries[o],
a = i.completion;
if ("root" === i.tryLoc) return handle("end");
if (i.tryLoc <= this.prev) {
var c = n.call(i, "catchLoc"),
u = n.call(i, "finallyLoc");
if (c && u) {
if (this.prev < i.catchLoc) return handle(i.catchLoc, !0);
if (this.prev < i.finallyLoc) return handle(i.finallyLoc);
} else if (c) {
if (this.prev < i.catchLoc) return handle(i.catchLoc, !0);
} else {
if (!u) throw new Error("try statement without catch or finally");
if (this.prev < i.finallyLoc) return handle(i.finallyLoc);
}
}
}
},
abrupt: function abrupt(t, e) {
for (var r = this.tryEntries.length - 1; r >= 0; --r) {
var o = this.tryEntries[r];
if (
o.tryLoc <= this.prev &&
n.call(o, "finallyLoc") &&
this.prev < o.finallyLoc
) {
var i = o;
break;
}
}
i &&
("break" === t || "continue" === t) &&
i.tryLoc <= e &&
e <= i.finallyLoc &&
(i = null);
var a = i ? i.completion : {};
return (
(a.type = t),
(a.arg = e),
i
? ((this.method = "next"), (this.next = i.finallyLoc), y)
: this.complete(a)
);
},
complete: function complete(t, e) {
if ("throw" === t.type) throw t.arg;
return (
"break" === t.type || "continue" === t.type
? (this.next = t.arg)
: "return" === t.type
? ((this.rval = this.arg = t.arg),
(this.method = "return"),
(this.next = "end"))
: "normal" === t.type && e && (this.next = e),
y
);
},
finish: function finish(t) {
for (var e = this.tryEntries.length - 1; e >= 0; --e) {
var r = this.tryEntries[e];
if (r.finallyLoc === t)
return this.complete(r.completion, r.afterLoc), resetTryEntry(r), y;
}
},
catch: function _catch(t) {
for (var e = this.tryEntries.length - 1; e >= 0; --e) {
var r = this.tryEntries[e];
if (r.tryLoc === t) {
var n = r.completion;
if ("throw" === n.type) {
var o = n.arg;
resetTryEntry(r);
}
return o;
}
}
throw new Error("illegal catch attempt");
},
delegateYield: function delegateYield(e, r, n) {
return (
(this.delegate = { iterator: values(e), resultName: r, nextLoc: n }),
"next" === this.method && (this.arg = t),
y
);
}
}),
e
);
}
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _asyncToGenerator(fn) {
return function () {
var self = this,
args = arguments;
return new Promise(function (resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
function foo() {
return _foo.apply(this, arguments);
}
function _foo() {
_foo = _asyncToGenerator(
/*#__PURE__*/ _regeneratorRuntime().mark(function _callee() {
return _regeneratorRuntime().wrap(function _callee$(_context) {
while (1)
switch ((_context.prev = _context.next)) {
case 0:
_context.next = 2;
return Promise.resolve();
case 2:
case "end":
return _context.stop();
}
}, _callee);
})
);
return _foo.apply(this, arguments);
}
Для реализации такой простой, с виду, конструкции, Babel имплементирует механизм генератором с набором проверок и настроек, что приводит к итоговому листингу в 490 строк кода.
async function foo() {
await Promise.resolve();
}
// == tsc ==
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
function foo() {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, Promise.resolve()];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
}
TypeScript тоже использует, в этом случае, генераторы, но импелементация гораздо проще и итоговый листинг — всего 48 строк.
Spread‑оператор появился в версии ES9. До этого, традиционно, по необходимости, применялся метод Object.assign
, но с появлением spread и rest операторов код стал выглядеть примерно вот так:
const a = {...({})}
// == babel ==
function _typeof(o) {
"@babel/helpers - typeof";
return (
(_typeof =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (o) {
return typeof o;
}
: function (o) {
return o &&
"function" == typeof Symbol &&
o.constructor === Symbol &&
o !== Symbol.prototype
? "symbol"
: typeof o;
}),
_typeof(o)
);
}
function ownKeys(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r &&
(o = o.filter(function (r) {
return Object.getOwnPropertyDescriptor(e, r).enumerable;
})),
t.push.apply(t, o);
}
return t;
}
function _objectSpread(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2
? ownKeys(Object(t), !0).forEach(function (r) {
_defineProperty(e, r, t[r]);
})
: Object.getOwnPropertyDescriptors
? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t))
: ownKeys(Object(t)).forEach(function (r) {
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
});
}
return e;
}
function _defineProperty(obj, key, value) {
key = _toPropertyKey(key);
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : String(i);
}
function _toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
var a = _objectSpread({}, {});
В случае с Babel, здесь создается функция _objectSpread
, которая обходит объект по его ключам и клонирует значения.
const a = {...({})}
// == tsc ==
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var a = __assign({}, ({}));
TypeScript же, пытается использовать классический Object.assign
, и только если его нет в данном окружении, имплементируется простой алгоритм обхода ключей объекта.
В статье приведен даклеко не полный список всех возможностей и нововведений в специфакацию ECMAScriptи документацию TypeScript. Здесь мы рассмотрели только те моменты, в которых могут расходиться, с точки зрения реализации.
В целом, резюмируя все выше сказанное, ES6+ (в частности, реализованный посредством Babel‑парсера) генерирует относительно безопасный итоговый код, более стойкий ошибкам разработки и к злонамеренному вмешательству. Однако, цена тому — громоздкие конструкции и, как следствие, скорость генерации.
TypeScript же, в противовес, придерживается подхода к валидации на этапе compile time, т. е. код с ошибками, в теории, не должен быть собран, или, по крайней мере, об ошибках будет сразу сообщено. Это позволяет опустить всевозможные проверки на уровне runtime, что делает итоговый код гораздо меньше и легче. Однако, всегда остается, вполне осязаемая опасность недобросовестной разработки (type-check
можно проигнорировать) и злонамеренного вмешательства.
Что считать лучшим подходом — вопрос сложный. Каждый разработчик и каждая команда решает его самостоятельно, исходя из своих собственных реалий. Часто, решение основывается на функциональных возможностях языка. Так, по мимо реализации возможностей стандарта ECMAScript, TypeScript имеет большие возможности в части типизации (собственно, это его прямо назначение), чего лишен ES6+. Этого критерия, часто, достаточно, чтобы сделать выбор в его пользу. С другой стороны, этот же плюс, не редко может оказаться минусом, так как требует определенных навыков и компетенций в команде разработки.
Между тем, оба варианта были и остаются востребованными, а работы по их совершенствованию будут продолжаться дальше, делая жизнь простого разработчика лучше.
Эту и другие мои статьи, так же, читайте в моем канале
RU: https://t.me/frontend_almanac_ru
EN: https://t.me/frontend_almanac