乐趣区

关于javascript:从一道毫无人性的刁钻面试题说起

前言

在 JavaScript 中,你能够不必英文字母与数字,就执行 console.log(1) 吗?

换句话说,就在于代码中不能呈现任何英文字母(a-zA-Z)与数字(0-9),除此之外(各种符号)都能够。执行式码之后,会执行 console.log(1),而后在控制台中输入 1

如果你想到能够用什么库或服务之类的货色做到,别急着说出答案。先本人想一下,看看有没有方法本人写进去。如果能从零开始本人写进去,就代表你对 js 这个语言以及各种主动类型转换应该是很相熟的。

剖析几个关键点

要能胜利执行题目所要求的的 console.log(1),必须要实现几个关键点:

  1. 找出执行代码的办法
  2. 如何不必字母与数字得出数字的办法
  3. 如何不必字母与数字失去字母的办法

只有这三点都解决了,就能达成题目的要求。

先解决第一点:找出执行代码的办法

找出执行代码的办法

间接 console.log 是不可能的,因为就算你用字符串拼出 console,你也沒方法像 PHP 那样拿字符串来执行函数。

那 eval 呢?evali 外面能够放字符串,能够是就能够。问题是咱们也没法应用 eval,因为不能用英文字母。

还有什么办法呢?还能够用 function constructor:new Function("console.log(1)") 来执行,但问题是咱们也不能用 new 这个关键字,所以乍一看也不行,不过不须要 new 也能够,只用 Function("console.log(1)") 就能够创立一个可能执行特定代码的函数。

所以接下来的问题就变成了怎样才能拿到 function constructor,只有能拿到就有机会

在 JS 中能够用 .Constructor 拿到某个对象的构造函数,例如 "".constructor 就会失去:ƒ String() { [native code] },如果你有一个函数,就能拿到 function constructor,像这样:(()=>{}).constructor,在这个问题中咱们不能间接用 .constructor ,应该用:(()=>{})['constructor']

如果不反对 ES6,不能用箭头函数怎么办,还有方法失去一个函数吗?

有,而且很容易,就是各种内置函数,例如说 []['fill']['constructor'],其实就是 [].fill.constructor,或者是 ""['slice']['constructor'],也能够拿到 function constructor,所以这不是个问题,就算没有箭头函数也没关系。

一开始咱们冀望的代码是这样:Function('console.log(1)')(),用后面的办法改写的话,应该把后面的 Function 替换成 (()=>{})['constructor'],变成 (()=>{})['constructor']('console.log(1)')()

只有想方法拼凑出这段代码问题就解决了。当初咱们解决了第一个问题:找到执行函数的办法。

如何失去数字

接下来的数字就比较简单了.

这里的要害在与 js 的 强制多态,如果你有看过 js 类型转换的文章,或者会记得 {} + [] 能够得出 0 这个数字。

假如你不晓得这个,我来解释一下:利用 ! 这个运算符,能够失去 false,例如 1[] 或者 !{} 都能够得出 false。而后两个 false 相加就可失去 0![] + ![],以此类推,既然 ![]false,那后面再加一个 !!![] 就是 true,所以 ![] + !![] 就等于 false + true,也就是 0 + 1,后果就是 1

或者用更简短的办法,用来 +[] 也能够利用主动类型转换失去 0 这个后果,那么 +!![] 就是 1

有了 1 之后,就能够失去所有数字了,只有始终一直暴力相加就行了,如果不想这样做,也能够利用位运算 << >> 或者是乘号,比如说要凑出 8,就是 1 << 3,或者是 2 << 2,要凑出 2 就是(+!![])+(+!![]),所以 (+!![])+(+!![]) << (+!![])+(+!![]) 就是 8,只须要四个 1 就行了,不须要本人加 8 次。

不过当初能够先不思考长度,只须要思考能不能凑进去就行了,只有能得出 1 就足够了。

如何失去字符串

最初就是要想方法凑出字符串了,或者说要失去 (()=>{})['constructor']('console.log(1)')() 中的每一个字符。

怎样才能失去字符呢?答案是和数字一样,即 强制多态

下面说过 ![] 能够失去 false,那在前面加一个空字符串:![] + '',不就能够失去 "false" 了吗?这样就能够拿到 a, e, f, l, s 这五个字符。例如 (![] +'')[1] 就是 a,为了不便纪录,咱们来写一小段代码:

const mapping = {a: "(![] +'')[1]",
  e: "(![] +'')[4]",
  f: "(![] +'')[0]",
  l: "(![] +'')[2]",
  s: "(![] +'')[3]",
}

那既然有了false,那么拿到 true 也不是什么难事了,!![] + '' 能够失去 true,当初把代码改成:

const mapping = {a: "(![] +'')[1]",
  e: "(![] +'')[4]",
  f: "(![] +'')[0]",
  l: "(![] +'')[2]",
  r: "(!![] +'')[1]",
  s: "(![] +'')[3]",
  t: "(!![] +'')[0]",
  u: "(!![] +'')[2]",
}

而后再用同样的办法,用 ''+{} 能够失去 "[object Object]"(或是你要用神奇的 []+{} 也行),当初代码能够更新成这样:

const mapping = {a: "(![] +'')[1]",
  b: "(''+{})[2]",
  c: "(''+{})[5]",
  e: "(![] +'')[4]",
  f: "(![] +'')[0]",
  j: "(''+{})[3]",
  l: "(![] +'')[2]",
  o: "(''+{})[1]",
  r: "(!![] +'')[1]",
  s: "(![] +'')[3]",
  t: "(!![] +'')[0]",
  u: "(!![] +'')[2]",
}

从数组或是对象取一个不存在的属性会返回 undefined,再把 undefined 加上字串,就能够拿到字串的 undefined,就像这样:[][{}]+'',能够失去 undefined

拿到之后,咱们的转换表就变得更加残缺了:

const mapping = {a: "(![] +'')[1]",
  b: "(''+{})[2]",
  c: "(''+{})[5]",
  d: "([][{}]+'')[2]",
  e: "(![] +'')[4]",
  f: "(![] +'')[0]",
  i: "([][{}]+'')[5]",
  j: "(''+{})[3]",
  l: "(![] +'')[2]",
  n: "([][{}]+'')[1]",
  o: "(''+{})[1]",
  r: "(!![] +'')[1]",
  s: "(![] +'')[3]",
  t: "(!![] +'')[0]",
  u: "(!![] +'')[2]",
}

看一下转换表,再看看咱们的指标字符串:(()=>{})['constructor']('console["log"](1)')(),略微比对一下,就会发现要凑出 constructor 是没有问题的,要凑出 console 也是没问题的,可是就唯独缺了 log 的 g,目前咱们的转换表外面没有这个字符。

所以还须要从某个中央把 g 拿进去,能力拼凑出咱们想要的字符串。或者也能够换个办法,用其余形式拿到字符。

我一开始想到两个办法,第一个是利用进制转换,把数字用 toString 转成字符串时能够带一个参数 radix,代表这个数字要转换成多少进制,像是 (10).toString(16) 就会失去 a,因为 10 进制的 10 就是 16 进制的 a

英文字母一共 26 个,数字有 10 个,所以只有用 (10).toString(36) 就能失去 a,用 (16).toString(36) 就能够失去 g 了,能够用这个办法失去所有的英文字母。可是问题来了,toString 自身也有 g,但当初咱们没有,所以这办法行不通。

另一个办法是用 base64,JS 有两个内置函数:btoaatobbtoa 是把一个字符串编码为 base64,例如 btoa('abc') 会失去 YWJj,而后再用 atob('YWJj') 解码就会失去 abc

只有想方法让 base64 编码后的后果有 g 就行了,能够写代码去跑,也能够本人缓缓试,侥幸的是 btoa(2) 能失去 Mg== 这个字符串。所以 btoa(2)[1] 的后果就是 g 了。

不过下一个问题又来了,怎么执行 btoa?一样只能通过下面的 function constructor:(()=>{})['constructor']('return btoa(2)[1]')(),这次每一个字符都凑得进去。

能够联合下面的 mapping,写一小段简略的代码来帮忙做转换,指标是把一个字符串转成没有字符的模式:

const mapping = {a: "(![] +'')[1]",
  b: "(''+{})[2]",
  c: "(''+{})[5]",
  d: "([][{}]+'')[2]",
  e: "(![] +'')[4]",
  f: "(![] +'')[0]",
  i: "([][{}]+'')[5]",
  j: "(''+{})[3]",
  l: "(![] +'')[2]",
  n: "([][{}]+'')[1]",
  o: "(''+{})[1]",
  r: "(!![] +'')[1]",
  s: "(![] +'')[3]",
  t: "(!![] +'')[0]",
  u: "(!![] +'')[2]",
}

const one = '(+!![])'
const zero = '(+[])'

function transformString(input) {return input.split('').map(char => {
    // 先假如数字只会有个位数,比拟好做转换
    if (/[0-9]/.test(char)) {if (char === '0') return zero
      return Array(+char).fill().map(_ => one).join('+')
    }
    if (/[a-zA-Z]/.test(char)) {return mapping[char]
    }
    return `"${char}"`
  })
  // 加上 () 保障执行程序
  .map(char => `(${char})`)
  .join('+')
}

const input = 'constructor'
console.log(transformString(input))

输入是:

((''+{})[5])+((''+{})[1])+(([][{}]+'')[1])+((![] +'')[3])+((!![] + '')[0])+((!![] +'')[1])+((!![] + '')[2])+((''+{})[5])+((!![] + '')[0])+((''+{})[1])+((!![] + '')[1])

能够再写一个函数只转换数字,把数字去掉:

function transformNumber(input) {return input.split('').map(char => {
    // 先假如数字只会有个位数,比拟好做转换
    if (/[0-9]/.test(char)) {if (char === '0') return zero
      let newChar = Array(+char).fill().map(_ => one).join('+')
      return`(${newChar})`
    }
    return char
  })
  .join('')
}

const input = 'constructor'
console.log(transformNumber(transformString(input)))

失去的后果是:

((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+(([][{}]+'')[((+!![]))])+((![] +'')[((+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((!![] +'')[((+!![]))])+((!![] + '')[((+!![])+(+!![]))])+((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((''+{})[((+!![]))])+((!![] + '')[((+!![]))])

把后果丢给 console 执行,发现失去的值就是 constructor 没错。所以综合以上代码,回到刚刚那一段:(()=>{})['constructor']('return btoa(2)[1]')(),要失去转换完的后果就是:

const con = transformNumber(transformString('constructor'))
const fn = transformNumber(transformString('return btoa(2)[1]'))
const result = `(()=>{})[${con}](${fn})()`
console.log(result)

后果很长就不贴了,但的确能失去一个 g

在持续之前,先把代码改一下,减少一个能间接转换代码的函数:

function transform(code) {const con = transformNumber(transformString('constructor'))
  const fn = transformNumber(transformString(code))
  const result = `(()=>{})[${con}](${fn})()`
  return result;
}

console.log(transform('return btoa(2)[1]'))

好了,到这里其实曾经接很近起点了,只有一件事还没有解决,那就是 btoa 是 WebAPI,浏览器才有,node.js 并没有这个函数,所以想要做得更丑陋,就必须找到其余形式来产生 g 这个字符。

回顾一下一开始所提的,用 function.constructor 能够拿到 function constructor,以此类推,用 ''['constructor'] 能够拿到 string constructor,只有再加上一个字串,就能够拿到 string constructor 的内容了!

像是这样:''['constructor'] +'',失去的后果是:"function String() { [native code] }",一下子就多了一堆字符串可用,而咱们梦寐以求的 g 就是:(''['constructor'] +'')[14]

因为咱们的转换器目前只能反对一位数的数字(因为做起来简略),咱们改成:(''['constructor'] +'')[7+7],能够写成这样:

mapping['g'] = transform(`return (''['constructor'] +'')[7+7]`)

整合所有成绩

经验过含辛茹苦之后,终于凑出了最麻烦的 g 这个字符,联合咱们刚刚写好的转换器,就能够顺利产生 console.log(1) 去除掉字母与数字后的版本:

const mapping = {a: "(![] +'')[1]",
  b: "(''+{})[2]",
  c: "(''+{})[5]",
  d: "([][{}]+'')[2]",
  e: "(![] +'')[4]",
  f: "(![] +'')[0]",
  i: "([][{}]+'')[5]",
  j: "(''+{})[3]",
  l: "(![] +'')[2]",
  n: "([][{}]+'')[1]",
  o: "(''+{})[1]",
  r: "(!![] +'')[1]",
  s: "(![] +'')[3]",
  t: "(!![] +'')[0]",
  u: "(!![] +'')[2]",
}

const one = '(+!![])'
const zero = '(+[])'

function transformString(input) {return input.split('').map(char => {
    // 先假如数字只会有个位数,比拟好做转换
    if (/[0-9]/.test(char)) {if (char === '0') return zero
      return Array(+char).fill().map(_ => one).join('+')
    }
    if (/[a-zA-Z]/.test(char)) {return mapping[char]
    }
    return `"${char}"`
  })
  // 加上 () 保障执行程序
  .map(char => `(${char})`)
  .join('+')
}

function transformNumber(input) {return input.split('').map(char => {
    // 先假如数字只会有个位数,比拟好做转换
    if (/[0-9]/.test(char)) {if (char === '0') return zero
      let newChar = Array(+char).fill().map(_ => one).join('+')
      return`(${newChar})`
    }
    return char
  })
  .join('')
}

function transform(code) {const con = transformNumber(transformString('constructor'))
  const fn = transformNumber(transformString(code))
  const result = `(()=>{})[${con}](${fn})()`
  return result;
}

mapping['g'] = transform(`return (''['constructor'] +'')[7+7]`)
console.log(transform('console.log(1)'))

最初的代码:

(()=>{})[((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+(([][{}]+'')[((+!![]))])+((![] +'')[((+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((!![] +'')[((+!![]))])+((!![] + '')[((+!![])+(+!![]))])+((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((''+{})[((+!![]))])+((!![] + '')[((+!![]))])](((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+(([][{}]+'')[((+!![]))])+((![] + '')[((+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+((![] + '')[((+!![])+(+!![]))])+((![] +'')[((+!![])+(+!![])+(+!![])+(+!![]))])+(".")+((![] + '')[((+!![])+(+!![]))])+((''+{})[((+!![]))])+((()=>{})[((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+(([][{}]+'')[((+!![]))])+((![] +'')[((+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((!![] +'')[((+!![]))])+((!![] + '')[((+!![])+(+!![]))])+((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((''+{})[((+!![]))])+((!![] + '')[((+!![]))])](((!![] +'')[((+!![]))])+((![] + '')[((+!![])+(+!![])+(+!![])+(+!![]))])+((!![] +'')[(+[])])+((!![] + '')[((+!![])+(+!![]))])+((!![] +'')[((+!![]))])+(([][{}]+'')[((+!![]))])+(" ")+("(")+("'")+("'")+("[")+("'")+((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+(([][{}]+'')[((+!![]))])+((![] +'')[((+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((!![] +'')[((+!![]))])+((!![] + '')[((+!![])+(+!![]))])+((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((''+{})[((+!![]))])+((!![] + '')[((+!![]))])+("'")+("]")+("")+("+")+(" ")+("'")+("'")+(")")+("[")+((+!![])+(+!![])+(+!![])+(+!![])+(+!![])+(+!![])+(+!![]))+("+")+((+!![])+(+!![])+(+!![])+(+!![])+(+!![])+(+!![])+(+!![]))+("]"))())+("(")+((+!![]))+((+!![])+(+!![]))+((+!![])+(+!![])+(+!![]))+(")"))()

用了 1800 个字符,胜利写出了只有:[],),{}"'+!=> 这 12 个字符的程序,并且可能顺利执行 console.log(1)

而因为咱们曾经能够顺利拿到 String 这几个字了,所以就能够用之前提过的位转换的办法,失去任意小写字符,像是这样:

mapping['S'] = transform(`return (''['constructor'] +'')[9]`)
mapping['g'] = transform(`return (''['constructor'] +'')[7+7]`)
console.log(transform('return (35).toString(36)')) // z

那要怎么拿到任意大写字符,或甚至任意字符呢?我也有想到几种形式。

如果想拿到任意字符,能够通过 String.fromCharCode,或是写成另一种模式:""['constructor']['fromCharCode'],就能够拿到任意字符。可是在这之前要先想方法拿到大写的 C,这个就要再想一下了。

除了這條路,還有另外一條,那就是靠編碼,例如說 '\u0043' 其實就是大寫的 C 了,所以我本来以為能够透過這種办法來湊,但我試了一下是不行的,像是 console.log("\u0043") 會印出 C 沒錯,然而 console.log(("\u00" + "43")) 就會间接噴一個錯誤給你,看來編碼沒有辦法這樣拼起來(仔細想想發現滿正当的)。

除了这条路,还有另外一个办法,那就是依附编码,例如说 '\u0043' 其实就是大写的 C,所以我本来认为能够通过这种办法来凑,但试了一下是不行的,像是 console.log("\u0043") 会印出 C 没错,然而 console.log(("\u00" + "43")) 就会间接报一个谬误,看来编码没有方法这样拼起来。不过认真想想还是很正当的。

总结

最初写进去的那个转换的函数其实并不残缺,没有方法执行任意代码码,没有持续做完是因为 jsfuck 这个库曾经写得很分明了,在 README 外面具体了形容它的转换过程,而且最初只用了 6 个字符而已,真的很拜服。

在它的代码当中也能够看出是怎么转换的,大写 C 的局部是用了一个 String 上名为 italics 的函数,能够产生 <i></i>,之后再调用 escape,就会失去 %3Ci%3E%3C/i%3E,而后就失去大写 C 了。

有些人可能会说我平时写 BUG 写得好好的,搞这些乌七八糟的有什么用,但这样做的重点并不在于最初的后果,而是在训练几个货色:

  1. 对于 js 语言的相熟度,咱们用了很多类型转换和内置办法来拼凑货色,可能有些是你素来没听到过的。
  2. 解决问题时放大范畴的能力,从如何把字符串当作函数执行,再到拼凑出数字和字符串,一步步的放大问题,子问题解决之后原问题就解决了


本文首发微信公众号:前端先锋

欢送扫描二维码关注公众号,每天都给你推送陈腐的前端技术文章


欢送持续浏览本专栏其它高赞文章:

  • 深刻了解 Shadow DOM v1
  • 一步步教你用 WebVR 实现虚拟现实游戏
  • 13 个帮你进步开发效率的古代 CSS 框架
  • 疾速上手 BootstrapVue
  • JavaScript 引擎是如何工作的?从调用栈到 Promise 你须要晓得的所有
  • WebSocket 实战:在 Node 和 React 之间进行实时通信
  • 对于 Git 的 20 个面试题
  • 深刻解析 Node.js 的 console.log
  • Node.js 到底是什么?
  • 30 分钟用 Node.js 构建一个 API 服务器
  • Javascript 的对象拷贝
  • 程序员 30 岁前月薪达不到 30K,该何去何从
  • 14 个最好的 JavaScript 数据可视化库
  • 8 个给前端的顶级 VS Code 扩大插件
  • Node.js 多线程齐全指南
  • 把 HTML 转成 PDF 的 4 个计划及实现

  • 更多文章 …
退出移动版