赋值运算符应用错误
在 JavaScript 程序中如果你在 if 条件语句中使用赋值运算符的等号 (=) 将会产生一个错误结果, 正确的方法是使用比较运算符的两个等号 (==)。
if 条件语句返回 false (是我们预期的)因为 x 不等于 10:
var x = 0;
if (x == 10)
if 条件语句返回 true (不是我们预期的)因为条件语句执行为 x 赋值 10,10 为 true:
var x = 0;
if (x = 10)
if 条件语句返回 false (不是我们预期的)因为条件语句执行为 x 赋值 0,0 为 false:
var x = 0;
if (x = 0)
赋值语句返回变量的值。 |
比较运算符常见错误
在常规的比较中,数据类型是被忽略的,以下 if 条件语句返回 true:
var x = 10;
var y = "10";
if (x == y)
在严格的比较运算中,=== 为恒等计算符,同时检查表达式的值与类型,以下 if 条件语句返回 false:
var x = 10;
var y = "10";
if (x === y)
这种错误经常会在 switch 语句中出现,switch 语句会使用恒等计算符(===)进行比较:
以下实例会执行 alert 弹窗:
var x = 10;
switch(x) {
case 10: alert("Hello");
}
以下实例由于类型不一致不会执行 alert 弹窗:
var x = 10;
switch(x) {
case "10": alert("Hello");
}
加法与连接注意事项
加法是两个数字相加。
连接是两个字符串连接。
JavaScript 的加法和连接都使用 + 运算符。
接下来我们可以通过实例查看两个数字相加及数字与字符串连接的区别:
var x = 10 + 5; // x 的结果为 15
var x = 10 + "5"; // x 的结果为 "105"
使用变量相加结果也不一致:
var x = 10;
var y = 5;
var z = x + y; // z 的结果为 15
var x = 10;
var y = "5";
var z = x + y; // z 的结果为 "105"
浮点型数据使用注意事项
JavaScript 中的所有数据都是以 64 位浮点型数据(float) 来存储。
所有的编程语言,包括 JavaScript,对浮点型数据的精确度都很难确定:
var x = 0.1;
var y = 0.2;
var z = x + y // z 的结果为 0.3
if (z == 0.3) // 返回 false
我解决以上问题,可以用整数的乘除法来解决:
实例
var z = (x * 10 + y * 10) / 10; // z 的结果为 0.3
JavaScript 字符串分行
JavaScript 运行我们在字符串中使用断行语句:
实例 1
var x =
"Hello World!";
但是,在字符串中直接使用回车换行是会报错的:
实例 2
var x = "Hello
World!";
我们可以在选择开发工具或按下 F12 来查看错误信息:
字符串断行需要使用反斜杠(\),如下所示:
实例 3
var x = "Hello \
World!";
错误的使用分号
以下实例中,由于分号使用错误,if 语句中的代码块将无法执行:
if (x == 19);
{
// code block
}
Return 语句使用注意事项
JavaScript 默认是在代码的最后一行自动结束。
以下两个实例返回结果是一样的(一个有分号一个没有):
实例 1
function myFunction(a) {
var power = 10
return a * power
}
实例 2
function myFunction(a) {
var power = 10;
return a * power;
}
JavaScript 也可以使用多行来结束一个语句。
以下实例返回相同的结果:
实例 3
function myFunction(a) {
var
power = 10;
return a * power;
}
但是,以下实例结果会返回 undefined:
实例 4
function myFunction(a) {
var
power = 10;
return
a * power;
}
为什么会有这样的结果呢?因为在 JavaScript 中,实例 4 的代码与下面的代码一致:
function myFunction(a) {
var
power = 10;
return; // 分号结束,返回 undefined
a * power;}
解析
如果是一个不完整的语句,如下所示:
var
JavaScript 将尝试读取第二行的语句:
power = 10;
但是由于这样的语句是完整的:
return
JavaScript 将自动关闭语句:
return;
在 JavaScript 中,分号是可选的 。
由于 return 是一个完整的语句,所以 JavaScript 将关闭 return 语句。
注意:不用对 return 语句进行断行。 |
数组中使用名字来索引
许多程序语言都允许使用名字来作为数组的索引。
使用名字来作为索引的数组称为关联数组(或哈希)。
JavaScript 不支持使用名字来索引数组,只允许使用数字索引。
实例
var person = [];
person[0] = "John";
person[1] = "Doe";
person[2] = 46;
var x = person.length; // person.length 返回 3
var y = person[0]; // person[0] 返回 "John"
在 JavaScript 中, 对象 使用 名字作为索引。
如果你使用名字作为索引,当访问数组时,JavaScript 会把数组重新定义为标准对象。
执行这样操作后,数组的方法及属性将不能再使用,否则会产生错误:
实例
var person = [];
person["firstName"] = "John";
person["lastName"] = "Doe";
person["age"] = 46;
var x = person.length; // person.length 返回 0
var y = person[0]; // person[0] 返回 undefined
定义数组元素,最后不能添加逗号
错误的定义方式:
points = [40, 100, 1, 5, 25, 10,];
正确的定义方式:
points = [40, 100, 1, 5, 25, 10];
定义对象,最后不能添加逗号
错误的定义方式:
websites = {site:"菜鸟教程", url:"www.runoob.com", like:460,}
正确的定义方式:
websites = {site:"菜鸟教程", url:"www.runoob.com", like:460}
Undefined 不是 Null
在 JavaScript 中, null 用于对象, undefined 用于变量,属性和方法。
对象只有被定义才有可能为 null,否则为 undefined。
如果我们想测试对象是否存在,在对象还没定义时将会抛出一个错误。
错误的使用方式:
if (myObj !== null && typeof myObj !== "undefined")
正确的方式是我们需要先使用 typeof 来检测对象是否已定义:
if (typeof myObj !== "undefined" && myObj !== null)
程序块作用域
在每个代码块中 JavaScript 不会创建一个新的作用域,一般各个代码块的作用域都是全局的。
以下代码的的变量 i 返回 10,而不是 undefined:
实例
for (var i = 0; i < 10; i++) {
// some code
}
return i;
如您还有不明白的可以在下面与我留言或是与我探讨QQ群308855039,我们一起飞!
言
最近与部门老大一起面试了许多前端求职者,其中「想换个学习氛围较好的人占多数」,但良好的学习氛围也是需要一点点营造出来的。
为此我们组建了我们团队内部的“「现代 JavaScript 突击队」”,第一期学习内容为《现代 JavaScript 教程》系列,帮助小组成员系统地进行学习巩固,并「让大家养成系统性学习和输出学习总结的学习方式」。
本文作为我输出的第一部分学习总结,希望作为一份自测清单,帮助大家巩固知识,温故知新。
这里也下面分享我们学习小组的“押金制度”和“押金记录表”
“押金制度”和“押金记录表”
接下来开始分享自测清单的内容。
初中级前端 JavaScript 自测清单.png
JavaScript 脚本引入方式有两种:
<script> 标签有以下常用属性:
src :指定外部脚本的URI, 如果设置了 src 特性,script 标签内容将会被忽略;
<script src="example-url.js"></script>
type :指定引用脚本的语言,属性值为 MIME 类型,包括text/javascript, text/ecmascript, application/javascript, 和application/ecmascript。如果没有定义这个属性,脚本会被视作JavaScript。
ES6 新增了属性值 module ,代码会被当做 JavaScript 模块。
<script type="text/javascript"></script>
async 规定一旦脚本可用,则会异步执行。 注意:async 属性仅适用于外部脚本(「只有在使用 src 属性时」)。 有多种执行外部脚本的方法: 如果 async="async" :脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行); 如果不使用 async 且 defer="defer" :脚本将在页面完成解析时执行; 如果既不使用 async 也不使用 defer :在浏览器继续解析页面之前,立即读取并执行脚本;
<script async="async"></script>
defer 属性规定是否对脚本执行进行延迟,直到页面加载为止。
如果您的脚本不会改变文档的内容,可将 defer 属性加入到 <script> 标签中,以便加快处理文档的速度。因为浏览器知道它将能够安全地读取文档的剩余部分而不用执行脚本,它将推迟对脚本的解释,直到文档已经显示给用户为止。
<script defer="defer"></script>
详细介绍可以阅读《MDN <script>章节 》。
语句是执行行为(action)的语法结构和命令。如: alert('Hello, world!') 这样可以用来显示消息的语句。
存在分行符时,多数情况下可以省略分号。但不全是,比如:
alert(3 +
1
+ 2);
建议新人最好不要省略分号。
「单行注释以两个正斜杠字符 // 开始。」
// 注释文本
console.log("leo");
「多行注释以一个正斜杠和星号开始 “/*” 并以一个星号和正斜杆结束 “*/”。」
/*
这是多行注释。
第二行注释。
*/
console.log("leo");
JavaScript 的严格模式是使用受限制的 JavaScript 的一种方式,从而隐式地退出“草率模式”。
"use strict" 指令将浏览器引擎转换为“现代”模式,改变一些内建特性的行为。
通过在脚本文件/函数开头添加 "use strict"; 声明,即可启用严格模式。 全局开启严格模式:
// index.js
"use strict";
const v = "Hi! I'm a strict mode script!";
函数内开启严格模式:
// index.js
function strict() {
'use strict';
function nested() {
return "And so am I!";
}
return "Hi! I'm a strict mode function! " + nested();
}
启用 "use strict" 后,为未定义元素赋值将抛出异常:
"use strict";
leo = 17; // Uncaught ReferenceError: leo is not defined
启用 "use strict" 后,试图删除不可删除的属性时会抛出异常:
"use strict";
delete Object.prototype; // Uncaught TypeError: Cannot delete property 'prototype' of function Object() { [native code] }
详细介绍可以阅读《MDN 严格模式章节 》。
变量是数据的“命名存储”。
目前定义变量可以使用三种关键字:var / let / const。三者区别可以阅读《let 和 const 命令》 。
let name = "leo";
let name = "leo", age, addr;
let name = "leo", age = 27, addr = "fujian";
变量命名有 2 个限制:
JavaScript 是一种「弱类型」或者说「动态语言」。这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据:
var foo = 42; // foo is a Number now
foo = "bar"; // foo is a String now
foo = true; // foo is a Boolean now
详细介绍可以阅读《MDN JavaScript 数据类型和数据结构 》。
前七种为基本数据类型,也称为原始类型(值本身无法被改变),而 object 为复杂数据类型。 八大数据类型分别是:
通过 typeof 运算符检查:
typeof "leo" // "string"
typeof undefined // "undefined"
typeof 0 // "number"
typeof NaN // "number"
typeof 10n // "bigint"
typeof true // "boolean"
typeof Symbol("id") // "symbol"
typeof [1,2,3,4] // "object"
typeof Math // "object" (1) Math 是一个提供数学运算的内建 object。
typeof null // "object" (2) JavaScript 语言的一个错误,null 不是一个 object。null 有自己的类型,它是一个特殊值。
typeof alert // "function" (3) alert 在 JavaScript 语言中是一个函数。
JavaScript 变量可以转换为新变量或其他数据类型:
通过全局方法 String() 将**其他类型数据(任何类型的数字,字母,布尔值,对象)**转换为 String 类型:
String(123); // "123"
// Number方法toString()/toExponential()/toFixed()/toPrecision() 也有同样效果。
String(false); // "false"
// Boolean方法 toString() 也有同样效果。
String(new Date()); // "Sun Jun 07 2020 21:44:20 GMT+0800 (中国标准时间)"
// Date方法 toString() 也有同样效果。
String(leo);
通过以下几种方式能将其他类型数据转换为 Number 类型:
const age = +"22"; // 22
const age = Number("22"); // 22
Number.parseFloat("22"); // 22
Number.parseInt("22"); // 22
// 布尔值
Number(false) // 返回 0
Number(true) // 返回 1
// 日期
const date = new Date();
Number(date); // 返回 1591537858154
date.getTime(); // 返回 1591537858154,效果一致。
// 自动转换
5 + null // 返回 5 null 转换为 0
"5" + null // 返回"5null" null 转换为 "null"
"5" + 1 // 返回 "51" 1 转换为 "1"
"5" - 1 // 返回 4 "5" 转换为 5
转换规则如下:
Boolean(1); // true
Boolean(0); // false
Boolean("hello"); // true
Boolean(""); // false
Boolean("0"); // true
Boolean(" "); // 空白, 也是 true (任何非空字符串是 true)
类型转换
常见运算符如加法 + 、减法 - 、乘法 * 和除法 / ,举一个例子,来介绍一些概念:
let sum = 1 + 2;
let age = +18;
其中:
let msg = "hello " + "leo"; // "hello leo"
let total = 10 + 20; // 30
let text1 = "1" + "2"; // "12"
let text2 = "1" + 2; // "12"
let text3 = 1 + "2"; // "12"
let text4 = 1 + 2 + "3"; // "33"
let num = +text1; // 12 转换为 Number 类型
运算符的优先级决定了表达式中运算执行的先后顺序,优先级高的运算符最先被执行。 下面的表将所有运算符按照优先级的不同从高(20)到低(1)排列。
优先级 运算类型 关联性 运算符 20 圆括号 n/a(不相关) ( … ) 19 成员访问 从左到右 … . … 需计算的成员访问 从左到右 … [ … ] new (带参数列表) n/a new … ( … ) 函数调用 从左到右 … ( … ) 可选链(Optional chaining) 从左到右 ?. 18 new (无参数列表) 从右到左 new … 17 后置递增(运算符在后) n/a … ++ 后置递减(运算符在后) … -- 16 逻辑非 从右到左 ! … 按位非 ~ … 一元加法 + … 一元减法 - … 前置递增 ++ … 前置递减 -- … typeof typeof … void void … delete delete … await await … 15 幂 从右到左 … ** … 14 乘法 从左到右 … * … 除法 … / … 取模 … % … 13 加法 从左到右 … + … 减法 … - … 12 按位左移 从左到右 … << … 按位右移 … >> … 无符号右移 … >>> … 11 小于 从左到右 … < … 小于等于 … <= … 大于 … > … 大于等于 … >= … in … in … instanceof … instanceof … 10 等号 从左到右 … == … 非等号 … != … 全等号 … === … 非全等号 … !== … 9 按位与 从左到右 … & … 8 按位异或 从左到右 … ^ … 7 按位或 从左到右 … | … 6 逻辑与 从左到右 … && … 5 逻辑或 从左到右 … || … 4 条件运算符 从右到左 … ? … : … 3 赋值 从右到左 … = … … += … … -= … … *= … … /= … … %= … … <<= … … >>= … … >>>= … … &= … … ^= … … |= … 2 yield 从右到左 yield … yield* yield* … 1 展开运算符 n/a ... … 0 逗号 从左到右 … , …
3 > 2 && 2 > 1
// return true
3 > 2 > 1
// 返回 false,因为 3 > 2 是 true,并且 true > 1 is false
// 加括号可以更清楚:(3 > 2) > 1
在 JS 中的值的比较与数学很类型:
// 使用 ==,非严格等于,不关心值类型
// == 运算符会对比较的操作数做隐式类型转换,再比较
'1' == 1; // true
// 使用 ===,严格相等,关心值类型
// 将数字值 -0 和 +0 视为相等,并认为 Number.NaN 不等于 NaN。
'1' === 1; // false
(图片来自:《MDN JavaScript 中的相等性判断》)
另外 ES6 新增 Object.is 方法判断两个值是否相同,语法如下:
Object.is(value1, value2);
以下任意项成立则两个值相同:
Object.is('foo', 'foo'); // true
Object.is(window, window); // true
Object.is('foo', 'bar'); // false
Object.is([], []); // false
var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo); // true
Object.is(foo, bar); // false
Object.is(null, null); // true
// 特例
Object.is(0, -0); // false
Object.is(0, +0); // true
Object.is(-0, -0); // true
Object.is(NaN, 0/0); // true
兼容性 Polyfill 处理:
if (!Object.is) {
Object.is = function(x, y) {
// SameValue algorithm
if (x === y) { // Steps 1-5, 7-10
// Steps 6.b-6.e: +0 != -0
return x !== 0 || 1 / x === 1 / y;
} else {
// Step 6.a: NaN == NaN
return x !== x && y !== y;
}
};
}
对于相等性判断比较简单:
null == undefined; // true
null === undefined; // false
对于其他比较,它们会先转换位数字: null 转换为 0 , undefied 转换为 NaN 。
null > 0; // false 1
null >= 0; // true 2
null == 0; // false 3
null < 1; // true 4
需要注意: null == 0; // false 这里是因为:undefined 和 null 在相等性检查 == 中「不会进行任何的类型转换」,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。
undefined > 0; // false 1
undefined > 1; // false 2
undefined == 0; // false 3
第 1、2 行都返回 false 是因为 undefined 在比较中被转换为了 NaN,而 NaN 是一个特殊的数值型值,它与任何值进行比较都会返回 false。 第 3 行返回 false 是因为这是一个相等性检查,而 undefined 只与 null 相等,不会与其他值相等。
显示一个警告对话框,上面显示有指定的文本内容以及一个“确定”按钮。 「注意:弹出模态框,并暂停脚本,直到用户点击“确定”按钮。」
// 语法
window.alert(message);
alert(message);
// 示例
alert('hello leo!');
message是要显示在对话框中的文本字符串,如果传入其他类型的值,会转换成字符串。
显示一个对话框,对话框中包含一条文字信息,用来提示用户输入文字。 「注意:弹出模态框,并暂停脚本,直到用户点击“确定”按钮。」 当点击确定返回文本,点击取消或按下 Esc 键返回 null。 语法如下:
let result = window.prompt(text, value);
Window.confirm() 方法显示一个具有一个可选消息和两个按钮(确定和取消)的模态对话框。 「注意:弹出模态框,并暂停脚本,直到用户点击“确定”按钮。」 语法如下:
let result = window.confirm(message);
当 if 语句当条件表达式,会将表达式转换为布尔值,当为 truthy 时执行里面代码。 转换规则如:
「条件(三元)运算符」是 JavaScript 仅有的使用三个操作数的运算符。一个条件后面会跟一个问号(?),如果条件为 truthy ,则问号后面的表达式A将会执行;表达式A后面跟着一个冒号(:),如果条件为 falsy ,则冒号后面的表达式B将会执行。本运算符经常作为 [if](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/if...else) 语句的简捷形式来使用。 语法:
condition ? exprIfTrue : exprIfFalse
let getUser = function(name){
return name === 'leo' ? 'hello leo!' : 'unknow user';
}
// 可以简写如下:
let getUser = name => name === 'leo' ? 'hello leo!' : 'unknow user';
getUser('leo'); // "hello leo!"
getUser('pingan'); // "unknow user"
详细可以阅读《MDN 逻辑运算符》 。
逻辑运算符如下表所示 (其中_expr_可能是任何一种类型, 不一定是布尔值):
运算符 语法 说明 逻辑与,AND(&&) _expr1_ && _expr2_ 若 expr**1** 可转换为 true,则返回 expr**2**;否则,返回 expr**1**。 逻辑或,OR(||) _expr1_ || _expr2_ 若 expr**1** 可转换为 true,则返回 expr**1**;否则,返回 expr**2**。 逻辑非,NOT(!) !_expr_ 若 expr 可转换为 true,则返回 false;否则,返回 true。
如果一个值可以被转换为 true,那么这个值就是所谓的 truthy,如果可以被转换为 false,那么这个值就是所谓的 falsy。 会被转换为 false 的表达式有:
a1 = true && true // t && t 返回 true
a2 = true && false // t && f 返回 false
a3 = false && true // f && t 返回 false
a4 = false && (3 == 4) // f && f 返回 false
a5 = "Cat" && "Dog" // t && t 返回 "Dog"
a6 = false && "Cat" // f && t 返回 false
a7 = "Cat" && false // t && f 返回 false
a8 = '' && false // f && f 返回 ""
a9 = false && '' // f && f 返回 false
o1 = true || true // t || t 返回 true
o2 = false || true // f || t 返回 true
o3 = true || false // t || f 返回 true
o4 = false || (3 == 4) // f || f 返回 false
o5 = "Cat" || "Dog" // t || t 返回 "Cat"
o6 = false || "Cat" // f || t 返回 "Cat"
o7 = "Cat" || false // t || f 返回 "Cat"
o8 = '' || false // f || f 返回 false
o9 = false || '' // f || f 返回 ""
n1 = !true // !t 返回 false
n2 = !false // !f 返回 true
n3 = !'' // !f 返回 true
n4 = !'Cat' // !t 返回 false
n1 = !!true // !!truthy 返回 true
n2 = !!{} // !!truthy 返回 true: 任何 对象都是 truthy 的…
n3 = !!(new Boolean(false)) // …甚至 .valueOf() 返回 false 的布尔值对象也是!
n4 = !!false // !!falsy 返回 false
n5 = !!"" // !!falsy 返回 false
n6 = !!Boolean(false) // !!falsy 返回 false
condi1 && confi2
// 转换为
!(!condi1 || !condi2)
condi1 || condi2
// 转换为
!(!condi1 && !condi2)
由于逻辑表达式的运算顺序是从左到右,也可以用以下规则进行"短路"计算:
function A(){ console.log('called A'); return false; }
function B(){ console.log('called B'); return true; }
console.log( A() && B() );
// logs "called A" due to the function call,
// then logs false (which is the resulting value of the operator)
console.log( B() || A() );
// logs "called B" due to the function call,
// then logs true (which is the resulting value of the operator)
与运算 && 的优先级比或运算 || 要高。 所以代码 a && b || c && d 完全跟 && 表达式加了括号一样:(a && b) || (c && d)。
详细可以阅读《MDN while》 。 「while 语句」可以在某个条件表达式为真的前提下,循环执行指定的一段代码,直到那个表达式不为真时结束循环。 如:
var n = 0;
var x = 0;
while (n < 3) {
n++;
x += n;
}
当循环体为单行时,可以不写大括号:
let i = 3;
while(i) console.log(i --);
详细可以阅读《MDN do...while》 。 do...while 语句创建一个执行指定语句的循环,直到condition值为 false。在执行statement 后检测condition,所以指定的statement至少执行一次。 如:
var result = '';
var i = 0;
do {
i += 1;
result += i + ' ';
} while (i < 5);
详细可以阅读《MDN for》 。 for 语句用于创建一个循环,它包含了三个可选的表达式,这三个表达式被包围在圆括号之中,使用分号分隔,后跟一个用于在循环中执行的语句(通常是一个块语句)。 语法如:
for (begin; condition; step) {
// ……循环体……
}
示例:
for (let i = 0; i < 3; i++) {
console.log(i);
}
描述:
begin i = 0 进入循环时执行一次。 condition i < 3 在每次循环迭代之前检查,如果为 false,停止循环。 body(循环体) alert(i) 条件为真时,重复运行。 step i++ 在每次循环体迭代后执行。
for 语句头部圆括号中的所有三个表达式都是可选的。
var i = 0;
for (; i < 3; i++) {
console.log(i);
}
for (var i = 0;; i++) {
console.log(i);
if (i > 3) break;
}
var i = 0;
for (;;) {
if (i > 3) break;
console.log(i);
i++;
}
详细可以阅读《MDN break》 。 break 语句中止当前循环,switch语句或label 语句,并把程序控制流转到紧接着被中止语句后面的语句。 在 while 语句中:
function testBreak(x) {
var i = 0;
while (i < 6) {
if (i == 3) {
break;
}
i += 1;
}
return i * x;
}
另外,也可以为代码块做标记,并在 break 中指定要跳过的代码块语句的 label:
outer_block:{
inner_block:{
console.log ('1');
break outer_block; // breaks out of both inner_block and outer_block
console.log (':-('); // skipped
}
console.log ('2'); // skipped
}
需要注意的是:break 语句需要内嵌在它所应用的标签或代码块中,否则报错:
block_1:{
console.log ('1');
break block_2; // SyntaxError: label not found
}
block_2:{
console.log ('2');
}
continue 声明终止当前循环或标记循环的当前迭代中的语句执行,并在下一次迭代时继续执行循环。 与 break 语句的区别在于, continue 并不会终止循环的迭代,而是:
i = 0;
n = 0;
while (i < 5) {
i++;
if (i === 3) {
continue;
}
n += i;
}
带 label:
var i = 0,
j = 8;
checkiandj: while (i < 4) {
console.log("i: " + i);
i += 1;
checkj: while (j > 4) {
console.log("j: "+ j);
j -= 1;
if ((j % 2) == 0)
continue checkj;
console.log(j + " is odd.");
}
console.log("i = " + i);
console.log("j = " + j);
}
「禁止 「break/continue」 在 ‘?’ 的右边:」
(i > 5) ? console.log(i) : continue; // continue 不允许在这个位置
这样会提示语法错误。 请注意非表达式的语法结构不能与三元运算符 ? 一起使用。特别是 break/continue 这样的指令是不允许这样使用的。
三种循环:
switch 语句用来将表达式的值与 case 语句匹配,并执行与情况对应的语句。 switch 语句可以替代多个 if 判断,为多个分支选择的情况提供一个更具描述性的方式。
switch 语句至少包含一个 case 代码块和一个可选的 default 代码块:
switch(expression) {
case 'value1':
// do something ...
[break]
default:
// ...
[break]
}
当 expression 表达式的值与 value1 匹配时,则执行其中代码块。 如果没有 case 子句匹配,则会选择 default 子句执行,若连 default 子句都没有,则直接执行到 switch 结束。
所谓 case 分组,就是与多个 case 分支共享同一段代码,如下面例子中 case 1 和 case 2:
let a = 2;
switch (a) {
case 1: // (*) 下面这两个 case 被分在一组
case 2:
console.log('case is 1 or 2!');
break;
case 3:
console.log('case is 3!');
break;
default:
console.log('The result is default.');
}
// 'case is 1 or 2!'
function f(n){
let a ;
switch(n){
case 1:
a = 'number';
break;
case '1':
a = 'string';
break;
default:
a = 'default';
break;
}
console.log(a)
}
f(1); // number
f('1'); // string
let a = 2 + 2;
switch (a) {
case 3:
console.log( 'Too small' );
case 4:
console.log( 'Exactly!' );
case 5:
console.log( 'Too big' );
default:
console.log( "I don't know such values" );
}
// Exactly!
// Too big
// I don't know such values
function f(n){
switch (n) {
case 2:
console.log(2);
break;
default:
console.log('default')
break;
case 1:
console.log('1');
break;
}
}
f(1); // 1
f(2); // 2
f(3); // default
// 以下定义会报错
function f(n){
switch(n){
case 1:
let msg = 'hello';
console.log(1);
break;
case 2:
let msg = 'leo';
break;
default:
console.log('default');
break;
}
}
// Error: Uncaught SyntaxError: Identifier 'msg' has already been declared
这是由于两个 let 处于同一个块级作用域,所以它们被认为是同一变量名的重复声明。 解决方式,只需要将 case 语句包装在括号内即可解决:
function f(n){
switch(n){
case 1:{ // added brackets
let msg = 'hello';
console.log(msg);
break;
}
case 2: {
let msg = 'leo';
console.log(msg);
break;
}
default:
console.log('default');
break;
}
}
函数可以让一段代码被多次调用,避免重复代码。 如之前学习到的一些内置函数: alert(msg) / prompt(msg, default) / confirm(quesyion) 等。
定义函数有两种方式:「函数声明」和「函数表达式」。
如定义一个简单 getUser 函数:
function getUser(name){
return 'hello ' + name;
}
getUser('leo"); // 函数调用
通过函数声明来定义函数时,需要由以下几部分组成:
类似声明变量,还是以 getUser 为例:
let getUser = function(name){
return 'hello ' + name;
}
另外,函数表达式也可以提供函数名,并用于函数内部指代函数本身:
let fun = function f(n){
return n < 3 ? 1 : n * f(n - 1);
}
fun(3); // 3
fun(5); // 60
当定义一个函数后,它并不会自动执行,而是需要使用函数名称进行调用,如上面例子:
fun(3); // 3
「只要注意:」 使用 「函数表达式」 定义函数时,调用函数的方法必须写在定义之后,否则报错:
console.log(fun()); // Uncaught ReferenceError: fun is not defined
let fun = function(){return 1};
而使用 「函数声明」 则不会出现该问题:
console.log(fun()); // 1
function fun(){return 1};
原因就是:函数提升仅适用于函数声明,而不适用于函数表达式。
在函数中,可以使用局部变量和外部变量。
函数中声明的变量只能在该函数内可见。
let fun = function(){
let name = 'leo';
}
fun();
console.log(name); // Uncaught ReferenceError: name is not defined
函数内可以使用外部变量,并且可以修改外部变量的值。
let name = 'leo';
let fun = function(){
let text = 'Hello, ' + name;
console.log(text);
}
fun(); // Hello, leo
当函数内也有与外部变量名称相同的变量,会忽略外部变量:
let name = 'leo';
let fun = function(){
let name = 'pingan8787';
let text = 'Hello, ' + name;
console.log(text);
}
fun(); // Hello, pingan8787
从ECMAScript 6开始,有两个新的类型的参数:默认参数,剩余参数。
若函数没有传入参数,则参数默认值为undefined,通常设置参数默认值是这样做的:
// ES6 之前,没有设置默认值
function f(a, b){
b = b ? b : 1;
return a * b;
}
f(2,3); // 6
f(2); // 2
// ES6,设置默认值
function f(a, b = 1){
return a * b;
}
f(2,3); // 6
f(2); // 2
可以将参数中不确定数量的参数表示成数组,如下:
function f (a, ...b){
console.log(a, b);
}
f(1,2,3,4); // a => 1 b => [2, 3, 4]
既然讲到参数,那就不能少了 arguments 对象。
函数的实际参数会被保存在一个「类似数组的arguments对象」中。在函数内,我们可以使用 arguments 对象获取函数的所有参数:
let fun = function(){
console.log(arguments);
console.log(arguments.length);
}
fun('leo');
// Arguments ["leo", callee: ƒ, Symbol(Symbol.iterator): ƒ]
// 1
以一个实际示例介绍,实现将任意数量参数连接成一个字符串,并输出的函数:
let argumentConcat = function(separator){
let result = '', i;
for(i = 1; i < arguments.length; i ++){
result += arguments[i] + separator;
}
return result;
}
argumentConcat(',', 'leo', 'pingan'); //"leo,pingan,"
在函数任意位置,指定 return 指令来停止函数的执行,并返回函数指定的返回值。
let sum = function(a, b){
return a + b;
};
let res = sum(1, 2);
console.log(res); // 3
默认空值的 return 或没有 return 的函数返回值为 undefined 。
函数表达式是一种函数定义方式,在前面章节中已经介绍到了,这个部分将着重介绍 「函数表达式」 和 「函数声明」 的区别:
// 函数表达式
let fun = function(){};
// 函数声明
function fun(){}
函数表达式会在代码执行到达时被创建,并且仅从那一刻可用。 而函数声明被定义之前,它就可以被调用。
// 函数表达式
fun(); // Uncaught ReferenceError: fun is not defined
let fun = function(){console.log('leo')};
// 函数声明
fun(); // "leo"
function fun(){console.log('leo')};
建议优先考虑函数声明语法,它能够为组织代码提供更多灵活性,因为我们可以在声明函数前调用该函数。
「本章节简单介绍箭头函数基础知识,后面章节会完整介绍。」 「函数箭头表达式」是ES6新增的函数表达式的语法,也叫「胖箭头函数」,变化:更简洁的函数和this。
// 有1个参数
let f = v => v;
// 等同于
let f = function (v){return v};
// 有多个参数
let f = (v, i) => {return v + i};
// 等同于
let f = function (v, i){return v + i};
// 没参数
let f = () => 1;
// 等同于
let f = function (){return 1};
let arr = [1,2,3,4];
arr.map(ele => ele + 1); // [2, 3, 4, 5]
function Person(){
this.age = 0;
setInterval(() => {
this.age++;
}, 1000);
}
var p = new Person(); // 定时器一直在执行 p的值一直变化
本文作为《初中级前端 JavaScript 自测清单》第一部分,介绍的内容以常用基础知识为主,并在学习资料中,将知识点结合实际开发中遇到的场景进行展开介绍。希望能帮助大家自测自己的 JavaScript 水平并查缺补漏,温故知新。
Author 王平安 E-mail pingan8787@qq.com 博 客 www.pingan8787.com 微 信 pingan8787 每日文章推荐 https://github.com/pingan8787/Leo_Reading/issues ES小册 js.pingan8787.com 语雀知识库 Cute-FrontEnd
bg
着大数据时代的发展,各个公司的数据保护意识越来越强,大家都在想尽办法保护自家产品的数据不轻易被爬虫爬走。由于网页是提供信息和服务的重要载体,所以对网页上的信息进行保护就成了至关重要的一个环节。
网页是运行在浏览器端的,当我们浏览一个网页时,其 HTML 代码、 JavaScript 代码都会被下载到浏览器中执行。借助浏览器的开发者工具,我们可以看到网页在加载过程中所有网络请求的详细信息,也能清楚地看到网站运行的 HTML 代码和 JavaScript 代码,这些代码中就包含了网站加载的全部逻辑,如加载哪些资源、请求接口是如何构造的、页面是如何渲染的等等。正因为代码是完全透明的,所以如果我们能够把其中的执行逻辑研究出来,就可以模拟各个网络请求进行数据爬取了。
然而,事情没有想象得那么简单。随着前端技术的发展,前端代码的打包技术、混淆技术、加密技术也层出不穷,借助于这些技术,各个公司可以在前端对 JavaScript 代码采取一定的保护,比如变量名混淆、执行逻辑混淆、反调试、核心逻辑加密等,这些保护手段使得我们没法很轻易地找出 JavaScript 代码中包含的的执行逻辑。
在前几章的案例中,我们也试着爬取了各种形式的网站。其中有的网站的数据接口是没有任何验证或加密参数的,我们可以轻松模拟并爬取其中的数据;但有的网站稍显复杂,网站的接口中增加了一些加密参数,同时对 JavaScript 代码采取了上文所述的一些防护措施,当时我们没有直接尝试去破解,而是用 Selenium 等类似工具来实现模拟浏览器执行的方式来进行“所见即所得“的爬取。其实对于后者,我们还有另外一种解决方案,那就是直接逆向 JavaScript 代码,找出其中的加密逻辑,从而直接实现该加密逻辑来进行爬取。如果加密逻辑实在过于复杂,我们也可以找出一些关键入口,从而实现对加密逻辑的单独模拟执行和数据爬取。这些方案难度可能很大,比如关键入口很难寻找,或者加密逻辑难以模拟,可是一旦成功找到突破口,我们便可以不用借助于 Selenium 等工具进行整页数据的渲染而实现数据爬取,这样爬取效率会大幅提升。
本章我们首先会对 JavaScript 防护技术进行介绍,然后介绍一些常用的 JavaScript 逆向技巧,包括浏览器工具的使用、Hook 技术、AST 技术、特殊混淆技术的处理、WebAssembly 技术的处理。了解了这些技术,我们可以更从容地应对 JavaScript 防护技术。
我们在爬取网站的时候,会遇到一些情况需要分析一些接口或 URL 信息,在这个过程中,我们会遇到各种各样类似加密的情形,比如说:
这些情况呢,基本上都是网站为了保护其本身的一些数据不被轻易抓取而采取的一些措施,我们可以把它归类为两大类:
这一节我们就来了解下这两类技术的基本原理和一些常见的示例。知己知彼,百战不殆,了解了这些技术的实现原理之后,我们才能更好地去逆向其中的逻辑,从而实现数据爬取。
当今大数据时代,数据已经变得越来越重要,网页和 App 现在是主流的数据载体,如果其数据的 API 没有设置任何保护措施,在爬虫工程师解决了一些基本的反爬如封 IP、验证码的问题之后,那么数据还是可以被轻松爬取到的。
那么,有没有可能在 URL/API 层面或 JavaScript 层面也加上一层防护呢?答案是可以。
网站运营者首先想到防护措施可能是对某些数据接口的参数进行加密,比如说对某些 URL 的一些参数加上校验码或者把一些 id 信息进行编码,使其变得难以阅读或构造;或者对某些 API 请求加上一些 token、sign 等签名,这样这些请求发送到服务器时,服务器会通过客户端发来的一些请求信息以及双方约定好的秘钥等来对当前的请求进行校验,如果校验通过,才返回对应数据结果。
比如说客户端和服务端约定一种接口校验逻辑,客户端在每次请求服务端接口的时候都会附带一个 sign 参数,这个 sign 参数可能是由当前时间信息、请求的 URL、请求的数据、设备的 ID、双方约定好的秘钥经过一些加密算法构造而成的,客户端会实现这个加密算法构造 sign,然后每次请求服务器的时候附带上这个参数。服务端会根据约定好的算法和请求的数据对 sign 进行校验,如果校验通过,才返回对应的数据,否则拒绝响应。
当然登录状态的校验也可以看作是此类方案,比如一个 API 的调用必须要传一个 token,这个 token 必须用户登录之后才能获取,如果请求的时候不带该 token,API 就不会返回任何数据。
倘若没有这种措施,那么基本上 URL 或者 API 接口是完全公开可以访问的,这意味着任何人都可以直接调用来获取数据,几乎是零防护的状态,这样是非常危险的,而且数据也可以被轻易地被爬虫爬取。因此对 URL/API 参数一些加密和校验是非常有必要的。
接口加密技术看起来的确是一个不错的解决方案,但单纯依靠它并不能很好地解决问题。为什么呢?
对于网页来说,其逻辑是依赖于 JavaScript 来实现的,JavaScript 有如下特点:
由于这两个原因,至使 JavaScript 代码是不安全的,任何人都可以读、分析、复制、盗用,甚至篡改。
所以说,对于上述情形,客户端 JavaScript 对于某些加密的实现是很容易被找到或模拟的,了解了加密逻辑后,模拟参数的构造和请求也就是轻而易举了,所以如果 JavaScript 没有做任何层面的保护的话,接口加密技术基本上对数据起不到什么防护作用。
如果你不想让自己的数据被轻易获取,不想他人了解 JavaScript 逻辑的实现,或者想降低被不怀好意的人甚至是黑客攻击。那么就需要用到 JavaScript 压缩、混淆和加密技术了。
这里压缩、混淆和加密技术简述如下:
下面我们对上面的技术分别予以介绍。
现在绝大多数网站的数据一般都是通过服务器提供的 API 来获取的,网站或 App 可以请求某个数据 API 获取到对应的数据,然后再把获取的数据展示出来。但有些数据是比较宝贵或私密的,这些数据肯定是需要一定层面上的保护。所以不同 API 的实现也就对应着不同的安全防护级别,我们这里来总结下。
为了提升接口的安全性,客户端会和服务端约定一种接口校验方式,一般来说会使用到各种加密和编码算法,如 Base64、Hex 编码,MD5、AES、DES、RSA 等对称或非对称加密。
举个例子,比如说客户端和服务器双方约定一个 sign 用作接口的签名校验,其生成逻辑是客户端将 URL Path 进行 MD5 加密然后拼接上 URL 的某个参数再进行 Base64 编码,最后得到一个字符串 sign,这个 sign 会通过 Request URL 的某个参数或 Request Headers 发送给服务器。服务器接收到请求后,对 URL Path 同样进行 MD5 加密,然后拼接上 URL 的某个参数,也进行 Base64 编码也得到了一个 sign,然后比对生成的 sign 和客户端发来的 sign 是否是一致的,如果是一致的,那就返回正确的结果,否则拒绝响应。这就是一个比较简单的接口参数加密的实现。如果有人想要调用这个接口的话,必须要定义好 sign 的生成逻辑,否则是无法正常调用接口的。
当然上面的这个实现思路比较简单,这里还可以增加一些时间戳信息增加时效性判断,或增加一些非对称加密进一步提高加密的复杂程度。但不管怎样,只要客户端和服务器约定好了加密和校验逻辑,任何形式加密算法都是可以的。
这里要实现接口参数加密就需要用到一些加密算法,客户端和服务器肯定也都有对应的 SDK 实现这些加密算法,如 JavaScript 的 crypto-js,Python 的 hashlib、Crypto 等等。
但还是如上文所说,如果是网页的话,客户端实现加密逻辑如果是用 JavaScript 来实现,其源代码对用户是完全可见的,如果没有对 JavaScript 做任何保护的话,是很容易弄清楚客户端加密的流程的。
因此,我们需要对 JavaScript 利用压缩、混淆等方式来对客户端的逻辑进行一定程度上的保护。
这个非常简单,JavaScript 压缩即去除 JavaScript 代码中的不必要的空格、换行等内容或者把一些可能公用的代码进行处理实现共享,最后输出的结果都压缩为几行内容,代码可读性变得很差,同时也能提高网站加载速度。
如果仅仅是去除空格换行这样的压缩方式,其实几乎是没有任何防护作用的,因为这种压缩方式仅仅是降低了代码的直接可读性。如果我们有一些格式化工具可以轻松将 JavaScript 代码变得易读,比如利用 IDE、在线工具或 Chrome 浏览器都能还原格式化的代码。
比如这里举一个最简单的 JavaScript 压缩示例,原来的 JavaScript 代码是这样的:
function echo(stringA, stringB) {
const name = "Germey";
alert("hello " + name);
}
压缩之后就变成这样子:
function echo(d, c) {
const e = "Germey";
alert("hello " + e);
}
可以看到这里参数的名称都被简化了,代码中的空格也被去掉了,整个代码也被压缩成了一行,代码的整体可读性降低了。
目前主流的前端开发技术大多都会利用 Webpack、Rollup 等工具进行打包,Webpack、Rollup 会对源代码进行编译和压缩,输出几个打包好的 JavaScript 文件,其中我们可以看到输出的 JavaScript 文件名带有一些不规则字符串,同时文件内容可能只有几行内容,变量名都是一些简单字母表示。这其中就包含 JavaScript 压缩技术,比如一些公共的库输出成 bundle 文件,一些调用逻辑压缩和转义成冗长的几行代码,这些都属于 JavaScript 压缩。另外其中也包含了一些很基础的 JavaScript 混淆技术,比如把变量名、方法名替换成一些简单字符,降低代码可读性。
但整体来说,JavaScript 压缩技术只能在很小的程度上起到防护作用,要想真正提高防护效果还得依靠 JavaScript 混淆和加密技术。
JavaScript 混淆是完全是在 JavaScript 上面进行的处理,它的目的就是使得 JavaScript 变得难以阅读和分析,大大降低代码可读性,是一种很实用的 JavaScript 保护方案。
JavaScript 混淆技术主要有以下几种:
总之,以上方案都是 JavaScript 混淆的实现方式,可以在不同程度上保护 JavaScript 代码。
在前端开发中,现在 JavaScript 混淆主流的实现是 javascript-obfuscator (https://github.com/javascript-obfuscator/javascript-obfuscator) 和 terser (https://github.com/terser/terser) 这两个库,其都能提供一些代码混淆功能,也都有对应的 Webpack 和 Rollup 打包工具的插件,利用它们我们可以非常方便地实现页面的混淆,最终可以输出压缩和混淆后的 JavaScript 代码,使得 JavaScript 代码可读性大大降低。
下面我们以 javascript-obfuscator 为例来介绍一些代码混淆的实现,了解了实现,那么自然我们就对混淆的机理有了更加深刻的认识。
javascript-obfuscator 的官网地址为:https://obfuscator.io/,其官方介绍内容如下:
A free and efficient obfuscator for JavaScript (including ES2017). Make your code harder to copy and prevent people from stealing your work.
它是支持 ES8 的免费、高效的 JavaScript 混淆库,它可以使得你的 JavaScript 代码经过混淆后难以被复制、盗用,混淆后的代码具有和原来的代码一模一样的功能。
怎么使用呢?首先,我们需要安装好 Node.js 12.x 版本及以上,确保可以正常使用 npm 命令,具体的安装方式可以参考:https://setup.scrape.center/nodejs。
接着新建一个文件夹,比如 js-obfuscate,然后进入该文件夹,初始化工作空间:
npm init
这里会提示我们输入一些信息,创建一个 package.json 文件,这就完成了项目初始化了。
接下来我们来安装 javascript-obfuscator 这个库:
npm i -D javascript-obfuscator
稍等片刻,即可看到本地 js-obfuscate 文件夹下生成了一个 node_modules 文件夹,里面就包含了 javascript-obfuscator 这个库,这就说明安装成功了,文件夹结构如图所示:
接下来我们就可以编写代码来实现一个混淆样例了,如新建一个 main.js 文件,内容如下:
const code = `
let x = '1' + 1
console.log('x', x)
`;
const options = {
compact: false,
controlFlowFlattening: true,
};
const obfuscator = require("javascript-obfuscator");
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode();
}
console.log(obfuscate(code, options));
在这里我们定义了两个变量,一个是 code,即需要被混淆的代码,另一个是混淆选项,是一个 Object。接下来我们引入了 javascript-obfuscator 这库,然后定义了一个方法,传入 code 和 options,来获取混淆后的代码,最后控制台输出混淆后的代码。
代码逻辑比较简单,我们来执行一下代码:
node main.js
输出结果如下:
var _0x53bf = ["log"];
(function (_0x1d84fe, _0x3aeda0) {
var _0x10a5a = function (_0x2f0a52) {
while (--_0x2f0a52) {
_0x1d84fe["push"](_0x1d84fe["shift"]());
}
};
_0x10a5a(++_0x3aeda0);
})(_0x53bf, 0x172);
var _0x480a = function (_0x4341e5, _0x5923b4) {
_0x4341e5 = _0x4341e5 - 0x0;
var _0xb3622e = _0x53bf[_0x4341e5];
return _0xb3622e;
};
let x = "1" + 0x1;
console[_0x480a("0x0")]("x", x);
看到了吧,那么简单的两行代码,被我们混淆成了这个样子,其实这里我们就是设定了一个「控制流平坦化」的选项。整体看来,代码的可读性大大降低,也大大加大了 JavaScript 调试的难度。
好,那么我们来跟着 javascript-obfuscator 走一遍,就能具体知道 JavaScript 混淆到底有多少方法了。
注意:由于这些例子中,调用 javascript-obfuscator 进行混淆的实现是一样的,所以下文的示例只说明 code 和 options 变量的修改,完整代码请自行补全。
这里 javascript-obfuscator 也提供了代码压缩的功能,使用其参数 compact 即可完成 JavaScript 代码的压缩,输出为一行内容。默认是 true,如果定义为 false,则混淆后的代码会分行显示。
示例如下:
const code = `
let x = '1' + 1
console.log('x', x)
`;
const options = {
compact: false,
};
这里我们先把代码压缩 compact 选项设置为 false,运行结果如下:
let x = "1" + 0x1;
console["log"]("x", x);
如果不设置 compact 或把 compact 设置为 true,结果如下:
var _0x151c = ["log"];
(function (_0x1ce384, _0x20a7c7) {
var _0x25fc92 = function (_0x188aec) {
while (--_0x188aec) {
_0x1ce384["push"](_0x1ce384["shift"]());
}
};
_0x25fc92(++_0x20a7c7);
})(_0x151c, 0x1b7);
var _0x553e = function (_0x259219, _0x241445) {
_0x259219 = _0x259219 - 0x0;
var _0x56d72d = _0x151c[_0x259219];
return _0x56d72d;
};
let x = "1" + 0x1;
console[_0x553e("0x0")]("x", x);
可以看到单行显示的时候,对变量名进行了进一步的混淆,这里变量的命名都变成了 16 进制形式的字符串,这是因为启用了一些默认压缩和混淆配置导致的。总之我们可以看到代码的可读性相比之前大大降低了。
变量名混淆可以通过在 javascript-obfuscator 中配置 identifierNamesGenerator 参数实现,我们通过这个参数可以控制变量名混淆的方式,如 hexadecimal 则会替换为 16 进制形式的字符串,在这里我们可以设定如下值:
该参数的值默认为 hexadecimal。
我们将该参数修改为 mangled 来试一下:
const code = `
let hello = '1' + 1
console.log('hello', hello)
`;
const options = {
compact: true,
identifierNamesGenerator: "mangled",
};
运行结果如下:
var a = ["hello"];
(function (c, d) {
var e = function (f) {
while (--f) {
c["push"](c["shift"]());
}
};
e(++d);
})(a, 0x9b);
var b = function (c, d) {
c = c - 0x0;
var e = a[c];
return e;
};
let hello = "1" + 0x1;
console["log"](b("0x0"), hello);
可以看到这里的变量命名都变成了 a、b 等形式。
如果我们将 identifierNamesGenerator 修改为 hexadecimal 或者不设置,运行结果如下:
var _0x4e98 = ["log", "hello"];
(function (_0x4464de, _0x39de6c) {
var _0xdffdda = function (_0x6a95d5) {
while (--_0x6a95d5) {
_0x4464de["push"](_0x4464de["shift"]());
}
};
_0xdffdda(++_0x39de6c);
})(_0x4e98, 0xc8);
var _0x53cb = function (_0x393bda, _0x8504e7) {
_0x393bda = _0x393bda - 0x0;
var _0x46ab80 = _0x4e98[_0x393bda];
return _0x46ab80;
};
let hello = "1" + 0x1;
console[_0x53cb("0x0")](_0x53cb("0x1"), hello);
可以看到选用了 mangled,其代码体积会更小,但 hexadecimal 其可读性会更低。
另外我们还可以通过设置 identifiersPrefix 参数来控制混淆后的变量前缀,示例如下:
const code = `
let hello = '1' + 1
console.log('hello', hello)
`;
const options = {
identifiersPrefix: "germey",
};
运行结果如下:
var germey_0x3dea = ["log", "hello"];
(function (_0x348ff3, _0x5330e8) {
var _0x1568b1 = function (_0x4740d8) {
while (--_0x4740d8) {
_0x348ff3["push"](_0x348ff3["shift"]());
}
};
_0x1568b1(++_0x5330e8);
})(germey_0x3dea, 0x94);
var germey_0x30e4 = function (_0x2e8f7c, _0x1066a8) {
_0x2e8f7c = _0x2e8f7c - 0x0;
var _0x5166ba = germey_0x3dea[_0x2e8f7c];
return _0x5166ba;
};
let hello = "1" + 0x1;
console[germey_0x30e4("0x0")](germey_0x30e4("0x1"), hello);
可以看到混淆后的变量前缀加上了我们自定义的字符串 germey。
另外 renameGlobals 这个参数还可以指定是否混淆全局变量和函数名称,默认为 false。示例如下:
const code = `
var $ = function(id) {
return document.getElementById(id);
};
`;
const options = {
renameGlobals: true,
};
运行结果如下:
var _0x4864b0 = function (_0x5763be) {
return document["getElementById"](_0x5763be);
};
可以看到这里我们声明了一个全局变量 $,在 renameGlobals 设置为 true 之后,$ 这个变量也被替换了。如果后文用到了这个 $ 对象,可能就会有找不到定义的错误,因此这个参数可能导致代码执行不通。
如果我们不设置 renameGlobals 或者设置为 false,结果如下:
var _0x239a = ["getElementById"];
(function (_0x3f45a3, _0x583dfa) {
var _0x2cade2 = function (_0x28479a) {
while (--_0x28479a) {
_0x3f45a3["push"](_0x3f45a3["shift"]());
}
};
_0x2cade2(++_0x583dfa);
})(_0x239a, 0xe1);
var _0x3758 = function (_0x18659d, _0x50c21d) {
_0x18659d = _0x18659d - 0x0;
var _0x531b8d = _0x239a[_0x18659d];
return _0x531b8d;
};
var $ = function (_0x3d8723) {
return document[_0x3758("0x0")](_0x3d8723);
};
可以看到,最后还是有 $ 的声明,其全局名称没有被改变。
字符串混淆,即将一个字符串声明放到一个数组里面,使之无法被直接搜索到。我们可以通过控制 stringArray 参数来控制,默认为 true。
我们还可以通过 rotateStringArray 参数来控制数组化后结果的的元素顺序,默认为 true。还可以通过 stringArrayEncoding 参数来控制数组的编码形式,默认不开启编码,如果设置为 true 或 base64,则会使用 Base64 编码,如果设置为 rc4,则使用 RC4 编码。另外可以通过 stringArrayThreshold 来控制启用编码的概率,范围 0 到 1,默认 0.8。
示例如下:
const code = `
var a = 'hello world'
`;
const options = {
stringArray: true,
rotateStringArray: true,
stringArrayEncoding: true, // 'base64' 或 'rc4' 或 false
stringArrayThreshold: 1,
};
运行结果如下:
var _0x4215 = ["aGVsbG8gd29ybGQ="];
(function (_0x42bf17, _0x4c348f) {
var _0x328832 = function (_0x355be1) {
while (--_0x355be1) {
_0x42bf17["push"](_0x42bf17["shift"]());
}
};
_0x328832(++_0x4c348f);
})(_0x4215, 0x1da);
var _0x5191 = function (_0x3cf2ba, _0x1917d8) {
_0x3cf2ba = _0x3cf2ba - 0x0;
var _0x1f93f0 = _0x4215[_0x3cf2ba];
if (_0x5191["LqbVDH"] === undefined) {
(function () {
var _0x5096b2;
try {
var _0x282db1 = Function(
"return\x20(function()\x20" +
"{}.constructor(\x22return\x20this\x22)(\x20)" +
");"
);
_0x5096b2 = _0x282db1();
} catch (_0x2acb9c) {
_0x5096b2 = window;
}
var _0x388c14 =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
_0x5096b2["atob"] ||
(_0x5096b2["atob"] = function (_0x4cc27c) {
var _0x2af4ae = String(_0x4cc27c)["replace"](/=+$/, "");
for (
var _0x21400b = 0x0,
_0x3f4e2e,
_0x5b193b,
_0x233381 = 0x0,
_0x3dccf7 = "";
(_0x5b193b = _0x2af4ae["charAt"](_0x233381++));
~_0x5b193b &&
((_0x3f4e2e =
_0x21400b % 0x4 ? _0x3f4e2e * 0x40 + _0x5b193b : _0x5b193b),
_0x21400b++ % 0x4)
? (_0x3dccf7 += String["fromCharCode"](
0xff & (_0x3f4e2e >> ((-0x2 * _0x21400b) & 0x6))
))
: 0x0
) {
_0x5b193b = _0x388c14["indexOf"](_0x5b193b);
}
return _0x3dccf7;
});
})();
_0x5191["DuIurT"] = function (_0x51888e) {
var _0x29801f = atob(_0x51888e);
var _0x561e62 = [];
for (
var _0x5dd788 = 0x0, _0x1a8b73 = _0x29801f["length"];
_0x5dd788 < _0x1a8b73;
_0x5dd788++
) {
_0x561e62 +=
"%" +
("00" + _0x29801f["charCodeAt"](_0x5dd788)["toString"](0x10))[
"slice"
](-0x2);
}
return decodeURIComponent(_0x561e62);
};
_0x5191["mgoBRd"] = {};
_0x5191["LqbVDH"] = !![];
}
var _0x1741f0 = _0x5191["mgoBRd"][_0x3cf2ba];
if (_0x1741f0 === undefined) {
_0x1f93f0 = _0x5191["DuIurT"](_0x1f93f0);
_0x5191["mgoBRd"][_0x3cf2ba] = _0x1f93f0;
} else {
_0x1f93f0 = _0x1741f0;
}
return _0x1f93f0;
};
var a = _0x5191("0x0");
可以看到这里就把字符串进行了 Base64 编码,我们再也无法通过查找的方式找到字符串的位置了。
如果将 stringArray 设置为 false 的话,输出就是这样:
var a = "hello\x20world";
字符串就仍然是明文显示的,没有被编码。
另外我们还可以使用 unicodeEscapeSequence 这个参数对字符串进行 Unicode 转码,使之更加难以辨认,示例如下:
const code = `
var a = 'hello world'
`;
const options = {
compact: false,
unicodeEscapeSequence: true,
};
运行结果如下:
var _0x5c0d = ["\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64"];
(function (_0x54cc9c, _0x57a3b2) {
var _0xf833cf = function (_0x3cd8c6) {
while (--_0x3cd8c6) {
_0x54cc9c["push"](_0x54cc9c["shift"]());
}
};
_0xf833cf(++_0x57a3b2);
})(_0x5c0d, 0x17d);
var _0x28e8 = function (_0x3fd645, _0x2cf5e7) {
_0x3fd645 = _0x3fd645 - 0x0;
var _0x298a20 = _0x5c0d[_0x3fd645];
return _0x298a20;
};
var a = _0x28e8("0x0");
可以看到,这里字符串被数字化和 Unicode 化,非常难以辨认。
在很多 JavaScript 逆向的过程中,一些关键的字符串可能会作为切入点来查找加密入口。用了这种混淆之后,如果有人想通过全局搜索的方式搜索 hello 这样的字符串找加密入口,也没法搜到了。
我们可以通过设置 selfDefending 参数来开启代码自我保护功能。开启之后,混淆后的 JavaScript 会以强制一行形式显示,如果我们将混淆后的代码进行格式化或者重命名,该段代码将无法执行。
示例如下:
const code = `
console.log('hello world')
`;
const options = {
selfDefending: true,
};
运行结果如下:
var _0x26da = ["log", "hello\x20world"];
(function (_0x190327, _0x57c2c0) {
var _0x577762 = function (_0xc9dabb) {
while (--_0xc9dabb) {
_0x190327["push"](_0x190327["shift"]());
}
};
var _0x35976e = function () {
var _0x16b3fe = {
data: { key: "cookie", value: "timeout" },
setCookie: function (_0x2d52d5, _0x16feda, _0x57cadf, _0x56056f) {
_0x56056f = _0x56056f || {};
var _0x5b6dc3 = _0x16feda + "=" + _0x57cadf;
var _0x333ced = 0x0;
for (
var _0x333ced = 0x0, _0x19ae36 = _0x2d52d5["length"];
_0x333ced < _0x19ae36;
_0x333ced++
) {
var _0x409587 = _0x2d52d5[_0x333ced];
_0x5b6dc3 += ";\x20" + _0x409587;
var _0x4aa006 = _0x2d52d5[_0x409587];
_0x2d52d5["push"](_0x4aa006);
_0x19ae36 = _0x2d52d5["length"];
if (_0x4aa006 !== !![]) {
_0x5b6dc3 += "=" + _0x4aa006;
}
}
_0x56056f["cookie"] = _0x5b6dc3;
},
removeCookie: function () {
return "dev";
},
getCookie: function (_0x30c497, _0x51923d) {
_0x30c497 =
_0x30c497 ||
function (_0x4b7e18) {
return _0x4b7e18;
};
var _0x557e06 = _0x30c497(
new RegExp(
"(?:^|;\x20)" +
_0x51923d["replace"](/([.$?*|{}()[]\/+^])/g, "$1") +
"=([^;]*)"
)
);
var _0x817646 = function (_0xf3fae7, _0x5d8208) {
_0xf3fae7(++_0x5d8208);
};
_0x817646(_0x577762, _0x57c2c0);
return _0x557e06 ? decodeURIComponent(_0x557e06[0x1]) : undefined;
},
};
var _0x4673cd = function () {
var _0x4c6c5c = new RegExp(
"\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}"
);
return _0x4c6c5c["test"](_0x16b3fe["removeCookie"]["toString"]());
};
_0x16b3fe["updateCookie"] = _0x4673cd;
var _0x5baa80 = "";
var _0x1faf19 = _0x16b3fe["updateCookie"]();
if (!_0x1faf19) {
_0x16b3fe["setCookie"](["*"], "counter", 0x1);
} else if (_0x1faf19) {
_0x5baa80 = _0x16b3fe["getCookie"](null, "counter");
} else {
_0x16b3fe["removeCookie"]();
}
};
_0x35976e();
})(_0x26da, 0x140);
var _0x4391 = function (_0x1b42d8, _0x57edc8) {
_0x1b42d8 = _0x1b42d8 - 0x0;
var _0x2fbeca = _0x26da[_0x1b42d8];
return _0x2fbeca;
};
var _0x197926 = (function () {
var _0x10598f = !![];
return function (_0xffa3b3, _0x7a40f9) {
var _0x48e571 = _0x10598f
? function () {
if (_0x7a40f9) {
var _0x2194b5 = _0x7a40f9["apply"](_0xffa3b3, arguments);
_0x7a40f9 = null;
return _0x2194b5;
}
}
: function () {};
_0x10598f = ![];
return _0x48e571;
};
})();
var _0x2c6fd7 = _0x197926(this, function () {
var _0x4828bb = function () {
return "\x64\x65\x76";
},
_0x35c3bc = function () {
return "\x77\x69\x6e\x64\x6f\x77";
};
var _0x456070 = function () {
var _0x4576a4 = new RegExp(
"\x5c\x77\x2b\x20\x2a\x5c\x28\x5c\x29\x20\x2a\x7b\x5c\x77\x2b\x20\x2a\x5b\x27\x7c\x22\x5d\x2e\x2b\x5b\x27\x7c\x22\x5d\x3b\x3f\x20\x2a\x7d"
);
return !_0x4576a4["\x74\x65\x73\x74"](
_0x4828bb["\x74\x6f\x53\x74\x72\x69\x6e\x67"]()
);
};
var _0x3fde69 = function () {
var _0xabb6f4 = new RegExp(
"\x28\x5c\x5c\x5b\x78\x7c\x75\x5d\x28\x5c\x77\x29\x7b\x32\x2c\x34\x7d\x29\x2b"
);
return _0xabb6f4["\x74\x65\x73\x74"](
_0x35c3bc["\x74\x6f\x53\x74\x72\x69\x6e\x67"]()
);
};
var _0x2d9a50 = function (_0x58fdb4) {
var _0x2a6361 = ~-0x1 >> (0x1 + (0xff % 0x0));
if (_0x58fdb4["\x69\x6e\x64\x65\x78\x4f\x66"]("\x69" === _0x2a6361)) {
_0xc388c5(_0x58fdb4);
}
};
var _0xc388c5 = function (_0x2073d6) {
var _0x6bb49f = ~-0x4 >> (0x1 + (0xff % 0x0));
if (
_0x2073d6["\x69\x6e\x64\x65\x78\x4f\x66"]((!![] + "")[0x3]) !== _0x6bb49f
) {
_0x2d9a50(_0x2073d6);
}
};
if (!_0x456070()) {
if (!_0x3fde69()) {
_0x2d9a50("\x69\x6e\x64\u0435\x78\x4f\x66");
} else {
_0x2d9a50("\x69\x6e\x64\x65\x78\x4f\x66");
}
} else {
_0x2d9a50("\x69\x6e\x64\u0435\x78\x4f\x66");
}
});
_0x2c6fd7();
console[_0x4391("0x0")](_0x4391("0x1"));
如果我们将上述代码放到控制台,它的执行结果和之前是一模一样的,没有任何问题。
如果我们将其进行格式化,然后贴到到浏览器控制台里面,浏览器会直接卡死无法运行。这样如果有人对代码进行了格式化,就无法正常对代码进行运行和调试,从而起到了保护作用。
控制流平坦化其实就是将代码的执行逻辑混淆,使其变得复杂难读。其基本思想是将一些逻辑处理块都统一加上一个前驱逻辑块,每个逻辑块都由前驱逻辑块进行条件判断和分发,构成一个个闭环逻辑,导致整个执行逻辑十分复杂难读。
比如说这里有一段示例代码:
console.log(c);
console.log(a);
console.log(b);
代码逻辑一目了然,依次在控制台输出了 c、a、b 三个变量的值,但如果把这段代码进行控制流平坦化处理后,代码就会变成这样:
const s = "3|1|2".split("|");
let x = 0;
while (true) {
switch (s[x++]) {
case "1":
console.log(a);
continue;
case "2":
console.log(b);
continue;
case "3":
console.log(c);
continue;
}
break;
}
可以看到,混淆后的代码首先声明了一个变量 s,它的结果是一个列表,其实是 ["3", "1", "2"],然后下面通过 switch 语句对 s 中的元素进行了判断,每个 case 都加上了各自的代码逻辑。通过这样的处理,一些连续的执行逻辑就被打破了,代码被修改为一个 switch 语句,原本我们可以一眼看出的逻辑是控制台先输出 c,然后才是 a、b,但是现在我们必须要结合 switch 的判断条件和对应 case 的内容进行判断,我们很难再一眼每条语句的执行顺序了,这就大大降低了代码的可读性。
在 javascript-obfuscator 中我们通过 controlFlowFlattening 变量可以控制是否开启控制流平坦化,示例如下:
const options = {
compact: false,
controlFlowFlattening: true,
};
使用控制流平坦化可以使得执行逻辑更加复杂难读,目前非常多的前端混淆都会加上这个选项。但启用控制流平坦化之后,代码的执行时间会变长,最长达 1.5 倍之多。
另外我们还能使用 controlFlowFlatteningThreshold 这个参数来控制比例,取值范围是 0 到 1,默认 0.75,如果设置为 0,那相当于 controlFlowFlattening 设置为 false,即不开启控制流扁平化 。
无用代码即不会被执行的代码或对上下文没有任何影响的代码,注入之后可以对现有的 JavaScript 代码的阅读形成干扰。我们可以使用 deadCodeInjection 参数开启这个选项,默认为 false。
比如这里有一段代码:
const a = function () {
console.log("hello world");
};
const b = function () {
console.log("nice to meet you");
};
a();
b();
这里就声明了方法 a 和 b,然后依次进行调用,分别输出两句话。
但经过无用代码注入处理之后,代码就会变成类似这样的结果:
const _0x16c18d = function () {
if (!![[]]) {
console.log("hello world");
} else {
console.log("this");
console.log("is");
console.log("dead");
console.log("code");
}
};
const _0x1f7292 = function () {
if ("xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)) {
console.log("this");
console.log("is");
console.log("dead");
console.log("code");
} else {
console.log("nice to meet you");
}
};
_0x16c18d();
_0x1f7292();
可以看到,每个方法内部都增加了额外的 if else 语句,其中 if 的判断条件还是一个表达式,其结果是 true 还是 false 我们还不太一眼能看出来,比如说 _0x1f7292 这个方法,它的 if 判断条件是:
"xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)
在不等号前面其实是从字符串中取出指定位置的字符,不等号后面则调用了 fromCharCode 方法来根据 ascii 码转换得到一个字符,然后比较两个字符的结果是否是不一样的。前者经过我们推算可以知道结果是 n,但对于后者,多数情况下我们还得去查一下 ascii 码表才能知道其结果也是 n,最后两个结果是相同的,所以整个表达式的结果是 false,所以 if 后面跟的逻辑实际上就是不会被执行到的无用代码,但这些代码对我们阅读代码起到了一定的干扰作用。
因此,这种混淆方式通过混入一些特殊的判断条件并加入一些不会被执行的代码,可以对代码起到一定的混淆干扰作用。
在 javascript-obfuscator 中,我们可以通过 deadCodeInjection 参数控制无用代码的注入,配置如下:
const options = {
compact: false,
deadCodeInjection: true,
};
另外我们还可以通过设置 deadCodeInjectionThreshold 参数来控制无用代码注入的比例,取值 0 到 1,默认是 0.4。
如果是一个对象,可以使用 transformObjectKeys 来对对象的键值进行替换,示例如下:
const code = `
(function(){
var object = {
foo: 'test1',
bar: {
baz: 'test2'
}
};
})();
`;
const options = {
compact: false,
transformObjectKeys: true,
};
输出结果如下:
var _0x7a5d = ["bar", "test2", "test1"];
(function (_0x59fec5, _0x2e4fac) {
var _0x231e7a = function (_0x46f33e) {
while (--_0x46f33e) {
_0x59fec5["push"](_0x59fec5["shift"]());
}
};
_0x231e7a(++_0x2e4fac);
})(_0x7a5d, 0x167);
var _0x3bc4 = function (_0x309ad3, _0x22d5ac) {
_0x309ad3 = _0x309ad3 - 0x0;
var _0x3a034e = _0x7a5d[_0x309ad3];
return _0x3a034e;
};
(function () {
var _0x9f1fd1 = {};
_0x9f1fd1["foo"] = _0x3bc4("0x0");
_0x9f1fd1[_0x3bc4("0x1")] = {};
_0x9f1fd1[_0x3bc4("0x1")]["baz"] = _0x3bc4("0x2");
})();
可以看到,Object 的变量名被替换为了特殊的变量,使得可读性变差,这样我们就不好直接通过变量名进行搜寻了,这也可以起到一定的防护作用。
可以使用 disableConsoleOutput 来禁用掉 console.log 输出功能,加大调试难度,示例如下:
const code = `
console.log('hello world')
`;
const options = {
disableConsoleOutput: true,
};
运行结果如下:
var _0x3a39 = [
"debug",
"info",
"error",
"exception",
"trace",
"hello\x20world",
"apply",
"{}.constructor(\x22return\x20this\x22)(\x20)",
"console",
"log",
"warn",
];
(function (_0x2a157a, _0x5d9d3b) {
var _0x488e2c = function (_0x5bcb73) {
while (--_0x5bcb73) {
_0x2a157a["push"](_0x2a157a["shift"]());
}
};
_0x488e2c(++_0x5d9d3b);
})(_0x3a39, 0x10e);
var _0x5bff = function (_0x43bdfc, _0x52e4c6) {
_0x43bdfc = _0x43bdfc - 0x0;
var _0xb67384 = _0x3a39[_0x43bdfc];
return _0xb67384;
};
var _0x349b01 = (function () {
var _0x1f484b = !![];
return function (_0x5efe0d, _0x33db62) {
var _0x20bcd2 = _0x1f484b
? function () {
if (_0x33db62) {
var _0x77054c = _0x33db62[_0x5bff("0x0")](_0x5efe0d, arguments);
_0x33db62 = null;
return _0x77054c;
}
}
: function () {};
_0x1f484b = ![];
return _0x20bcd2;
};
})();
var _0x19f538 = _0x349b01(this, function () {
var _0x7ab6e4 = function () {};
var _0x157bff;
try {
var _0x5e672c = Function(
"return\x20(function()\x20" + _0x5bff("0x1") + ");"
);
_0x157bff = _0x5e672c();
} catch (_0x11028d) {
_0x157bff = window;
}
if (!_0x157bff[_0x5bff("0x2")]) {
_0x157bff[_0x5bff("0x2")] = (function (_0x7ab6e4) {
var _0x5a8d9e = {};
_0x5a8d9e[_0x5bff("0x3")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x4")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x5")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x6")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x7")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x8")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x9")] = _0x7ab6e4;
return _0x5a8d9e;
})(_0x7ab6e4);
} else {
_0x157bff[_0x5bff("0x2")][_0x5bff("0x3")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x4")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")]["debug"] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x6")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x7")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x8")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x9")] = _0x7ab6e4;
}
});
_0x19f538();
console[_0x5bff("0x3")](_0x5bff("0xa"));
此时,我们如果执行这个代码,发现是没有任何输出的,这里实际上就是将 console 的一些功能禁用了。
我们知道,在 JavaScript 代码中如果加入 debugger 这个关键字,那么在执行到该位置的时候控制它就会进入断点调试模式。如果在代码多个位置都加入 debugger 这个关键字,或者定义某个逻辑来反复执行 debugger,那就会不断进入断点调试模式,原本的代码无法就无法顺畅地执行了。这个过程可以称为调试保护,即通过反复执行 debugger 来使得原来的代码无法顺畅执行。
其效果类似于执行了如下代码:
setInterval(() => {
debugger;
}, 3000);
如果我们把这段代码粘贴到控制台,它就会反复地执行 debugger 语句进入断点调试模式,从而干扰正常的调试流程。
在 javascript-obfuscator 中可以使用 debugProtection 来启用调试保护机制,还可以使用 debugProtectionInterval 来启用无限 Debug ,使得代码在调试过程中会不断进入断点模式,无法顺畅执行,配置如下:
const options = {
debugProtection: true,
debugProtectionInterval: true,
};
混淆后的代码会不断跳到 debugger 代码的位置,使得整个代码无法顺畅执行,对 JavaScript 代码的调试形成一定的干扰。
我们还可以通过控制 domainLock 来控制 JavaScript 代码只能在特定域名下运行,这样就可以降低代码被模拟或盗用的风险。
示例如下:
const code = `
console.log('hello world')
`;
const options = {
domainLock: ["cuiqingcai.com"],
};
这里我们使用了 domainLock 指定了一个域名叫做 cuiqingcai.com,也就是设置了一个域名白名单,混淆后的代码结果如下:
var _0x3203 = [
"apply",
"return\x20(function()\x20",
"{}.constructor(\x22return\x20this\x22)(\x20)",
"item",
"attribute",
"value",
"replace",
"length",
"charCodeAt",
"log",
"hello\x20world",
];
(function (_0x2ed22c, _0x3ad370) {
var _0x49dc54 = function (_0x53a786) {
while (--_0x53a786) {
_0x2ed22c["push"](_0x2ed22c["shift"]());
}
};
_0x49dc54(++_0x3ad370);
})(_0x3203, 0x155);
var _0x5b38 = function (_0xd7780b, _0x19c0f2) {
_0xd7780b = _0xd7780b - 0x0;
var _0x2d2f44 = _0x3203[_0xd7780b];
return _0x2d2f44;
};
var _0x485919 = (function () {
var _0x5cf798 = !![];
return function (_0xd1fa29, _0x2ed646) {
var _0x56abf = _0x5cf798
? function () {
if (_0x2ed646) {
var _0x33af63 = _0x2ed646[_0x5b38("0x0")](_0xd1fa29, arguments);
_0x2ed646 = null;
return _0x33af63;
}
}
: function () {};
_0x5cf798 = ![];
return _0x56abf;
};
})();
var _0x67dcc8 = _0x485919(this, function () {
var _0x276a31;
try {
var _0x5c8be2 = Function(_0x5b38("0x1") + _0x5b38("0x2") + ");");
_0x276a31 = _0x5c8be2();
} catch (_0x5f1c00) {
_0x276a31 = window;
}
var _0x254a0d = function () {
return {
key: _0x5b38("0x3"),
value: _0x5b38("0x4"),
getAttribute: (function () {
for (var _0x5cc3c7 = 0x0; _0x5cc3c7 < 0x3e8; _0x5cc3c7--) {
var _0x35b30b = _0x5cc3c7 > 0x0;
switch (_0x35b30b) {
case !![]:
return (
this[_0x5b38("0x3")] +
"_" +
this[_0x5b38("0x5")] +
"_" +
_0x5cc3c7
);
default:
this[_0x5b38("0x3")] + "_" + this[_0x5b38("0x5")];
}
}
})(),
};
};
var _0x3b375a = new RegExp("[QLCIKYkCFzdWpzRAXMhxJOYpTpYWJHPll]", "g");
var _0x5a94d2 = "cuQLiqiCInKYkgCFzdWcpzRAaXMi.hcoxmJOYpTpYWJHPll"
[_0x5b38("0x6")](_0x3b375a, "")
["split"](";");
var _0x5c0da2;
var _0x19ad5d;
var _0x5992ca;
var _0x40bd39;
for (var _0x5cad1 in _0x276a31) {
if (
_0x5cad1[_0x5b38("0x7")] == 0x8 &&
_0x5cad1[_0x5b38("0x8")](0x7) == 0x74 &&
_0x5cad1[_0x5b38("0x8")](0x5) == 0x65 &&
_0x5cad1[_0x5b38("0x8")](0x3) == 0x75 &&
_0x5cad1[_0x5b38("0x8")](0x0) == 0x64
) {
_0x5c0da2 = _0x5cad1;
break;
}
}
for (var _0x29551 in _0x276a31[_0x5c0da2]) {
if (
_0x29551[_0x5b38("0x7")] == 0x6 &&
_0x29551[_0x5b38("0x8")](0x5) == 0x6e &&
_0x29551[_0x5b38("0x8")](0x0) == 0x64
) {
_0x19ad5d = _0x29551;
break;
}
}
if (!("~" > _0x19ad5d)) {
for (var _0x2b71bd in _0x276a31[_0x5c0da2]) {
if (
_0x2b71bd[_0x5b38("0x7")] == 0x8 &&
_0x2b71bd[_0x5b38("0x8")](0x7) == 0x6e &&
_0x2b71bd[_0x5b38("0x8")](0x0) == 0x6c
) {
_0x5992ca = _0x2b71bd;
break;
}
}
for (var _0x397f55 in _0x276a31[_0x5c0da2][_0x5992ca]) {
if (
_0x397f55["length"] == 0x8 &&
_0x397f55[_0x5b38("0x8")](0x7) == 0x65 &&
_0x397f55[_0x5b38("0x8")](0x0) == 0x68
) {
_0x40bd39 = _0x397f55;
break;
}
}
}
if (!_0x5c0da2 || !_0x276a31[_0x5c0da2]) {
return;
}
var _0x5f19be = _0x276a31[_0x5c0da2][_0x19ad5d];
var _0x674f76 =
!!_0x276a31[_0x5c0da2][_0x5992ca] &&
_0x276a31[_0x5c0da2][_0x5992ca][_0x40bd39];
var _0x5e1b34 = _0x5f19be || _0x674f76;
if (!_0x5e1b34) {
return;
}
var _0x593394 = ![];
for (var _0x479239 = 0x0; _0x479239 < _0x5a94d2["length"]; _0x479239++) {
var _0x19ad5d = _0x5a94d2[_0x479239];
var _0x112c24 = _0x5e1b34["length"] - _0x19ad5d["length"];
var _0x51731c = _0x5e1b34["indexOf"](_0x19ad5d, _0x112c24);
var _0x173191 = _0x51731c !== -0x1 && _0x51731c === _0x112c24;
if (_0x173191) {
if (
_0x5e1b34["length"] == _0x19ad5d[_0x5b38("0x7")] ||
_0x19ad5d["indexOf"](".") === 0x0
) {
_0x593394 = !![];
}
}
}
if (!_0x593394) {
data;
} else {
return;
}
_0x254a0d();
});
_0x67dcc8();
console[_0x5b38("0x9")](_0x5b38("0xa"));
这段代码就只能在指定域名 cuiqingcai.com 下运行,不能在其他网站运行。这样的话,如果一些相关 JavaScript 代码被单独剥离出来,想在其他网站运行或者使用程序模拟运行的话,运行结果只有是失败,这样就可以有效降低被代码被模拟或盗用的风险。
另外还有一些特殊的工具包,如使用 aaencode、jjencode、jsfuck 等工具对代码进行混淆和编码。
示例如下:
var a = 1
jsfuck 的结果:
[][(![]+[])[!+[]+!![]+!![]]+([]+{})[+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]][([]+{})[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]]+(![]+[])[!+[]+!![]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!![]]+([][[]]+[])[+[]]+([]+{})[!+[]+!![]+!![]+!![]+!![]]+(!![]+[])[+[]]+([]+{})[+!![]]+(!![]+[])[+!![]]]([][(![]+[])[!+[]+!![]+!![]]+([]+{})[+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]][([]+{})[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]]+
...
([]+{})[+!![]]+(!![]+[])[+!![]]]((!![]+[])[+!![]]+([][[]]+[])[!+[]+!![]+!![]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!![]]+([][[]]+[])[+!![]]+([]+{})[!+[]+!![]+!![]+!![]+!![]+!![]+!![]]+(![]+[])[!+[]+!![]]+([]+{})[+!![]]+([]+{})[!+[]+!![]+!![]+!![]+!![]]+(+{}+[])[+!![]]+(!![]+[])[+[]]+([][[]]+[])[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]])(!+[]+!![]+!![]+!![]+!![]))[!+[]+!![]+!![]]+([][[]]+[])[!+[]+!![]+!![]])(!+[]+!![]+!![]+!![]+!![])(([]+{})[+[]])[+[]]+(!+[]+!![]+!![]+[])+([][[]]+[])[!+[]+!![]])+([]+{})[!+[]+!![]+!![]+!![]+!![]+!![]+!![]]+(+!![]+[]))(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])
aaencode 的结果:
゚ω゚ノ= /`m´)ノ ~┻━┻ / ['_']; o=(゚ー゚) =_=3; c=(゚Θ゚) =(゚ー゚)-(゚ー゚); (゚Д゚) =(゚Θ゚)= (o^_^o)/ (o^_^o);(゚Д゚)={゚Θ゚: '_' ,゚ω゚ノ : ((゚ω゚ノ==3) +'_') [゚Θ゚] ,゚ー゚ノ :(゚ω゚ノ+ '_')[o^_^o -(゚Θ゚)] ,゚Д゚ノ:((゚ー゚==3) +'_')[゚ー゚] }; (゚Д゚) [゚Θ゚] =((゚ω゚ノ==3) +'_') [c^_^o];(゚Д゚) ['c'] = ((゚Д゚)+'_') [ (゚ー゚)+(゚ー゚)-(゚Θ゚) ];(゚Д゚) ['o'] = ((゚Д゚)+'_') [゚Θ゚];(゚o゚)=(゚Д゚) ['c']+(゚Д゚) ['o']+(゚ω゚ノ +'_')[゚Θ゚]+ ((゚ω゚ノ==3) +'_') [゚ー゚] + ((゚Д゚) +'_') [(゚ー゚)+(゚ー゚)]+ ((゚ー゚==3) +'_') [゚Θ゚]+((゚ー゚==3) +'_') [(゚ー゚) - (゚Θ゚)]+(゚Д゚) ['c']+((゚Д゚)+'_') [(゚ー゚)+(゚ー゚)]+ (゚Д゚) ['o']+((゚ー゚==3) +'_') [゚Θ゚];(゚Д゚) ['_'] =(o^_^o) [゚o゚] [゚o゚];(゚ε゚)=((゚ー゚==3) +'_') [゚Θ゚]+ (゚Д゚) .゚Д゚ノ+((゚Д゚)+'_') [(゚ー゚) + (゚ー゚)]+((゚ー゚==3) +'_') [o^_^o -゚Θ゚]+((゚ー゚==3) +'_') [゚Θ゚]+ (゚ω゚ノ +'_') [゚Θ゚]; (゚ー゚)+=(゚Θ゚); (゚Д゚)[゚ε゚]='\'; (゚Д゚).゚Θ゚ノ=(゚Д゚+ ゚ー゚)[o^_^o -(゚Θ゚)];(o゚ー゚o)=(゚ω゚ノ +'_')[c^_^o];(゚Д゚) [゚o゚]='\"';(゚Д゚) ['_'] ( (゚Д゚) ['_'] (゚ε゚+(゚Д゚)[゚o゚]+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚ー゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+((゚ー゚) + (o^_^o))+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚ー゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+((o^_^o) +(o^_^o))+ (゚Θ゚)+ (゚Д゚)[゚o゚])(゚Θ゚))((゚Θ゚)+(゚Д゚)[゚ε゚]+((゚ー゚)+(゚Θ゚))+(゚Θ゚)+(゚Д゚)[゚o゚]);
jjencode 的结果:
$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+"\"+$.__$+$.$$_+$.$$_+$.$_$_+"\"+$.__$+$.$$_+$._$_+"\"+$.$__+$.___+$.$_$_+"\"+$.$__+$.___+"=\"+$.$__+$.___+$.__$+"\"")())();
可以看到,通过这些工具,原本非常简单的代码被转化为一些几乎完全不可读的代码,但实际上运行效果还是相同的。这些混淆方式比较另类,看起来虽然没有什么头绪,但实际上找到规律是非常好还原的,其没有真正达到强力混淆的效果。
以上便是对 JavaScript 混淆方式的介绍和总结。总的来说,经过混淆的 JavaScript 代码其可读性大大降低,同时防护效果也大大增强。
随着技术的发展,WebAssembly 逐渐流行起来。不同于 JavaScript 混淆技术, WebAssembly 其基本思路是将一些核心逻辑使用其他语言(如 C/C++ 语言)来编写,并编译成类似字节码的文件,并通过 JavaScript 调用执行,从而起到二进制级别的防护作用。
WebAssembly 是一种可以使用非 JavaScript 编程语言编写代码并且能在浏览器上运行的技术方案,比如借助于我们能将 C/C++ 利用 Emscripten 编译工具转成 wasm 格式的文件, JavaScript 可以直接调用该文件执行其中的方法。
WebAssembly 是经过编译器编译之后的字节码,可以从 C/C++ 编译而来,得到的字节码具有和 JavaScript 相同的功能,运行速度更快,体积更小,而且在语法上完全脱离 JavaScript,同时具有沙盒化的执行环境。
比如这就是一个基本的 WebAssembly 示例:
WebAssembly.compile(
new Uint8Array(
`
00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01
7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61
64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02
08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c
0f 0b`
.trim()
.split(/[\s\r\n]+/g)
.map((str) => parseInt(str, 16))
)
).then((module) => {
const instance = new WebAssembly.Instance(module);
const { add, square } = instance.exports;
console.log("2 + 4 =", add(2, 4));
console.log("3^2 =", square(3));
console.log("(2 + 5)^2 =", square(add(2 + 5)));
});
这里其实是利用 WebAssembly 定义了两个方法,分别是 add 和 square,可以分别用于求和和开平方计算。那这两个方法在哪里声明的呢?其实它们被隐藏在了一个 Uint8Array 里面,仅仅查看明文代码我们确实无从知晓里面究竟定义了什么逻辑,但确实是可以执行的,我们将这段代码输入到浏览器控制台下,运行结果如下:
2 + 4 = 6
3^2 = 9
(2 + 5)^2 = 49
由此可见,通过 WebAssembly 我们可以成功将核心逻辑“隐藏”起来,这样某些核心逻辑就不能被轻易找出来了。
所以,很多网站越来越多使用 WebAssembly 技术来保护一些核心逻辑不被轻易被人识别或破解,可以起到更好的防护效果。
以上,我们就介绍了接口加密技术和 JavaScript 的压缩、混淆技术,也对 WebAssembly 技术有了初步的了解,知己知彼方能百战不殆,了解了原理,我们才能更好地去实现 JavaScript 的逆向。
本节代码:https://github.com/Python3WebSpider/JavaScriptObfuscate。
由于本节涉及一些专业名词,部分内容参考来源如下:
*请认真填写需求信息,我们会在24小时内与您取得联系。