Bug 937855 - Implement Object.assign. Patch is a tag-team effort with till. Also add tests: most courtesy of André Bargull, some from me, some suggested by jwalden. r=jwalden

--HG--
extra : rebase_source : 4dbbb9de2a833769d34e5ad80d041d4e6de9ab18
This commit is contained in:
Nathan Braswell 2014-08-06 16:55:18 -07:00
Родитель 0d41d157de
Коммит e545fb34ce
5 изменённых файлов: 401 добавлений и 0 удалений

Просмотреть файл

@ -320,6 +320,7 @@ selfhosting_srcs := \
$(srcdir)/builtin/Iterator.js \
$(srcdir)/builtin/Map.js \
$(srcdir)/builtin/Number.js \
$(srcdir)/builtin/Object.js \
$(srcdir)/builtin/String.js \
$(srcdir)/builtin/Set.js \
$(srcdir)/builtin/TypedObject.js \

Просмотреть файл

@ -1233,6 +1233,7 @@ const JSFunctionSpec js::object_static_methods[] = {
* in a different array.
*/
const JSFunctionSpec js::object_static_selfhosted_methods[] = {
JS_SELF_HOSTED_FN("assign", "ObjectStaticAssign", 2,0),
JS_FS_END
};

67
js/src/builtin/Object.js Normal file
Просмотреть файл

@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* ES6 draft 2014-07-18 19.1.2.1. */
function ObjectStaticAssign(target, firstSource) {
// Steps 1-2.
var to = ToObject(target);
// Step 3.
if (arguments.length < 2)
return to;
// Step 4.
var i = 1;
do {
// Step 5.a-b.
var nextSource = arguments[i];
var from = ToObject(nextSource);
// Step 5.c-d.
var keysArray = std_Object_getOwnPropertyNames(from);
// Steps 5.e-f.
var len = keysArray.length;
// Step 5.h.
var nextIndex = 0;
// Step 5.i (Modified a bit because we can't catch and store the
// actual Completion Record). Instead we have a marker object.
const MISSING = {};
var pendingException = MISSING;
// Step 5.j.
while (nextIndex < len) {
// Step 5.j.i-ii.
var nextKey = keysArray[nextIndex];
// Step 5.j.iii-v.
try {
// We'd like to use Object.propertyIsEnumerable here, but we
// can't, because if a property doesn't exist, it won't properly
// call getOwnPropertyDescriptor (important if |from| is a
// proxy).
var desc = std_Object_getOwnPropertyDescriptor(from, nextKey);
if (desc !== undefined && desc.enumerable)
to[nextKey] = from[nextKey];
} catch (e) {
if (pendingException === MISSING)
pendingException = e;
}
// Step 5.j.vi.
nextIndex++;
}
// Step 5.k.
if (pendingException !== MISSING)
throw pendingException;
i++;
} while (i < arguments.length);
// Step 6.
return to;
}

Просмотреть файл

@ -64,6 +64,7 @@ var std_Object_create = Object.create;
var std_Object_getOwnPropertyNames = Object.getOwnPropertyNames;
var std_Object_hasOwnProperty = Object.prototype.hasOwnProperty;
var std_Object_getPrototypeOf = Object.getPrototypeOf;
var std_Object_getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
var std_RegExp_test = RegExp.prototype.test;
var std_String_fromCharCode = String.fromCharCode;
var std_String_charCodeAt = String.prototype.charCodeAt;

Просмотреть файл

@ -0,0 +1,331 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
function checkDataProperty(object, propertyKey, value, writable, enumerable, configurable) {
var desc = Object.getOwnPropertyDescriptor(object, propertyKey);
assertEq(desc === undefined, false);
assertEq('value' in desc, true);
assertEq(desc.value, value);
assertEq(desc.writable, writable);
assertEq(desc.enumerable, enumerable);
assertEq(desc.configurable, configurable);
}
/* 19.1.2.1 Object.assign ( target, ...sources ) */
assertEq(Object.assign.length, 2);
// Basic functionality works with multiple sources
function basicMultipleSources() {
var a = {};
var b = { bProp : 7 };
var c = { cProp : 8 };
Object.assign(a, b, c);
assertEq(a.bProp, 7);
assertEq(a.cProp, 8);
}
basicMultipleSources();
// Calls ToObject() for target and source
function testToObject() {
assertThrowsInstanceOf(() => Object.assign(null, null), TypeError);
assertThrowsInstanceOf(() => Object.assign(), TypeError);
assertThrowsInstanceOf(() => Object.assign(null, {}), TypeError);
assertThrowsInstanceOf(() => Object.assign({}, null), TypeError);
assertEq(Object.assign(true, {}) instanceof Boolean, true);
assertEq(Object.assign(1, {}) instanceof Number, true);
assertEq(Object.assign("string", {}) instanceof String, true);
var o = {};
assertEq(Object.assign(o, {}), o);
}
testToObject();
// Invokes [[OwnPropertyKeys]] on ToObject(source)
function testOwnPropertyKeys() {
assertThrowsInstanceOf(() => Object.assign(null, new Proxy({}, {
getOwnPropertyNames: () => { throw new Error("not called"); }
})), TypeError);
var ownKeysCalled = false;
Object.assign({}, new Proxy({}, {
ownKeys: function() {
ownKeysCalled = true;
return [];
}
}));
assertEq(ownKeysCalled, true);
};
testOwnPropertyKeys();
// Ensure correct property traversal
function correctPropertyTraversal() {
var log = "";
var source = new Proxy({a: 1, b: 2}, {
ownKeys: () => ["b", "c", "a"],
getOwnPropertyDescriptor: function(t, pk) {
log += "#" + pk;
return Object.getOwnPropertyDescriptor(t, pk);
},
get: function(t, pk, r) {
log += "-" + pk;
return t[pk];
},
});
Object.assign({}, source);
assertEq(log, "#b-b#c#a-a");
}
correctPropertyTraversal();
// Only [[Enumerable]] properties are assigned to target
function onlyEnumerablePropertiesAssigned() {
var source = Object.defineProperties({}, {
a: {value: 1, enumerable: true},
b: {value: 2, enumerable: false},
});
var target = Object.assign({}, source);
assertEq("a" in target, true);
assertEq("b" in target, false);
}
onlyEnumerablePropertiesAssigned();
// Enumerability is decided on-time, not before main loop (1)
function testEnumerabilityDeterminedInLoop1()
{
var getterCalled = false;
var sourceTarget = {
get a() { getterCalled = true },
get b() { Object.defineProperty(sourceTarget, "a", {enumerable: false}) },
};
var source = new Proxy(sourceTarget, { ownKeys: () => ["b", "a"] });
Object.assign({}, source);
assertEq(getterCalled, false);
}
testEnumerabilityDeterminedInLoop1();
// Enumerability is decided on-time, not before main loop (2)
function testEnumerabilityDeterminedInLoop2()
{
var getterCalled = false;
var sourceTarget = {
get a() { getterCalled = true },
get b() { Object.defineProperty(sourceTarget, "a", {enumerable: true}) },
};
var source = new Proxy(sourceTarget, {
ownKeys: () => ["b", "a"]
});
Object.defineProperty(sourceTarget, "a", {enumerable: false});
Object.assign({}, source);
assertEq(getterCalled, true);
}
testEnumerabilityDeterminedInLoop2();
// Properties are retrieved through Get() and assigned onto
// the target as data properties, not in any sense cloned over as descriptors
function testPropertiesRetrievedThroughGet() {
var getterCalled = false;
Object.assign({}, {get a() { getterCalled = true }});
assertEq(getterCalled, true);
}
testPropertiesRetrievedThroughGet();
// Properties are retrieved through Get()
// Properties are assigned through Put()
function testPropertiesAssignedThroughPut() {
var setterCalled = false;
Object.assign({set a(v) { setterCalled = v }}, {a: true});
assertEq(setterCalled, true);
}
testPropertiesAssignedThroughPut();
// Properties are retrieved through Get()
// Properties are assigned through Put(): Existing property attributes are not altered
function propertiesAssignedExistingNotAltered() {
var source = {a: 1, b: 2, c: 3};
var target = {a: 0, b: 0, c: 0};
Object.defineProperty(target, "a", {enumerable: false});
Object.defineProperty(target, "b", {configurable: false});
Object.defineProperty(target, "c", {enumerable: false, configurable: false});
Object.assign(target, source);
checkDataProperty(target, "a", 1, true, false, true);
checkDataProperty(target, "b", 2, true, true, false);
checkDataProperty(target, "c", 3, true, false, false);
}
propertiesAssignedExistingNotAltered();
// Properties are retrieved through Get()
// Properties are assigned through Put(): Throws TypeError if non-writable
function propertiesAssignedTypeErrorNonWritable() {
var source = {a: 1};
var target = {a: 0};
Object.defineProperty(target, "a", {writable: false});
assertThrowsInstanceOf(() => Object.assign(target, source), TypeError);
checkDataProperty(target, "a", 0, false, true, true);
}
propertiesAssignedTypeErrorNonWritable();
// Properties are retrieved through Get()
// Put() creates standard properties; Property attributes from source are ignored
function createsStandardProperties() {
var source = {a: 1, b: 2, c: 3, get d() { return 4 }};
Object.defineProperty(source, "b", {writable: false});
Object.defineProperty(source, "c", {configurable: false});
var target = Object.assign({}, source);
checkDataProperty(target, "a", 1, true, true, true);
checkDataProperty(target, "b", 2, true, true, true);
checkDataProperty(target, "c", 3, true, true, true);
checkDataProperty(target, "d", 4, true, true, true);
}
createsStandardProperties();
// Properties created during traversal are not copied
function propertiesCreatedDuringTraversalNotCopied() {
var source = {get a() { this.b = 2 }};
var target = Object.assign({}, source);
assertEq("a" in target, true);
assertEq("b" in target, false);
}
propertiesCreatedDuringTraversalNotCopied();
// Properties deleted during traversal are not copied
function testDeletePropertiesNotCopied() {
var source = new Proxy({
get a() { delete this.b },
b: 2,
}, {
getOwnPropertyNames: () => ["a", "b"]
});
var target = Object.assign({}, source);
assertEq("a" in target, true);
assertEq("b" in target, false);
}
testDeletePropertiesNotCopied();
function testDeletionExposingShadowedProperty()
{
var srcProto = { b: 42 };
var src =
Object.create(srcProto,
{ a: { enumerable: true, get: function() { delete this.b; } },
b: { value: 2, configurable: true, enumerable: true } });
var source = new Proxy(src, { getOwnPropertyNames: () => ["a", "b"] });
var target = Object.assign({}, source);
assertEq("a" in target, true);
assertEq("b" in target, false);
}
testDeletionExposingShadowedProperty();
// Properties first deleted and then recreated during traversal are copied (1)
function testDeletedAndRecreatedPropertiesCopied1() {
var source = new Proxy({
get a() { delete this.c },
get b() { this.c = 4 },
c: 3,
}, {
getOwnPropertyNames: () => ["a", "b", "c"]
});
var target = Object.assign({}, source);
assertEq("a" in target, true);
assertEq("b" in target, true);
assertEq("c" in target, true);
checkDataProperty(target, "c", 4, true, true, true);
}
testDeletedAndRecreatedPropertiesCopied1();
// Properties first deleted and then recreated during traversal are copied (2)
function testDeletedAndRecreatedPropertiesCopied2() {
var source = new Proxy({
get a() { delete this.c },
get b() { this.c = 4 },
c: 3,
}, {
ownKeys: () => ["a", "c", "b"]
});
var target = Object.assign({}, source);
assertEq("a" in target, true);
assertEq("b" in target, true);
assertEq("c" in target, false);
}
testDeletedAndRecreatedPropertiesCopied2();
// String and Symbol valued properties are copied
function testStringAndSymbolPropertiesCopied() {
var keyA = "str-prop";
var source = {"str-prop": 1};
var target = Object.assign({}, source);
checkDataProperty(target, keyA, 1, true, true, true);
}
testStringAndSymbolPropertiesCopied();
// Intermediate exceptions do not stop property traversal, first exception is reported (1)
function testExceptionsDoNotStopFirstReported1() {
var ErrorA = function ErrorA() {};
var ErrorB = function ErrorB() {};
var log = "";
var source = new Proxy({}, {
getOwnPropertyDescriptor: function(t, pk) {
log += pk;
throw new (pk === "a" ? ErrorA : ErrorB);
},
ownKeys: () => ["b", "a"]
});
assertThrowsInstanceOf(() => Object.assign({}, source), ErrorB);
assertEq(log, "ba");
}
testExceptionsDoNotStopFirstReported1();
// Properties are retrieved through Get()
// Intermediate exceptions do not stop property traversal, first exception is reported (2)
function testExceptionsDoNotStopFirstReported2() {
var ErrorA = function ErrorA() {};
var ErrorB = function ErrorB() {};
var log = "";
var source = new Proxy({
get a() { log += "a"; throw new ErrorA },
get b() { log += "b"; throw new ErrorB },
}, {
ownKeys: () => ["b", "a"]
});
assertThrowsInstanceOf(() => Object.assign({}, source), ErrorB);
assertEq(log, "ba");
}
testExceptionsDoNotStopFirstReported2();
// Intermediate exceptions do not stop property traversal, first exception is reported (3)
function testExceptionsDoNotStopFirstReported3() {
var ErrorA = function ErrorA() {};
var ErrorB = function ErrorB() {};
var log = "";
var source = new Proxy({a: 1, b: 2}, {
ownKeys: () => ["b", "a"]
});
var target = {
set a(v) { log += "a"; throw new ErrorA },
set b(v) { log += "b"; throw new ErrorB },
};
assertThrowsInstanceOf(() => Object.assign(target, source), ErrorB);
assertEq(log, "ba");
}
testExceptionsDoNotStopFirstReported3();
// Intermediate exceptions do not stop property traversal, first exception is reported (4)
function testExceptionsDoNotStopFirstReported4() {
var ErrorGetOwnProperty = function ErrorGetOwnProperty() {};
var ErrorGet = function ErrorGet() {};
var ErrorSet = function ErrorSet() {};
var source = new Proxy({
get a() { throw new ErrorGet }
}, {
getOwnPropertyDescriptor: function(t, pk) {
throw new ErrorGetOwnProperty;
}
});
var target = {
set a(v) { throw new ErrorSet }
};
assertThrowsInstanceOf(() => Object.assign({}, source), ErrorGetOwnProperty);
}
testExceptionsDoNotStopFirstReported4();
if (typeof reportCompare == "function")
reportCompare(true, true);