让你彻底搞懂 JavaScript 原型/原型链

65 阅读18分钟

第一章:JavaScript 对象基础

1.1 对象的概念与创建方式

基本概念

JavaScript 对象是键值对的集合,是 JavaScript 的核心数据类型。对象可以看作是无序的属性集合,每个属性都有一个名称(键)和一个值。

对象的特点有以下几点:

  • 动态性:随时可添加/删除属性
  • 引用类型:对象是通过引用访问的复合值
  • 原型继承:每个对象都有原型链
  • 属性特性:对象的属性可以拥有可读、可写、可枚举、可配置等特性

创建方式

(1)对象字面量

const person = {
  name: '张三',
  age: 30,
  greet() {
    console.log(`你好,我是${this.name}`);
  }
};

(2)new Object()构造函数

const car = new Object();
car.brand = 'Toyota';
car.model = 'Camry';

(3)构造函数方式

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
  };
}

const person1 = new Person('李四', 25);

(4)Object.create()方式

const prototypeObj = {
  greet() {
    console.log('Hello!');
  }
};

const myObj = Object.create(prototypeObj);
myObj.name = '王五';

(5)ES6 class 语法糖

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

const dog = new Animal('Dog');

1.2 属性的访问与操作

访问属性

// 点表示法
console.log(person.name);

// 方括号表示法
console.log(person['age']);

属性操作

(1)添加/删除属性

person.job = 'Developer';
person['hobby'] = 'Reading';

(2)删除属性

delete person.age;

(3)检查属性是否存在

console.log('name' in person); // true
console.log(person.hasOwnProperty('name')); // true

1.3 对象与原始类型的区别

  • 存储与访问机制:原始类型存储在栈内存中,对象类型存储在堆内存中,变量保存其引用地址
  • 可变性差异:原始类型不可变,对象类型可变
  • 比较行为:原始类型对比值,对象类型对比引用
  • 方法与属性:原始类型不能添加属性和方法,对象类型可以自由添加/修改

第二章:初识原型(Prototype)

2.1 什么是原型?

原型(Prototype)是 JavaScript 实现继承的核心机制。每个 JavaScript 对象都有一个内置的 [[Prototype]] 属性(可通过 __proto__ 访问),指向它的原型对象

2.2 prototype 属性的作用

prototype 是 JavaScript 中函数对象特有的属性,它的核心作用是为基于该构造函数创建的实例提供共享的属性和方法

function Person(name) {
  this.name = name;
}

// 添加到prototype的方法会被所有实例共享
Person.prototype.sayHi = function() {
  console.log(`Hi, I'm ${this.name}`);
};

const p1 = new Person('Alice');
p1.sayHi(); // Hi, I'm Alice

2.3 默认原型:Object.prototype

Object.prototype 是 JavaScript 中所有对象的终极原型,是原型链的顶端(终点是 null)

const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
function Foo() {}
const f = new Fool()
console.log(f.prototype) // undefined
console.log(f.__proto__ === Foo.prototype) // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true

它包含所有对象继承的基本方法

// 常见方法
.toString()
.valueOf()
.hasOwnProperty()
.isPrototypeOf()

默认继承关系如下:

graph LR
    A[你的对象] --> B[Object.prototype]
    C[数组] --> D[Array.prototype] --> B
    E[函数] --> F[Function.prototype] --> B
    B --> G[null]

2.4 通过原型共享属性和方法

function Person(name) {
  this.name = name; // 实例属性(不共享)
}

// 添加到prototype的方法会被所有实例共享
Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

const p1 = new Person('Alice');
const p2 = new Person('Bob');

p1.sayHello === p2.sayHello // true(共享同一方法)

第三章:构造函数与实例

3.1 构造函数的作用与定义

作用

  1. 创建对象模版:定义一类对象的共同属性和方法
  2. 初始化对象状态:为新对象设置初始属性值
  3. 实现原型继承:通过 prototype 共享属性和方法
  4. 类型标识:通过 instanceof 识别对象类型

定义方式

(1)传统定义方式

function Person(name, age) {
  // 实例属性
  this.name = name; 
  this.age = age;
  
  // 不推荐:每个实例都会创建新函数
  this.sayHi = function() {
    console.log(`Hi, I'm ${this.name}`);
  };
}

// 推荐:共享方法
Person.prototype.introduce = function() {
  console.log(`I'm ${this.name}, ${this.age} years old`);
};

(2)class 语法糖

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  // 自动添加到prototype
  introduce() {
    console.log(`I'm ${this.name}, ${this.age} years old`);
  }
}

构造函数具有以下特征:

  1. 命名约定:首字母大写(如 Person)
  2. 必须以 new 调用:否则 this 指向全局对象(严格模式下会报错)
  3. 隐式返回:自动返回新对象(除非手动返回非原始值)

3.2 new 操作符的执行过程

当使用 new 调用构造函数时,JavaScript 引擎会完成以下操作:

  1. 隐式创建新对象:创建一个新的空对象
  2. 设置原型链接:将该对象的 [[Prototype]] 属性(即 __proto__)链接到构造函数的 prototype 属性
  3. 绑定 this:将新对象绑定到构造函数内部的 this 上下文
  4. 自动返回:
    • 如果构造函数没有显式 return 语句,自动返回新创建的对象
    • 如果 return 非对象类型,仍返回新对象(忽略 return)
    • 如果 return 对象类型,则返回该对象(覆盖默认行为)

代码示例

(1)无 return 语句

function Person(name) {
  this.name = name;
  // 无 return 语句
}

const p = new Person('Alice');
console.log(p); // Person { name: "Alice" }(返回新对象)

(2)return 非对象(原始值)

function Car(model) {
  this.model = model;
  return 123; // 原始值被忽略
}

const c = new Car('Tesla');
console.log(c); // Car { model: "Tesla" }(仍返回新对象)

(3)return 对象类型(覆盖默认行为)

function Dog() {
  this.name = "Default";
  return { name: "Override" }; // 返回这个对象
}

const d = new Dog();
console.log(d); // { name: "Override" }(不是 Dog 实例)

关键注意事项

(1)忘记 new 的后果

const p = Person('Alice'); // 没有 new
console.log(p); // undefined(this 会指向全局对象/报错)

(2)防御性编程

function User(name) {
  // 确保构造函数被正确使用
  if (!new.target) {
    return new User(name); // 自动补 new
  }
  this.name = name;
}

3.3 实例的 __proto__ 与构造函数的 prototype

核心概念对比

特性prototype__proto__
归属函数对象特有所有对象都有
作用构造函数创建实例时的原型模板对象实际继承的原型对象
访问方式Constructor.prototypeObject.getPrototypeOf(obj) 或 obj.__proto__
默认值自动创建包含 constructor 属性的对象取决于对象创建方式
标准程度ES标准属性非标准,已由Object.getPrototypeOf()取代

两者关系图解

graph LR
    A[构造函数 Constructor] -->|prototype 属性| B[原型对象 Prototype]
    C[实例 instance] -->|"__proto__" 链接| B
    B -->|constructor 属性| A
    B -->|"__proto__"| D[Object.prototype]
    D -->|"__proto__"| E[null]

3.4 constructor 属性的意义

constructor 是原型对象上的一个重要属性,它指向创建当前对象的构造函数。

基本特性

(1)默认存在于所有函数对象的 prototype 上

function Person() {}
console.log(Person.prototype.constructor === Person); // true

(2)实例通过原型链访问

const p = new Person();
console.log(p.constructor === Person); // true(通过原型链访问)

核心作用

(1)标识对象来源,提供追踪对象是由哪个构造函数创建的路径

const arr = [];
console.log(arr.constructor === Array); // true

(2)实现构造函数复用,可以通过 constructor 创建新实例

function Car(make) {
  this.make = make;
}

const toyota = new Car('Toyota');
const honda = new toyota.constructor('Honda');
console.log(honda.make); // "Honda"

(3)类型检查的补充

function checkType(obj) {
  return obj.constructor.name;
}
checkType([]); // "Array"

第四章:原型链(Prototype Chain)

4.1 原型链的形成机制

原型链由对象的 __proto__ 链接串联而成,形成一条访问属性和方法的查找路径

原型链核心组件

  1. 实例对象:包含基本数据和 __proto__ 链接
  2. 原型对象:包含共享属性和方法
  3. 链式结构:通过 __proto__ 逐级链接形成,终点是 null

具体形成过程

(1)构造函数创建时的默认设置

function Person(name) {
  this.name = name;
}

// 自动创建的 prototype 对象
console.log(Person.prototype);
/* {
 *   constructor: Person,
 *   __proto__: Object.prototype
 * }
 */

(2)实例创建时的原型链建立(new 操作符执行过程)

const p = new Person('Alice');

// 实例的原型指向构造函数的prototype
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
console.log(p.__proto__ === Person.prototype);             // true

// 原型对象的constructor指回构造函数
console.log(Person.prototype.constructor === Person);      // true

图解

graph LR
    A[实例] -->|__proto__| B[构造函数的prototype]
    B -->|__proto__| C[Object.prototype]
    C -->|__proto__| D[null]

4.2 属性查找规则:从实例到原型链顶端

基本查找流程

当查找对象属性/方法时,JavaScript 引擎会按照以下顺序进行查找

  1. 检查实例自身属性:首先在实例对象自身属性中查找,可使用 hasOwnProperty() 验证属性是否为自有
  2. 沿原型链向上查找:如果实例自身没有该属性,则访问 __proto__,重复此过程直到找到属性或到达原型链终点
  3. 终止条件:找到属性立即返回,或到达 null(原型链中断)时返回 undefined

图解查找流程

graph TD
    A[访问 obj.property] --> B{obj有自有属性?}
    B -->|是| C[返回该属性值]
    B -->|否| D[获取 obj.__proto__]
    D --> E{proto为null?}
    E -->|是| F[返回undefined]
    E -->|否| G[在proto对象上查找property]
    G --> H{找到属性?}
    H -->|是| I[返回属性值]
    H -->|否| D

示例代码说明

function Person(name) {
  this.name = name; // 自有属性
}

Person.prototype.sayHi = function() { // 原型属性
  console.log(`Hi, ${this.name}`);
};

const p = new Person('Alice');

// 查找过程演示:
p.name;    // 1. 找到自有属性 → "Alice"
p.sayHi(); // 2. 自有无 → Person.prototype找到 → 执行方法
p.age;     // 3. 自有无 → Person.prototype无 → Object.prototype无 → null → undefined

4.3 原型链的终点:Object.prototype 和 null

JavaScript 的原型链最终会指向两个特殊的终点:

  1. Object.prototype:所有常规对象的最终原型对象
  2. null:原型链的真正终点,表示“无原型”

Object.prototype 的作用

(1)提供基础对象方法

// 所有对象继承的方法举例
const obj = {};
obj.toString();      // 继承自Object.prototype
obj.hasOwnProperty(); // 继承自Object.prototype
obj.valueOf();       // 继承自Object.prototype

(2)原型链的枢纽站:基本所有原型链都会经过 Object.prototype(Object.create(null)创建的纯净对象除外)

(3)内置方法的存储库

console.log(Object.prototype);
/* 包含:
 * - constructor: Object()
 * - toString()
 * - valueOf()
 * - hasOwnProperty()
 * - isPrototypeOf()
 * - ...
 */

null 的意义

(1)原型链的终止标识

console.log(Object.getPrototypeOf(Object.prototype)); // null

(2)设计目的:表示“无原型”的最终状态,防止原型链查找进入无限循环

(3)特殊对象的对比

const normalObj = {};
const bareObj = Object.create(null);

console.log('toString' in normalObj); // true
console.log('toString' in bareObj);   // false

4.4 原型链 vs 作用域链

作用域链简述

作用域链是 JavaScript 中实现变量查找的机制,每个函数执行时都会创建一个上下文,其中包含一个作用域链。作用域链由当前函数的活动对象和所有外层函数的变量对象组成,当查找变量时,JavaScript 引擎会从当前执行作用域开始查找,当前作用域未找到时会沿作用域链向上查找,直到找到变量或达到全局作用域返回 undefined

两者对比

特性原型链作用域链
目的实现对象间的属性/方法的共享和继承实现变量的查找和访问
组成对象的 __proto__链接形成函数执行上下文中的变量对象链
查找方向从实例对象往原型对象方向查找从内层函数往外层函数方向查找
触发时机访问对象属性时访问变量时
终点null全局执行上下文(window/global)
创建方式new、Object.create()或 __proto_通过函数定义和调用

代码示例

(1)原型链查找

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

const john = new Person('John');

// 原型链查找过程:
// 1. john 自身有 name 属性
console.log(john.name); // 直接找到

// 2. john 自身没有 sayHello 方法,查找原型链
john.sayHello(); // 在 Person.prototype 上找到

// 3. toString 方法查找
// john -> Person.prototype -> Object.prototype -> null
john.toString(); // 最终在 Object.prototype 上找到

(2)作用域链查找

const globalVar = 'global';

function outer() {
  const outerVar = 'outer';
  
  function inner() {
    const innerVar = 'inner';
    console.log(innerVar);    // 当前作用域找到
    console.log(outerVar);    // 外层作用域找到
    console.log(globalVar);   // 全局作用域找到
    console.log(notDefined);  // 报错,所有作用域都找不到
  }
  
  inner();
}

outer();

第五章:继承与原型链

5.1 基于原型的继承实现(Object.create)

代码实现

const animal = {
  init: function(name) {
    this.name = name;
  },
  getName: function() {
    return this.name;
  }
};

const dog = Object.create(animal);
dog.bark = function() {
  return 'Woof!';
};

const myDog = Object.create(dog);
myDog.init('Buddy');
console.log(myDog.getName()); // "Buddy"
console.log(myDog.bark());    // "Woof!"

特点

  • 更纯粹的原型继承,不需要构造函数
  • 适合简单对象继承场景
  • 无法实现私有属性和方法

5.2 组合继承(构造函数 + 原型链)

代码实现

// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}

Animal.prototype.getName = function() {
  return this.name;
};

// 子类
function Dog(name, age) {
  Animal.call(this, name); // 调用父类构造函数,继承实例属性
  this.age = age;
}

// 设置原型链继承
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复构造函数指向

Dog.prototype.bark = function() {
  return 'Woof!';
};

// 使用
const dog1 = new Dog('Buddy', 2);
console.log(dog1.getName()); // "Buddy"
console.log(dog1.bark());   // "Woof!"

关键点分析

  • Animal.call(this, name) 实现实例属性的继承(类似其他语言的 super())
  • Object.create(Animal.prototype) 创建以父类原型为原型的新对象,避免直接赋值导致原型污染
  • 修复 constructor 属性保证对象类型正确识别

5.3 寄生组合继承(优化方案)

代码实现

function inheritPrototype(child, parent) {
  const prototype = Object.create(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
}

// 父类
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function() {
  console.log(this.name);
};

// 子类
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

// 继承
inheritPrototype(Dog, Animal);

Dog.prototype.bark = function() {
  console.log('Woof!');
};

// 使用
const myDog = new Dog('Max', 'Labrador');
myDog.sayName(); // "Max"
myDog.bark();    // "Woof!"

优势

  • 只调用一次父类构造函数
  • 原型链保持不变
  • 能够正常使用 instanceof 和 isPrototypeOf

5.4 ES6 class 语法与原型链的关系

代码实现

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  getName() {
    return this.name;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }
  
  bark() {
    return 'Woof!';
  }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.getName()); // "Buddy"
console.log(myDog.bark());    // "Woof!"

注意

  • 这本质上是语法糖,底层仍然是基于原型的继承
  • extends 关键字自动设置了原型链
  • super() 必须在使用 this 之前调用

第六章:原型相关方法与操作

6.1 Object.create() 与纯净对象

Object.create 是 JavaScript 中创建新对象的方法,它允许你明确指定新对象的原型

基本语法

Object.create(proto[, propertiesObject])
  • proto:新创建对象的原型对象(必须为对象或 null)
  • propertiesObject(可选):属性描述符集合(同 Object.defineProperties() 的第二个参数)

纯净对象

纯净对象是指没有原型链(__proto__为 null)的对象,创建方式:

const pureObject = Object.create(null);

其特点有:

  • 无原型,防止污染
  • 无任何默认属性和方法
  • 属性查找更快,只需查找自身属性
  • 更安全的属性存储

6.2 Object.getPrototypeOf() 和 Object.setPrototypeOf()

基本概念

  • Object.getPrototypeOf() 方法用于获取指定对象的原型(即内部 [[Prototype]] 属性的值)
  • Object.setPrototypeOf() 方法设置一个指定对象的原型(即内部 [[Prototype]] 属性)到另一个对象或 null。

语法和参数

Object.getPrototypeOf(obj)
// obj:要获取其原型的对象,注意,如果参数不是对象,会强制转为对象

Object.setPrototypeOf(obj, prototype)
// obj:要设置其原型的对象
// prototype:该对象的新原型(必须为对象或 null)

返回值

  • Object.getPrototypeOf():返回给定对象的原型(可能是对象或 null)
  • Object.setPrototypeOf():返回设置新原型后的对象

6.3 instanceof 的原理与局限性

instanceof 是 JavaScript 中用于检查对象原型链的操作符,其核心功能是检查构造函数的 prototype 属性是否出现在对象的原型链上

语法

object instanceof Constructor

内部实现机制

instanceof 的底层行为可以用以下伪代码表示:

function instanceOf(obj, Constructor) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(obj);
  
  // 获取构造函数的 prototype 对象
  const prototype = Constructor.prototype;
  
  // 沿着原型链向上查找
  while (proto !== null) {
    if (proto === prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
  
  return false;
}

实际示例分析

function Person() {}
const p = new Person();

console.log(p instanceof Person); // true

其执行过程为:

  1. 获取 p.__proto__(指向 Person.prototype)
  2. 比较 Person.prototype 和 Person.prototype -> 匹配成功

主要局限性

(1)跨执行环境问题(iframe/window)

<!-- 主页面 -->
<script>
  function Person() {}
  const p = new Person();
  
  // 创建iframe并获取其Array构造函数
  const iframe = document.createElement('iframe');
  document.body.appendChild(iframe);
  const IframeArray = iframe.contentWindow.Array;
  
  console.log(p instanceof Person); // true
  console.log([] instanceof IframeArray); // false
</script>

原因:不同执行环境有其各自独立的全局对象和构造函数

(2)原始类型检测不可靠

console.log('hello' instanceof String); // false
console.log(123 instanceof Number);    // false
console.log(true instanceof Boolean);  // false

原因:原始类型不是对象,除非用包装对象

(3)构造函数 prototype 被修改

function Person() {}
const p = new Person();

// 修改原型后
Person.prototype = {};
console.log(p instanceof Person); // false

(4)对象原型被手动修改

function A() {}
function B() {}
const obj = new A();

// 修改原型链
Object.setPrototypeOf(obj, B.prototype);
console.log(obj instanceof A); // false
console.log(obj instanceof B); // true

(5)对纯净对象无效

const pureObj = Object.create(null);
console.log(pureObj instanceof Object); // false

6.4 hasOwnProperty 与 in 操作符的区别

核心区别

特性hasOwnPropertyin 操作符
检测范围仅对象自身属性自身属性 + 原型链属性
继承方法来自 Object.prototypeJavaScript 语言操作符
对纯净对象需要外部调用(Object.prototype.hasOwnProperty.call)可直接使用
ES6+替代方案Object.hasOwn()Reflect.has()

深度解析

(1)hasOwnProperty 实现原理(伪代码)

function hasOwnProperty(obj, prop) {
  // 获取对象的所有自有属性
  const ownKeys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));
  return ownKeys.includes(prop);
}

(2)in 操作符实现原理(伪代码)

function inOperator(obj, prop) {
  while (obj !== null) {
    if (Object.getOwnPropertyDescriptor(obj, prop)) {
      return true;
    }
    obj = Object.getPrototypeOf(obj);
  }
  return false;
}

关键差异对比

(1)原型链处理差异

function Person() {}
Person.prototype.name = 'Proto';

const p = new Person();
p.age = 30;

console.log(p.hasOwnProperty('name')); // false
console.log('name' in p);             // true

(2)属性描述符影响

const parent = {};
Object.defineProperty(parent, 'secret', {
  value: 123,
  enumerable: false
});

const child = Object.create(parent);

console.log(child.hasOwnProperty('secret')); // false ❗️(正确,非自有属性)
console.log('secret' in child);             // true ❗️(能检测到继承属性)

第七章:高级原型应用

7.1 原型污染与安全问题

基本概念

原型污染是指攻击者通过某种方式修改 JavaScript 对象的原型(通常是 Object.prototype),从而影响所有基于该原型的对象行为的安全漏洞

污染原理

// 正常对象
const obj = { a: 1 };

// 污染原型
Object.prototype.isAdmin = true;

// 所有对象现在都有isAdmin属性
console.log(obj.isAdmin); // true
console.log({}.isAdmin); // true

常见的污染途径

(1)不安全的对象合并

function merge(target, source) {
  for (const key in source) {
    target[key] = source[key]; // 可能覆盖原型属性
  }
}

const maliciousPayload = {
  __proto__: { isAdmin: true },
  constructor: { prototype: { isAdmin: true } }
};

merge({}, maliciousPayload);

(2)不安全的 JSON 解析

const maliciousJSON = '{"__proto__":{"isAdmin":true}}';
const obj = JSON.parse(maliciousJSON); // 在某些引擎中会污染原型

(3)路径赋值操作

function setValue(obj, path, value) {
  const parts = path.split('.');
  let current = obj;
  
  for (let i = 0; i < parts.length - 1; i++) {
    if (!current[parts[i]]) {
      current[parts[i]] = {};
    }
    current = current[parts[i]];
  }
  
  current[parts[parts.length - 1]] = value;
}

// 攻击者可以传入恶意路径
setValue({}, '__proto__.isAdmin', true);

安全影响分析

(1)权限提升

// 假设系统检查权限的方式
function checkPermission(user) {
  if (user.isAdmin) {
    grantAdminAccess();
  }
}

// 污染后所有用户都变成管理员
Object.prototype.isAdmin = true;
checkPermission({}); // 获得管理员权限

(2)XSS 攻击增强

// 污染toString方法
Object.prototype.toString = function() {
  return '<script>maliciousCode()</script>';
};

// 当系统调用toString时
document.body.innerHTML = someObject.toString(); // 执行恶意代码

(3)服务端漏洞

// 假设Express中有这样的代码
app.get('/user', (req, res) => {
  if (req.query.isAdmin) {
    return sensitiveData();
  }
});

// 攻击者可以发送污染请求
fetch('/user?__proto__.isAdmin=true');

7.2 如何防御原型污染?

(1)使用 Object.create(null)创建纯净对象

const safeObj = Object.create(null);
safeObj.a = 1;
console.log(safeObj.hasOwnProperty); // undefined

(2)安全的对象合并

function safeMerge(target, source) {
  return Object.assign({}, target, source);
  // 或使用展开运算符
  // return { ...target, ...source };
}

(3)原型属性检查

function setValue(obj, path, value) {
  const parts = path.split('.');
  let current = obj;
  
  for (let i = 0; i < parts.length - 1; i++) {
    if (parts[i] === '__proto__' || parts[i] === 'constructor') {
      throw new Error('Prototype pollution attempt');
    }
    // ...其余逻辑
  }
}

(4)使用 Map 代替 Object

const safeMap = new Map();
safeMap.set('key', 'value');
// 无法通过原型链污染

(5)冻结原型

Object.freeze(Object.prototype);
Object.prototype.isAdmin = true; // 在严格模式下会报错

7.3 使用 WeakMap 替代原型扩展

传统原型扩展存在的问题

// 向原型添加方法
Array.prototype.last = function() {
  return this[this.length - 1];
};

const arr = [1, 2, 3];
console.log(arr.last()); // 3

(1)全局污染:影响所有同类对象

for (const key in []) {
  console.log(key); // 会输出 "last"
}

(2)命名冲突

Array.prototype.last = function() { return 'A' };
Array.prototype.last = function() { return 'B' }; // 被覆盖

(3)安全风险

// 恶意代码可能修改原生方法
Array.prototype.map = function() { /* 恶意操作 */ };

替代方案

(1)私有数据存储模式

const privateData = new WeakMap();

class Person {
  constructor(name) {
    privateData.set(this, { name });
  }
  
  getName() {
    return privateData.get(this).name;
  }
}

const person = new Person('Alice');
console.log(person.getName()); // "Alice"
console.log(person.name);      // undefined

(2)方法扩展替代方案

const arrayExtensions = new WeakMap();

function extendArray(arr) {
  if (!arrayExtensions.has(arr)) {
    arrayExtensions.set(arr, {
      last: function() {
        return this[this.length - 1];
      }
    });
    
    // 将方法绑定到实例
    arr.last = arrayExtensions.get(arr).last.bind(arr);
  }
  return arr;
}

const myArr = extendArray([1, 2, 3]);
console.log(myArr.last()); // 3

// 不影响其他数组
const normalArr = [4, 5];
console.log(normalArr.last); // undefined

(3)元数据关联模式

const metadata = new WeakMap();

function addMetadata(obj, data) {
  metadata.set(obj, data);
}

function getMetadata(obj) {
  return metadata.get(obj);
}

const obj = {};
addMetadata(obj, { created: Date.now() });
console.log(getMetadata(obj)); // { created: 162... }

7.4 性能优化:减少原型链深度

原型链深度对性能的影响机制

  • 属性查找:每级原型链增加 10% ~ 20% 的查找时间(V8 引擎数据)
  • 缓存失效:现代 JavaScript 引擎的隐藏类优化会被过深原型链破坏
  • 内存占用:每级原型链需要额外的内存存储引用

测试原型链深度的影响

基准测试用例

// 创建不同深度的原型链
function createChain(depth) {
  let current = {};
  for (let i = 0; i < depth; i++) {
    const newObj = Object.create(current);
    newObj[`prop${i}`] = i;
    current = newObj;
  }
  return current;
}

// 测试属性访问速度
function testAccess(obj, prop) {
  const start = Date.now();
  for (let i = 0; i < 1000000; i++) {
    obj[prop];
  }
  return Date.now() - start;
}

const depths = [1, 3, 5, 10];
depths.forEach(depth => {
  const obj = createChain(depth);
  const time = testAccess(obj, `prop${depth-1}`);
  console.log(`Depth ${depth}: ${time.toFixed(2)}ms`);
});

测试结果(Chrome 137.0.7151.120(正式版本) (x86_64))

Depth 1: 3.00ms
Depth 3: 11.00ms
Depth 5: 12.00ms
Depth 10: 10.00ms

优化策略

(1)扁平化继承结构

优化前(传统深继承):

class A { methodA() {} }
class B extends A { methodB() {} }
class C extends B { methodC() {} }
// 原型链:C → B → A → Object

优化后(组合+扁平):

class Base {
  constructor() {
    this.methods = {
      ...require('./methodA'),
      ...require('./methodB'),
      ...require('./methodC')
    };
  }
  
  methodA() { return this.methods.methodA.call(this); }
  methodB() { return this.methods.methodB.call(this); }
  methodC() { return this.methods.methodC.call(this); }
}
// 原型链:Base → Object

(2)方法直接挂载实例

优化前:

class MyClass {
  constructor() {
    this.data = heavyData;
  }
  
  process() { /* 频繁调用的方法 */ }
}
// 方法在原型上,每次访问需要查链

优化后:

class MyClass {
  constructor() {
    this.data = heavyData;
    this.process = () => { /* 直接绑定到实例 */ };
  }
}
// 首次创建成本稍高,但访问更快

(3)使用对象组合替代继承

优化前:

class User { /* 基础方法 */ }
class Admin extends User { /* 特权方法 */ }
// 原型链深度:2

优化后:

const userMethods = { /* 基础方法 */ };
const adminMethods = { /* 特权方法 */ };

function createAdmin() {
  return Object.assign({}, userMethods, adminMethods);
}
// 原型链深度:0

(4)选择性缓存高频访问方法

class HeavyUsedClass {
  constructor() {
    // 缓存高频方法引用
    this.frequentMethod = this.frequentMethod.bind(this);
  }
  
  frequentMethod() { /* ... */ }
}

const instance = new HeavyUsedClass();
// 后续调用直接使用 instance.frequentMethod() 不需要查原型链

第八章:内置对象的原型结构

8.1 数组的原型链:Array.prototype

JavaScript 中数组的原型链遵循以下层次结构

[实际数组实例] 
→ Array.prototype 
  → Object.prototype
    → null

Array.prototype 层详解 - 核心方法分类

方法类型示例方法ES版本
修改器方法push(), pop(), splice(), sort()ES1
访问方法concat(), slice(), join()ES1
迭代方法forEach(), map(), filter()ES5
查找方法find(), findIndex(), includes()ES6+
转换方法toString(), toLocaleString()ES1

8.2 函数的原型链:Function.prototype

函数原型链层次结构

[函数实例]
→ Function.prototype
  → Object.prototype
    → null

Function.prototype 层详解 - 核心方法分类

方法名作用描述返回类型
apply()指定this值并以数组形式传参函数返回值
call()指定this值并逐个传参函数返回值
bind()创建绑定this的新函数函数
toString()返回函数源码字符串字符串

特殊行为

// Function.prototype 本身是一个函数
console.log(typeof Function.prototype); // "function"

// 但无法正常调用
try {
  Function.prototype();
} catch (e) {
  console.error(e); // TypeError: Function.prototype requires 'this'
}

8.3 包装类型(String/Number/Boolean)的原型

JavaScript 中的基本类型(string/number/boolean)在被当成对象使用时,会被自动转换为对应的包装对象,这些包装对象拥有各自的原型链结构

String 包装类型

原型链结构如下:

[String实例]
→ String.prototype
  → Object.prototype
    → null

String.prototype 的核心方法有:

// 字符串操作方法
'abc'.concat('def')      // 'abcdef'
'hello'.slice(1, 3)      // 'el'
'ABC'.toLowerCase()      // 'abc'

// ES6+ 新增方法
'  abc  '.trim()         // 'abc'
'hello'.startsWith('he') // true
'abc'.repeat(2)          // 'abcabc'

// 迭代方法
'😊'.codePointAt(0)      // 128522

Number 包装类型

原型链结构如下:

[Number实例]
→ Number.prototype
  → Object.prototype
    → null

Number.prototype 核心方法:

// 转换方法
(123.456).toFixed(2)    // '123.46'
(255).toString(16)      // 'ff'

// ES6+ 新增方法
Number.isInteger(5.0)   // true
(1000).toExponential()  // '1e+3'

// 数值格式化
(123456.789).toLocaleString('de-DE') // '123.456,789'

Boolean 包装类型

原型链结构:

[Boolean实例]
→ Boolean.prototype
  → Object.prototype
    → null

Boolean.prototype 方法:

// 基本方法
true.toString()         // 'true'
false.valueOf()         // false

// 注意:Boolean包装对象的方法较少

自动装箱与拆箱机制

(1)自动装箱

// 原始值访问属性时自动创建包装对象
const str = 'hello';
console.log(str.length); // 5 (临时创建String对象)

// 等价于
const temp = new String(str);
console.log(temp.length);
temp = null; // 使用后立即销毁

(2)手动装箱与拆箱

// 手动创建包装对象
const strObj = new String('hello');
const numObj = new Number(123);
const boolObj = new Boolean(true);

// 拆箱操作
console.log(strObj.valueOf()); // 'hello'
console.log(numObj + 1);      // 124 (自动调用valueOf)

原始值与包装对象的区别

typeof 'hello'           // 'string'
typeof new String('hello') // 'object'

'hello' instanceof String   // false
new String('hello') instanceof String // true

valueOf() 的重要性

const numObj = new Number(42);
console.log(numObj === 42);       // false
console.log(numObj.valueOf() === 42); // true

8.4 修改内置原型的风险与最佳实践

主要风险

(1)全局污染

// 默认添加的属性是可枚举的
Array.prototype.customMethod = function() {};

for (const key in []) {
  console.log(key); // 会输出 "customMethod" (污染for-in循环)
}

Object.keys(Array.prototype); // 包含自定义方法

(2)命名冲突和覆盖风险

// 不同库可能修改同一原型
Array.prototype.find = function() { /* A库的实现 */ };
Array.prototype.find = function() { /* B库的实现 */ }; // 后者覆盖前者

// 未来ECMAScript标准可能引入同名方法
Array.prototype.includes = function() { /* 自定义实现 */ };
// 当ES2016引入标准includes方法时产生冲突

(3)性能影响

// 修改原型会破坏引擎优化
Object.prototype.newMethod = function() {};

// V8引擎的隐藏类机制受影响
const obj = {};
obj.newMethod(); // 触发隐藏类转换,降低性能

(4)安全漏洞

// 污染toString方法
Object.prototype.toString = function() {
  return '<script>maliciousCode()</script>';
};

// 当系统调用toString时
document.body.innerHTML = someObject.toString(); // 执行恶意代码

(5)预期外行为

// 修改基础对象原型会影响所有对象
Object.prototype.isEmpty = function() {
  return Object.keys(this).length === 0;
};

// 意外影响第三方代码
const set = new Set();
set.isEmpty(); // 抛出TypeError(Set没有keys方法)

最佳实践

(1)安全扩展模式

// 使用Object.defineProperty控制属性特性
Object.defineProperty(Array.prototype, 'safeMethod', {
  value: function() { /* 实现 */ },
  writable: true,      // 允许后续修改
  enumerable: false,   // 不会出现在for-in循环
  configurable: true   // 允许后续删除
});

// 检查方法是否已存在
if (!Array.prototype.safeMethod) {
  // 安全添加
}

(2)使用 Symbol 作为键

// 创建唯一Symbol键
const arrayUniqueMethod = Symbol('arrayUniqueMethod');

Array.prototype[arrayUniqueMethod] = function() {
  return [...new Set(this)];
};

[1,2,2,3][arrayUniqueMethod](); // [1,2,3]

(3)模块化扩展模式

// 不修改原型,提供工具函数
const ArrayUtils = {
  unique: function(arr) {
    return [...new Set(arr)];
  },
  shuffle: function(arr) { /* ... */ }
};

// 使用方式
ArrayUtils.unique([1,2,2,3]);

(4)子类化替代方案

class SafeArray extends Array {
  unique() {
    return [...new Set(this)];
  }
}

const arr = new SafeArray(1,2,2,3);
arr.unique(); // [1,2,3]

(5)使用 Proxy 封装

function createEnhancedArray(arr) {
  return new Proxy(arr, {
    get(target, prop) {
      if (prop === 'unique') {
        return () => [...new Set(target)];
      }
      return target[prop];
    }
  });
}

const arr = createEnhancedArray([1,2,2,3]);
arr.unique(); // [1,2,3]

决策流程

是否需要修改内置原型?
├─ 是 → 
│  ├─ 是否为标准Polyfill? → 使用Object.defineProperty规范实现
│  ├─ 是否为项目特有需求? → 考虑子类化或工具函数
│  └─ 是否影响第三方代码? → 全面测试兼容性
└─ 否 → 
   ├─ 使用工具函数/模块
   ├─ 使用现代语法特性
   └─ 考虑函数组合模式

总结建议

  1. 基本原则:永远不要修改 Object.prototype;避免修改其他内置原型,除非绝对必要;
  2. 必须修改时要有安全措施
  3. 长期维护考量:记录所有内置原型修改;为自定义方法/属性添加前缀(比如_myLibUnique);定期检查与最新 ECMAScript 标准的兼容性
  4. 团队协作规范:代码 review 中严格检查原型修改;使用 ESLint 规则限制;项目文档中明确记录扩展方法

第九章:现代 JavaScript 中的原型

9.1 ES6 class 语法糖的本质

class 语法并非 ES6 全新引入的面向对象的继承模型,而是 JavaScript 现有的原型继承的语法糖

结构对比

// ES5构造函数写法
function Person(name) {
  this.name = name;
}

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

const person = new Person('Alice');

// ES6 class 写法
class Person {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log(`Hello, ${this.name}`);
  }
}

const person = new Person('Alice');

Babel 转译后的代码

"use strict";

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

var Person = /*#__PURE__*/function () {
  function Person(name) {
    _classCallCheck(this, Person);
    this.name = name;
  }

  _createClass(Person, [{
    key: "sayHello",
    value: function sayHello() {
      console.log("Hello, ".concat(this.name));
    }
  }]);

  return Person;
}();

核心特性解析

(1)构造函数对应关系

class Foo {
  constructor() {
    this.bar = 'baz';
  }
}

// 等价于
function Foo() {
  this.bar = 'baz';
}

(2)方法定义本质

class Foo {
  method() {}
}

// 等价于
Foo.prototype.method = function() {};

(3)静态成员实现

class Foo {
  static staticMethod() {}
}

// 等价于
Foo.staticMethod = function() {};

(4)继承机制原理

class Child extends Parent {
  constructor() {
    super();
  }
}

// 近似等价于
function Child() {
  Parent.call(this);
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

本质特性

  • 原型继承的语法糖:没有引入新继承模型
  • 更严格的语法:强制使用 new,默认严格模式
  • 更清晰的封装:明确区分构造函数、方法和静态成员
  • 更好的可读性:类似传统OOP语言的语法结构
  • 不可枚举的方法:避免 for-in 循环污染

9.2 super 关键字的原型链逻辑

super 是 ES6 class 中用于访问父类内容的关键字

两种调用方式

(1)作为函数调用(仅在构造函数中)

class Parent {
  constructor(name) {
    this.name = name;
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类构造函数
    this.age = age;
  }
}

原型链逻辑如下:

  1. 创建 Child 实例时,先调用 super() 相当于 Parent.call(this)
  2. 确保 this 先被父类初始化
  3. 建立正确的原型链关系:ChildInstance.__proto__Child.prototype.__proto__Parent.prototype

(2)作为对象引用(在方法中)

class Parent {
  sayHello() {
    return 'Hello from Parent';
  }
}

class Child extends Parent {
  sayHello() {
    return super.sayHello() + ' and Child';
  }
}

原型链逻辑如下:

  1. super 指向父类原型:super = Object.getPrototypeOf(Child.prototype)
  2. 方法调用时 this 仍指向当前实例
  3. 相当于 Parent.prototype.sayHello.call(this)

原型链查找机制

(1)方法中的 super 查找路径

class A { method() {} }
class B extends A { method() { super.method(); } }
class C extends B { method() { super.method(); } }

const c = new C();
c.method();

其查找过程如下:

  1. C.prototype.method 中的 super → B.prototype
  2. B.prototype.method 中的 super → A.prototype
  3. 最终调用 A.prototype.method

(2)静态方法中的 super

class Parent {
  static staticMethod() {
    return 'Parent static';
  }
}

class Child extends Parent {
  static staticMethod() {
    return super.staticMethod() + ' + Child static';
  }
}

其查找逻辑:

  1. super 指向父类本身:super = Object.getPrototypeOf(Child)
  2. 相当于 Parent.staticMethod.call(this)

底层实现原理

(1)Babel 转译后的代码

class Child extends Parent {
  method() {
    super.parentMethod();
  }
}

// 转译为
var Child = (function(_Parent) {
  _inherits(Child, _Parent);
  
  function Child() {
    _classCallCheck(this, Child);
    return _possibleConstructorReturn(this, _getPrototypeOf(Child).apply(this, arguments));
  }

  _createClass(Child, [{
    key: "method",
    value: function method() {
      _get(_getPrototypeOf(Child.prototype), "parentMethod", this).call(this);
    }
  }]);

  return Child;
})(Parent);

(2)关键帮助函数

function _getPrototypeOf(o) {
  return Object.getPrototypeOf(o);
}

function _get(target, property, receiver) {
  // 模拟 super 属性查找
  if (typeof Reflect !== "undefined" && Reflect.get) {
    return Reflect.get(target, property, receiver || this);
  }
  var base = _getPrototypeOf(target);
  if (!base) return;
  var desc = Object.getOwnPropertyDescriptor(base, property);
  if (desc.get) {
    return desc.get.call(receiver || this);
  }
  return base[property];
}

9.3 静态方法与原型的关系

静态方法是 JavaScript 类中直接绑定到构造函数本身而非原型上的方法

核心概念对比

特性静态方法原型方法
存储位置构造函数本身构造函数的 prototype 对象
调用方式通过类名调用(ClassName.method())通过实例调用(instance.method())
this 指向类构造函数实例对象
继承行为可被子类继承实例可访问
使用场景工具方法/工厂方法实例相关操作

内存结构与原型链分析

class MyClass {
  static staticMethod() {
    console.log('Static');
  }
  
  prototypeMethod() {
    console.log('Prototype');
  }
}

// 内存结构等价于:
function MyClass() {}

// 静态方法(直接挂载构造函数)
MyClass.staticMethod = function() {
  console.log('Static');
};

// 原型方法
MyClass.prototype.prototypeMethod = function() {
  console.log('Prototype');
};

底层实现(Babel 转译)

// 静态方法继承的实现
function _inherits(subClass, superClass) {
  // 设置原型继承
  subClass.__proto__ = superClass;
  
  // 复制静态方法
  if (superClass) {
    Object.setPrototypeOf 
      ? Object.setPrototypeOf(subClass, superClass)
      : subClass.__proto__ = superClass;
  }
}

9.4 符号属性(Symbol)对原型的影响

Symbol 在原型系统中的行为

(1)原型上的 Symbol 属性

const customMethod = Symbol('custom');

class MyClass {
  [customMethod]() {
    console.log('Symbol method called');
  }
}

const instance = new MyClass();
instance[customMethod](); // "Symbol method called"

// 检查原型链
console.log(customMethod in MyClass.prototype); // true
console.log(instance.hasOwnProperty(customMethod)); // false

(2)内置 Symbol 与原型

class CustomIterable {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
}

[...new CustomIterable()]; // [1, 2, 3]

Symbol 对原型系统的特殊影响

(1)避免属性名冲突

// 不同库安全扩展原型
const lib1Method = Symbol('lib1');
const lib2Method = Symbol('lib2');

Array.prototype[lib1Method] = function() { /* Lib1实现 */ };
Array.prototype[lib2Method] = function() { /* Lib2实现 */ };

// 互不干扰
[][lib1Method]();
[][lib2Method]();

(2)隐藏原型方法

const internalLogic = Symbol('internal');

class SecureAPI {
  constructor() {
    this.publicData = 'safe';
  }
  
  [internalLogic]() {
    console.log('内部处理逻辑');
  }
  
  publicMethod() {
    this[internalLogic]();
    return this.publicData;
  }
}

const api = new SecureAPI();
api.publicMethod(); // 正常工作
api[internalLogic](); // 报错:除非能获取Symbol引用

(3)不可枚举性对原型遍历的影响

Array.prototype[Symbol('test')] = function() {};

// 不影响常规遍历
for (const key in []) {
  console.log(key); // 无输出(无枚举属性)
}

// 也不影响Object.keys
console.log(Object.keys(Array.prototype)); // 不包含Symbol属性

内置 Symbol 与原型行为定制

(1)Symbol.hasInstance

class MySpecialArray {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}

console.log([] instanceof MySpecialArray); // true

(2)Symbol.species

class MyArray extends Array {
  static get [Symbol.species]() {
    return Array; // 控制衍生对象的构造函数
  }
}

const myArr = new MyArray(1,2,3);
const mapped = myArr.map(x => x*2);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true

(3)Symbol.toStringTag

class MyCollection {
  get [Symbol.toStringTag]() {
    return 'MyCollection';
  }
}

console.log(new MyCollection().toString()); 
// "[object MyCollection]"

Symbol 属性的继承

const parentSymbol = Symbol('parent');

class Parent {
  [parentSymbol]() {
    console.log('Parent symbol method');
  }
}

class Child extends Parent {}

const child = new Child();
child[parentSymbol](); // "Parent symbol method"

// 检查继承关系
console.log(
  Object.getOwnPropertySymbols(Parent.prototype).includes(parentSymbol)
); // true
console.log(
  Object.getOwnPropertySymbols(Child.prototype).includes(parentSymbol)
); // false(继承而非自有)

第十章:实战与面试题解析

10.1 手写 new 操作符实现

function myNew(Constructor, ...args) {
  // 验证输入
  if (typeof Constructor !== 'function') {
    throw new TypeError('First argument must be a function');
  }
  
  // 创建新对象并设置原型
  const obj = Object.create(Constructor.prototype);
  
  // 执行构造函数
  const result = Constructor.apply(obj, args);
  
  // 处理返回值
  return (result && (typeof result === 'object' || typeof result === 'function'))
    ? result 
    : obj;
}

10.2 手写 Object.create

function myObjectCreate(proto) {
  if (typeof proto !== 'object' && typeof proto !== 'function' && proto !== null) {
    throw new TypeError('Object prototype may only be an Object or null');
  }
  
  function F() {}
  F.prototype = proto;
  return new F();
}

使用示例:

const parent = { name: 'Parent' };
const child = myObjectCreate(parent);

console.log(child.name); // 'Parent'
console.log(Object.getPrototypeOf(child) === parent); // true

10.3 如何实现一个完美的深拷贝(处理原型链)

完整代码

function deepClone(source, map = new WeakMap()) {
  // 处理基本类型和函数
  if (source === null || typeof source !== 'object') {
    return source;
  }

  // 处理循环引用
  if (map.has(source)) {
    return map.get(source);
  }

  // 获取原型
  const proto = Object.getPrototypeOf(source);
  
  // 处理特殊对象类型
  if (source instanceof Date) {
    return new Date(source);
  }
  
  if (source instanceof RegExp) {
    return new RegExp(source);
  }
  
  if (source instanceof Map) {
    const cloneMap = new Map();
    map.set(source, cloneMap);
    source.forEach((value, key) => {
      cloneMap.set(deepClone(key, map), deepClone(value, map));
    });
    return cloneMap;
  }
  
  if (source instanceof Set) {
    const cloneSet = new Set();
    map.set(source, cloneSet);
    source.forEach(value => {
      cloneSet.add(deepClone(value, map));
    });
    return cloneSet;
  }
  
  if (source instanceof ArrayBuffer) {
    return source.slice(0);
  }
  
  if (ArrayBuffer.isView(source)) {
    return new source.constructor(
      source.buffer.slice(0),
      source.byteOffset,
      source.byteLength
    );
  }

  // 创建目标对象并保留原型链
  let target;
  if (Array.isArray(source)) {
    target = new proto.constructor(source.length);
  } else {
    target = Object.create(proto);
  }
  
  // 记录已拷贝对象
  map.set(source, target);

  // 处理Symbol属性
  const symbolKeys = Object.getOwnPropertySymbols(source);
  const allKeys = [...Object.keys(source), ...symbolKeys];
  
  // 递归拷贝所有属性
  for (const key of allKeys) {
    // 跳过原型属性
    if (!source.hasOwnProperty(key)) continue;
    
    // 处理属性描述符
    const descriptor = Object.getOwnPropertyDescriptor(source, key);
    if (descriptor && !descriptor.enumerable) continue;
    
    target[key] = deepClone(source[key], map);
  }
  
  return target;
}

使用示例

// 测试原型链保持
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function() {
  return `Hello, ${this.name}`;
};

const person = new Person('Alice');
const clonedPerson = deepClone(person);

console.log(person.greet()); // "Hello, Alice"
console.log(clonedPerson.greet()); // "Hello, Alice"
console.log(Object.getPrototypeOf(clonedPerson) === Person.prototype); // true

// 测试循环引用
const obj = { a: 1 };
obj.self = obj;
const clonedObj = deepClone(obj);
console.log(clonedObj.self === clonedObj); // true

// 测试特殊对象
const map = new Map([['key', 'value']]);
const clonedMap = deepClone(map);
console.log(clonedMap.get('key')); // 'value'

const date = new Date();
const clonedDate = deepClone(date);
console.log(clonedDate.getTime() === date.getTime()); // true

关键特性

  • 原型链保留:
    • 使用 Object.create(proto) 创建对象
    • 保持原型链不变
  • 循环引用处理:
    • 使用 WeakMap 跟踪已拷贝对象
    • 避免无限递归
  • 全面类型支持:
    • 基本类型
    • 对象/数组
    • 函数(直接引用)
    • Date/RegExp
    • Map/Set
    • ArrayBuffer/TypedArray
    • Symbol 属性
  • 属性描述符处理:
    • 跳过不可枚举属性
    • 保留属性特性
  • 性能优化:
    • 使用 WeakMap 避免内存泄漏
    • 最小化属性遍历