奇怪,奇怪,真的好奇怪的javascript

孔融
• 阅读 983

奇怪的 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: 如果各位大佬还有这四道题的其它答案,欢迎评论区留言。
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Karen110 Karen110
4年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
凯特林 凯特林
4年前
您知道JavaScript中的0.1 + 0.2 ≠ 0.3吗?
嘿👋自从我使用JavaScript已有一段时间了。昨天,我经历了一个非常奇怪的行为。同时我真的很困惑和惊讶😕。最初我以为,我发现了一个论点再次诅咒JavaScript。但是,经过一些研究,我发现这不是错误。这是数学,也是计算机处理数字的方式。好吧,还有其他一些怪异的东西幕后发生了什么?它背后的简单逻辑是计算机使用以2为基的(二进制)浮点数系统。让我们用一个
小嫌 小嫌
3年前
Javascript中的变量提升
定义JavaScript中奇怪的一点是你可以在变量和函数声明之前使用它们。就好像是变量声明和函数声明被提升了代码的顶部一样。sayHi()//Hithere!functionsayHi()console.log('Hithere!')name'JohnDoe'console.log(name)//JohnDoevarn
Stella981 Stella981
3年前
ImageMagick处理gif图片的奇怪问题
今天在裁剪gif图片的时候遇到问题了。gif图片裁剪后,图片大小仍然是原图大小,只保留了裁剪区域的图像,其余部分变成背景色透明了。查找后,发现时ImageMagick的问题,有解决办法,但是jmagick目前没有任何解决方案,大家有知道的吗?我在本地把这种gif用工具转成jpeg都再裁剪都不好用,真是神奇。别人的帖子如下:http://d
Stella981 Stella981
3年前
OpenJDK11与Spring Cloud Finchley的不兼容问题与解决
本文的环境:OpenJDK11.0.4,SpringCloudfinchleySR4,SpringBoot2.0.3最近遇到了一个问题,在feign调用的时候,时常会出现这样一个奇怪的错误:2019100708:00:00.620ERRORxxx,e1ba4c7540954aa3,871b99c4576d42e3
Wesley13 Wesley13
3年前
02. react 初次见面
1、JSX简介我们来观察一下声明的这个变量:constelement<h1Hello,world!</h1;这种看起来可能有些奇怪的标签语法既不是字符串也不是HTML.它被称为JSX,一种JavaScript的语法扩展。我们推荐在React中使用JSX来描述用户界面。JSX乍
奇怪!应用的日志呢??
1.问题回顾问题背景是在进行中台应用中间件迁移过程中,发现存在项目启动失败或者项目正常启动(jsf正常挂载并正常运行,mq正常发送和消费)但是无任何日志打印现象。更奇怪的是不打印日志竟然是偶发的,在测试环境中多次部署都未出现项目启动但无日志打印情况,而且玄
贾蔷 贾蔷
3个月前
力扣1137题 解题思路和步骤 C++代码实现,力扣一共多少题
一、题目分析力扣1137题要求我们找到第N个泰波那契数。泰波那契数的定义是:T00,T11,T21,且在n0的条件下Tn3TnTn1Tn2。,当n4时,T4T3T2T14。这道题主要考查我们对递归或动态规划的理解和运用。在思考解题方法时,我们