一、對象
對象是:
-
鍵值對集合
-
所有非原始類型(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) 對比 |