奇怪的 javascript
ps: 本文的题目解答都是以谷歌浏览器为准。
本文就从四道题来分析 javascript 的奇怪行为,首先我们来看第一道题,如下所示:
题目 1
js
let a = 1;
function fn() {
let a = 2;
// 这里写代码,使得最后的打印是3
}
fn();
console.log(a); // 3
问题很简单,先使用 let 在全局中定义了一个变量 a,并定义初始值为 1,然后定义了一个函数,在函数的内部又定义了一个同样的变量 a,然后调用这个函数,在调用函数之后打印变量 a,问题就是在函数内部添加一些代码使得最终打印变量 a 的结果是 3。
思路分析
首先我们知道如果没有特殊的办法,那么最外层的打印将始终打印的是 a 变量最初的值,那就是 1。函数内部如果没有定义 a 变量,那么我们是可以访问到 a 变量的,而有了 a 变量,那么我们只能在函数访问到 2,这就导致我们在函数内部似乎没有任何办法访问到外部的变量 1,因此我们无法修改外部的变量 a。要解决这道题,我们可以从 2 个方向入手,第一个方向就是如何在函数内部访问到外部的变量 a,从而修改变量 a,第二个方向则是从 console.log 函数入手。
我们先看第一个方向,如何在函数内部访问到外部的变量 a,这听起来似乎很不可思议,但我们确实可以做到,使用 eval 函数就可以了,eval 函数支持传一个字符串当做参数,我想这大多数开发者都知道,但是很少有人知道 eval 函数的调用方式分为直接调用和间接调用,什么是直接调用,什么又是间接调用。我们来看两段代码:
js
eval('console.log(1)'); // 直接调用
js
(0, eval)('console.log(1)'); // 间接调用
看了以上代码我们就知道了,直接调用就是直接调用 eval 方法即可,而间接调用,是让 eval 函数像自调用函数(当然这里并不是自调用函数)那样去调用,这里用到了逗号操作符,逗号操作符可以用来在一条语句中执行多个操作,如下所示:
js
let num1 = 1,
num2 = 2,
num3 = 3;
不过,也可以使用逗号操作符来辅助赋值。在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值:
js
let num = (5, 1, 4, 8, 0); // num 的值为 0
因此这里的逗号操作符前面的第一个操作数 0 其实没有什么意义,用 1 也可以,2 也行,这里我们主要搞清楚间接调用和直接调用的区别就行了,直接调用 eval 执行的环境就是当下的环境,那么访问到的也就是当下环境中的变量,而间接调用可以让 eval 执行的环境暴露在全局环境中。比如:
js
let a = 1;
function fn() {
let a = 2;
console.log(eval('a')); // 2
}
fn();
js
let a = 1;
function fn() {
let a = 2;
console.log((0, eval)('a')); // 1
}
fn();
看了以上两段代码的结果就不难看出直接调用和间接调用的区别了,有了间接调用,我们就可以修改在全局环境下的 a 变量,这样也就能解答本题了。如下:
js
let a = 1;
function fn() {
let a = 2;
(0, eval)('a+2'); // 或者 (0,eval)('a = 3')
}
fn();
console.log(a); // 3
有了 eval 函数,那么我们同样想到了可以使用 Function 来模拟 eval 函数的功能,如下所示;
js
const equalEval = str => new Function('return ' + str)();
因此以上的代码还可以这么解答:
js
const equalEval = str => new Function('return ' + str)();
let a = 1;
function fn() {
let a = 2;
equalEval('a+2'); // 或者 equalEval('a = 3')
}
fn();
console.log(a); // 3
以上是我们说的第一个方向,接下来我们来谈谈第二个方向,那就是修改 console.log 函数,很简单,如下所示:
js
let a = 1;
function fn() {
let a = 2;
const originLog = console.log;
console.log = v => {
originLog(v + 2);
};
}
fn();
console.log(a); // 3
可以看到我们使用一个变量缓存 console.log 方法,然后改写 console.log,让最终的打印值加 2 就可以得到 3。
javascript 是真的好奇怪,这些莫名其妙的特性总是让人难以理解,并烦恼,正常谁会想到这样的解答?
题目 2
js
window.eval = () => {
throw new Error('eval is not allowed');
};
window.Function = () => {
throw new Error('Function is not allowed');
};
//这里写代码使得后续的打印返回注释的结果
console.log(eval); // eval(){[native code]}
console.log(eval('1 + 2')); // 3
思路分析
正如第一题那样,我们改写了 javascript 的 console.log 方法,这第 2 题由于改写了 javascript 的 eval 和 Function 方法,让我们想要恢复原来的方法就显得比较困难。因此这道题的办法就是怎么样才能够恢复 eval 方法,这时候我们就可以想到内联框架,内联框架也有一个 window 对象,说明同样也有 eval 方法,因此我们可以获取到内联框架的 eval 方法然后恢复 eval 方法的定义,如下所示:
js
window.eval = () => {
throw new Error('eval is not allowed');
};
window.Function = () => {
throw new Error('Function is not allowed');
};
const iframe = document.createElement('iframe'); // 创建一个iframe对象
document.body.appendChild(iframe); //注意一定要把iframe添加到dom中
const eval = iframe.contentWindow.eval; //获取内联框架下的eval函数
document.body.removeChild(iframe); // 获取到了之后从dom中移除iframe元素
console.log(eval); // eval(){[native code]}
console.log(eval('1 + 2')); // 3
如此一来,本题就轻松的解决了。
javascript 就是这么奇怪,居然允许我们修改内置函数的定义,你就说它奇不奇怪?
题目 3
js
let a; // a = ? 这里写代码使得后续的打印返回注释的结果;
if (!a) {
console.log(a + 1); // 2
}
思路分析
这道题也是很有意思的,这道题的难点在于既要满足条件是 false,又要满足 a 变量经过转换后的值一定是 1,否则不可能得到结果为 2。如果大家可以想到 valueOf 这个方法,离解答这道题就不远了,valueOf 方法返回任意数据的原始值,也就是说,如果我们修改变量 a 的原始值,那么 a 最终会以原始值参与 + 1 的计算,然后得到 2。也就是说,我们只需要这样:
js
a.valueOf = () => 1; // 将a的原始值设置为1
也许有人说,那好,这里的 a = 0 即可,记住这里的 a 不能为原始数据类型,因为原始数据类型的原始值就相当于是它本身调用 Number 方法得到的一个数字 0,也就是说:
js
let a = false; // 或者 a = '' a = 0
以上都是错误的写法,并不能得到 a 的原始值为 1,因此我们需要将 a 设置为对象,而能够是 false 值的对象只有 document.all,它的返回值在除 ie 浏览器上都是 false,因此也就满足了既是 false,又修改原始值,能够让变量 a 读取到修改后的原始值。因此最终的解答就是:
js
let a = document.all;
a.valueOf = () => 1;
if (!a) {
console.log(a + 1); // 2
}
那么问题来了,谁会想到 document.all 的返回值是 false?javascript 好奇怪,明明这里是返回 document 的整个集合,为什么在谷歌浏览器上将它转成布尔值的时候,是 false 而不是 true。
题目 4
js
let a; // a = ? 这里写代码使得后续的打印返回注释的结果;
console.log(typeof a); // number
console.log(1 + a === 1); // false
console.log(2 + a === 2); // true
思路分析
这道题咋一看,有这样的数字吗?既要满足 1 + a = 1,又要满足 2 + a = 2,还别说,翻阅了文档,还真能找到这个数字,这个数字就是 Number.EPSILON,这是一个啥玩意儿,估计很少有人知道。mdn 文档上是这样说的:
Number.EPSILON 属性表示 1 与 Number 可表示的大于 1 的最小的浮点数之间的差值。EPSILON 属性的值接近于 2.2204460492503130808472633361816E-16,或者 2^-52。这玩意儿加 1 的结果是:1.0000000000000002,加 2 的结果就是 2,你就说它奇不奇怪。因此本题的答案就是:
js
let a = Number.EPSILON; // 或者 let a = 2.2204460492503130808472633361816E-16;或者let a = Math.pow(2,-52)
console.log(typeof a); // number
console.log(1 + a === 1); // false
console.log(2 + a === 2); // true
从以上的四道题,我们可以看到 javascript 的奇怪之处,你就说 javascript 奇不奇怪?
ps: 如果各位大佬还有这四道题的其它答案,欢迎评论区留言。