背景
以下几个好玩的问题,我都不是原创,
解题方法除2.2和4.2之外,都不是我自己想出来,特此声明。
借此向各位JS前辈致敬。
1. 怎样让a同时满足几个不同的条件
if (a === 1 && a === 2 && a === 3) {
console.log('hello world');
}
// 方法:
let i = 0;
Object.defineProperty(window, 'a', {
get () {
return [1, 2, 3][i++];
}
});
2. 怎样让乘法报错
// 问:如何得到key的值
(function() {
const key = Math.random();
function doMath(x) {
return x * x;
}
apiX = function(x) {
try {
return doMath(x);
} catch (err) {
return key;
}
}
})();
let key;
// write code here
// ...
2.1 Symbol
// 方法一:
key = apiX(Symbol());
原理解析:
我们查看ECMAScript规范,12.7 Multiplicative Operators,
12.7.3 Runtime Semantics: Evaluation
其中 ToNumber的会针对不同的参数,分情况处理,
我们看到如果ToNumber的参数是一个Symbol就会直接抛异常,
所以,我们第一方法中,给apiX
传入了一个Symbol()
,doMath
就抛异常了。
// 方法一:
key = apiX(Symbol());
2.2 valueOf,toString,Symbol.toPrimitive
// 方法二:
key = apiX({
[Symbol.toPrimitive]: { }
});
key = apiX({
[Symbol.toPrimitive]() {
throw 0;
}
});
// 或者
key = apiX({
valueOf: { }
});
key = apiX({
valueOf () {
throw 0;
}
});
// 或者
key = apiX({
toString: { }
});
key = apiX({
toString () {
throw 0;
}
});
除了传入Symbol之外,我们还可以传入一个Object,
因为ToNumber对于Object会调用ToPrimitive,它也可能抛异常。
我们看到,在ToPrimitive中会先查看对象有没有@@toPrimitive
方法,
如果有这个方法,且这个方法返回了Object,就报错。
其中,@@toPrimitive
在JS中,就是一个以符号Symbol.toPrimitive
为名字的方法,
key = apiX({
[Symbol.toPrimitive]: { }
});
key = apiX({
[Symbol.toPrimitive]() {
throw 0;
}
});
我们传入了一个Object,它的属性名是符号Symbol.toPrimitive
,值是一个Object。
当然在这个方法中直接抛异常也是可以的。
再看ToPrimitive的逻辑,如果没有@@toPrimitive
方法,
就会设置hint
为number
,调用OrdinaryToPrimitive,
即,如果hint
是number
,就按valueOf
,toString
的顺序调用对象的方法。
以下方法都是可以的。
key = apiX({
valueOf: { }
});
key = apiX({
valueOf () {
throw 0;
}
});
key = apiX({
toString: { }
});
key = apiX({
toString () {
throw 0;
}
});
3. 怎样让任意函数报错
(function() {
const key = Math.random();
function internal(x) {
return x;
}
apiX = function(x) {
try {
return internal(x);
} catch (err) {
return key;
}
}
})();
let key;
// write code here
// …
// 方法:
function F() {
var ret = apiX(2);
if (ret < 1) {
key = ret; // key 的范围是 0~1
}
return F(); // 无限递归
}
try {
F();
} catch (err) { }
console.log(key);
由于Chrome中函数的调用栈空间是有限的,
所以我们可以利用栈溢出,让任何一个函数调用报错。
总有一个分界点,让apiX
被调用的时候栈未满,而apiX
调用internal
的时候栈溢出了。
注: 该方法只在Chrome中测试有效,FireFox中测试无效。
4. 如何判断一个对象是否Proxy
const math = new Proxy(Math, {
get(obj, prop) {
return obj[prop];
}
});
约束条件:不能用===
和Object.is
进行判断。
4.1 利用栈溢出
由于Proxy调用比直接调用多一层get方法调用,
因此,可以在栈溢出的临界点上调用对象的get方法进行判断。
// 方法一:
const math = new Proxy(Math, {
get(obj, prop) {
return obj[prop];
}
});
// 先获取栈溢出时的最大深度
let max = 0;
function getStack() {
max++;
return getStack();
}
try {
getStack();
} catch (err) { }
// 在栈溢出的临界点上,检查get方法
let cur = 0, obj;
function check() {
if (cur > max - 10) {
obj.a;
}
cur++;
return check();
}
cur = 0;
obj = math;
try {
check();
} catch (err) { }
console.log('math: ', cur !== max);
cur = 0;
obj = Math;
try {
check();
} catch (err) { }
console.log('Math: ', cur !== max);
注:
栈帧中的局部变量会影响帧的大小,因此也会影响栈溢出时栈的深度,
所以,getStack
和check
中应该保持一致,本例中都没有包含局部变量。
4.2 利用异常堆栈
// 方法二:
// 给math增加一个会抛异常的get属性
Object.defineProperty(math, 'a', {
get() {
throw new Error(1);
}
});
// 获取异常堆栈,Proxy的异常堆栈会多一层
/*
getErrorStack(Math)
"Error: 1
at Math.get (<anonymous>:10:11)
at getErrorStack (<anonymous>:17:7)
at <anonymous>:1:1"
getErrorStack(math)
"Error: 1
at Math.get (<anonymous>:10:11)
at Object.get (<anonymous>:3:15)
at getErrorStack (<anonymous>:17:7)
at <anonymous>:1:1"
*/
const getErrorStack = x => {
try {
x.a;
} catch (err) {
return err.stack;
}
};
const isProxy = x => {
const errorStack = getErrorStack(x);
return /Object\.get/.test(errorStack);
};
console.log('math: ', isProxy(math)); // true
console.log('Math: ', isProxy(Math)); // false
注:
该思路在FireFox或者Node.js也可用,只是异常堆栈的判断逻辑需要调整一下。
也可以在get方法里面检查错误堆栈,
Object.defineProperty(math, 'a', {
get() {
try {
throw Error(1);
} catch (err) {
console.log(err.stack); // 检测多余的栈
}
}
});
/*
math.a
Error: 1
at Math.get (<anonymous>:10:13)
at Object.get (<anonymous>:3:19)
at <anonymous>:1:6
Math.a
Error: 1
at Math.get (<anonymous>:10:13)
at <anonymous>:1:6
*/
4.3 利用函数的caller属性
// 方法三:
Object.defineProperty(math, 'a', {
get: function F() {
return F.caller;
}
});
/*
math.a
ƒ get(obj, prop) {
return obj[prop];
}
Math.a
null
*/
F.caller
,就相当于已经废弃的arguments.caller
,
只是目前arguments.caller
会返回undefined
,从IE9开始就不支持了,
它们都不是规范的ECMAScript,但是大多数JS引擎实现了它,
mdn: Function.caller
mdn: arguments.caller
注: 在严格模式下,调用F.caller
会抛异常,
Object.defineProperty(math, 'a', {
get: function F() {
'use strict';
return F.caller;
}
});
/*
math.a
Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
at Math.F (<anonymous>:10:14)
at Object.get (<anonymous>:3:15)
at <anonymous>:1:6
Math.a
Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
at Math.F (<anonymous>:10:14)
at <anonymous>:1:6
*/