diff --git a/js/src/builtin/Object.cpp b/js/src/builtin/Object.cpp index ef132be4b424..5e51b3ef4cd0 100644 --- a/js/src/builtin/Object.cpp +++ b/js/src/builtin/Object.cpp @@ -593,6 +593,187 @@ obj_setPrototypeOf(JSContext* cx, unsigned argc, Value* vp) return true; } +static bool +PropertyIsEnumerable(JSContext* cx, HandleObject obj, HandleId id, bool* enumerable) +{ + PropertyResult prop; + if (obj->isNative() && + NativeLookupOwnProperty(cx, &obj->as(), id, &prop)) + { + if (!prop) { + *enumerable = false; + return true; + } + + unsigned attrs = GetPropertyAttributes(obj, prop); + *enumerable = (attrs & JSPROP_ENUMERATE) != 0; + return true; + } + + Rooted desc(cx); + if (!GetOwnPropertyDescriptor(cx, obj, id, &desc)) + return false; + + *enumerable = desc.object() && desc.enumerable(); + return true; +} + +static bool +TryAssignNative(JSContext* cx, HandleObject to, HandleObject from, bool* optimized) +{ + *optimized = false; + + if (!from->isNative() || !to->isNative()) + return true; + + // Don't use the fast path if |from| may have extra indexed or lazy + // properties. + NativeObject* fromNative = &from->as(); + if (fromNative->getDenseInitializedLength() > 0 || + fromNative->isIndexed() || + fromNative->is() || + fromNative->getClass()->getNewEnumerate() || + fromNative->getClass()->getEnumerate()) + { + return true; + } + + // Get a list of |from| shapes. As long as from->lastProperty() == fromShape + // we can use this to speed up both the enumerability check and the GetProp. + + using ShapeVector = GCVector; + Rooted shapes(cx, ShapeVector(cx)); + + RootedShape fromShape(cx, fromNative->lastProperty()); + for (Shape::Range r(fromShape); !r.empty(); r.popFront()) { + // Symbol properties need to be assigned last. For now fall back to the + // slow path if we see a symbol property. + if (MOZ_UNLIKELY(JSID_IS_SYMBOL(r.front().propidRaw()))) + return true; + if (MOZ_UNLIKELY(!shapes.append(&r.front()))) + return false; + } + + *optimized = true; + + RootedShape shape(cx); + RootedValue propValue(cx); + RootedId nextKey(cx); + RootedValue toReceiver(cx, ObjectValue(*to)); + + for (size_t i = shapes.length(); i > 0; i--) { + shape = shapes[i - 1]; + nextKey = shape->propid(); + + // Ensure |from| is still native: a getter/setter might have turned + // |from| or |to| into an unboxed object or it could have been swapped + // with a non-native object. + if (MOZ_LIKELY(from->isNative() && + from->as().lastProperty() == fromShape && + shape->hasDefaultGetter() && + shape->hasSlot())) + { + if (!shape->enumerable()) + continue; + propValue = from->as().getSlot(shape->slot()); + } else { + // |from| changed shape or the property is not a data property, so + // we have to do the slower enumerability check and GetProp. + bool enumerable; + if (!PropertyIsEnumerable(cx, from, nextKey, &enumerable)) + return false; + if (!enumerable) + continue; + if (!GetProperty(cx, from, from, nextKey, &propValue)) + return false; + } + + ObjectOpResult result; + if (MOZ_UNLIKELY(!SetProperty(cx, to, nextKey, propValue, toReceiver, result))) + return false; + if (MOZ_UNLIKELY(!result.checkStrict(cx, to, nextKey))) + return false; + } + + return true; +} + +static bool +AssignSlow(JSContext* cx, HandleObject to, HandleObject from) +{ + // Step 4.b.ii. + AutoIdVector keys(cx); + if (!GetPropertyKeys(cx, from, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, &keys)) + return false; + + // Step 4.c. + RootedId nextKey(cx); + RootedValue propValue(cx); + for (size_t i = 0, len = keys.length(); i < len; i++) { + nextKey = keys[i]; + + // Step 4.c.i. + bool enumerable; + if (MOZ_UNLIKELY(!PropertyIsEnumerable(cx, from, nextKey, &enumerable))) + return false; + if (!enumerable) + continue; + + // Step 4.c.ii.1. + if (MOZ_UNLIKELY(!GetProperty(cx, from, from, nextKey, &propValue))) + return false; + + // Step 4.c.ii.2. + if (MOZ_UNLIKELY(!SetProperty(cx, to, nextKey, propValue))) + return false; + } + + return true; +} + +// ES2018 draft rev 48ad2688d8f964da3ea8c11163ef20eb126fb8a4 +// 19.1.2.1 Object.assign(target, ...sources) +static bool +obj_assign(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject to(cx, ToObject(cx, args.get(0))); + if (!to) + return false; + + // Note: step 2 is implicit. If there are 0 arguments, ToObject throws. If + // there's 1 argument, the loop below is a no-op. + + // Step 4. + RootedObject from(cx); + for (size_t i = 1; i < args.length(); i++) { + // Step 4.a. + if (args[i].isNullOrUndefined()) + continue; + + // Step 4.b.i. + from = ToObject(cx, args[i]); + if (!from) + return false; + + // Steps 4.b.ii, 4.c. + bool optimized; + if (!TryAssignNative(cx, to, from, &optimized)) + return false; + if (optimized) + continue; + + if (!AssignSlow(cx, to, from)) + return false; + } + + // Step 5. + args.rval().setObject(*to); + return true; +} + #if JS_HAS_OBJ_WATCHPOINT bool @@ -1295,7 +1476,7 @@ static const JSPropertySpec object_properties[] = { }; static const JSFunctionSpec object_static_methods[] = { - JS_SELF_HOSTED_FN("assign", "ObjectStaticAssign", 2, 0), + JS_FN("assign", obj_assign, 2, 0), JS_SELF_HOSTED_FN("getPrototypeOf", "ObjectGetPrototypeOf", 1, 0), JS_FN("setPrototypeOf", obj_setPrototypeOf, 2, 0), JS_FN("getOwnPropertyDescriptor", obj_getOwnPropertyDescriptor,2, 0), diff --git a/js/src/builtin/Object.js b/js/src/builtin/Object.js index 239eb54a3d56..63af363228f8 100644 --- a/js/src/builtin/Object.js +++ b/js/src/builtin/Object.js @@ -2,45 +2,6 @@ * 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 rev36 2015-03-17 19.1.2.1 -function ObjectStaticAssign(target, firstSource) { - // Steps 1-2. - var to = ToObject(target); - - // Step 3. - if (arguments.length < 2) - return to; - - // Steps 4-5. - for (var i = 1; i < arguments.length; i++) { - // Step 5.a. - var nextSource = arguments[i]; - if (nextSource === null || nextSource === undefined) - continue; - - // Steps 5.b.i-ii. - var from = ToObject(nextSource); - - // Steps 5.b.iii-iv. - var keys = OwnPropertyKeys(from, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS); - - // Step 5.c. - for (var nextIndex = 0, len = keys.length; nextIndex < len; nextIndex++) { - var nextKey = keys[nextIndex]; - - // Steps 5.c.i-iii. We abbreviate this by calling propertyIsEnumerable - // which is faster and returns false for not defined properties. - if (callFunction(std_Object_propertyIsEnumerable, from, nextKey)) { - // Steps 5.c.iii.1-4. - to[nextKey] = from[nextKey]; - } - } - } - - // Step 6. - return to; -} - // ES stage 4 proposal function ObjectGetOwnPropertyDescriptors(O) { // Step 1. diff --git a/js/src/jit-test/tests/basic/object-assign.js b/js/src/jit-test/tests/basic/object-assign.js new file mode 100644 index 000000000000..0b1297849a75 --- /dev/null +++ b/js/src/jit-test/tests/basic/object-assign.js @@ -0,0 +1,135 @@ +function test() { + var from, to; + + // Property changes value. + from = {x: 1, y: 2}; + to = {set x(v) { from.y = 5; }}; + Object.assign(to, from); + assertEq(to.y, 5); + + // Property becomes a getter. + from = {x: 1, y: 2}; + to = {set x(v) { Object.defineProperty(from, "y", {get: () => 4}); }}; + Object.assign(to, from); + assertEq(to.y, 4); + + // Property becomes non-enumerable. + from = {x: 1, y: 2}; + to = {set x(v) { Object.defineProperty(from, "y", {value: 2, + enumerable: false, + configurable: true, + writable: true}); }}; + Object.assign(to, from); + assertEq("y" in to, false); + to = {}; + Object.assign(to, from); + assertEq("y" in to, false); + + // Property is deleted. Should NOT get Object.prototype.toString. + from = {x: 1, toString: 2}; + to = {set x(v) { delete from.toString; }}; + Object.assign(to, from); + assertEq(to.hasOwnProperty("toString"), false); + + from = {toString: 2, x: 1}; + to = {set x(v) { delete from.toString; }}; + Object.assign(to, from); + assertEq(to.toString, 2); + + from = {x: 1, toString: 2, y: 3}; + to = {set x(v) { delete from.toString; }}; + Object.assign(to, from); + assertEq(to.hasOwnProperty("toString"), false); + assertEq(to.y, 3); + + // New property is added. + from = {x: 1, y: 2}; + to = {set x(v) { from.z = 3; }}; + Object.assign(to, from); + assertEq("z" in to, false); + + // From getter. + var c = 7; + from = {x: 1, get y() { return ++c; }}; + to = {}; + Object.assign(to, from); + Object.assign(to, from, from); + assertEq(to.y, 10); + + // Frozen object. + from = {x: 1, y: 2}; + to = {x: 4}; + Object.freeze(to); + var ex; + try { + Object.assign(to, from); + } catch (e) { + ex = e; + } + assertEq(ex instanceof TypeError, true); + assertEq(to.x, 4); + + // Non-writable property. + from = {x: 1, y: 2, z: 3}; + to = {}; + Object.defineProperty(to, "y", {value: 9, writable: false}); + ex = null; + try { + Object.assign(to, from); + } catch(e) { + ex = e; + } + assertEq(ex instanceof TypeError, true); + assertEq(to.x, 1); + assertEq(to.y, 9); + assertEq(to.z, undefined); + + // Array with dense elements. + from = [1, 2, 3]; + to = {}; + Object.assign(to, from); + assertEq(to[2], 3); + assertEq("length" in to, false); + + // Object with sparse elements and symbols. + from = {x: 1, 1234567: 2, 1234560: 3,[Symbol.iterator]: 5, z: 3}; + to = {}; + Object.assign(to, from); + assertEq(to[1234567], 2); + assertEq(Object.keys(to).toString(), "1234560,1234567,x,z"); + assertEq(to[Symbol.iterator], 5); + + // Symbol properties need to be assigned last. + from = {x: 1, [Symbol.iterator]: 2, y: 3}; + to = {set y(v) { throw 9; }}; + ex = null; + try { + Object.assign(to, from); + } catch (e) { + ex = e; + } + assertEq(ex, 9); + assertEq(to.x, 1); + assertEq(to.hasOwnProperty(Symbol.iterator), false); + + // Typed array. + from = new Int32Array([1, 2, 3]); + to = {}; + Object.assign(to, from); + assertEq(to[1], 2); + + // Primitive string. + from = "foo"; + to = {}; + Object.assign(to, from); + assertEq(to[0], "f"); + + // String object. + from = new String("bar"); + to = {}; + Object.assign(to, from); + assertEq(to[2], "r"); +} +test(); +test(); +test();