关于 js:2. 对象与原型链

#王者杯·14天创作挑战营·第1期#

一、对象

对象是:

  • 键值对集合

  • 所有非原始类型(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'
访问器属性getset 函数,比如下面代码
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. 继承语法糖:extendssuper

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 BA.prototype = Object.create(B.prototype)
构造函数继承super()B.call(this)
静态方法static f()A.f = function() {}
静态继承自动继承父类静态方法Object.setPrototypeOf(A, B)
属性访问器get/setObject.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: functionset: 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.nameObf

  • 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) 对比
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值