2

JavaScript 类型转换的有趣应用

 2 years ago
source link: https://knightyun.github.io/2019/10/07/js-magic-expression
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

JavaScript 类型转换的有趣应用

可以访问这个网站提前预览:https://knightyun.github.io/magic-expression/

先来看一串代码:

(!(~+[])+{})[--[~+''][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]]*~+[]]

也许你在其他地方看见过这种黑科技操作,那么不妨猜一下上面的代码的值等于多少,实在猜不到可以复制它粘贴到浏览器 console 中回车看看;

这里剧透一下,会得到下面的结果(手动解释,仅供娱乐并无其他意思):

"sb"

当然,你可能还见过这种形式的 "hello world!" 版本的,即最后输出的字符串是 "hello world!",这里由于它的代码过长就不粘贴出来了,原理和上面类似,我们暂且称它为魔法表达式,下面就以上面的例子还分析一下这窜代码的“魔法”;

分析之前先来了解一些基础,在 js 的世界中,存在一种类型转换的机制,大致就是字面意思,下面分别举例说明;

Number / String

console.log(1 + 1); // 2
console.log('1' + '1'); // "11"
console.log(1 + '1'); // "11"
console.log('1' + 1); // "11"

console.log(1 - 1); // 0
console.log('1' - '1'); // 0
console.log(1 - '1'); // 0
console.log('1' - 1); // 0

console.log('a' - 'b'); // NaN
console.log('a' - '1'); // NaN
console.log('a' - 1); // NaN
// *,/,% 等运算符与 - 运算类似

可以看到,上面的行为可能有点怪异,因为在其他语言中可能会报错,但是在 js 中确实是这样执行的,即静默地尽可能地把运算符两边的类型转换一致再运算;

Number / Boolean

当然这种转换不限于字符串和数字类型之间,也包括其他类型,比如很经典的一中转换:

console.log(1 == true); // true
console.log(1 === true); // false

上面就是把数字类型和 Boolean 类型的值进行比较,第一行的输出结果是因为使用 == 操作符时会自行把 1 转换为 true,所以两边相等(可以理解为执行 Boolean(0) 操作);而 === 操作符除了比较两边的值,还会比较两边类型,二者都相同才判断相等,即没有进行类型转换;

其他类型的转换情况:

console.log(Boolean([])); // true
console.log(Boolean('')); // false
console.log(Boolean(String(''))); // false
console.log(Boolean(new String(''))); // true
console.log(Boolean(' ')); // true
console.log(Boolean({})); // true
console.log(Boolean(0)); // false
console.log(Boolean(Number(0))); // false
console.log(Boolean(new Number(0))); // true

下面是该机制的一些实际应用:

console.log(+'1', typeof +'1'); // 1 "number"
console.log(1 + '', typeof (1 + '')); // 1 "number"
console.log('' + 1, typeof ('' + 1)); // 1 "string"

console.log(+[], typeof +[]); // 0 "number"
console.log(-[], typeof -[]); // -0 "number"
console.log(+[1], typeof +[1]); // 1 "number"
console.log(+[1,2], typeof +[1,2]); // NaN "number"
console.log('' + [], typeof ('' + []), ('' + []).length); // '' "string" 0
console.log([] + '', typeof ('' + []), ('' + []).length); // '' "string" 0

console.log(+{}, typeof +{}); // NaN "number"
console.log({} + '', typeof ({} + '')); // [object Object] "string"
console.log('' + {}, typeof ('' + {})); // [object Object] "string"

另外,”!””~” 运算符也算是 js 中较为常见的,其中 ! 是逻辑运算符,代表,而 ~ 是位运算符,代表按位取反,它们有以下关系:

console.log(!true); // false
console.log(!false); // true
console.log(!!true); // true

console.log(~0); // -1
console.log(~3); // -4
console.log(~~3); // 3

现在开始分析最早提到的那串代码,它的神奇之处就在于整个代码在不包括任何一个字母的情况下输出了字母,代码中都是一些运算符和操作符,代码串挨在一起不利于观察,我们先稍微格式化一下:

( !(~+[]) + {} )
[
    --[~+''][+[]] * 
    [~+[]] + 
    ~~!+[]
]

+

( {} + [] ) 
[
    [~!+[]] *
    ~+[]
];

这里只是拆分美化了一下格式,输出结果不变,然后一行一行进行分析,根据拆分结果,其实整个代码就是两大部分相加;

第一部分中,第一行是 (!(~+[]) + {}),也是两部分加和,根据前面的基础,可以得到如下分析结果:

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

            ↓
     
(!(~0) + "[object Object]")
     
            ↓
            
 (!-1 + "[object Object]")

            ↓
            
(false + "[object Object]")
            
            ↓
            
(false + "[object Object]")
            
            ↓
            
  ("false[object Object]")

第一大部分剩下的内容:

[
    --[~+''] [+[]] * 
    [~+[]] + 
    ~~!+[]
]

分析结果如下:

--[~+''][+[]]  *  [~+[]]   +  ~~!+[]

      ↓             ↓           ↓
      
  --[~0][0]    *   [~0]    +  ~~!0
      
      ↓             ↓           ↓
      
  --[-1][0]    *   [-1]    + ~~true
      
      ↓                         ↓
      
   --(-1)                      ~~1
      
      ↓                         ↓
      
     -2                         1
     
=> -2 * [-1] + 1 = -2 * -1 + 1
                 = 3

所以第一大部分的结果是:

("false[object Object]")[3]; // "s"

第二大部分也执行类似的分解,第一行内容是 ({} + []),其实也是在拼接字符串,分析如下:

      ({} + [])
      
          ↓
      
("[object Object]" + "")
      
          ↓
          
  ("[object Object]")

余下内容是:

[
    [~!+[]] *
    ~+[]
];

分析如下:

 [~!+[]]  *  ~+[]
 
    ↓         ↓
    
  [~!0]   *   ~0
    
    ↓         ↓
    
 [~true]  *   -1
    
    ↓         ↓
    
   [~1]   *   -1
    
    ↓         ↓

   [-2]   *   -1
   
         ↓
         
      -2 * -1
         
         ↓
         
         2

所以第二大部分结果就是:

("[object Object]")[2]; // "b"

最后两个大部分内容字符串拼接就是最终结果,这里汇总一下:

( !(~+[]) + {} ) // "false[object Object]"
[
    --[~+''] [+[]] * // -2
    [~+[]] + // -1
    ~~!+[] // 1
] // => ("false[object Object]")[3] => "s"

+

({} + []) // [object Object]
[
    [~!+[]] * // -2
    ~+[] // -1
]; // => ("[object Object]")[2] => "b"

// => "s" + "b" = "sb"

费了这么大功夫就得出两个字母,想必这个过程对于理解 js 中的类型转换机制是很有帮助的;

然后就是回顾之前那个问题,为什么整个代码没有出现字母却在结果中出现了,根据上面的分析可以看出,字符串是通过类似以下方式得到的:

console.log([] + {}); // "[object Object]"
console.log([] + true); // "true"
console.log([] + false); // "false"

然后在 js 中字符串也可以通过类似数组的方式获取某个字符:

console.log(("[object Objact]")[2]); // "b"
// "b" 在字符串中的索引为 2

那么前面提到的输出 hello world! 的代码,其实也是通过类似的方式获取字符然后拼接而成,只是需要思考从哪些格式化输出中获取想要的那个字符而已;

顺着上面的思路,如果我们想要输出任意指定字符,该如何实现呢?即字符中可能包含 a-z, A-Z, 0-9 中的任何一个字符,甚至是特殊字符;

可能的输出

前面提到输出的关键是存在这个一个标准格式化输出(如 true, false),然后就能从里面扣取字符了,作者目前能想到的标准输出的字符串有如下(可能疏漏):

console.log([]+[]); // ""
console.log([]+!![]); // "true"
console.log([]+![]); // "false"
console.log([]+{}); // "[object Object]"
console.log([]+!![]-[]); // "NaN"
console.log([]+[][+[]]); // "undefined"
console.log([]+~~!![]/+[]) // "Infinity"
console.log(([]+~[])[~~[]]); // "-"

即使这样,汇总下来的字母也只有:

abcdefiIjlnNoOrstuy-

离目标似乎有点远~~,数字就比较好弄了:

console.log(+[]); // 0
console.log(+!![]); // 1
console.log(!![]+!![]); // 2(后续的数字可以这样叠加)
console.log(-~+!![]); // 2(也可以换个简短的方法)
console.log(!![]+!![]+!![]); // 3
console.log(!![]+!![]+!![]+!![]); // 4
console.log(!![]+!![]+!![]+!![]+!![]); // 5
console.log(!![]+!![]+!![]+!![]+!![]+!![]); // 6
console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 7
console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 8
console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 9

更大的数字就可以通过字符拼接或者四则运算获得;

扩大输出范围

目前还是没有获得大部分大写字母和特殊字符 ~!@#$%^&*()_+-=\|[]{};':",./<>?,甚至是汉字或者是其他国字符,等等,说到这里是不是想起了什么?没错就是Unicode(号称万国符来着),首先 js 中使用 Unicode 的形式是 "\uXXXX",后面的 XXXX 是四个十六进制字符,例如:

console.log('\u0061'); // "a"

// 获取 a 的字符编码
console.log('a'.charCodeAt()); // 97

// 转换为 16 进制
console.log('a'.charCodeAt().toString(16)); // "61"
// 0061 = 61

// 汉字
console.log('黄'.charCodeAt().toString(16)); // "9ec4"
console.log('\u9ec4'); // "黄"

所以只要能够表示 \, u, a-f, 0-9 这几个字符,就能表示所有 Unicode 字符了!根据前面的总结,其实我们已经能够表示这几个字符了!不过呢,直接拼接 Unicode 的话会出现下面的问题:

console.log('\u0061'); // "a"
console.log('\u' + '0061'); // SyntaxError: Invalid Unicode escape sequence
console.log('\\u' + '0061'); // "\u0061"

所以我们不能通过直接拼接 Unicode 字符串来获得能被解析的 Unicode 符的,因此不得不换个思路;既然不能拼接直接 Unicode,那么有么有间接的方法或者 API 呢? 可能会想到使用 eval()

var s = eval('"' + '\\u' + '0061' + '"');
console.log(s); // "a"

虽然成功了,但是目前我们似乎还无法获得 eval 中的字母 v,所以要继续换个方法;

Function

其实还有一个函数可以实现类似的功能,它就是 Function(),即构造函数的函数,也是声明函数的另一种方法,举例:

var fn = Function('a', 'b', 'return a + b');
var fn2 = Function('return 4');

console.log(fn(1, 2)); // 3
console.log(fn2()); // 4

然后我们就可以像这样拼接 Unicode 了:

console.log(Function('return ' + '"\\u' + '0061' + '"')());
// "a"

另外我们需要知道的是:[]['constructor']['constructor'] === Function,所以最后只要构造出这样的字符串就行了:

[]['constructor']['constructor']('return '+'"'+'\\u0061'+'"')();

根据前面的基础,我们已经能够获取 constructor, return 里面的所有字母了,这里再把需要用到的字符全部汇总一下:

([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]; // " ""
([]+/\\\\/)[+!![]]; // "\"
[]+(+[]); // "0"
[]+(+!![]); // "1"
[]+(!![]+!![]); // "2"
[]+(!![]+!![]+!![]); // "3"
[]+(!![]+!![]+!![]+!![]); // "4"
[]+(!![]+!![]+!![]+!![]+!![]); // "5"
[]+(!![]+!![]+!![]+!![]+!![]+!![]); // "6"
[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "7"
[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "8"
[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "9"
([]+![])[+!![]]; // "a"
([]+{})[!![]+!![]]; // "b"
([]+{})[!![]+!![]+!![]+!![]+!![]]; // "c"
([]+[][+[]])[!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]; // "d"
([]+!![])[!![]+!![]+!![]]; // "e"
([]+![])[+[]]; // "f"
([]+[][+[]])[+!![]]; // "n"
([]+{})[+!![]]; // "o"
([]+!![])[+!![]]; // "r"
([]+![])[!![]+!![]+!![]]; // "s"
([]+!![])[+[]]; // "t"
([]+!![])[!![]+!![]; // "u"

最后剩下的就是把想要的输出,转换为 Unicode,再拆分为单个字符对应上面的表达式进行拼接就行了,我们来试一下效果(拿走不谢 :)):

var s1 = 
[][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]](([]+!![])[+!![]]+([]+!![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[!![]+!![]]+([]+!![])[+!![]]+([]+[][+[]])[+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]+'"'+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+[])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+![])[+[]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+[])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+![])[+[]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+!![])+'"')();

console.log(s1);
// "I love you!"

var s2 = 
[][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]](([]+!![])[+!![]]+([]+!![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[!![]+!![]]+([]+!![])[+!![]]+([]+[][+[]])[+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]+'"'+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![])+[]+(+!![])+[]+(+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![])+[]+(!![]+!![]+!![])+[]+(+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![])+([]+![])[+[]]+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(+[])+'"')();

console.log(s2);
// "我爱你"

这里放一个在线转换的网站: 点我在线转换


Recommend

  • 45
    • zhuanlan.zhihu.com 6 years ago
    • Cache

    有趣也有用的现代类型系统

  • 77
    • www.10tiao.com 6 years ago
    • Cache

    基本数据类型转换

    用字符数据类型变量接收一个整型数据会输出什么?

  • 31
    • studygolang.com 5 years ago
    • Cache

    golang: 类型转换和类型断言

    类型转换在程序设计中都是不可避免的问题。当然有一些语言将这个过程给模糊了,大多数时候开发者并不需要去关注这方面的问题。但是golang中的类型匹配是很严格的,不同的类型之间通常需要手动转换,编译器不会代你去做这个事。我之所以说...

  • 66
    • studygolang.com 5 years ago
    • Cache

    Golang类型转换模块 - gconv

    文章来源: https://gfer.me/util/gconv/index gf 框架提供了非常强大的类型转换包 gconv ,可以实现将任何数据类型转换为指定的数据...

  • 54
    • blog.csdn.net 5 years ago
    • Cache

    常用编程语言 类型转换函数

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35543858/article/details/88827603 Number() 转换为数字 String() toString() 转换为字符串

  • 6
    • segmentfault.com 3 years ago
    • Cache

    有趣的JS-隐式类型转换

    有趣的JS-隐式类型转换当两个不同数据类型的操作数在做运算,或者操作数与操作符不匹配的时候,js引擎不会报错,会把操作数转成对应的数据类型继续执行下去,这个转换是自动完成的,经常被叫做隐式类型转换。其实大部分开发者都或多或少了解过这一点,...

  • 9

    December 31, 2019javascript类型转换 Javascript 隐式类型转换,一篇就够了 ! 一图胜千言 Javascript发生隐式类型转换时的各种猫腻,相信各位开发者已经饱受折磨。

  • 2
    • segmentfault.com 2 years ago
    • Cache

    了解JavaScript中的类型转换

    1.什么是类型转换?类型转换的定义很容易理解,就是值从一个类型转换为另一个类型,比如说从String类型转换为Number类型,'42'→42。但是,如果我们对JS中的类型转换规则了解的并不足够的话,我们就会遇到很多...

  • 9
    • www.fly63.com 2 years ago
    • Cache

    Javascript 里的类型转换规则

    Javascript 里的类型转换是一个你永远绕不开的话题,不管你是在面试中还是工作写代码,总会碰到这类问题和各种的坑,所以不学好这个那是不行滴。关于类型转换我也看过不少的书和各种博...

  • 6

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK