1. 介绍

由于浏览器环境与node环境的差别,会导致浏览器的JavaScript代码没办法在node环境中正常执行,js代码会根据浏览器所提供的属性来判断你执行的环境是不是通过浏览器来执行的。如果不是,就不会返回正确的响应信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// node 中内置的关键字
require
global
globalThis // 浏览器环境也有globalThis, 与global没有任何区别(globalThis支持ES2020语法)
process
__filename

__dirname


// 浏览器支持的关键字(对象)
/*

* window
* document
* navigator
* location
* ....
* */

2. 浏览器中常见对象

1. window:窗口对象

window在浏览器中属于顶级变量,任何在全局作用域中声明的变量或函数都会成为window对象的属性。例如:如果你声明一个全局变量var name = 'admin';,你可以通过window.name获取。window仅在浏览器环境中存在,在node或者其他环境中,顶级对象为global。

1
2
3
4
5
6
7
8
9
10
11
addEventListener: function
setInterval: function
setTimeout: function
name
self
document
navigator
location
screen
history
toString: function

2. document:文档对象

1
2
3
4
5
6
7
8
body
documentElement
cookie
createElement: function
getElementById
getElementsByTagName
getElementsByClassName
toString

3. navigator:导航对象

1
2
userAgent
toString: function

4. location:地址栏对象

1
2
href
toString: function

5. screen:屏幕对象

1
2
3
4
5
availHeight
availLeft
availTop
availWidth
toString: function

6. history:历史对象

1
2
3
4
back: function
forward: function
go: function
toString: function

7. localStorage和sessionStorage:本地存储对象

1
2
3
4
getItem: function
setItem: function
removeItem: function
toString: function

8. element:标签对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 常用标签 */
h1 - h6标签
p标签
input标签
div与span标签
img标签
table标签
form标签
canvas标签(toDataURL: function)

/* 标签对象常用的属性与方法 */
ele.style
ele.innerHTML
ele.getAttribute()

3. 检测环境的代码示例

1. 检测代码执行环境是否存在navigator

1
2
3
4
5
6
7
8
9
10
11
12
navigator = {}
navigator.userAgent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'

function ps() {
if (navigator['userAgent']) {
return 'hello world'
} else {
return '失败'
}
}

console.log(ps());

2. 检测属性长度,根据长度判断数据是否正确

1
2
3
4
5
6
7
8
9
10
11
12
location = {}
location.href = 'https://www.baidu.com'

function ps() {
if (location['href'].length > 3) {
return 'hello world'
} else {
return '失败'
}
}

console.log(ps());

3. 异常日志输出

在前端开发中为了代码的健壮性通常会对容易出错的代码进行异常捕获,但是生产环境中的代码一般不会直接输出错误日志到浏览器,需要我们爬虫开发者自行输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// location = {}
// location.host = '8888'
// navigator = {}
// navigator.userAgent = 'abc'

function pn() {
try {
console.log(verify_local());
if (navigator['userAgent']) {
return 'hello world'
}
} catch (e) {
// console.log(e) // 输出错误日志
return '错误的数据'
}
}

function verify_local() {
if (location.host.length > 2) {
return '浏览器环境...'
}
return '非浏览器环境...'
}

console.log(pn());

4. 判断浏览器执行环境与node执行环境来验证是否为爬虫程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 浏览器和node差异
/*

"undefined" != typeof exports:检查 exports 变量的类型是否不是 "undefined"
在 Node.js 环境中,exports 是一个预定义的对象,用于模块导出
在浏览器环境中,exports 通常是未定义的(undefined)
如果 exports 存在(即在 Node.js 环境中),则将 exports 赋给 browser_or_node(object)
如果 exports 不存在(即在浏览器环境中),则将 void 0(等同于 undefined)赋给 browser_or_node

*/
// let browser_or_node = "undefined" != typeof exports ? exports : void 0
// console.log(typeof browser_or_node);

let glb = "undefined" == typeof window ? global : window
console.log(glb)

5. 基于toString检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// window = global;
// window.addEventListener = function () {
// }
//
// navigator = {
// toString() {
// return '[object Navigator]'
// },
// userAgent: "'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'"
// }
//
// kw = {
// getAttribute: function () {
// }
// }
//
// document = {
// getElementById(ele) {
// console.log("document getElementById", ele)
// if (ele === "kw") {
// return kw
// }
// },
// createElement(ele) {
// console.log("document createElement ", ele)
// if (ele === "canvas") {
// return canvas
// }
// }
// }
//
// ctx = {
// fillRect() {
// }
// }
//
// canvas = {
// getContext() {
// return ctx
// }
// }


function get_sign() {
window.addEventListener("test")
let kw = document.getElementById("kw")
let _class = kw.getAttribute("class")
let canvas = document.createElement("canvas")
let ctx = canvas.getContext("2d");
ctx.fillRect(10, 10, 100, 100);

try {
if (navigator.toString() === '[object Navigator]') {
return btoa("admin") // 输出base64编码
} else {
return '假数据 - 1'
}
} catch (e) {
return '假数据 - 2'
}
}

console.log(get_sign())

4. Proxy代理脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function getEnv(proxy_array) {
for (let i = 0; i < proxy_array.length; i++) {
handler = `{
get: function(target, property, receiver) {
console.log('方法:get',' 对象:${proxy_array[i]}',' 属性:',property,' 属性类型:',typeof property,' 属性值类型:',typeof target[property]);
return target[property];
},
set: function(target, property, value, receiver){
console.log('方法:set',' 对象:${proxy_array[i]}',' 属性:',property,' 属性类型:',typeof property,' 属性值类型:',typeof target[property]);
return Reflect.set(...arguments);
}
}`;
eval(`
try {
${proxy_array[i]};
${proxy_array[i]} = new Proxy(${proxy_array[i]}, ${handler});
} catch (e) {
${proxy_array[i]} = {};
${proxy_array[i]} = new Proxy(${proxy_array[i]}, ${handler});
}
`);
}
}

proxy_array = ['window', 'document', 'location', 'navigator', 'history', 'screen']
getEnv(proxy_array);

5. 原型和原型链

1. 基础

JavaScript 中,对象需要一个机制来共享属性和方法。如果每个对象都存储自己的方法副本,内存会严重浪费。原型系统正是为了解决这个问题而设计的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 每个实例都独立存储方法(浪费内存)
function createUser(name) {
return {
name: name,
sayHi: function() { // 每次调用都创建新函数
console.log('Hi, ' + this.name);
}
};
}

// 使用原型共享方法(所有实例共享同一个函数)
function User(name) {
this.name = name;
}
User.prototype.sayHi = function() {
console.log('Hi, ' + this.name);
};

2. 属性

prototype属性(函数独有)

普通函数(function 声明/表达式)都有一个prototype属性,指向通过new创建的实例的原型,箭头函数没有

1
2
function Person() {}
console.log(Person.prototype); // { constructor: Person }

__proto__属性(对象独有)

每一个对象(除了null)都有一个__proto__属性,指向它的原型对象

1
2
3
4
5
const obj = {};
console.log(obj.__proto__); // Object.prototype

const arr = [];
console.log(arr.__proto__); // Array.prototype

constructor属性

原型对象上有一个constructor属性,指回构造函数本身

1
2
3
4
5
function Dog() {}
console.log(Dog.prototype.constructor === Dog); // true

const dog = new Dog();
console.log(dog.__proto__.constructor === Dog); // true

3. 使用

传统方法创建类,并将方法存储到公共空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function FuncStudent(name, score) {
this.name = name;
this.score = score;
}
// 在类中存在一个默认的prototype属性, 这个属性可以帮助我们创建一个独立公共空间
/*
* 在类对象中的prototype被称之为原型, 通过这个类对象实例化出来的实例对象中也存在原型(__proto__)
*
* prototype: 函数对象上的属性
* __proto__: 对象上的属性
*
* */

FuncStudent.prototype.print_info = function () {
console.log(`我是${this.name}, 考了${this.score}分`);
}

func_student_1 = new FuncStudent('张三', 100)
func_student_2 = new FuncStudent('李四', 90)
console.log(func_student_1)
console.log('-'.repeat(30))
console.log(func_student_2)

func_student_1.print_info()
console.log(FuncStudent.prototype === func_student_1.__proto__) // true

使用class关键字创建类并实例化对象(会将类中的方法放入公共空间)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ClsStudent {
constructor(name, score) {
this.name = name;
this.score = score;
}

print_info() {
console.log(`我是${this.name}, 考了${this.score}分`);
}
}


cls_student_1 = new ClsStudent('张三', 100)
console.log(cls_student_1)

4. 原型链

在继承多个类的时候,原型层数会比较多,形成一个原型网络。沿着这个网络逐层向上查找属性的链路就称为原型链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Person {
constructor(name) {
this.name = name;
}
drink() {
console.log('正在喝水...');
}
}

class Teacher extends Person {
constructor(name, subject) {
super(name);
this.subject = subject;
}
print_info() {
console.log(`我是${this.name}, 教${this.subject}科目...`);
}
}

let teacher = new Teacher('张三', '语文');
// teacher.print_info()
// teacher.drink()

// 查看原型链关系(通过实例对象的方式查看)
console.log(teacher.hasOwnProperty('print_info')) // false
// 查看方法是否在声明这个实例对象的类中
console.log(teacher.__proto__.hasOwnProperty('print_info')) // true
console.log(teacher.__proto__.hasOwnProperty('drink')) // false
// 看方法是否在声明这个实例对象的类的父类中
console.log(teacher.__proto__.__proto__.hasOwnProperty('drink')) // true
console.log('-'.repeat(30))
// 查看原型链关系(通过类查询)
console.log(Teacher.prototype.hasOwnProperty('print_info')) // true
console.log(Teacher.prototype.hasOwnProperty('drink')) //false
// 通过prototype得到的结果是一个原型对象, 如果继续向下查询需要使用__proto__
console.log(Teacher.prototype.__proto__.hasOwnProperty('drink')) // true

5. 原型链检测

Object.getOwnPropertyDescriptor() 静态方法返回一个对象,该对象描述给定对象上特定属性(即直接存在于对象上而不在对象的原型链中的属性)的配置。返回的对象是可变的,但对其进行更改不会影响原始属性的配置。

认识

node.js 环境中

1
2
3
4
5
6
// 原型检测代码示例
navigator = {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36'
}

console.log(Object.getOwnPropertyDescriptor(navigator, 'userAgent'))
1
2
3
4
5
6
{
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
writable: true,
enumerable: true,
configurable: true
}

浏览器环境中

补原型(实例对象)

1
2
3
4
navigator = {}
navigator.__proto__.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36'
console.log(Object.getOwnPropertyDescriptor(navigator, 'userAgent'))
console.log(Object.getOwnPropertyDescriptor(navigator.__proto__, 'userAgent'))
1
2
3
4
5
6
7
undefined
{
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
writable: true,
enumerable: true,
configurable: true
}

补原型(类对象)

1
2
3
4
5
Navigator = function () {}
Navigator.prototype.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36'
navigator = new Navigator()
console.log(Object.getOwnPropertyDescriptor(navigator, 'userAgent'))
console.log(Object.getOwnPropertyDescriptor(navigator.__proto__, 'userAgent'))
1
2
3
4
5
6
7
undefined
{
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
writable: true,
enumerable: true,
configurable: true
}

补原型(方法)

1
2
3
4
5
6
7
8
9
// 补原型(方法)
Document = function () {}
Document.prototype.createElement = function () {}
HTMLDocument = function () {}
Object.setPrototypeOf(HTMLDocument.prototype, Document.prototype) // 第一个参数为子类, 第二个参数为父类
document = new HTMLDocument()
console.log(Object.getOwnPropertyDescriptor(document, 'createElement'))
console.log(Object.getOwnPropertyDescriptor(document.__proto__, 'createElement'))
console.log(Object.getOwnPropertyDescriptor(document.__proto__.__proto__, 'createElement'))
1
2
3
4
5
6
7
8
undefined
undefined
{
value: [Function (anonymous)],
writable: true,
enumerable: true,
configurable: true
}

代理脚本

这个脚本不用来补环境,只用于检测网站代码是否有原型链调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
function watch(obj, name, visited = new WeakSet()) {
// 防止循环引用导致无限递归
if (obj === null || typeof obj !== 'object' || visited.has(obj)) {
return obj;
}
visited.add(obj);
// 检查原型链访问
const checkPrototypeChain = (target, property) => {
let current = target;
while (current) {
if (Object.prototype.hasOwnProperty.call(current, property)) {
return false; // 属性直接存在于当前对象上
}
current = Object.getPrototypeOf(current);
if (current && current !== Object.prototype && current !== null) {
console.log(`原型链检测:true (对象: ${name}, 属性: ${property})`);
return true;
}
}
return false;
};
return new Proxy(obj, {
get: function (target, property, receiver) {
try {
// 排除一些不常见的或可能导致问题的属性
if (typeof property === 'symbol' || property === 'constructor' || property === '__proto__') {
return Reflect.get(target, property, receiver);
}

const value = Reflect.get(target, property, receiver);

// 深度监听嵌套对象
if (typeof value === 'object' && value !== null) {
// 为嵌套对象生成一个更具体的名称
const nestedName = `${name}.${String(property)}`;
return watch(value, nestedName, visited);
}

// 打印属性访问信息
console.log(`对象 => ${name}, 读取属性: ${String(property)}, 值为: ${typeof value === 'function' ? 'function' : value}, 类型为: ${typeof value}`);

// 检测原型链访问
// 如果属性不在 target 上,但通过原型链访问到,则标记为 true
if (!Object.prototype.hasOwnProperty.call(target, property)) {
checkPrototypeChain(target, property);
}

// 检测描述符
const descriptor = Object.getOwnPropertyDescriptor(target, property);
if (descriptor) {
if (descriptor.get || descriptor.set) {
console.log(`特殊检测: 存在Getter/Setter (对象: ${name}, 属性: ${String(property)})`);
}
if (!descriptor.writable && !descriptor.get) {
console.log(`特殊检测: 只读属性 (对象: ${name}, 属性: ${String(property)})`);
}
if (!descriptor.configurable) {
console.log(`特殊检测: 不可配置属性 (对象: ${name}, 属性: ${String(property)})`);
}
}
} catch (e) {
console.error(`Error in get trap for ${name}.${String(property)}:`, e);
}
return Reflect.get(target, property, receiver);
},
set: (target, property, newValue, receiver) => {
try {
console.log(`对象 => ${name}, 设置属性: ${String(property)}, 值为: ${typeof newValue === 'function' ? 'function' : newValue}, 类型为: ${typeof newValue}`);
} catch (e) {
console.error(`Error in set trap for ${name}.${String(property)}:`, e);
}
return Reflect.set(target, property, newValue, receiver);
},
// 捕获 in 操作符
has: function (target, property) {
console.log(`对象 => ${name}, in 操作符检测属性: ${String(property)}`);
return Reflect.has(target, property);
},
// 捕获 delete 操作符
deleteProperty: function (target, property) {
console.log(`对象 => ${name}, 删除属性: ${String(property)}`);
return Reflect.deleteProperty(target, property);
},
// 捕获 Object.keys(), Object.values(), Object.entries() 等操作
ownKeys: function (target) {
console.log(`对象 => ${name}, 获取自身所有键`);
return Reflect.ownKeys(target);
},
// 捕获 Object.defineProperty()
defineProperty: function (target, property, descriptor) {
console.log(`对象 => ${name}, 定义属性: ${String(property)}`);
return Reflect.defineProperty(target, property, descriptor);
},
// 捕获 Object.setPrototypeOf()
setPrototypeOf: function (target, prototype) {
console.log(`特殊检测: setPrototypeOf 被调用 (对象: ${name})`);
return Reflect.setPrototypeOf(target, prototype);
},
// 捕获 Object.getPrototypeOf()
getPrototypeOf: function (target) {
console.log(`特殊检测: getPrototypeOf 被调用 (对象: ${name})`);
return Reflect.getPrototypeOf(target);
},
getOwnPropertyDescriptor: function (target, property) {
console.log(`特殊检测: getOwnPropertyDescriptor 被调用 (对象: ${name}, 属性: ${String(property)})`);
return Reflect.getOwnPropertyDescriptor(target, property);
}
// Proxy 没有 toString 捕获器,对 toString 的读取已经由上面的 get 捕获并打印
});
}

使用