JavaScript

[JS] 几个好玩的问题

2018-11-17  本文已影响12人  何幻

背景

以下几个好玩的问题,我都不是原创,
解题方法除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方法,
就会设置hintnumber,调用OrdinaryToPrimitive

即,如果hintnumber,就按valueOftoString的顺序调用对象的方法。
以下方法都是可以的。

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);

注:
栈帧中的局部变量会影响帧的大小,因此也会影响栈溢出时栈的深度,
所以,getStackcheck中应该保持一致,本例中都没有包含局部变量。

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也可用,只是异常堆栈的判断逻辑需要调整一下。

上一篇 下一篇

猜你喜欢

热点阅读