共计 4016 个字符,预计需要花费 11 分钟才能阅读完成。
奇怪的 javascript
ps: 本文的题目解答都是以谷歌浏览器为准。
本文就从四道题来剖析 javascript 的奇怪行为,首先咱们来看第一道题,如下所示:
题目 1
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 函数的调用形式分为间接调用和间接调用,什么是间接调用,什么又是间接调用。咱们来看两段代码:
eval('console.log(1)'); // 间接调用
(0, eval)('console.log(1)'); // 间接调用
看了以上代码咱们就晓得了,间接调用就是间接调用 eval 办法即可,而间接调用,是让 eval 函数像自调用函数(当然这里并不是自调用函数)那样去调用,这里用到了逗号操作符,逗号操作符能够用来在一条语句中执行多个操作,如下所示:
let num1 = 1,
num2 = 2,
num3 = 3;
不过,也能够应用逗号操作符来辅助赋值。在赋值时应用逗号操作符分隔值,最终会返回表达式中最初一个值:
let num = (5, 1, 4, 8, 0); // num 的值为 0
因而这里的逗号操作符后面的第一个操作数 0 其实没有什么意义,用 1 也能够,2 也行,这里咱们次要搞清楚间接调用和间接调用的区别就行了,间接调用 eval 执行的环境就是当下的环境,那么拜访到的也就是当下环境中的变量,而间接调用能够让 eval 执行的环境裸露在全局环境中。比方:
let a = 1;
function fn() {
let a = 2;
console.log(eval('a')); // 2
}
fn();
let a = 1;
function fn() {
let a = 2;
console.log((0, eval)('a')); // 1
}
fn();
看了以上两段代码的后果就不难看出间接调用和间接调用的区别了,有了间接调用,咱们就能够批改在全局环境下的 a 变量,这样也就能解答本题了。如下:
let a = 1;
function fn() {
let a = 2;
(0, eval)('a+2'); // 或者 (0,eval)('a = 3')
}
fn();
console.log(a); // 3
有了 eval 函数,那么咱们同样想到了能够应用 Function 来模仿 eval 函数的性能,如下所示;
const equalEval = str => new Function('return' + str)();
因而以上的代码还能够这么解答:
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 函数,很简略,如下所示:
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
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 办法的定义,如下所示:
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
let a; // a = ? 这里写代码使得后续的打印返回正文的后果;
if (!a) {console.log(a + 1); // 2
}
思路剖析
这道题也是很有意思的,这道题的难点在于既要满足条件是 false,又要满足 a 变量通过转换后的值肯定是 1,否则不可能失去后果为 2。如果大家能够想到 valueOf 这个办法,离解答这道题就不远了,valueOf 办法返回任意数据的原始值,也就是说,如果咱们批改变量 a 的原始值,那么 a 最终会以原始值参加 + 1 的计算,而后失去 2。也就是说,咱们只须要这样:
a.valueOf = () => 1; // 将 a 的原始值设置为 1
兴许有人说,那好,这里的 a = 0 即可,记住这里的 a 不能为原始数据类型,因为原始数据类型的原始值就相当于是它自身调用 Number 办法失去的一个数字 0,也就是说:
let a = false; // 或者 a = '' a = 0
以上都是谬误的写法,并不能失去 a 的原始值为 1,因而咱们须要将 a 设置为对象,而可能是 false 值的对象只有 document.all,它的返回值在除 ie 浏览器上都是 false,因而也就满足了既是 false,又批改原始值,可能让变量 a 读取到批改后的原始值。因而最终的解答就是:
let a = document.all;
a.valueOf = () => 1;
if (!a) {console.log(a + 1); // 2
}
那么问题来了,谁会想到 document.all 的返回值是 false?javascript 好奇怪,明明这里是返回 document 的整个汇合,为什么在谷歌浏览器上将它转成布尔值的时候,是 false 而不是 true。
题目 4
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,你就说它奇不奇怪。因而本题的答案就是:
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: 如果各位大佬还有这四道题的其它答案,欢送评论区留言。