一、对象
对象是:
-
键值对集合
-
所有非原始类型(number、string、boolean、null、undefined、symbol、bigint)都是对象
-
支持动态增删属性
-
每个对象都继承自
Object.prototype
,具备原型链结构
1. 对象的创建方式
字面量方式(最常见)
const user = {
name: 'Tom',
age: 30
};
使用 new Object()
const user = new Object();
user.name = 'Tom';
使用构造函数
function Person(name) {
this.name = name;
}
const p = new Person('Tom');
使用 Object.create()
const proto = { sayHi() { console.log('hi'); } };
const obj = Object.create(proto);
2. 对象属性的类型
属性可以分为两种:
类型 | 描述 |
---|---|
数据属性 | 有 value 值,比如 obj.name = 'Tom' |
访问器属性 | 有 get 和 set 函数,比如下面代码 |
const obj = {
get age() {
return 18;
}
};
3. 对象与原型链关系
每个对象都有一个隐藏属性 [[Prototype]]
,即 __proto__
,它指向另一个对象,从而形成“原型链”:
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
4. 总结
对象的用途
-
存储和组织数据
-
构造复杂结构(如 DOM、组件、Vue/React 对象)
-
原型链和继承的核心单位
-
JavaScript 中一切皆对象(函数、数组、日期等)
对象就是一个动态的、键值对的集合,它可以存储数据、函数、还可以通过原型链实现继承,是 JavaScript 编程的核心结构。
二、对象属性
1. 枚举性(enumerable)
什么是“可枚举”?
在 JavaScript 中,对象的属性有一个 enumerable
属性,它表示这个属性是否可以通过枚举方式被遍历出来,如:
-
for...in
-
Object.keys()
默认创建的属性(如 obj.a = 1
)是 可枚举的。
const obj = {
a: 1,
b: 2
};
for (let key in obj) {
console.log(key); // 输出 a 和 b
}
不可枚举属性
可以使用 Object.defineProperty
创建不可枚举属性:
/*
* Object.defineProperty:这是 JavaScript 提供的精细控制属性行为的方式。
* hidden:为 obj 创建的属性
* enumerable: false 不可枚举(非 enumerable)
* */
const obj = {};
Object.defineProperty(obj, 'hidden', {
value: 42,
enumerable: false
});
console.log(Object.keys(obj)); // []
console.log('hidden' in obj); // true 仍然存在,只是不参与遍历
判断枚举性的方法
/* 输出:
{
value: 42,
writable: false,
enumerable: false,
configurable: false
}
*/
Object.getOwnPropertyDescriptor(obj, 'hidden');
Object.propertyIsEnumerable.call(obj, 'hidden'); // false
2. in
操作符
in
是 JavaScript 中用来检查某个 属性是否存在于对象中 的运算符,包括对象自身属性和原型链上的属性。
对象自身属性
const obj = { name: 'Alice' };
console.log('name' in obj); // true
console.log('age' in obj); // false
原型链属性
const arr = [];
console.log('push' in arr); // true,来自 Array.prototype
console.log('toString' in arr); // true,来自 Object.prototype
不可枚举属性也能检测
const obj = {};
Object.defineProperty(obj, 'hidden', {
value: 42,
enumerable: false
});
console.log('hidden' in obj); // true
in
不关心属性是否可枚举,只要属性在对象本身或其原型链上,就返回 true
。
in
与其他方式的对比
检查方式 | 是否包含原型链属性 | 是否受 enumerable 影响 |
---|---|---|
'prop' in obj | 是 | 否 |
obj.hasOwnProperty('prop') | 否(只查自身) | 否 |
Object.keys(obj).includes() | 否(只查自身) | 是(仅枚举属性) |
例子:
const obj = Object.create({ a: 1 });
obj.b = 2;
console.log('a' in obj); // true,来自原型
console.log(obj.hasOwnProperty('a')); // false
在逆向中的典型使用
判断反调试钩子是否注入
if ('debugger' in window) {
// 表明 window 上定义了 debugger 属性(哪怕不可见)
}
检测对象是否被注入属性(例如:hook、proxy)
if ('__hooked__' in window) {
alert('被 hook 了');
}
3. getter 和 setter(访问器属性)
在 JavaScript 中,对象的属性分为两类:
类型 | 行为方式 |
---|---|
数据属性 | 储存具体的值 |
访问器属性 | 通过函数获取或设置值(getter/setter) |
访问器属性不直接保存值,而是通过函数动态返回或设置值。
基本语法:定义 getter 和 setter
使用对象字面量
const person = {
firstName: 'John',
lastName: 'Doe',
get fullName() {
return this.firstName + ' ' + this.lastName;
},
set fullName(value) {
const parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
}
};
console.log(person.fullName); // John Doe(调用 getter)
person.fullName = 'Jane Smith'; // 调用 setter
console.log(person.firstName); // Jane
使用 Object.defineProperty
const obj = {};
Object.defineProperty(obj, 'secret', {
get() {
return 'Hidden value';
},
set(val) {
console.log('Attempted to set secret to:', val);
},
enumerable: true
});
console.log(obj.secret); // "Hidden value"
obj.secret = '123'; // 打印: Attempted to set secret to: 123
识别访问器属性
Object.getOwnPropertyDescriptor(obj, 'secret');
输出:
{
get: ƒ,
set: ƒ,
enumerable: true,
configurable: true
}
二、构造函数
构造函数就是用来批量创建对象的函数模板,约定俗成首字母大写。
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHi = function() {
console.log('Hi, I am ' + this.name);
};
}
const p1 = new Person('Tom', 18);
const p2 = new Person('Jerry', 20);
每次 new Person()
:
-
创建一个新对象
{}
; -
将这个对象的
__proto__
指向Person.prototype
; -
把构造函数中的
this
绑定到新对象; -
自动返回这个对象(如果没手动
return
其他对象)。
1. 构造函数的工作原理
function Foo(x) {
this.x = x;
}
let obj = new Foo(10);
等价于底层执行过程:
let obj = {}; // 创建一个新对象
obj.__proto__ = Foo.prototype; // 原型链接
Foo.call(obj, 10); // 执行构造函数
return obj; // 返回新对象
2. 构造函数 vs 普通函数
特性 | 构造函数 | 普通函数 |
---|---|---|
调用方式 | new Constructor() | func() |
this 指向 | 新对象 | 全局 / 调用者 / 严格模式 |
返回值默认是什么? | 新建的对象 | undefined (或函数返回) |
是否连接原型链 | 是,obj.__proto__ = Foo.prototype | 否 |
3. 构造函数与原型的关系
构造函数本身有一个默认属性 prototype
,用于存储共享给所有实例的方法和属性。
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi, I am ' + this.name);
};
const p = new Person('Alice');
p.sayHi(); // 来自原型
构造函数与实例的关系图:
Person ——prototype——→ { sayHi }
↑ ↑
| |
new __proto__
| |
+——→ p(实例对象) ←——+
核心关系:
p.__proto__ === Person.prototype
Person.prototype.constructor === Person
4. 构造函数中的共享陷阱
如果在构造函数中定义引用类型(如数组/对象),每个实例都会拥有独立的副本。
function Dog() {
this.favorites = [];
}
let d1 = new Dog();
let d2 = new Dog();
d1.favorites.push('bone');
console.log(d2.favorites); // [],互不影响
但如果把数组定义在 prototype
上,就会被所有实例共享:
function Cat() {}
Cat.prototype.favorites = [];
let c1 = new Cat();
let c2 = new Cat();
c1.favorites.push('fish');
console.log(c2.favorites); // ['fish'] 被共享了
5. 构造函数返回值规则
function A() {
this.name = 'A';
return { name: 'B' }; // 返回的是对象
}
console.log(new A().name); // 'B'
function B() {
this.name = 'B';
return 123; // 原始值被忽略
}
console.log(new B().name); // 'B'
结论: 构造函数显式返回一个“对象”时会覆盖默认返回值;返回原始值时会被忽略。
6.总结
概念 | 说明 |
---|---|
构造函数本质 | 使用 function 定义的模板函数,用于 new 创建对象 |
this | 指向新创建的对象 |
prototype | 构造函数用于定义所有实例共享属性的方法位置 |
__proto__ | 实例对象的隐藏属性,链接到构造函数的 prototype |
关键关系 | obj.__proto__ === Constructor.prototype |
返回值特例 | 返回对象会替换新建对象,返回原始值无效 |
三、原型对象
每一个通过 构造函数 创建的函数(非箭头函数)都有一个默认属性:prototype
,它指向一个对象,这个对象用于为所有实例对象共享方法和属性。
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi, I am ' + this.name);
};
const p1 = new Person('Tom');
p1.sayHi(); // 来自原型对象
构造函数、实例和原型对象的关系
结构图:
Person ——prototype——→ { sayHi }
↑ ↑
| |
new __proto__
| |
+——→ p1(实例对象) ←——+
对比关键点:
名称 | 指向 / 作用 |
---|---|
Person.prototype | 构造函数的原型对象 |
p1.__proto__ | 实例的原型对象,等于 Person.prototype |
sayHi 方法 | 定义在 Person.prototype 上,被所有实例共享 |
几个关键关系
p1.__proto__ === Person.prototype // 实例指向构造函数原型
Person.prototype.constructor === Person // 原型对象的 constructor 回指
Object.getPrototypeOf(p1) === Person.prototype // 等价写法
原型对象上的属性会被共享
原型上的方法和属性不是复制到每个实例中,而是通过原型链访问:
function Animal() {}
Animal.prototype.legs = 4;
const dog = new Animal();
console.log(dog.legs); // 4,来自原型
dog.legs = 2; // 给实例添加同名属性(不会修改原型)
console.log(dog.legs); // 2
console.log(Animal.prototype.legs); // 4
1. 使用原型的场景
1)为实例添加方法(推荐)
User.prototype.login = function() {};
如果在构造函数中定义方法,那每个实例都一份,占内存:
function User() {
this.login = function() {}; // 每次 new 都新建一个函数
}
2)动态修改原型对象
可以随时给原型对象加方法:
Person.prototype.walk = function() {};
实例会立即感知变更,因为访问属性是“沿原型链查找”的。
2. 手动设置或获取原型对象
获取原型对象:
Object.getPrototypeOf(obj) === obj.__proto__;
设置原型对象:
Object.setPrototypeOf(obj, newProto);
不推荐频繁使用 __proto__
,它虽然广泛兼容但不是标准推荐方式。
3. 构造函数 vs 原型对象 vs 实例的对比表
名称 | 示例 | 作用 |
---|---|---|
构造函数 | function A() {} | 用来 new 实例 |
原型对象 | A.prototype | 定义共享方法,实例的 __proto__ 指向它 |
实例对象 | new A() | 拥有自身属性和访问原型属性 |
实例原型 | instance.__proto__ | 即 A.prototype |
原型构造函数 | A.prototype.constructor | 回指构造函数 |
4. 在逆向分析中的应用
在混淆代码中,很多功能方法被挂在原型对象上,而实例调用时并未直接暴露名字。分析这些结构时常见技巧:
// 发现某个对象 obj
let proto = Object.getPrototypeOf(obj);
// 看它继承自谁
console.log(proto.constructor.name);
// 查看原型链所有方法
console.log(Object.getOwnPropertyNames(proto));
// 拦截其原型方法进行 hook
proto.targetFunc = function() { debugger; };
5. 构造函数没有 prototype 的例外
有两个函数没有 prototype 属性:
-
箭头函数
-
class
中的 static 静态方法
const arrow = () => {};
console.log(arrow.prototype); // undefined
6. 总结
原型对象作用 | 说明 |
---|---|
共享方法 | 所有实例共享原型上的方法节省内存 |
连接构造函数与实例 | constructor 和 __proto__ 建立关系 |
继承链条的基础 | 是原型链中向上查找的中间节点 |
调试混淆代码的切入点 | 找到对象原型能看到隐藏方法或防护逻辑 |
四、继承机制(__proto__、prototype)
核心理念:原型链继承
每一个对象都有一个内部属性 [[Prototype]]
(表现为 __proto__
),它指向其“父对象”的原型,这就形成了一条原型链。JS 会在这条链上查找属性或方法
1. prototype vs proto 的区别
名称 | 类型 | 属于谁? | 用途 |
---|---|---|---|
prototype | 对象 | 函数(构造函数) | 创建实例时作为原型 |
__proto__ | 对象 | 实例对象 | 指向构造函数的 prototype |
举例说明:
function Animal() {}
let a = new Animal();
console.log(Animal.prototype); // 原型对象
console.log(a.__proto__); // 指向 Animal.prototype
console.log(a.__proto__ === Animal.prototype); // true
2. 原型链继承机制图示
function Parent() {
this.name = 'Parent';
}
Parent.prototype.sayHi = function() {
console.log('Hi from Parent');
};
function Child() {
this.age = 10;
}
Child.prototype = new Parent(); // 继承核心:原型链继承
Child.prototype.constructor = Child;
let c = new Child();
结构图如下:
c
│
├── __proto__ → Child.prototype
│ │
│ ├── __proto__ → Parent.prototype
│ │
│ ├── sayHi
│ └── __proto__ → Object.prototype → null
3. 手动实现继承的几种方式
原型链继承(早期写法)
Child.prototype = new Parent();
缺点:
-
父类构造函数会执行两次(继承和实例化)
-
子类不能向父类传参
-
所有实例共享父类引用属性(如数组)
经典组合继承(构造继承 + 原型继承)
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHi = function() {
console.log('Hi from Parent');
};
function Child(name, age) {
Parent.call(this, name); // 第一次调用父构造函数
this.age = age;
}
Child.prototype = Object.create(Parent.prototype); // 原型继承
Child.prototype.constructor = Child;
优点:
-
不共享引用类型
-
支持向父类传参
-
保持原型链关系
ES6 class 语法糖继承(推荐)
class Parent {
constructor(name) {
this.name = name;
}
sayHi() {
console.log('Hi from Parent');
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
}
等价于组合继承,只是语法更清晰。
4. 属性/方法的查找过程(原型链的运行机制)
const obj = new Child();
obj.sayHi(); // 怎么找到这个方法的?
// 查找顺序:
1. 先查 obj 自身有没有 sayHi
2. 再查 obj.__proto__(即 Child.prototype)
3. 再查 Child.prototype.__proto__(即 Parent.prototype)
4. 再查 Object.prototype(比如 toString)
5. 到 null 停止(找不到就报错)
判断继承关系的方法
// 检查 obj 的原型链上是否 有一个原型等于 Parent.prototype
obj instanceof Parent // true
// 检查 Parent.prototype 是否存在于 obj 的原型链中
Parent.prototype.isPrototypeOf(obj) // true
在逆向分析中的应用
很多混淆代码通过手动设置原型来实现多级继承或结构伪装:
Object.setPrototypeOf(obj, SomeHiddenPrototype);
分析时可以:
-
用
Object.getPrototypeOf(obj)
逐层查看原型链 -
遍历所有继承的方法和属性
-
判断某个对象是哪个类的实例(用 constructor.name 或 instanceof)
原型链的终点
所有对象最终都会继承自:
Object.prototype
并且:
Object.prototype.__proto__ === null // 原型链终点
小结
概念 | 说明 |
---|---|
prototype | 构造函数专属,用于实例共享方法 |
__proto__ | 实例专属,指向构造函数的 prototype |
继承的关键机制 | 是设置子类 prototype 的 __proto__ 指向父类 prototype |
原型链查找顺序 | 自身 → 构造函数 prototype → Object.prototype |
五、class 语法与继承语法糖
1. 什么是 class
?
class
是 ES6 引入的语法糖,本质上还是基于 原型链 的封装,只不过写法更接近传统面向对象语言(如 Java、C++)。
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi, I'm ${this.name}`);
}
}
const p = new Person('Tom');
p.sayHi(); // Hi, I'm Tom
等价于(底层写法):
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log(`Hi, I'm ${this.name}`);
};
2. class 的基本组成
关键词 | 用法 | 说明 |
---|---|---|
constructor() | 类的构造方法 | 初始化实例 |
方法名() | 定义原型方法 | 所有实例共享 |
static 方法名() | 定义静态方法(不在原型上) | 类调用,不可实例调用 |
get/set | 定义访问器属性(属性拦截) | 控制读取或设置某属性行为 |
3. 继承语法糖:extends
和 super
class Parent {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi from ${this.name}`);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
sayAge() {
console.log(`I am ${this.age} years old.`);
}
}
const c = new Child('Alice', 20);
c.sayHi(); // 来自父类
c.sayAge(); // 子类自己的方法
对比底层写法(等价):
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHi = function () { ... };
function Child(name, age) {
Parent.call(this, name); // 继承构造函数
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayAge = function () { ... };
4. class 的本质仍然是函数 + 原型
typeof Parent; // 'function'
console.log(Object.getPrototypeOf(Child)); // Parent
console.log(Child.prototype.__proto__ === Parent.prototype); // true
class 的继承背后做了两件事:
-
设置
Child.prototype.__proto__ = Parent.prototype
-
设置
Child.__proto__ = Parent
(静态继承)
5. class 的静态方法不会被实例继承
class A {
static sayHello() {
console.log("Hello");
}
}
const a = new A();
a.sayHello(); // 报错
A.sayHello(); // 正确调用
但静态方法可以被子类继承:
class B extends A {}
B.sayHello(); // 从 A 继承了静态方法
6. 访问器(getter / setter)
class Rectangle {
constructor(width, height) {
this._w = width;
this._h = height;
}
get area() {
return this._w * this._h;
}
set width(value) {
this._w = value;
}
}
const r = new Rectangle(2, 3);
console.log(r.area); // 6
r.width = 5;
console.log(r.area); // 15
7. 在逆向分析中的典型特征(混淆代码也用 class)
会经常看到如下结构:
class t {
constructor(e) {
this._v = e;
}
get d() {
return this._v.split('').reverse().join('');
}
static f(x) {
return new t(x);
}
}
分析技巧:
-
先找构造函数构造了什么字段(如 this._v)
-
再看原型方法是对字段如何处理的
-
静态方法大多是工厂函数/入口
-
利用
Object.getOwnPropertyNames(Object.getPrototypeOf(obj))
找所有原型方法
8. 总结:class 与继承的原理
特性 | class 语法糖 | 底层原理说明 |
---|---|---|
类定义 | class A {} | function A() {} |
方法定义 | say() {} | A.prototype.say = function() {} |
继承父类 | extends B | A.prototype = Object.create(B.prototype) |
构造函数继承 | super() | B.call(this) |
静态方法 | static f() | A.f = function() {} |
静态继承 | 自动继承父类静态方法 | Object.setPrototypeOf(A, B) |
属性访问器 | get/set | Object.defineProperty |
六、原型链调试技巧(逆向中常看到混淆的对象结构)
在混淆代码中,经常会遇到这些情况:
-
对象被构造函数动态生成,不直接明示类名
-
方法被挂载在 prototype 或
__proto__
上 -
属性被 getter 包装,或动态通过
Object.defineProperty
定义 -
某些类或函数名被压缩为
t
,e
,n
,难以识别 -
Object.setPrototypeOf
或__proto__
被手动修改,原型链人为构造
调试时必须:
- 看清对象的真实构造函数是谁
- 弄明白方法/属性从哪里继承来的
- 把被混淆命名的对象关系“还原成人话”
1. 常用调试工具和方法
1)console.dir(obj)
—— 看原型链结构树
这个方法比 console.log
更适合看“展开结构”。
console.dir(obj)
输出可以看到:
-
[[Prototype]]
(即__proto__
)指向谁 -
构造函数 constructor 是哪个函数
-
是否有 getter/setter
-
继承来的方法在哪里
2)Object.getPrototypeOf(obj)
// 返回对象的内部原型,即 obj.__proto__
let proto = Object.getPrototypeOf(obj);
逐层向上找:
while (proto) {
console.log(proto.constructor.name, proto);
proto = Object.getPrototypeOf(proto);
}
能清晰地打印出原型链结构,即使构造函数被压缩了名,也能知道它的层级关系。
3)Object.getOwnPropertyNames(obj)
这个方法可以获取一个对象自身的所有属性,包括不可枚举属性。
Object.getOwnPropertyNames(obj)
常用于识别 prototype 对象上挂了哪些方法,尤其是混淆时你可能看到:
class t {
constructor(e) {
this.a = e;
}
['\x73\x61\x79']() {
// 混淆后的方法名 say
}
}
用 getOwnPropertyNames(t.prototype)
能看出真实方法名。
4)判断对象归属哪个类
obj.constructor.name // 类名(如果没被改)
obj instanceof 某类 // 是否继承
某类.prototype.isPrototypeOf(obj) // 判断是否是子孙类
逆向时经常这样判断当前对象到底是哪个类实例。
5)看 getter/setter
使用:
// 获取 obj.prop 这个属性的完整定义信息(描述符),包括它是普通数据属性还是带有 getter/setter 的访问器属性。
let desc = Object.getOwnPropertyDescriptor(obj, 'prop');
console.log(desc);
输出如果有 get: function
或 set: function
,就表示它是通过访问器定义的,可能在干某种加解密、编码等逻辑。
6)利用断点观察对象变化
在浏览器 DevTools 中:
-
给构造函数打断点:在构造阶段观察
this
的属性变化 -
给访问器打断点:右键属性 → Break on → Property access/change
-
使用
debugger
:手动断点进入混淆函数体,观察this
和局部变量
7)逆向实战示例
发现:
Object.getOwnPropertyDescriptor(window, 'crypto');
// 输出:
{ get: ƒ() { return customDecryptor(); }, configurable: true }
说明:
访问 window.crypto
实际上是在执行一个函数 customDecryptor()
,它可能返回了某个解密后的对象,或者某种伪造的数据。
2. 真实逆向场景举例(常见结构)
示例 1:混淆类结构还原
class t {
constructor(e) {
this._ = e;
}
get v() {
return this._.split('').reverse().join('');
}
}
如果看到实例:
let o = new t("abc123");
console.log(o.v); // "321cba"
调试方法:
-
console.dir(o)
→ 找到[[Prototype]]
是t.prototype
-
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(o), 'v')
→ 发现是get
-
把 t 改名为 DecodeStr → 增强可读性
示例 2:构造函数伪装结构
function Obf() {
this.token = Math.random().toString(36).slice(2);
}
Object.setPrototypeOf(Obf.prototype, AnotherClass.prototype);
调试方法:
-
obj.constructor.name
是Obf
-
但
obj.__proto__.__proto__ === AnotherClass.prototype
-
多层继承伪装 →
Object.getPrototypeOf(Object.getPrototypeOf(obj))
示例 3:多个对象共用原型(对象池/策略模式)
const sharedProto = {
doThing() { console.log('shared logic'); }
};
const a = Object.create(sharedProto);
const b = Object.create(sharedProto);
调试方式:
-
Object.getPrototypeOf(a) === Object.getPrototypeOf(b)
-
所有方法来自
sharedProto
,便于还原公共逻辑
3. 总结
目标 | 技术手段 |
---|---|
找到构造函数是谁 | obj.constructor.name / Object.getPrototypeOf |
理解对象属性来自哪里 | 查看 __proto__ 、prototype、constructor |
识别 getter/setter 是否干活 | Object.getOwnPropertyDescriptor() |
解开类结构混淆 | 重命名类、调试构造函数、查找类方法 |
判断多个对象是否共用原型 | Object.getPrototypeOf(obj) 对比 |