补环境-js原型链检测

深入理解JavaScript原型链

JavaScript 是一种基于原型的语言。理解原型和原型链对于掌握 JavaScript 的继承机制和对象模型至关重要。在本文中,我们将深入探讨 JavaScript 原型链的概念及其在实际编程中的应用。

1.什么是原型

在 JavaScript 中,每个对象都有一个与之关联的原型对象。这个原型对象也是一个对象,它可以拥有自己的原型,以此类推,形成一个链条,这就是所谓的原型链。

2.构造函数和原型
1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
this.name = name;
}

Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};

let person1 = new Person('Alice');
let person2 = new Person('Bob');

person1.sayHello(); // Hello, my name is Alice
person2.sayHello(); // Hello, my name is Bob

在上述的实例中Person是一个构造函数,Person.prototype是Person构造函数的原型对象,sayHello方法被添加到Person.prototype上,这意味着所有 Person 的实例都可以访问这个方法。

3.property和__proto__属性

在JavaScript中,对象属性分为两类:自身属性和原型链属性。
自身属性是直接定义在对象自身上的属性,原型属性是通过原型链继承的属性。

1
2
3
4
5
6
7
8
const person = {
name: "Alice",
age: 30
};

console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('age')); // true
console.log(person.hasOwnProperty('toString')); // false,因为 toString 是从 Object.prototype 继承来的

__proto__属性是每一个对象都有的一个内部属性,它指向对象的原型(即构造函数的 prototype 属性)。

4.原型链

当我们访问对象的属性或方法时,JavaScript 引擎会首先在对象自身的属性中查找。如果找不到,则会沿着原型链向上查找,直到找到属性或到达原型链的顶端(Object.prototype),其 proto 为 null。

1
2
3
console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

原型链检测

在我们扣代码时通常会遇到一些环境检测,环境检测通过才会返回正确的结果,但我们在补环境时通常不会按照实际浏览器的原型链去补,下面是常用的补环境方式:

1
2
navightor = {}
navightor.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0'

当js代码中存在原型链检测,这种补环境的方式是无法通过的,比如:

1
2
3
4
5
6
7
8
navightor = {}
navightor.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0'
if (Object.hasOwnProperty(navigator, 'userAgent')) {
return '错误的结果'
} else {
return '正确的结果'
}
// hasOwnProperty方法是JavaScript中所有对象从 Object.prototype 继承而来的一个方法。它用于检查一个对象是否拥有指定的属性(该属性必须是对象自身的属性,而不是从原型链继承来的属性)。

上面这种常规补环境的方式如果遇到原型链的检测那无法得到正确的结果,我们必须符合浏览器的环境,所以我们要将对应的属性补在原型链上,如下是补环境的方式符合浏览器的真实环境:

1
2
3
4
5
6
7
Navigator = function () {
}
Navigator.prototype = {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0'
}
navigator = new Navigator()
// 以上将userAgent属性补在了原型对象上面,符合真实的浏览器环境

描述符检测

Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

1
2
3
4
5
6
7
8
9
// 浏览器里面执行
Object.getOwnPropertyDescriptor(navigator,'userAgent') // ---> undefined

// node环境执行
var navigator = {
userAgent:"aaaaa"
}
Object.getOwnPropertyDescriptor(navigator,'userAgent')
// 输出{value: 'aaaaa',writable: true,enumerable: true,configurable: true}

value:与属性关联的值(仅限数据描述符)。
writable:当且仅当与属性关联的值可以更改时,为 true(仅限数据描述符)
configurable:当且仅当此属性描述符的类型可以更改且该属性可以从相应对象中删除时,为 true
enumerable:当且仅当此属性在相应对象的属性枚举中出现时,为 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
// 第1种方式,直接通过navigator来补他的隐式原型
navigator = {}
navigator.__proto__.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'

console.log(navigator.userAgent);
console.log(Object.getOwnPropertyDescriptor(navigator, 'userAgent'));

// 第2种方式, 通过navigator创建的类来补显式原型
var Navigator = function() {};
Navigator.prototype = {"userAgent": "123123123"};
navigator = new Navigator();
console.log(navigator.userAgent)
console.log(Object.getOwnPropertyDescriptor(navigator, 'userAgent'));

// 补方法
Document = function Document(){}
// Object.defineProperty 直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。
Object.defineProperty(Document.prototype,'createElement',{
configurable: true,
enumerable: true,
value: function createElement(){},
writable: true,
})
HTMLDocument = function HTMLDocument(){}
//可以将一个指定对象的原型(即内部的隐式原型属性)设置为另一个对象
Object.setPrototypeOf(HTMLDocument.prototype,Document.prototype)
document = new HTMLDocument()

console.log(Object.getOwnPropertyDescriptor(document.__proto__.__proto__, 'createElement'));

toString检测

在 JavaScript 中,toString() 是一个内置函数,用于将一个值转换为字符串。
toString() 方法可以应用于大部分 JavaScript 值类型,包括数字、布尔值、对象、数组等。它的返回值是表示该值的字符串。
如果js代码中检测toSting方法,那么即使我们按照原型链的方式去补环境也无法通过检测

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
Document = function Document(){}
Object.defineProperty(Document.prototype,'createElement',{
configurable: true,
enumerable: true,
value: function createElement(){},
writable: true,
})
HTMLDocument = function HTMLDocument(){}
Object.setPrototypeOf(HTMLDocument.prototype,Document.prototype)
document = new HTMLDocument()

console.log(document.toString()); --> 输出function createElement(){}

// 浏览器控制台输出 --> 'function createElement() { [native code] }'
// 这个时候如果检测了toString即使补原型链也无法通过
// 我们可以重写toString方法来达到目的
Document = function Document(){}
Object.defineProperty(Document.prototype,'createElement',{
configurable: true,
enumerable: true,
value: function createElement(){},
writable: true,
})

HTMLDocument = function HTMLDocument(){}
Object.setPrototypeOf(HTMLDocument.prototype,Document.prototype)
document = new HTMLDocument()
document.createElement.toString = function(){return "function createElement() { [native code] }"}
console.log(document.createElement.toString());