diff --git a/src/interfaces.ts b/src/interfaces.ts index e7d31fe53..0a1670fe9 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -136,3 +136,9 @@ export type MicroAppStateActions = { setGlobalState: (state: Record) => boolean; offGlobalStateChange: () => boolean; }; + +export enum WindowType { + Window = '[object Window]', + DOMWindow = '[object DOMWindow]', + global = '[object global]', +} diff --git a/src/sandbox/__tests__/common.test.ts b/src/sandbox/__tests__/common.test.ts index 4030a9d28..000f1f3f7 100644 --- a/src/sandbox/__tests__/common.test.ts +++ b/src/sandbox/__tests__/common.test.ts @@ -25,19 +25,23 @@ describe('getTargetValue', () => { this.field = field; } const notConstructableFunction = getTargetValue(window, prototypeAddedAfterFirstInvocation); - // `this` of not constructable function will be bound automatically, and it can not be changed by calling with special `this` + // `this` of not constructable function will be bound automatically, but it can be changed by calling with special `this` const result = {}; - notConstructableFunction.call(result, '123'); + notConstructableFunction('123'); expect(result).toStrictEqual({}); expect(window.field).toEqual('123'); + notConstructableFunction.call(result, '456'); + expect(result).toStrictEqual({ field: '456' }); + // window.field not be affected + expect(window.field).toEqual('123'); + prototypeAddedAfterFirstInvocation.prototype.addedFn = () => {}; const constructableFunction = getTargetValue(window, prototypeAddedAfterFirstInvocation); - // `this` coule be available if it be predicated as a constructable function + // `this` coule also be available when it be predicated as a constructable function const result2 = {}; - constructableFunction.call(result2, '456'); - expect(result2).toStrictEqual({ field: '456' }); - // window.field not be affected + constructableFunction.call(result2, '789'); + expect(result2).toStrictEqual({ field: '789' }); expect(window.field).toEqual('123'); }); diff --git a/src/sandbox/__tests__/proxySandbox.test.ts b/src/sandbox/__tests__/proxySandbox.test.ts index 3deec820d..d30e5bf2b 100644 --- a/src/sandbox/__tests__/proxySandbox.test.ts +++ b/src/sandbox/__tests__/proxySandbox.test.ts @@ -3,7 +3,6 @@ * @since 2020-03-31 */ -import { isBoundedFunction } from '../../utils'; import { getCurrentRunningSandboxProxy } from '../common'; import ProxySandbox from '../proxySandbox'; @@ -290,7 +289,26 @@ test('bounded function should not be rebounded', () => { expect(proxy.fn1 === fn).toBeFalsy(); expect(proxy.fn2 === boundedFn).toBeTruthy(); - expect(isBoundedFunction(proxy.fn1)).toBeTruthy(); +}); + +test('this should refer correctly', () => { + const proxy = new ProxySandbox('this-refer-test').proxy as any; + const obj = {}; + proxy.getTemplate = function getTemplate() { + this.a = 'a'; + this.b = 'b'; + }; + // this refers to global; + const fn = proxy.getTemplate; + fn(); + expect(proxy.a).toBe('a'); + expect(proxy.b).toBe('b'); + expect(window.a).toBeUndefined(); + expect(window.b).toBeUndefined(); + + // this refers to obj; + proxy.getTemplate.apply(obj); + expect(obj).toStrictEqual({ a: 'a', b: 'b' }); }); test('the prototype should be kept while we create a function with prototype on proxy', () => { diff --git a/src/sandbox/common.ts b/src/sandbox/common.ts index 8366d10a4..60f6856e7 100644 --- a/src/sandbox/common.ts +++ b/src/sandbox/common.ts @@ -4,6 +4,7 @@ */ import { isBoundedFunction, isCallable, isConstructable } from '../utils'; +import { WindowType } from '../interfaces'; let currentRunningSandboxProxy: WindowProxy | null; export function getCurrentRunningSandboxProxy() { @@ -14,6 +15,17 @@ export function setCurrentRunningSandboxProxy(proxy: WindowProxy | null) { currentRunningSandboxProxy = proxy; } +const isWindow = (obj: any) => { + if (!obj || typeof obj !== 'object') { + return false; + } + const typeofObj = Object.prototype.toString.call(obj); + if (typeofObj === WindowType.Window || typeofObj === WindowType.DOMWindow || typeofObj === WindowType.global) { + return true; + } + return obj.window === obj; +}; + export function getTargetValue(target: any, value: any): any { /* 仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常 @@ -21,7 +33,9 @@ export function getTargetValue(target: any, value: any): any { @warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常) */ if (isCallable(value) && !isBoundedFunction(value) && !isConstructable(value)) { - const boundValue = Function.prototype.bind.call(value, target); + const boundValue = function boundValue(this: any, ...rest: any[]) { + return Function.prototype.apply.call(value, typeof this === 'undefined' || isWindow(this) ? target : this, rest); + } as Record; // some callable function has custom fields, we need to copy the enumerable props to boundValue. such as moment function. // use for..in rather than Object.keys.forEach for performance reason @@ -32,7 +46,7 @@ export function getTargetValue(target: any, value: any): any { // copy prototype if bound function not have but target one have // as prototype is non-enumerable mostly, we need to copy it from target function manually - if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) { + if (value.hasOwnProperty('prototype')) { // we should not use assignment operator to set boundValue prototype like `boundValue.prototype = value.prototype` // as the assignment will also look up prototype chain while it hasn't own prototype property, // when the lookup succeed, the assignment will throw an TypeError like `Cannot assign to read only property 'prototype' of function` if its descriptor configured with writable false or just have a getter accessor diff --git a/src/sandbox/proxySandbox.ts b/src/sandbox/proxySandbox.ts index c46fa7fba..9f08e935d 100644 --- a/src/sandbox/proxySandbox.ts +++ b/src/sandbox/proxySandbox.ts @@ -263,12 +263,8 @@ export default class ProxySandbox implements SandBox { } // eslint-disable-next-line no-nested-ternary - const value = propertiesWithGetter.has(p) - ? (rawWindow as any)[p] - : p in target - ? (target as any)[p] - : (rawWindow as any)[p]; - return getTargetValue(rawWindow, value); + const source = propertiesWithGetter.has(p) ? rawWindow : p in target ? target : rawWindow; + return getTargetValue(source === target ? proxy : rawWindow, (source as any)[p]); }, // trap in operator