Javascript进阶
变量提升
看以下代码,或多或少会有些问题的。
function fn(){
console.log(name);
var name = '大马猴';
}
fn() // undefined
发现问题了么. 这么写代码, 在其他语言里. 绝对是不允许的. 但是在js里. 不但允许, 还能执行. 为什么呢? 因为在js执行的时候. 它会首先检测你的代码. 发现在代码中会有name使用. OK. 运行时就会变成这样的逻辑:
function fn(){
var name;
console.log(name);
name = '大马猴';
}
fn()
console.log(a);
看到了么. 实际运行的时候和我们写代码的顺序可能会不一样....这种把变量提前到代码块第一部分运行的逻辑被称为变量提升. 这在其他语言里是绝对没有的. 并且也不是什么好事情. 正常的逻辑不应该是这样的. 那么怎么办? 在新的ES6中. 就明确了, 这样使用变量是不完善的. es6提出. 用let来声明变量. 就不会出现该问题了.
function fn(){
console.log(name); // 直接报错, let变量不可以变量提升.
let name = '大马猴';
}
fn()
结论一, 用let声明变量是新版本javascript提倡的一种声明变量的方案。
只能提升变量的声明,不能提升变量的值。
let还有哪些作用呢?
function fn(){
// console.log(name); // 直接报错, let变量不可以变量提升.
// let name = '大马猴';
var name = "周杰伦";
var name = "王力宏";
console.log(name);
}
fn()
显然一个变量被声明了两次. 这样也是不合理的. var本意是声明变量. 同一个东西. 被声明两次. 所以ES6规定. let声明的变量. 在同一个作用域内. 只能声明一次.
function fn(){
// console.log(name); // 直接报错, let变量不可以变量提升.
// let name = '大马猴';
let name = "周杰伦";
console.log(name);
let name = "王力宏";
console.log(name);
}
fn()
注意, 报错是发生在代码检查阶段. 所以. 上述代码根本就执行不了.
结论二, 在同一个作用域内. let声明的变量只能声明一次. 其他使用上和var没有差别。
let, const: 在同一个作用域里不允许声明两个一样的变量,解决了变量提升。
const表示常量。
let表示变量。
闭包函数
我们先看一段代码.
let name = "周杰伦"
function chi(){
name = "吃掉"
}
chi();
console.log(name); // 结果是:吃掉
发现没有, 在函数内部想要修改外部的变量是十分容易的一件事. 尤其是全局变量. 这是非常危险的. 试想, 我写了一个函数. 要用到name, 结果被别人写的某个函数给修改掉了. 多难受.
接下来. 我们来看一个案例:
我准备两个工具人. 来编写代码. 分别是js01和js02.
// 1号工具人.
var name = "alex"
setTimeout(function(){
console.log("一号工具人:"+name) // 一号工具人还以为是alex呢, 但是该变量是不安全的.
}, 5000);
// 2号工具人
var name = "周杰伦"
console.log("二号工具人", name);
html:
<script src="js01.js"></script>
<script src="js02.js"></script>
此时运行的结果:

很明显, 虽然各自js在编写时是分开的. 但是在运行时, 是在同一个空间内执行的. 他们拥有相同的作用域. 此时的变量势必是非常非常不安全的. 那么如何来解决呢? 注意, 在js里. 变量是有作用域的. 也就是说一个变量的声明和使用是有范围的. 不是无限的. 这一点, 很容易验证.
function fn(){
let love = "爱呀"
}
fn()
console.log(love)
直接就报错了. 也就是说. 在js里是有全局和局部的概念的.
直接声明在最外层的变量就是全局变量. 所有函数, 所有代码块都可以共享的. 但是反过来就不是了. 在函数内和代码块内声明的变量. 尤其是函数内. 声明出来的变量它是一个局部变量. 外界是无法进行访问的. 我们就可以利用这一点来给每个工具人创建一个局部空间. 就像这样:
// 1号工具人.
(function(){
var name = "alex";
setTimeout(function(){
console.log("一号工具人:"+name) // 一号工具人还以为是alex呢, 但是该变量是不安全的.
}, 5000);
})()
// 二号工具人
(function(){
var name = "周杰伦"
console.log("二号工具人", name);
})()
运行结果

这样虽然解决了变量的冲突问题. 但是...我们想想. 如果在外面需要函数内部的一些东西来帮我进行相关操作怎么办...比如, 一号工具人要提供一个功能(加密). 外界要调用. 怎么办?
// 1号工具人.
let jiami = (function(){
let key = "10086" // 假装我是秘钥
// 我是一个加密函数
let mi = function(data){ // 数据
console.log("接下来, 我要加密了,rsa哦. 很厉害的")
console.log("秘钥:"+key);
console.log("数据:"+data);
// 返回密文
return "我要返回密文";
}
// 外面需要用到这个功能啊. 你得把这个东东返回啊. 返回加密函数
return mi;
})();
好像有点儿复杂了哈. 别着急. 注意了. 我们如果封装一个加密js包的时候. 好像还得准备出解密的功能. 并且, 不可能一个js包就一个功能吧..那也太痛苦了(起名字). 那怎么办? 我们可以返回一个对象. 对象里面可以存放好多个功能. 而一些不希望外界触碰的功能. 就可以很好的保护起来.
// 1号工具人.
let jiami = (function(){
let key = "10086" // 加装我是秘钥
// 我是一个加密函数
let rsa_jiami = function(data){ // 数据
console.log("接下来, 我要加密了,rsa哦. 很厉害的")
console.log("秘钥:"+key);
console.log("数据:"+data);
// 返回密文
return "我要返回密文";
}
// 该函数只属于该模块内部. 外界无法访问.
let n = {
abc:function(){
console.log("我是abc. 你叫我干什么?")
}
}
// 外面需要用到的功能.进行返回.
return {
rsa_jiami: function(data){
console.log("接下来, 我要加密了,rsa哦. 很厉害的")
console.log("秘钥:"+this.get_rsa_key() + key);
n.abc();
console.log("数据:"+data);
return "我要返回密文";
},
aes_jiami: function(data){
console.log("接下来, 我要加密了,aes哦. 很厉害的")
console.log("秘钥:"+this.get_aes_key());
n.abc();
console.log("秘钥:"+key);
console.log("数据:"+data);
return "我要返回密文";
},
get_rsa_key: function() {
return this.rsa_key = "rsa的key", this.rsa_key
},
get_aes_key: function() {
return this.rsa_key = "aes的key", this.rsa_key
}
}
})();
html里面使用时:
<script>
miwen = jiami.rsa_jiami("吃你的糖葫芦吧");
console.log(miwen);
</script>
OK. 至此. 何为闭包? 上面这个就是闭包. 相信你百度一下就会知道. 什么内层函数使用外层函数变量. 什么让一个变量常驻内存.等等. 其实你细看. 它之所以称之为闭包~. 它是一个封闭的环境. 在内部. 自己和自己玩儿. 避免了对该模块内部的冲击和改动. 避免的变量之间的冲突问题.
闭包的特点:
-
内层函数对外层函数变量的使用.
-
会让变量常驻与内存.
这俩玩意就不解释了. 和python的闭包是一个意思. 不懂没关系. 能看懂他的执行过程就好.
JS中的各种操作(非交互)
定时器
这玩意一般和无限debugger一起出现。
在JS中,有两种设置定时器的方案。
setTimeout
// 经过xxx时间后, 执行xxx函数
// 时间单位是毫秒
t = setTimeout(函数, 时间)
t = setTimeout(function(){
console.log("我爱你")
}, 5);
window.clearTimeout(t) // 停止一个定时器,但是一般不这样干
setInterval
// 每隔 xxx时间, 执行一次xxx函数
// 时间单位是毫秒
t = setInterval(函数, 时间)
// 每隔5秒钟, 打印`我爱你`
t = setInterval(function(){
console.log("我爱你")
}, 5000)
window.clearInterval(t) // 停止一个定时器
for(let i = 0; i <= 9999; i++)window.clearInterval(i); // 清理掉所有定时器
关于时间
var d = new Date(); // 获取系统时间
var d = new Date("2018-12-01 15:32:48"); // 得到一个具体时间
// 时间格式化
year = d.getFullYear(); // 拿到年份
month = d.getMonth() + 1; // 拿到月份. 注意月份从0开始,所以要 +1
date = d.getDate(); // 拿到日期
hour = d.getHours(); // 拿到小时
minute = d.getMinutes(); // 分钟
seconds = d.getSeconds(); //秒
// 字符串拼接, 两种拼接方案
// 方案一,
let fmt_time = year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second;
console.log(fmt_time);
// 方案二, 模板字符串, 反引号 -> f-string
// `${变量}`
let my_fmt_time = `${year}-${month}-${day} ${hour}:${minute}:${second}`;
console.log(my_fmt_time);
// 获取时间戳的方案
// 方案一.
console.log(Date.now());
// 方案二.
// var d = new Date();
var d = new Date;
console.log(d.getTime());
// 1680875906884 时间戳 相当于python 中 time.time()
// Python中的时间戳单位是秒,前端的时间戳单位是毫秒,相差一千倍。
// 在加密算法中,基本都能看到时间戳。
// 在今天的互联网环境里. 13位或者10位的167开头的数字 大概率是时间戳
math数学逻辑
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
// 如果服务器可以接受随机值. ok. 我们就可以固定该值..
// console.log(Math.random()); // 随机数.
function a() { // 随机获得一个字符串. 该字符串长度是a
var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; d < 16; d++) // 循环16次
// random随机出来的玩意. (0,1) * 62 [0, 61]
e = Math.random() * b.length,
e = Math.floor(e), // 取整.
c += b.charAt(e); // 从b里面获取某个字符 c: "Fx1SfAAGB"
return c
}
console.log(a(16));
</script>
</head>
<body>
</body>
</html>
测试无限debugger
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="测试debugger.js"></script>
</head>
<body>
</body>
</html>
// debugger; 在运行的时候
// 如果你打开了F12, devtools, 代码运行到debugger会停下来.
// 如果没有打开F12, 没有devtools, 代码运行到debugger就直接过掉.
let x = 10;
let y = 20;
// 当打开F12的时候. 运行到该代码的时候. 会暂停.
setInterval(function (){
debugger;
}, 1000)
console.log(x);
console.log(y);
打开了F12,停在下面的界面。很多调试工具无法使用。

debugger只有在打开了devtools的时候才有效...爬虫工程师或者黑客.....会打开devtools
解决无限debugger的方案
1. 直接在debugger前面, 右键 -> never pause here 最简单的一个方案(首选的方案)


2. 置空法. 在setInterval前面鼠标左键点击行号设置断点,刷新页面,然后在console中对setInterval置空。
置空法(hook的逻辑), 可以对无限debugger的入口进行置空, 可以对setTimeout或者setInterval进行置空。
该方案需要找到进入无限debugger的入口. 或者定时器的入口. 需要打断点, 然后置空。
setInterval = function (){}
eval函数(必须会)
eval本身在js里面正常情况下使用的并不多. 但是很多网站会利用eval的特性来完成反爬操作. 我们来看看eval是个什么鬼?
eval的参数一定是字符串,而且可以运行。
从功能上讲, eval非常简单. 它和python里面的eval是一样的. 它可以动态的把字符串当成js代码进行运行.
s = "console.log('我爱你')"; //有一个可以运行的字符串的代码。
eval(s); // 用eval可以运行这个字符串的代码。运行结果:我爱你
也就是说. eval里面传递的应该是即将要执行的代码(字符串). 那么在页面中如果看到了eval加密该如何是好? 其实只要记住了一个事儿. 它里面不论多复杂. 一定是个字符串.
http://tools.jb51.net/password/evalencode 一个在线JS处理eval的网站. 大多数的eval加密. 都可以搞定了.
比如: "console.log('我爱你')" 这个字符串经过eval加密后,就是下面这串。
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('0.1(\'我爱你\')',62,2,'console|log'.split('|'),0,{}))
这一坨看起来, 肯定很不爽. 怎么变成我们看着很舒服的样子呢? 记住. eval()里面是字符串. 记住~!!
如果一个函数,比如下面的空函数,直接在Console中直接敲入,是会报错的,但是用括号把函数括起来就可以执行这个函数,不会报错。

想看代码怎么办? 去console中. (eval中的所有内容) 回车。
删掉了eval. 剩下的东西跑出来一定是字符串
那我想看看这个字符串长什么样? 就把eval()里面的东西拷贝出来. 执行一下. 最终一定会得到一个字符串. 要不然eval()执行不了的. 对不...于是就有了下面的操作.
如果把这一堆直接运行,会报错,报错原因是缺少函数名。

解决办法是让这个匿名函数自运行即可,即只要删掉了eval就行,不要去掉最外面的括号:

当然直接赋值给一个变量也行:

在逆向时,碰到eval的时候,我们就像上面处理就行。
神奇的window
例如:
eval === window.eval // true
setInterval === window.setInterval // true
var a = 10;
a === window.a // true
function fn(){}
fn === window.fn // true
window.mm = "爱你"
console.log(mm); //"爱你"
// 这里用let就没有var的效果...let声明的变量没有在window中.
// 可以把一个局部的内容. 丢给全局
// window是一个顶级作用域...
function fn(){
function gn(){
console.log("我要测试....");
}
// 可以把一个局部的内容. 丢给全局
window.xxxx = gn; // 2. 才能给window中多一个xxxx
}
fn(); // 1. 这句话得执行
综上, 我们可以得出一个结论. 全局变量可以用window.xxx来表示。在window中存入一个变量. 这个变量就是全局的。这里的一个应用:可以在闭包中向外界传递信息。
ok. 接下来. 注意看了. 我要搞事情了
(function(){
let chi = function(){
console.log("我是吃")
}
window.chi = chi
})();
chi()
//换一种写法. 你还认识么?
(function(w){
let chi = function(){
console.log("我是吃")
}
w.chi = chi
})(window);
//再复杂一点
(function(w){
let tools = {
b64: function(){
console.log("我是计算B64");
return "b64";
},
md5: function(){
console.log("我是计算MD5");
return "MD5"
}
}
w.jiami = {
AES: function(msg){
return tools.b64(),
tools.md5(),
'god like';
},
DES: function(){
console.log("我是DES");
},
RSA: function(){
console.log("我是RSA");
}
}
})(window);
jiami.AES("吃了么");
window是整个浏览器的全局作用域.
在爬虫逆向中,有时候看到一个变量,比如$_ts,不知道是什么,但是在console中打印,如下所示,有一系列的Window等等。说明他就是一个Window。(经验之谈)

一个爬虫逆向的例子:
!function() {
function a(a) {
var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; a > d; d += 1)
e = Math.random() * b.length,
e = Math.floor(e),
c += b.charAt(e);
return c
}
function b(a, b) {
var c = CryptoJS.enc.Utf8.parse(b)
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a)
, f = CryptoJS.AES.encrypt(e, c, {
iv: d,
mode: CryptoJS.mode.CBC
});
return f.toString()
}
function c(a, b, c) {
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}
function d(d, e, f, g) {
var h = {}
, i = a(16);
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}
function e(a, b, d, e) {
var f = {};
return f.encText = c(a + e, b, d),
f
}
window.asrsea = d,
window.ecnonasr = e
}();
// 看似是执行asrsea 但是在debug模式下. 发现执行的是d函数.
// 我们不要去纠结函数的名字,在代码的其他地方一定会有 window.asrsea(xxxxxxx);
call和apply
对于咱们逆向工程师而言. 并不需要深入的理解call和apply的本质作用. 只需要知道这玩意执行起来的逻辑顺序是什么即可。
call的语法是: 函数.call(对象, 参数1, 参数2, 参数3....)
// call的作用:改变this的指向。
// 执行逻辑是: 执行函数. 并把对象传递给函数中的this. 其他参数照常传递给函数
在运行时. 正常的js调用:
function People(name, age){
this.name = name;
this.age = age;
this.chi = function(){
console.log(this.name, "在吃东西")
}
}
p1 = new People("alex", 18);
p2 = new People("wusir", 20);
p1.chi();
p2.chi();
接下来, 我们可以使用call和apply也完成同样的函数调用
function People(name, age){
this.name = name;
this.age = age;
this.chi = function(what_1, what_2){
console.log(this.name, "在吃", what_1, what_2);
}
}
p1 = new People("alex", 18);
p2 = new People("wusir", 20);
p1.chi("馒头", "大饼");
p2.chi("大米饭", "金坷垃");
function eat(what_1, what_2){
console.log(this.name, "在吃", what_1, what_2);
}
eat.call(p1, "查克拉", "元宇宙");
eat(); //this是window
apply和他几乎一模一样. 区别是: apply传递参数要求是一个数组
eat.apply(p1, ["苞米茬子", "大饼子"]);
ES6中的箭头函数
在ES6中简化了函数的声明语法。
注意事项:
1. 箭头函数如果只有1个参数, 小括号可以省略。
2. this逻辑和普通的function不一样.(我们作为爬虫工程师, 这里不用管)。
var fn = function(){};
var fn = () => {};
var fn = function(name){}
var fn = name => {}
var fn = (name) => {}
var fn = function(name, age){}
var fn = (name, age) => {}
var username = "qiaofu"; // 属性 username 和 "username" 效果一样。
var age = 18;
var password = 123456;
var chi = function(){
console.log("吃饭睡觉, 打豆豆")
}
var person = {
// username: username,
// age: age,
// password: password,
// chi: chi
// 在es6中, 简化了上述逻辑
// 如果属性名和某一个变量名完全一样, 并且, 你需要用该变量来表示该属性
username, // 等价的写法: username: username,
age,
password,
chi,
he: function(){
console.log("我要喝东西")
},
// 上面这个he也可以进行简化
la(){
console.log("我要ladongxi")
}
// 等价的写法: la: function(){}
}
person.la();
var obj = {
"name": "汪峰",
age: 18,
wife:{
name: "子怡",
age: 28
},
sing(){
console.log("汪峰会唱歌")
}
}
// 需求如下, 需要从对象中获取某个属性. 刚刚好, 该属性需要设置成变量, 名字一样
// var age = obj.age;
// var wife = obj.wife;
// var name = obj.name;
// 前端的解构
var {age, wife, name, sing} = obj;
// 拆分出wife中的name , 赋值给sb_name变量
var {name:sb_name} = wife;
// console.log(age);
// console.log(wife);
// console.log(name);
// sing();
console.log(sb_name);
逗号运算符
从左到右,依次执行逗号之间的内容。表达式的结果是最后一项内容。
var a;
a = (1,2,3);
console.log(a); // 3
a = (console.log(1), console.log(2), console.log(3), 4); // 依次打印 1 2 3
console.log(a); // 4
function fn(){
var a = 10;
var b = 20;
return a+=3,a--,a++,a+=b,a;
// 可改下成如下:
// a+=3; // 13
// a--; // 12
// a++; // 13
// a+=b;
// return a;
}
console.log(fn()); // 33
三元运算符
三元运算可以做平坦流。
阿里的平坦流:switch+if+三元+递归。
# Python 代码
a = 10
b = 20
# 三元运算. 逻辑是
# 如果中间计算的结果是真,
# 返回if前面的结果
# 否则返回else后面的结果
c = a if a > b else b
print(c) # 结果是: 20
let a = 10;
let b = 20;
let d = a > b? a: b // x ? y : z 运算规则:x是条件,如果条件成立,返回y。条件不成立,返回z。
console.log(d); // 结果是20
// 看一个恶心的:
let a = 10;
let b = 20;
let c = 5;
let d = 17;
let e;
let m;
e = (e = a > 3 ? b : c, m = e < b++ ? c-- : a = 3 > b % d ? 27: 37, m++)
// 看括号里面有逗号运算符,所以 e = m++
// e = a > 3 ? b : c; // e = b = 20;
// m = e < b++ ? c-- : a = 3 > b % d ? 27: 37
// 上面解读: 如果 e < b++ 成立,就是 c-- ,如果不成立就是 a = 3 > b % d ? 27: 37
// b++ 结果是20 ,所以条件就是不成立的。走后面(走后面的话,那b就得加1,所以b=21) a = 3 > b % d ? 27: 37
// 来看 a = 3 > b % d ? 27: 37 // 首先看条件 3 > b % d 这是一伙的。 b % d 就是 21 % 17 结果是 4
// 3 > 4 条件不成立,所以走后面的 37。所以 a = 37
// 所以 m = 37
// e = m++; // e = 37, m = 38
// 别忘了c-- 没执行,所以 c = 5
console.log(e); // 37
console.log(c); // 5
console.log(m); // 38
console.log(a); // 37
console.log(b); // 21
prototype(超重点内容)
prototype是js里面给类增加功能扩展的一种模式.
写个类看看
function People(name, age){
this.name = name;
this.age = age;
this.run = function(){
console.log(this.name+"在跑")
}
}
p1 = new People("张三", 18);
p2 = new People("李四", 19);
p1.run();
p2.run();
我现在代码写完了. 突然之间, 我感觉好像少了个功能. 人不应该就一个功能. 光会吃是不够的. 还得能够ooxx. 怎么办? 直接改代码? 可以. 但不够好. 如果这个类不是我写的呢? 随便改别人代码是很不礼貌的. 也很容易出错. 怎么办? 我们可以在我们自己代码中对某个类型动态增加功能. 此时就用到了prototype.
function People(name, age){
this.name = name;
this.age = age;
this.run = function(){
console.log(this.name+"在跑")
}
}
// 通过prototype可以给People增加功能
People.prototype.xxoo = function(){
console.log(this.name+"还可以xxoo");
}
p1 = new People("张三", 18);
p2 = new People("李四", 19);
p1.run();
p2.run();
p1.xxoo();
p2.xxoo();
能看到一些效果了是吧. 也就是说. 可以通过prototype给我们的对象增加一些功能.
接下来. 聊几个重要的概念.
构造器
一个函数在被new的时候,会在新开辟的内存中给对象进行初始化操作,该函数被称为构造函数(constructor)。 类比python中的__init__。
function People(){ //这个东西就是构造器 constractor
}
var p = new People(); // 调用构造器
p.constractor == People; // true
原型对象
注意:在es6出现之前. 是没有类的。在老版本的js里面,实用原型来表示类。在js中,所有的函数在创建的时候,js就会产生一个该函数的原型对象。该原型对象,我们可以看成是该函数创建出来的对象的类。
每一个js对象中. 都有一个隐藏属性__proto__指向该对象的原型对象. 在执行该对象的方法或者查找属性时. 首先, 对象自己是否存在该属性或者方法. 如果存在, 就执行自己的. 如果自己不存在. 就去找原型对象.
每个函数都有prototype,他是给该函数的对象使用的。
function Friend(){
this.chi = function(){
console.log("我的朋友在吃");
}
}
Friend.prototype.chi = function(){
console.log("我的原型在吃")
}
f = new Friend();
f.chi(); // 此时. 该对象中. 有chi这个方法. 同时, 它的原型对象上, 也有chi这个方法.
// 运行结果:
// 我的朋友在吃
prototype和__proto__有什么关系?
在js中. 构造器的prototype属性和对象的__proto__是一个东西. 都是指向这个原型对象.
f.__proto__ === Friend.prototype // true
原型链
这个比较绕了. 我们从前面的学习中, 了解到. 每个对象身体里. 都隐藏着__proto__也就是它的原型对象. 那么我们看哈, 原型对象也是对象啊, 那么也就是说. 原型对象也有__proto__属性.
类似于.....这样:
f.__proto__.__proto__
打印出来的效果是这样的:

此时. 又出现一堆看不懂的玩意. 这些玩意是什么? 这些其实是Object的原型.
f.__proto__.__proto__ === Object.prototype
所以, 我们在执行f.toString()的时候不会报错. 反而可以正常运行. 原因就在这里.
执行过程,前端js的方法的查找路径(MRO):
在执行`对象.方法()`的时候. 先去找`对象`中是否含有该`方法`, 如果有, 就执行.
如果没有, 就会自动查找 `对象.__proto__`, 如果`对象.__proto__`中找到了该方法就执行.
如果没有, 就会自动查找 `对象.__proto__.__proto__`, 如果有, 就执行,
如果没有, 就会自动查找 `对象.__proto__.__proto__.__proto__`, 如果有, 就执行,
直到找到object. 在js中, 只有object的__proto__是null, 所有的原型对象的默认原型对象是object
object如果没有此时程序报错。
(上述逻辑叫: 原型链)
f.hahahahahahah() // 报错.
原型链是js 方法查找的路径指示标。
Function:
function fn(){
}
console.log(fn.__proto__ === Function.prototype);
// 前端所有的函数的原型是Function
// 默认情况下. 函数.__proto__ 应该是Function.prototype
Function.prototype.chi = function(){
console.log("大Function也会吃");
}
fn.chi(); // 会不会报错?不会报错,会执行这个函数
fn.chi.chi(); // 仍然不会报错,会执行这个函数
fn.chi.chi.chi(); // 仍然不会报错,会执行这个函数
fn.chi.chi.chi.chi.chi(); // 仍然不会报错,会执行这个函数
fn.chi.chi.chi.chi.chi.chi.chi.chi.chi.chi.chi.chi.chi.chi.chi.chi.chi(); // 仍然不会报错,会执行这个函数。
// Function决定了函数对象能执行哪些东西,如果改变了Function.prototype 中的内容, 那么整个js中所有函数的执行逻辑都将改变
// 下面这个函数, Function的一个对象。
function fn(){}
console.log(fn.constructor === Function); // 会不会报错?不报错。
console.log(fn.constructor === Function.prototype.constructor); // 会不会报错?不报错。
console.log(fn.__proto__.constructor === Function.prototype.constructor); // 会不会报错?不报错。
// 对象.xxxx 永远都是先找对象自身. 自身没有就去找__proto__
var func = new Function("console.log('上帝');");
func(); // 函数() 运行该函数
函数总结:
1. 所有函数都是Function的对象。
2. 所有函数.constructor. 通过__proto__可以找到Function.prototype.constructor。
3. 可以使用Function 来构建一个函数。
通过原型链, 我们能做什么?
可以实现继承关系。
function Fu(){}
function Zi(){}
// console.log(Zi.prototype.__proto__);
// Zi.prototype.__proto__ = Fu.prototype; // 改变了原型链的指向
// f.__proto__.__proto__ == 爹.prototype
Object.setPrototypeOf(Zi.prototype, Fu.prototype); // 和上面一样
// 在查找方法的时候. 首先会查找Zi的对象, 对象里面没有会找Zi.prototype/对象.__proto__
// 如果没有怎么办?
// 找 对象.__proto__.__proto__, 此时就是Fu.prototype
// console.log(Zi.prototype.__proto__);
Fu.prototype.fu_chi = function(){
console.log("爹的吃");
}
var zi = new Zi();
zi.fu_chi();
我们用原型链能做什么?
我们来看一段神奇的代码.
(function(){debugger})();
这样一段代码可以看到. 浏览器进入了debugger断点.
那么这段代码的背后是什么呢? 注意. 在js代码执行时. 每一个function的对象都是通过Function()来创建的. 也就是说. 函数是Function()的对象.
function fn(){}
console.log(fn.__proto__.constructor); // ƒ Function() { [native code] }
函数就是Function的对象. 那么. 我们可以通过Function来构建一个函数.
new Function('debugger')();
跑起来的效果一样的.
OK. 这东西对我们来说有什么用. 上代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="haha.js"></script>
<script>
txsdefwsw();
</script>
</head>
<body>
有内鬼. 终止交易
</body>
</html>
haha.js 中的内容如下:
function txsdefwsw() {
var r = "V", n = "5", e = "8";
function o(r) {
if (!r) return "";
for (var t = "", n = 44106, e = 0; e < r.length; e++) {
var o = r.charCodeAt(e) ^ n;
n = n * e % 256 + 2333, t += String.fromCharCode(o)
}
return t
}
try {
var a = ["r", o("갯"), "g", o("갭"), function (t) {
if (!t) return "";
for (var o = "", a = r + n + e + "7", c = 45860, f = 0; f < t.length; f++) {
var i = t.charCodeAt(f);
c = (c + 1) % a.length, i ^= a.charCodeAt(c), o += String.fromCharCode(i)
}
return o
}("@"), "b", "e", "d"].reverse().join("");
!function c(r) {
(1 !== ("" + r / r).length || 0 === r) && function () {
}.constructor(a)(), c(++r)
}(0)
} catch (a) {
setTimeout(txsdefwsw, 100);
}
}
代码可简化为:
function txsdefwsw() {
var r = "V", n = "5", e = "8";
function o(r) {
if (!r) return "";
for (var t = "", n = 44106, e = 0; e < r.length; e++) {
var o = r.charCodeAt(e) ^ n;
n = n * e % 256 + 2333, t += String.fromCharCode(o)
}
return t
}
try {
var a = 'debugger';
!function c(r) { // r = 0
if (1 !== ("" + r / r).length || 0 === r){
// 在r = 0的时候. 这里是真. 会执行下面的代码
// Function.prototype.constructor(a) => 构建了一个函数
// function(){debugger}() debugger
// 修改Function.prototype.constructor
(function (){}.__proto__.constructor('debugger')()); // 运行debugger, Function.prototype.constructor(a)()
Function(a)();// => 修改Function
}
// console.log(r);
c(++r) // 递归死循环. 内存溢出...会报错.
}(0)
} catch (a) { // 报错就执行这里的catch
setTimeout(txsdefwsw, 100); // 0.1秒又重新启动该函数
}
}
页面跑起来没什么问题. 但是会无限debugger;
解决方案:
1. 找到断点出. 右键-> never pause here
2. 找到入口 -> 置空 setTimeout,或者干掉入口函数
3. 写js hook代码:
var x = Function; // 保留原来的Function
Function = function(arg){
arg = arg.replace("debugger", "");
return new x(arg);
}
var qiaofu_function_constructor = (function(){}).__proto__.constructor;
(function(){}).__proto__.constructor = function(arg){
console.log("我爱你大大");
if(arg ==='debugger'){
return function(){}
} else {
return new qiaofu_function_constructor(arg);
}
}
[[prototype]]
__proto__
构造器.prototype
function jiami(){
}
jiami.prototype.md5 = function(){}
jiami.prototype.aes = function(){}
jiami.prototype.encrypt = function(){}
var s = new jiami();
s.md5();
JS hook
hook又称钩子。可以在调用系统函数之前, 先执行我们的函数(执行你的代码之前会先执行我的代码)。这玩意其实就是木马。 例如, hook eval。
eval_ = eval; // 先保存系统的eval函数
eval = function(s){
console.log(s);
debugger; // debugger 在这里了,就可以做很多事情。比如看变量,看函数
return eval_(s);
}
eval()
eval.toString = function(){return 'function eval() { [native code] }'} // 可能会被检测到, 用这种方案来进行
对Function的hook, 主要为了解决无限debugger
var qiaofu_function_constructor = (function(){}).__proto__.constructor;
(function(){}).__proto__.constructor = function(arg){
console.log("我爱你大大");
if(arg ==='debugger'){
return function(){}
} else {
return new qiaofu_function_constructor(arg);
}
}
上面都是hook的系统函数. 但有时, 我们需要hook某个属性. 此时应该怎么办?
var v;
Object.defineProperty(document, "cookie", {
set: function(val) {
console.log("有人来存cookie了");
v = val;
if(val.indexOf("uuid")){debugger;}
return val;
},
get() {
console.log("有人提取cookie了");
debugger;
return v;
}
});
剩下的咱就不再赘述了. 在逆向时, 常用的主要有: hook eval 、hook Function 、hook JSON.stringify、JSON.parse 、setInterval, setTimeout, hook cookie。
(JSON.stringify、JSON.parse 作用:加密字符串之前要进行的操作,因为这些操作和加密操作离得很近。我们可以找到JSON.stringify、JSON.parse等函数,进而缩短、方便的找到加密操作的位置)
我希望在执行之前(而eval在哪里执行并不知道,所以需要找), 对eval进行hook. 主要指的是系统函数(js原生)
它会执行eval -> 执行的位置不确定 -> 通过hook的手段找到eval调用的位置. 或者参数. 或者其他信息.
植入hook代码的位置,比如下面是需要找到eval调用示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="樵夫.js"></script>
</head>
<body>
</body>
</html>
樵夫.js
console.log("我是一个超级厉害的代码")
console.log("我是一个超级厉害的代码")
console.log("我是一个超级厉害的代码")
console.log("我是一个超级厉害的代码")
console.log("我是一个超级厉害的代码")
console.log("我是一个超级厉害的代码")
console.log("我是一个超级厉害的代码")
let code = "console.log('哈哈哈哈哈')";
let x = "e"+"v"+ "a"+"l";
window[x](code);
console.log("垃圾代码离我远去");
方法一. 在浏览器中设置断点.....
通常可以在js文件的开头进行设置 (理论上只要还没有执行该函数都可以进行hook),注意打完断定再刷新浏览器,打完断点停了之后才能植入需要到hook。
最后执行原函数一般要用apply 去执行。如果你能确定参数是什么,就可以正常调。如果参数不能确定,对原函数的调用只能用apply。
会被写的下面函数所捕获:
var eval_ = eval; // 把原来的函数保存起来
eval = function(arg1){
console.log("断点");
debugger; // 设置断点
console.log("放开了");
return eval_.apply(this, arguments); // 半固定的逻辑.
}
点下面的按钮

然后跑到debugger了。通过这个断点,就可以看到调用栈了。我们可以在调用栈看到哪里调用的eval。想知道eval在哪,就点下面的,就可以知道了。

然后就回到了我们需要看的eval位置,如下所示:

这样就找到代码执行的位置,我们的目的就达到了。然后就可以做很多事情了。
在断点状态下. 可以看到当前作用域中所有的内容(console, sources都能看到);
在断点状态下, 可以看到在哪里执行的该函数(重要):在source的右侧(下方), call stack里放着调用过程(如下图所示)
如果a函数中调用了b函数, b函数中调用c函数, 断点在c函数中. call stack就是:
c
b
a

方法二. 在浏览器中设置全局断点,这种方法是在所有js执行之前....(小心浏览器插件的干扰,如果有干扰先关闭这些插件)
打开下面的 Event Listener Breakpoints

然后只把 Script 打上勾(打上勾的意思是只要加载Script就会停,所有JS都没运行,也就是所有js执行之前)。

停在这里后,把hook代码扔进Console:
var eval_ = eval; // 把原来的函数保存起来
eval = function(arg1){
console.log("断点");
debugger; // 设置断点
console.log("放开了");
return eval_.apply(this, arguments); // 半固定的逻辑.
}
跑完后,到Sources里面去,把下面的Script去掉,如果不去掉,每加载一个就运行一次,如果加载JS比较多的话,就比较头疼了。

然后点开下面的按钮:

就可以进入下面所需的位置了。

如果对于 JSON.stringify 这类操作,逻辑上和上面两个方法是一样的,比如对于下面这种来说(下面代码简化了过程)
var obj = {
}
console.log(JSON.stringify(obj));
把hook代码扔进Console:
var json_stringify_hook = JSON.stringify;
JSON.stringify = function(){
debugger;
return json_stringify_hook.apply(this, arguments);
}
hook 变量(cookie):
document.cookie = "alex=xxxxyyyyyzzzzz";
// hook 变量(cookie), 在几十万行代码里找到cookie的设置位置很困难。 用hook的方式找到设置cookie的位置
var v; // 声明一个变量. 临时存储的地方...
// 给某个对象中的属性进行设置...
Object.defineProperty(document, "cookie", {
set: function(val) { // 给这个属性赋值的时候. 执行set函数 document.cookie = xxxx
console.log("有人来存cookie了");
v = val;
debugger;
return val;
},
get() { // 从属性中获取数据的时候. 自动执行get函数. xxx = document.cookie;
console.log("有人提取cookie了");
debugger;
return v;
}
});
JSON
var data = {
username: "qiaofu",
password: "123456",
verify_code: "acef"
}
// 所有的数据加密都要围绕着字符串展开.
// 在js中. 可以使用JSON.stringify() 把一个对象, 转化成(json)字符串
// 下面的两个输出放到浏览器中就可以看到区别了。
console.log(data); // 对象
console.log(JSON.stringify(data)); // json字符串
var s = '{"code":9,"msg":"请填写正确图形码","data":null,"operation_date":"2023-04-12 22:39:16"}'
// 把(json)字符串转化成js的对象
var d = JSON.parse(s);
console.log(d['msg']);
// 准备数据(普通对象) => JSON.stringify(普通对象) => 加密(请求参数, 请求头) => 发请求
// 响应 => 解密 => JSON.parse(解密好的字符串) => 获取数据
// JSON.stringify_ = JSON.stringify;
// JSON.stringify = function(){
// debugger; // 断下来. 为了去找它前面或者后面做了什么
// return JSON.stringify_.apply(this, arguments);
// }
JSON.parse_ = JSON.parse;
JSON.parse = function(){
debugger; // 断下来. 为了去找它前面或者后面做了什么
return JSON.parse_.apply(this, arguments);
}
localStorage和sessionStorage
本地存储. 存储在浏览器端的数据. 可以理解成一个小型的非关系型数据库.

存储, 获取. 删除.
这俩玩意使用上是一样的. 区别在于. 一个是永久存储一个是临时存储.
localStorage 永久存储
sessionStorage 临时存储, 浏览器关闭后. 数据就没了.
document.cookie也是本地存储.
cookie的本质是一个超大号字符串. 有失效时间的.
都继承自Storage常见的操作:
setItem(key, value); // 设置key = value, 如果是相同的key, 会把数据覆盖
removeItem(key); // 根据key删除数据
getItem(key); // 根据key来获取数据
// 存储数据的时候. 如果把浏览器关掉. 重新打开. 数据依然存在
window.localStorage.setItem('qiao', "樵夫很厉害!");
// 存储数据的时候. 如果把浏览器关掉. 重新打开. 数据就不在了
sessionStorage.setItem("qiao", "樵夫很伟大");
localStorage.setItem("qiao","哈哈");
localStorage.removeItem("qiao");
// 删除数据
localStorage.removeItem("qiao");
// 存储数据
localStorage.setItem('qiao', "123");
//获取数据
localStorage.getItem("qiao");
ES6中的promise(难)
// 死亡回调.
// 需求: 完成页面登陆功能
// 1. 获取用户填写的...用户名, 密码, 验证码, 发送请求到服务器.
// 2. 服务器返回登陆状态. 1. 登陆成功, 2. 登陆失败
// 3. 登陆成功了. 页面跳转->拿到首页的内容进行渲染.
// 4. 加载菜单
// 5. 加载用户信息
// 6. 加载统计信息
// 登陆成功了 -> 页面跳转 -> 加载菜单 -> 加载用户信息 -> 统计信息 -> 呈现给用户
// 获取用户名. 密码. 验证码
username = "admin";
password = "123456";
verify_code = "abcd";
// 发请求到服务器. 来验证登陆是否成功
// 网络请求. 1. 有延迟. 2. 可能这辈子都回不来了...
// 前端js是单线程,如果不返回结果,后面的代码将无法运行。
setTimeout(function(){ // 回调函数
console.log("我叫樵夫");
}, 1000); // 这玩意的优点是. 不会让程序卡死在这.
// 在js中使用的是异步请求.(协程), 等到服务器返回了数据之后. 自动触发某个函数的运行
// 利用setTimeout来模拟上述逻辑
var username = "admin";
var password = "123456";
var verify_code = "abcd";
console.log("1.我要开始发送请求了.....")
setTimeout(function(){
// 这里接收到服务器返回的内容
console.log("2.接收到服务器返回的内容....");
var resp = "成功"; // 服务器返回的数据. 成功或者失败
if (resp === "成功"){
// 模拟....
console.log("3. 恭喜你, 登陆成功了. 接下来要发送新的请求.跳转到新的页面");
setTimeout(function(){
console.log("4. 进入主页面.....");
console.log("5. 开始加载菜单")
setTimeout(function(){
console.log("6. 菜单加载完毕...");
setTimeout(function(){
console.log("7.加载用户信息成功");
setTimeout(function(){ // 等待服务器返回
console.log("8. 加载统计信息成功!");
}, 500);
}, 500);
}, 1000)
}, 1000)
} else {
console.log("登陆失败了. 请检查你的用户名和密码");
}
}, 1000);
// 上面就是死亡回调
// 前面的一件事儿. 完成了之后. 才能开始后面的一件事儿...
// 这里的每一件事儿, 什么时候结束. 没人知道,时间可能很长很长也不一定
// es6提供了promise解决了死亡回调的问题
// 负责发请求
function send(url){
// Promise 保证.会给你一个结果
// 结果:
// resolve: 成了
// reject: 没成.
// resolve和reject对应的是两个函数....
return new Promise(function(resolve, reject){
// 发请求
console.log("发请求", url);
setTimeout(function(){ // 用 setTimeout 来模拟
console.log("请求回来了");
// 我保证你的这件事儿. 成了. 成了之后. 你要干啥。
resp = "正确的响应结果";
// 如何把信息传递出去
if (true){ // 成功还是不成功
resolve(resp); // 成了
} else {
reject("没成."); // 没成
}
}, 1000);
});
}
// promise对象在执行resolve和reject之前本身是出于`运行中`状态的.
// 得到一个promise对象之后. 会对应两个逻辑, 一个是这件事儿成了, 一个是这件事儿没成.
// then表示, 在promise对象执行结束之后. 得到结果了. 之后. 要干啥 .
// then中的两个函数对应的就是resolve和reject
// catch表示. 前面的操作中, 如果出现了错误. 处理异常的逻辑就是catch
// 语法:
send("login").then(function(data){
console.log("成了,,,,,接收到返回数据.", data);
}).catch(function(){
console.log("这里是报错信息处理. 你能看到这条打印, 说明前面的逻辑中出现了错误. ");
});
// then中的第二个函数可以没有...可以换成catch(), 也是一样的.. catch可以代替then中的第二个函数.
// 下面能否解决死亡回调的问题?
send("login").then(function(data){
console.log("成了,,,,,接收到返回数据.", data);
// 在promise的then中. 可以返回一个新的promise
return send("页面跳转")
}).then(function(){
return send("加载菜单")
}).then(function(){
return send("加载用户信息")
}).then(function(){
console.log("用户信息加载完毕!!!!!!")
}).catch(function(){ // 前面的promise任何一个出现了问题. 都会执行最后的这个catch()
console.log("这里是报错信息处理. 你能看到这条打印, 说明前面的逻辑中出现了错误. ");
console.log("服务器出错,请练习管理员.");
});
// 上面代码解决了死亡回调的问题。
// 注意点.
// 1. Promise(function(a, b){
// a(); // ok
// b(); // 不ok
// })
// 2. then()里面可以有1个函数,也可以有2个函数.
// 如果只有1个函数, 那么reject对应的就是catch
// 如果有2个函数, 那么第二个函数对应的是reject
// 3. 当你遇到了promise的时候. 怎么办?
// 在promise后面可以 .then(function(data){console.log("xxxx:", data);});
// 然后一定要释放断点. 等待运行完毕. 一定会执行你设置的then
JS和HTML交互
在HTML中可以直接在标签上给出一些事件的触发. 例如, 页面上的一个按钮
<input type="button" value="点我就爱你"/>
我们能够知道此时在页面中会产生一个按钮. 但是该按钮无论如何进行点击. 都不会触发任何事件. 但, 此时我要告诉你, 人家其实触发了. 只是你没处理而已. 在我们点击该按钮的时候. 浏览器其实收集到了点击事件. 但是由于我们没有给出任何发生了点击事件应该做什么的事情. 所以也就没有了反应. 我们可以通过onclick属性. 来给点击事件添加上具体要做什么
<input type='button' value="点我就爱你" onclick="fn()" />
看到了吧. 多了个onclick, 其含义是, 当发生点击事件时. 去执行fn(). fn() 是什么? fn就是我们javascript的一个函数.
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
function fn(){
alert("臭不要脸")
}
</script>
</head>
<body>
<input type="button" value="点我就爱你" onclick="fn()">
</body>
</html>

有效果了. 发现了么. 至此, 我们成功的实现了. 从HTML中调用JS的这条路打通了.
那么在HTML中有多少种事件可以触发呢? 非常多....多到令人发指. 我们就记住几个就好了 html中的事件
click 点击事件
focus 获取焦点
blur 失去焦点
submit 提交表单
change 更换选项
scroll 滚动条滚动
mouseover 鼠标滑过
mouseout 鼠标滑出
mousemove 鼠标滑动
上述是第一种绑定事件的方案. 可以直接在html标签中使用onxxx系列属性来完成事件的绑定. 同时js还提供了以下事件绑定方案:
<input type="button" id="btn" value="别点我了">
<script>
// 注意, 必须等到页面加载完毕了. 才可以这样
document.querySelector("#btn").addEventListener("click", function(){
console.log("你点我干什么?? ")
})
</script>
document.querySelector() 给出一个css选择器, 就可以得到一个html页面上标签元素的句柄(控制该标签).
获取句柄的方案有好多. 常见的有:
document.getElementById(); // 根据id的值获取句柄
document.getElementsByClassName(); // 根据class的值获取句柄
// <form name='myform'><input type="myusername"/></form>
document.form的name.表单元素的name; // document.myform.myusername;
那么, 我们现在相当于可以从html转到JS中了. 并且在js中可以捕获到html中的内容了. 此时, 对应的表单验证也可以完成了.
<form action="服务器地址" id="login_form">
<label for="username">用户名:</label><input type="text" name="username" id="username"><span id="username_info"></span><br/>
<label for="password">密码:</label><input type="text" name="password" id="password"><span id="password_info"></span><br/>
<input type="button" id="btn" value="点我登录">
</form>
<script>
// 在页面加载的时候
window.onload = function(){
document.getElementById('btn').addEventListener("click", function(){
// 清空提示信息
document.getElementById('username_info').innerText = "";
document.getElementById('password_info').innerText = "";
let username = document.getElementById('username').value; // 获取username标签中的value属性
let password = document.getElementById('password').value; // 获取密码
let flag = true; // 最终是否可以提交表单?
if(!username){
document.getElementById('username_info').innerText = "用户名不能为空";
flag = false;
}
if(!password){
document.getElementById('password_info').innerText = "密码不能为空";
flag = false;
}
if (flag){
document.getElementById('login_form').submit();
}
})
}
</script>
发现了么. 不经意间, 我们通过js可以改变html中的内容了.
浙公网安备 33010602011771号