Herylee

Test262 是 JavaScript的一个辅助

Herylee · 2016-11-25翻译 · 717阅读 原文链接

Sue Lockwood说明

TC-39,该标准定义的JavaScript,维持着对该语言大量的成套的测试。测试组的名字是Test262当我们开始延伸Test262替代全新的语言时,我们知道我们必定会有些惊喜。 即使如此,我们从来没有预料到我们会发现这些震惊的。

JavaScript是一个功能强大的语言。它提供的抽象允许开发人员以少量的(通常是可读的)代码来表达复杂的算法,这些语言的特征有时以怪异的方式进行交互。自从开始与V8团队合作,在18个月以来我们一次又一次的见证这一现象

在这篇博客中,我们已经组建了一些怪异的测试我们贡献-一堆我们自己有问题的代码。所有都是合法的JavaScript,由义,ECMA262完全定义语言规范。

所以今天,我将是你的导游,指引你通过语言的怪诞角落。不要顾虑,如果你心脏不好,请考虑不参加。

辅助

展示 #1: 重构和生成

首先,考虑解构的任务(spec, MDN)。如代码[x, y] = [1, 2],将值1赋给x,2赋给y。在这个场景下,运行时预先检索迭代数组[ 1,2 ],并按着顺序依次迭代“绑定”(XY)。

它远比知道的更复杂。就像在非解构的任务,目标本身也可以是复杂的表达式。想象[foo.bar] = [3]或者甚至[baz["q" + "ux"]] = [4]。根据这些表达式的结果,上面提到的迭代可以被中断。我们首先演示它是如何混乱,具体见下:

var returnCount = 0;
var unreachable = 0;
var iterable = {};
var iterator = {
   return: function() {
     returnCount += 1;
     return {};
   }
};
var iter, result;
iterable[Symbol.iterator] = function() {
  return iterator;
};

function* g() {
  [ {}[yield] ] = iterable;
  unreachable += 1;
}
iter = g();
iter.next();
result = iter.return(777);

assert.sameValue(returnCount, 1);
assert.sameValue(unreachable, 0, 'Unreachable statement was not executed');
assert.sameValue(result.value, 777);
assert(result.done, 'Iterator correctly closed');

(来源:language/expressions/assignment/dstr-array-elem-iter-rtrn-close.js)

在这里,我们已经把解构分配到函数发生器(spec, MDN)和包含在迭代过程中计算的yield表达式。

当迭代在这一步中断时,所有这些允许我们断定预期的行为。调用返回触发器创建一个return completion,这类似于一个由一个 throw声明(我们也对它进行了测试) 创建的throw completion

他们说,“情人眼里出西施,”但是你很难发现任何人愿意长时间关注这件事。

展示 #2: 尾部调用优化和标签模板

有时会由于简化,称为“TCO”,尾调用优化(规范功能说明)允许递归的函数调用在它们真正执行完毕之前清除自身(调用栈信息)。这是函数以正确的方式创立,自动发生的,,它对以计算为代价的算法非常有用,这种算法使用“分裂和战胜”的策略。

在这里,我们确保优化是在函数使用es2015的新“标签模板”特性调用时发生(spec, MDN):

(function() {
  var finished = false;
  function getF() {
    return f;
  }
  function f(_, n) {
    if (n === 0) {
      finished = true;
      return;
    }
    return getF()${n-1};
  }
  f(null, 100000);
  return finished;
}());

(来源language/expressions/tagged-template/tco-call.js)

正如没有特殊的语法使TCO有效,同样没有现成的方法来确保它的发生。所以,在这个测试中,我们确保TCO通过循环调用很多次时发生。如果不能优化,这个测试将会失败,因为程序会耗尽可用的存储资源,崩溃。这使测试有点粗鲁,但你不能指望一个已经被自然遗忘的生物有好的行为。

(顺便提一下,同样隐含的(使测试很难读写)[已经引起了大范围]https://github.com/tc39/tc39-notes/blob/2b9722db9b90011d6083a5f1c8ff1559cbe01c0b/es7/2016-05/may-24.md#syntactic-tail-calls-bt)关于改进这个功能需要清楚的语法的讨论

展示 #3: 数组类型和反射

当你从其他数组类型创建一个数组,通常你会期望新创建的继承同一个类。例如,new Int8Array(anotherInt8Array)产生第二个Int8Array。微妙的是,构造函数可以由actually定义的NewTarget值的确定(spec, MDN)。

这种差别一般不重要,大部分应用使用new关键字调用构造函数。这样可以设置构造函数的NewTarget值,会导致上面描述Int8Array例子的行为。

然而!ES2015引入Reflect API (spec, MDN),而且Reflect.construct允许NewTarget使用任意值调用构造函数。我们正在做在下面的测试:

function newTarget() {}
newTarget.prototype = null;

var sample = new Int8Array(8);

var ta = Reflect.construct(Int8Array, [sample], newTarget);

assert.sameValue(ta.constructor, Int8Array);
assert.sameValue(Object.getPrototypeOf(ta), Int8Array.prototype);

(来源built-ins/TypedArrays/typedarray-arg-use-default-proto-if-custom-proto-is-not-object.js)

“但是等待下!” 你正在说,“NewTarget的值不在被重视!ta的原型仍然是Int8Array的原型。”嗯,这就是在附属中得到这个试验的详细信息。不论什么缘由,如果NewTarget值有一个没有对象的原型(正如这个所示),那么标准指向的“激活函数”(这里是Int8Array)应该使用替代的。呱呱。

我们即将进入“Promise”所在的部分。我希望你已经准备好了,因为这一部分成两部分展示。

展示 #4: Promises和数组

Promises (spec, MDN) 是来实现异步操作的有力途径。管理这些操作其中一个重要的方法是Promise.all:一旦许多操作成功地完成,它允许定义什么应该发生的。

这基本上是一种方便的方法;这是JavaScript开发者可以编写(写过)自己的。它的行为规范反映了这一行为,因为它使用一个类似所谓的“ use land”版本的方法。

具体而言:该方法期望将创建一个具有“分辨率值”的数组,并通过该数组的内部承诺机制。后来,这个数组本身解释为一个分辨率值。因为这些值可能表示更多的异步操作(在这种情况下,他们称之为“thenables”),以Array奇异的扩展可以达到惊人结果…

var value = {};
var nonThenable = [];
var promise;
nonThenable.then = null;

promise = Promise.all(nonThenable);

Object.defineProperty(Array.prototype, 'then', {
  get: function() {
    throw value;
  }
});

promise.then(function() {
    $DONE('The promise should not be fulfilled.');
  }, function(val) {
    if (val !== value) {
      $DONE('The promise should be rejected with the expected value.');
      return;
    }

    $DONE();
  });

(来源:built-ins/Promise/all/resolve-poisoned-then.js)

我们传递一个空数组至Promise.resolve,因为我们实际上对对这个测试的控制流不感兴趣。

当涉及到解析promise,运行时解析隐藏的数组,比如其他任何解析值。运行时对它创建这个数组本身没有特别的考虑,所以它是从检查值是否是“thenable”的开始。在这个测试中,我们用一个“有毒的”then属性 -被访问时抛出一个错误的 - 破坏Array原型。这就是为什么我们期望Promise被拒绝。

虽然可能不容易看到为什么运行时拒绝这个Promise,它是显而易见的,为什么社会拒绝了这个测试。

展示 #5: Promises 和 …恩,更多Promises

Promises的最有力的方面之一是他们是"链”在一起的能力。通过使用一个Promise解析另一个Promise,我们能够安排的事件以一种非常自然的方式发生。(我们知道在上面的展示中看到这种安排)

天真的设计,这个特性可以产生灾难性的错误:如果一个Promise是参照itself解析,在运行时可能进入一个无限循环。这听起来有点抽象,但它并不难证明:

// The arrow function is invoked asynchronously, *after*
// the promise has been created and assigned to p.
var p = Promise.resolve().then(() => p);

幸运的是,ES2015不是幼稚的设计。这里有针对这种情况的具体的专门监控,所以在上面的例子中,运行时调用箭头函数,确认返回值和promise本身一样,然后用一个TypeError拒绝p

Promise.prototype.then必须在这种情况下保护,因为提供的回调是异步调用的,随后创建promise。

将这种情况和Promise.resolve相比。Promise.resolve同步创建一个Promise,而这个Promise可以被一些值解析。第一次看到,“自我解决”的问题似乎并没有相关

// The promise is created *before* the value is assigned to
// p1, so this promise is resolved with the value undefined.
var p1 = Promise.resolve(p1);

// ...and unlike Promise.prototype.then, Promise.resolve
// doesn't invoke function arguments, so this promise is
// resolved with the arrow function (not its return value).
var p2 = Promise.resolve(() => p2);

信不信由你, Promise.resolve可以返回一个已经被自己解析的Promise。它只是需要一点努力:

var resolve, reject;
var only = new Promise(function(_resolve, _reject) {
  resolve = _resolve;
  reject = _reject;
});
var P = function(executor) {
  executor(resolve, reject);
  return only;
};

Promise.resolve.call(P, only)
  .then(function() {
    $DONE('The promise should not be fulfilled.');
  }, function(value) {
    if (!value) {
      $DONE('The promise should be rejected with a value.');
      return;
    }
    if (value.constructor !== TypeError) {
      $DONE('The promise should be rejected with a TypeError instance.');
      return;
    }

    $DONE();
  });

(来源built-ins/Promise/resolve/resolve-self.js)

为了证明这一个行为,我们必须创建一个定制的Promise构造函数。这个“构造体”返回一个有效的Promise实例,但是它总是返回一个命名为only一样的Promise实例。所以当我们使用构造函数作为Promise.resolvethis值(通过Promise.prototype.call- spec, MDN),Promise.resolve试图创建一个新的Promise,但是不能使用onlyPromise实例。因为我们也指定onlypromise作为解析的值,这同样触发了在上面看到的监控,将导致预料的TypeError。 Okay, the Promises are getting anxious. We should move along. 好吧,Promise使我们变得忧虑。我们应该一起前进。

展示 #6:默认参数 和 eval

一个在ES2015更激进的增加是默认参数(spec,MDN。我确信很多人会不同意,但是我认为ES2015是如此严厉,因为它允许任意表达式在一个完全新的位置。当涉及到定义这个特性,简单地说是不足够的,“现在你可以在这里写表达,玩得很开心。”这个标准,作者认真地考虑了所有可能隐含的地方,然后他们指出一些出乎人意料的边缘案例。当然,这些需要测试。

在这些事情中,你可能会在一个默认参数值的位置写是一个直接eval(spec, MDN)。这个饱受诟病的特性有一个独特的属性:它是一个表达式,可以创建一个变量绑定。这意味着当一个默认的参数本身创建一个变量,我们需要定义(和测试)会发生什么。

var x = 'outside';
var probe1, probe2, probeBody;

(function(
    _ = (eval('var x = "inside";'), probe1 = function() { return x; }),
    __ = probe2 = function() { return x; }
  ) {
  probeBody = function() { return x; };
}());

assert.sameValue(probe1(), 'inside');
assert.sameValue(probe2(), 'outside');
assert.sameValue(probeBody(), 'outside');

(来源language/expressions/function/scope-param-elem-var-close.js

该规范指出,对于默认的参数应该有一个专用的变量范围。这进一步说明,每个参数应该有它自己的变化范围。这就是为什么在测试的过程中,我们定义了probe1probe2;只有probe1可以看到由eval创建的x。实现者仅仅创建一个由所有参数共享的简单作用域不会通过这个测试。

…总结今天这些内容通过这个辅助。如果你热血澎湃,可以了解这个核心内容

让一切可控的价值

呆呆的看着一个编程语言的特点,会很有趣 - (Gary Bernhardt)。而一种健康和批判意识是对所有编程者的一个要求,这篇文章不是为了取笑JavaScript。

很多年前,Bocoup的拥有者Ben Alman写下try陈述这个特殊地方在跨浏览器的不同。那时,它似乎很容易了一个bug,找到它的一种方式进入Web浏览器。不久后,一些专业的web站点/图书馆开始依靠这种行为,而且从那时起,bug阻碍着web平台

在Test262,我们有一个工具来自动验证规范一致性非常精确。不仅如此,在早期功能的写作测试的练习经常暴露在他们的设计中的错误和含糊之处。

如果这个奇怪的现象是任何指示的话,那么测试组不会阻止怪人找到他们自己语言的路… 但这不是目标。旨在收集 JavaScript 特性中的所有怪行为,Test262 几乎能让我们彻底理解每个边界 case 并接受这些结果,这样我们就不会在我们的实际应用代码里踩坑了。考虑投递的是一个受人尊敬的不断完善的网络平台。

相关文章