🐛 constructable checking should not be cached if the function prototype function was added after first running (#1381)

This commit is contained in:
Kuitos 2021-04-13 14:24:48 +08:00 committed by GitHub
parent 8e521521b9
commit 6953ba3d0b
4 changed files with 81 additions and 23 deletions

View File

@ -28,7 +28,6 @@ declare global {
cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void;
}
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface Navigator {
connection: {
saveData: boolean;

View File

@ -0,0 +1,43 @@
/**
* @author Kuitos
* @since 2021-04-12
*/
import { getTargetValue } from '../common';
describe('getTargetValue', () => {
it('should work well', () => {
const a1 = getTargetValue(window, undefined);
expect(a1).toEqual(undefined);
const a2 = getTargetValue(window, null);
expect(a2).toEqual(null);
const a3 = getTargetValue(window, function bindThis(this: any) {
return this;
});
const a3returns = a3();
expect(a3returns).toEqual(window);
});
it('should work well while function added prototype methods after first running', () => {
function prototypeAddedAfterFirstInvocation(this: any, field: string) {
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`
const result = {};
notConstructableFunction.call(result, '123');
expect(result).toStrictEqual({});
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
const result2 = {};
constructableFunction.call(result2, '456');
expect(result2).toStrictEqual({ field: '456' });
// window.field not be affected
expect(window.field).toEqual('123');
});
});

View File

@ -14,13 +14,7 @@ export function setCurrentRunningSandboxProxy(proxy: WindowProxy | null) {
currentRunningSandboxProxy = proxy;
}
const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>();
export function getTargetValue(target: any, value: any): any {
const cachedBoundFunction = functionBoundedValueMap.get(value);
if (cachedBoundFunction) {
return cachedBoundFunction;
}
/*
isCallable && !isBoundedFunction && !isConstructable window.consolewindow.atob prototype
@warning edge case lodash.isFunction iframe top window
@ -37,10 +31,10 @@ export function getTargetValue(target: any, value: any): any {
// copy prototype if bound function not have
// mostly a bound function have no own prototype, but it not absolute in some old version browser, see https://github.com/umijs/qiankun/issues/1121
if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype'))
if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) {
boundValue.prototype = value.prototype;
}
functionBoundedValueMap.set(value, boundValue);
return boundValue;
}

View File

@ -22,21 +22,34 @@ export function nextTick(cb: () => void): void {
Promise.resolve().then(cb);
}
const constructableMap = new WeakMap<any | FunctionConstructor, boolean>();
const fnRegexCheckCacheMap = new WeakMap<any | FunctionConstructor, boolean>();
export function isConstructable(fn: () => any | FunctionConstructor) {
if (constructableMap.has(fn)) {
return constructableMap.get(fn);
// prototype methods might be added while code running, so we need check it every time
const hasPrototypeMethods =
fn.prototype && fn.prototype.constructor === fn && Object.getOwnPropertyNames(fn.prototype).length > 1;
if (hasPrototypeMethods) return true;
if (fnRegexCheckCacheMap.has(fn)) {
return fnRegexCheckCacheMap.get(fn);
}
const constructableFunctionRegex = /^function\b\s[A-Z].*/;
const classRegex = /^class\b/;
/*
1. prototype prototype constructor
2.
3. class
*/
let constructable = hasPrototypeMethods;
if (!constructable) {
// fn.toString has a significant performance overhead, if hasPrototypeMethods check not passed, we will check the function string with regex
const fnString = fn.toString();
const constructableFunctionRegex = /^function\b\s[A-Z].*/;
const classRegex = /^class\b/;
constructable = constructableFunctionRegex.test(fnString) || classRegex.test(fnString);
}
// 有 prototype 并且 prototype 上有定义一系列非 constructor 属性,则可以认为是一个构造函数
const constructable =
(fn.prototype && fn.prototype.constructor === fn && Object.getOwnPropertyNames(fn.prototype).length > 1) ||
constructableFunctionRegex.test(fn.toString()) ||
classRegex.test(fn.toString());
constructableMap.set(fn, constructable);
fnRegexCheckCacheMap.set(fn, constructable);
return constructable;
}
@ -47,9 +60,18 @@ export function isConstructable(fn: () => any | FunctionConstructor) {
* We need to discriminate safari for better performance
*/
const naughtySafari = typeof document.all === 'function' && typeof document.all === 'undefined';
export const isCallable = naughtySafari
? (fn: any) => typeof fn === 'function' && typeof fn !== 'undefined'
: (fn: any) => typeof fn === 'function';
const callableFnCacheMap = new WeakMap<CallableFunction, boolean>();
export const isCallable = (fn: any) => {
if (callableFnCacheMap.has(fn)) {
return true;
}
const callable = naughtySafari ? typeof fn === 'function' && typeof fn !== 'undefined' : typeof fn === 'function';
if (callable) {
callableFnCacheMap.set(fn, callable);
}
return callable;
};
const boundedMap = new WeakMap<CallableFunction, boolean>();
export function isBoundedFunction(fn: CallableFunction) {