🐛 constructable checking should not be cached if the function prototype function was added after first running (#1381)
This commit is contained in:
parent
8e521521b9
commit
6953ba3d0b
|
|
@ -28,7 +28,6 @@ declare global {
|
|||
cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface Navigator {
|
||||
connection: {
|
||||
saveData: boolean;
|
||||
|
|
|
|||
43
src/sandbox/__tests__/common.test.ts
Normal file
43
src/sandbox/__tests__/common.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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.console、window.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;
|
||||
}
|
||||
|
||||
|
|
|
|||
50
src/utils.ts
50
src/utils.ts
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user