本文将会把我平时在JS逆向中遇到的一些JS基础知识和遇到的问题写在这里。(比较杂)
JS逆向需要掌握JS基础知识
nodejs和v8引擎的关系
参考文章:
nodejs和v8引擎的关系
NodeJS核心基础
- 什么是Node.js?
Node.js是一个基于"Chrome V8引擎"的JavaScript"运行环境"。 - 什么是V8引擎?
V8引擎是一款专门解释和执行JavaScript代码的虚拟机。任何程序只要集成了V8引擎,就可以执行JavaScript代码。浏览器集成了V8引擎,可以执行JavaScript代码;将V8引擎嵌入到NodeJS中,那么我们写的JavaScript代码就会被NodeJS所执行。
总之NodeJS不是一门编程语言,NodeJS是一个运行环境,。由于这个运行环境集成了V8引擎,所以在这个运行环境下可以运行JavaScript代码。这个运行环境最大的特点就是提供了可以操作”操作系统底层的API“。通过这些底层API,我们可以编写出网页中无法实现的功能,比如:打包工具, 网站服务器等等。
NodeJS环境和浏览器环境执行JS代码区别
NodeJS环境和浏览器环境一样都是一个JS的运行环境,都可以执行JS代码。但是由于宿主不同,所以特点也有所不同。
- 内置对象不同
浏览器环境中提供了全局对象window,NodeJS环境中的全局对象不叫window,叫global。 - this默认指向不同
浏览器环境中全局this默认指向window
,NodeJS环境中全局this默认指向global
。
有一个坑需要注意,可以参考文章:记Node中this指向的问题。
如果是直接打印this的话,会输出{}:
console.log(this) //输出{}
function test_this(){
console.log(this)
}
test_this() //此时输出global对象
造成这种现象的原因,是因为Node在执行js文件中的代码的时候,并不是放在全局环境中执行的。而是把js文件外面又包了一层成“模块”,再把这个“模块”放到了全局变量中执行。所以,此时的this,指的是包含这个js文件的“模块”。所以,如果想获取全局变量中的this,那么就可以使用globalThis
属性。
- API不同
浏览器环境中提供了操作节点的DOM相关API和操作浏览器的BOM相关API。NodeJS环境中没有HTML节点也没有浏览器,所以NodeJS环境中没有DOM/BOM。
webpack
JS逆向中常见的window.webpackJsonp分析
文章链接:https://blog.csdn.net/Xzike/article/details/123874734
Axios
axios 是一个非常出名的框架,除了jquery(10%),剩下的要有 89%是它,在日常的逆向中,也经常可以看见这个框架。
// 框架源码,在使用下面的代码时,在控制台中先注入源码:https://unpkg.com/axios/dist/axios.min.js
const service = axios.create({
baseURL: 'https://www.python-spider.com/api/combat?page=1&count=10',
timeout: 5000,
responseType: "json",
withCredentials: true,
headers: {
"Content-Type": "application/json;charset=utf-8",
}
})
service.interceptors.request.use(
config => {
if(config.method === "post") {
} else {
config.headers['X-Token'] = 'anlan:1698751:wq2s313sdre3' //往headers里面加一个加密参数,这一步往往可以定位到加密函数,所以我们找这里就行
}
return config;
},
error => {
Message({
showClose: true,
message: error,
type: "warning"
});
return Promise.reject(error);
}
)
特征
如果这个库没有经过混淆的话,打个xhr断点往前面找找是可以找到以下一段代码的:
如何处理?
再往上翻堆栈的时候,往往找到这里就找不下去了:
这时候我们找到这里,是这个框架的拦截器:
这时候我们可以从这个地方找到发送请求的位置,也就是大部分加密函数所在的位置:
JS基础语法
声明时用"var"与不用"var"的区别
参考文章链接:
https://blog.csdn.net/xhf852963/article/details/79416440
https://blog.csdn.net/juejiang_walter/article/details/52084230
var是js的一个关键字,它是用来声明变量的。
声明一个变量有两种方式:
- var num=1。如果在方法中声明,则为局部变量;如果在全局中声明,则为全局变量。
- num=1。事实上这是对属性进行赋值操作。首先,它会尝试在当前作用域链(如果在方法中声明,则当前作用域代表全局作用域和方法局部作用域)中解析num,如果在任何当前作用域链中找到num,则会对num属性进行赋值,如果没有找到num,他会在全局对象(即当前作用域链的最顶层对象,如window对象)中创造num属性并赋值。
注意!它并不是声明了一个全局变量,而是创建了一个全局对象的属性
由于变量声明自带不可删除属性,比较var num=1跟num=1,前者是变量声明,带不可删除属性,因此无法被删除;后者为全局变量的一个属性,因此可以从全局变量中删除。
变量提升
JavaScript引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行的运行,这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升。
例子:
console.log(a);
var a=1;
以上的语句不会报错,只是提示undefined。实际的运行过程:
var a; //表示变量a已声明,但还未赋值
console.log(a);
a = 1;
但是变量提升只对var命令声明的变量有效,如果一个变量不是var变量声明的,就不会发生变量提升,例如以下实例:
console.log(aa);
aa=1;//以上代码将会报错aa is not defined
与普通变量一样,js中的function也可看做变量,也存在变量提升的情况:
a();
function a(){
console.log(1);
}
表面上,上面的代码好像在声明之前就调用了函数a。但是实际上,由于变量提升,函数a定义部分被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript就会报错:
a();
var a=function(){
console.log(1);
}//会报 a is not a function
因为实际运行过程:
var a;
a();
a=function(){
console.log(1);
}//这时候a是个变量,并非function
赋值操作
var abc = 11;
function test4(){
abc = 22;
}
test4();
console.log(abc); //输出结果:22
对于这个例子来说,首先是声明了一个变量abc,并赋值11,然后在运行test4(),此时再对abc重新赋值22,由于abc是在外部作用域(window)声明的,所以此时console.log(abc)的输出结果是22。
function test4(){
var aaa = 22;
}
test4();
console.log(aaa); //结果:ReferenceError: aaa is not defined
function test5(){
var aaa = 22;
}
test5();
console.log(test5.aaa);//输出结果:undefined
对于这个例子来说,函数或者对象构造内声明的变量是私有的,外部无法访问到,包括原型继承后的对象。
function test4(){
bbb = 33;
}
test4();
console.log(bbb); //输出结果:33
对于这个例子来说,不加var,在函数或者构造内就是赋值, 从函数内往上一层层寻找变量bbb,一直到顶层没有,就在顶层声明一个 var bbb。
可能出现的问题:
假如一个大的项目,在这里改变了bbb的值,并没有添加var,碰巧整个项目全局变量有个同名bbb被改变,不加var不是只作用在这个函数或对象内,出了错误很难找。
JS中的构造函数
参考文章:
JS中的构造函数
构造函数的概念
- 构造函数其实是一种特殊的函数,主要用来初始化对象,也就是为对象成员变量赋初始值,它总与new关键字一起使用。
- 我们可以把对象里面一些公有的属性和方法抽象出来封装到这个函数里面。这样我们就可以通过一个构造函数创建多个对象,这些对象拥有相同的构造,都可以使用这个构造函数的方法和属性。
构造函数的创建
- 构造函数一般首字母会大写,为了和普通函数区分。
- 构造函数的属性和方法前必须加this关键字,指代要生成的对象。
- new就是执行构造函数,返回一个对象,不写new就是普通函数的调用,没有创造对象的能力。
- 调用函数时,如果不传递参数,()可以不写,如果传递参数就必须写,建议都写上。
- 构造函数中的this,由于和new连用的关系,是指向当前实例对象的。
- 构造函数不需要return就可以返回结果。
function Dog(){
//构造函数中的属性
this.name = "贝贝";
//构造函数中的方法
this.bark = function(){
console.log("汪汪汪");
}
console.log(666)
}
d = Dog() //控制台输出 666
dd = new Dog() //控制台输出 666
console.log(d) //控制台输出 undefined
console.log(dd) //控制台输出 Dog {name: '贝贝', bark: ƒ}
JS中的prototype、__proto__与constructor
参考文章:
帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)
B站视频讲解:面向对象 – 实例、原型、继承
prototype和__proto__的关系是什么?
注:
__proto__属性的两边是各由两个下划线构成(这里为了方便大家看清,在两下划线之间加入了一个空格:_ proto ,读作“dunder proto”,“double underscore proto”的缩写),实际上,该属性在ES标准定义中的名字应该是[[Prototype]],具体实现是由浏览器代理自己实现,谷歌浏览器的实现就是将[[Prototype]]命名为__proto_,大家清楚这个标准定义与具体实现的区别即可(名字有所差异,功能是一样的),可以通过该方式检测引擎是否支持这个属性:Object.getPrototypeOf({proto: null}) === null。
- 我们需要牢记两点:①__proto__和constructor属性是对象所独有的;② prototype属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__和constructor属性。这句话换一个说法:所有对象都有__proto__属性,函数这个特殊对象除了具有__proto__属性,还有特有的原型属性prototype。prototype对象默认有两个属性,constructor属性和__proto__属性。
- __proto__属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,再往上找就相当于在null上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。
- prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即f1.__proto__ === Foo.prototype。
- constructor属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function。
- constructor储存不需要共享的属性和方法,而prototype对象储存需要共享的属性和方法。
例1:
function Foo() {
this.name = 'abc'
};
var f1 = new Foo();
console.log(f1.__proto__ === Foo.prototype) //控制台输出 true
console.log(Foo.__proto__ === Function.prototype) //控制台输出 true
console.log(f1.__proto__ === Foo.prototype) //控制台输出 true
console.log(Foo.prototype.__proto__ === Object.prototype) //控制台输出 true
console.log(Object.__proto__ === Function.prototype) //控制台输出 true
console.log(f1.__proto__.__proto__ === Object.prototype) //控制台输出 true
console.log(f1.__proto__.__proto__ === Foo.prototype.__proto__) //控制台输出 true
例2:
function Foo() {
this.name = 'abc'
};
Foo.prototype = {
aa:10,
bb:function(){
console.log(123)
}
}
var f1 = new Foo();
console.log(f1.__proto__.aa) //控制台输出10
console.log(f1.aa) //控制台输出10
f1.bb() //控制台输出123
f1.__proto__.bb() //控制台输出123
此处我们在Foo.prototype上写入了aa和bb,f1.aa时,实际上在f1内是没有aa和bb的,会去f1.__proto__上查找。
箭头函数
参考:
JavaScript箭头函数与普通函数的区别
JS基础知识(二十八):箭头函数
js 箭头函数详解
箭头函数的简介与使用
- ES6 新增了使用胖箭头(
=>
)语法定义函数表达式的能力,很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数。
箭头函数类型 | 代码 |
---|---|
没有参数 | () => 100 即function(){ return 100} |
一个参数 | x => x+1 即function(x){ return x + 1} |
多个参数 | (x, y) => x + y 即function(x, y) { return x + y} |
一个表达式 | x => x+1 即function(x){return x+1} |
多个表达式 | x => { if (x>0){ return x*x }else{ return x } } |
普通函数 | 箭头函数 | |
---|---|---|
语法格式 | function(){} | ()=>{} |
new和原型 | 有 | 没有 |
arguments | 有 | 没有,可调用外围 |
this指向 | 动态 | 一般是全局对象,被普通函数包含指向上一层 |
call、apply和bind | 修改this值 | 不可修改this值 |
箭头函数的使用注意点
- 箭头函数创建的时候不会为其创建
Construct
方法,
Test.prototype.name = '666';
function Test(){};
const test = new Test();
console.log(test.name); //输出 666
const Arrow = () => {};
const arrow = new Arrow(); //报错:TypeError: Arrow is not a constructor
- 箭头函数没有
arguments
对象
function test (){
return arguments.length
}
console.log(test(1,2,3,4)) //输出 4
let arrow = () => arguments.length;
console.log(arrow(1,2,3,4)) //报错 ReferenceError: arguments is not defined
如果非要实现类似的功能,可以这样写:
function test(){
return () => arguments.length;
}
let arrow = test(1,2,3)
console.log(arrow()) //输出 3
- 箭头函数中this指向
let num = 11;
const obj1 = {
num: 22,
fn1: function() {
let num = 33;
const obj2 = {
num: 44,
fn2: () => {
console.log(this.num);
}
}
obj2.fn2();
}
}
obj1.fn1(); // 输出 22
箭头函数没有this,箭头函数的this是继承父执行上下文里面的this ,这里箭头函数的执行上下文是函数fn1(),所以它就继承了fn1()的this,obj1调用的fn1,所以fn1的this指向obj1, 所以obj1.num为22。
call、apply和bind
参考:
call ,apply和bind方法 详解
「干货」细说 call、apply 以及 bind 的区别和用法
手写call、apply、bind实现及详解
- 简单的用法示例:
var year = 2021
function getDate(month, day) {
return this.year + '-' + month + '-' + day
}
let obj = {year: 2022}
getDate.call(null, 3, 8) //2021-3-8
getDate.call(obj, 3, 8) //2022-3-8
getDate.apply(obj, [6, 8]) //2022-6-8
getDate.bind(obj)(3, 8) //2022-3-8
- 它们有什么不同?怎么用?
- call 接收多个参数,第一个为函数上下文也就是this,后边参数为函数本身的参数。
let obj = {
name: "一个"
}
function allName(firstName, lastName) {
console.log(this)
console.log(`我的全名是“${firstName}${this.name}${lastName}”`)
}
// 很明显此时allName函数是没有name属性的
allName('我是', '前端') //我的全名是“我是前端” this指向window
allName.call(obj, '我是', '前端') //我的全名是“我是一个前端” this指向obj
- apply接收两个参数,第一个参数为函数上下文this,第二个参数为函数参数只不过是通过一个数组的形式传入的。
allName.apply(obj, ['我是', '前端'])//我的全名是“我是一个前端” this指向obj
- bind 接收多个参数,第一个是bind返回值返回值是一个函数上下文的this,不会立即执行。
let obj = {
name: "一个"
}
function allName(firstName, lastName, flag) {
console.log(this)
console.log(`我的全名是"${firstName}${this.name}${lastName}"我的座右铭是"${flag}"`)
}
allName.bind(obj) //不会执行
let fn = allName.bind(obj)
fn('我是', '前端', '好好学习天天向上')
// 也可以这样用,参数可以分开传。bind后的函数参数默认排列在原函数参数后边
fn = allName.bind(obj, "你是")
fn('前端', '好好学习天天向上')
Promise
参考:
尚硅谷Web前端Promise教程从入门到精通
这一次,彻底搞懂Promise
Promise初步详解(resolve,reject,catch)
ES6 Promise用法小结
Promise的介绍
- Promise是什么?
- 抽象表达:Promise是ES6出现的一门新的技术,是JS中进行异步编程的新解决方案(旧方案是单纯使用回调函数)
- 具体表达:从语法上来说Promise是一个构造函数;从功能上来说Promise对象是用来封装一个异步操作并可以获取其成功或失败的结果值。
- 为什么要用Promise?
- 指定回调函数的方式更加灵活,启动异步任务>返回Promise对象>给Promise对象绑定回调函数(甚至可以在异步任务结束后指定/多个)
- 支持链式调用,可以解决回调地狱问题
Promise初体验
不用promise实现一个异步操作:
<html>
<body>
<div class = "container">
<h2 class = "page-header">
Promise初体验
</h2>
<button class = "btn btn-primary" id = "btn">
点击抽奖
</button>
</div>
<script>
function rand(m,n){ //获取m-n间的一个随机数
return Math.ceil(Math.random() * (n-m+1)) + m - 1;
}
const btn = document.querySelector('#btn'); //获取元素对象
btn.addEventListener('click',function(){ //绑定单击事件
//定时器
setTimeout(() => {
let n = rand(1,100);
if (n<=30){
alert('恭喜中奖');
}else{
alert('未中奖!');
}
},1000)
});
</script>
</body>
</html>
使用promise
实现一个异步操作:
<html>
<body>
<div class = "container">
<h2 class = "page-header">
Promise初体验
</h2>
<button class = "btn btn-primary" id = "btn">
点击抽奖
</button>
</div>
<script>
function rand(m,n){ //获取m-n间的一个随机数
return Math.ceil(Math.random() * (n-m+1)) + m - 1;
}
const btn = document.querySelector('#btn'); //获取元素对象
btn.addEventListener('click',function(){ //绑定单击事件
const p = new Promise((resolve,reject) => { //resolve解决——异步任务成功时调用,函数类型的参数 reject拒绝——异步任务失败时调用,函数类型的参数
setTimeout(() => {
let n = rand(1,100);
if (n<=30){
resolve(n); //将promise的状态设置为成功
}else{
reject(n); //将promise的状态设置为失败
}
},1000)
});
p.then((n)=>{ //then接受两个参数,第一个是成功时的回调,第二个是失败时的回调
alert('恭喜中奖!号码为:' + n + '!')
},(n)=>{
alert('未中奖!号码为:' + n + '!')
});
});
</script>
</body>
</html>
promise实践
- 使用promise读取文件
const fs = require('fs');
//读取文件
fs.readFile('./test.txt',(err,data) => {
if (err) throw err;
console.log(data.toString())
});
//promise读取文件
let p = new Promise((resolve,reject) => {
fs.readFile('./test.txt',(err,data) => {
if (err) reject(err);
resolve(data);
})
})
p.then(value => {
console.log(value.toString());
},reason => {
console.log(reason);
})
- promise发送ajax请求
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Promise 封装 Ajax</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
</head>
<body>
<div class="container">
<h2 class="page-header">
Promise封装Ajax
</h2>
<button class="btn btn-primary" id="btn">点击发送ajax</button>
</div>
<script>
const btn = document.querySelector('#btn');
//不用promise的写法
btn.addEventListener('click',function(){
const xhr = new XMLHttpRequest(); //创建对象
xhr.open('GET','http://127.0.0.1:5125/'); //初始化
xhr.send(); //发送
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
//判断响应状态码
if (xhr.status >= 200 && xhr.status <= 300){
console.log(xhr.response) //控制台输出响应体
}else{
console.log(xhr.status) //控制台输出响应状态码
}
}
}
})
//使用promise的写法
btn.addEventListener('click',function(){
const p = new Promise((resolve,reject) => {
const xhr = new XMLHttpRequest(); //创建对象
xhr.open('GET','http://127.0.0.1:5125/'); //初始化
xhr.send(); //发送
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
//判断响应状态码
if (xhr.status >= 200 && xhr.status <= 300){
resolve(xhr.response);
}else{
reject(xhr.status);
}
}
}
});
p.then(value => {
console.log(value);
},reason => {
console.warn(reason);
})
})
</script>
</body>
</html>
const、var和let
-
const:块级作用域,声明的变量保持常量值,
不能被修改
并且不能被重新声明
。因此,每个const声明都必须在声明时进行初始化。
-
let:块级作用域,声明的变量保持常量值就像var一样,用let声明的变量
可以在其范围内被修改
,无法在其作用域内被重新声明
。用let声明的变量
同样会被提升到其作用域的顶部
,但不会对值进行初始化
,因此,如果你尝试在声明前使用let变量,则会收到Reference Error
的报错。
-
var:var声明的变量会被提升到其作用域的顶部,并使用 undefined 值对其进行初始化。
Proxy
参考:
JS Proxy(代理)
官方文档
什么是代理?
Proxy
也就是代理,可以帮助我们完成很多事情,例如对数据的处理,对构造函数的处理,对数据的验证,说白了就是在我们访问对象前添加了一层拦截,可以过滤很多操作,而这些过滤由你来定义。
let p = new Proxy(target, handler);
参数
- target :需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
- handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器)。具体的handler相关函数请查阅官网
一个简单的例子
let test = {
name: "小红"
};
test = new Proxy(test, {
get(target, key) {
console.log('获取了getter属性');
return target[key];
}
});
console.log(test.name);
Web Api相关
addEventListener()与removeEventListener()
参考文章:
js添加事件和移除事件:addEventListener()与removeEventListener()
addEventListener() 方法
- addEventListener()与removeEventListener()用于处理指定和删除事件处理程序操作。
- 它们都接受3个参数,如:addEventListener(“事件名” , “事件处理函数” , “布尔值”); (注:事件名不含"on",如“click”)。
- 现在的版本可以省略第三个参数,默认值为false。
例1:
要在body上添加事件处理程序,可以使用下列代码:
document.body.addEventListener('touchmove', function (event) {
event.preventDefault();
},false);
通过addEventListener()
添加的事件处理程序只能使用removeEventListener()
来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过addEventListener()添加的匿名函数无法移除。
错误用法示例:
document.body.addEventListener('touchmove', function (event) {
event.preventDefault();
},false);
document.body.removeEventListener('touchmove', function (event) {
event.preventDefault();
},false);
这个例子中,使用addEventListener()添加一个事件处理程序。虽然调用removeEventListener()是看似使用了相同的参数,但实际上,第二个参数与传入addEventListener()中的那一个完全不同的函数。而传入removeEventListener()中的事件处理程序函数必须与传addEventListener()中的相同。
正确用法示例:
function bodyScroll(event){
event.preventDefault();
}
document.body.addEventListener('touchmove',bodyScroll,false);
document.body.removeEventListener('touchmove',bodyScroll,false);
例2:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<button id="demo">点击</button>
<script>
document.getElementById("demo").addEventListener("click",function (){
console.log(55555)
});
</script>
</body>
</html>
Load
参考文档:
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/load_event
当整个页面及所有依赖资源如样式表和图片都已完成加载时,将触发load事件。
它与DOMContentLoaded不同,后者只要页面DOM加载完成就触发,无需等待依赖资源的加载。