0%

对象与原型系统(原型链、原型对象)

目标:掌握对象的创建、属性特性、原型与原型链、各类继承方式,以及 ES6 类语法的本质与坑点。

目录

  1. 对象基础与属性特性
  2. 构造函数、实例与原型
  3. 内建对象与原型
  4. 继承方式详解
  5. 常见坑与排错
  6. 代码片段(直接可用)
  7. 实战应用场景
  8. 速记与总结

1. 对象基础与属性特性

1.1 对象的定义方式

在 JavaScript 中,对象是键值对的集合,是 JavaScript 中最基本的数据结构之一。有多种方式可以创建对象:

1.1.1 对象字面量(最常用)

这是最简单直接的方式,适合创建单个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 基本对象字面量
const person = {
name: '张三',
age: 25,
greet() {
console.log(`你好,我是${this.name}`);
}
};

// 使用计算属性名
const key = 'dynamicKey';
const obj = {
[key]: 'value',
[`${key}2`]: 'value2'
};

// 简写语法(ES6)
const name = '李四';
const age = 30;
const person2 = { name, age }; // 等价于 { name: name, age: age }

适用场景:创建配置对象、数据传递、单例对象等。

1.1.2 构造函数 + new(传统方式)

使用构造函数可以创建多个相似的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义构造函数(约定首字母大写)
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
console.log(`你好,我是${this.name}`);
};
}

// 使用 new 创建实例
const person1 = new Person('张三', 25);
const person2 = new Person('李四', 30);

console.log(person1.name); // '张三'
console.log(person2.name); // '李四'

注意:每个实例都会创建自己的 greet 方法,造成内存浪费。更好的做法是将方法放在原型上(见后续章节)。

1.1.3 Object.create()(精确控制原型)

Object.create() 可以创建一个以指定对象为原型的新对象,这是最灵活的方式:

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
30
// 创建一个以 null 为原型的对象(完全干净的对象)
const cleanObj = Object.create(null);
console.log(cleanObj.toString); // undefined(没有继承 Object 的方法)

// 创建一个以其他对象为原型的对象
const parent = {
name: '父对象',
say() {
console.log('我是父对象');
}
};

const child = Object.create(parent);
child.age = 10;
console.log(child.name); // '父对象'(继承自 parent)
console.log(child.age); // 10(自有属性)

// 同时定义属性描述符
const obj = Object.create(parent, {
id: {
value: 1,
writable: false, // 不可写
enumerable: true, // 可枚举
configurable: false // 不可配置
},
name: {
get() { return '自定义名称'; },
enumerable: true
}
});

适用场景:需要精确控制原型链、创建纯净对象、实现继承等。

1.1.4 ES6 类语法(现代推荐)

class 是 ES6 引入的语法糖,本质仍然是构造函数 + 原型,但语法更清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}

greet() {
console.log(`你好,我是${this.name}`);
}

// 静态方法(属于类本身,不属于实例)
static createDefault() {
return new Person('默认名称', 0);
}
}

const person = new Person('张三', 25);
person.greet(); // '你好,我是张三'

// 调用静态方法
const defaultPerson = Person.createDefault();

优势

  • 语法更接近传统面向对象语言
  • 自动处理原型链
  • 支持继承(extends)
  • 代码更易读易维护

1.2 属性类型与特性(属性描述符)

JavaScript 中的每个属性都有描述符(descriptor),用于控制属性的行为。理解描述符是掌握对象高级特性的关键。

1.2.1 数据属性(Data Property)

数据属性包含一个值,有四个特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const obj = {};

// 方式1:直接定义(默认特性)
obj.name = '张三';
// 等价于:
Object.defineProperty(obj, 'name', {
value: '张三',
writable: true, // 可写
enumerable: true, // 可枚举(for...in 可见)
configurable: true // 可配置(可删除、可修改描述符)
});

// 方式2:使用 defineProperty 精确控制
Object.defineProperty(obj, 'id', {
value: 1,
writable: false, // 不可修改
enumerable: true, // 可枚举
configurable: false // 不可删除、不可重新配置
});

obj.id = 2; // 静默失败(严格模式下会报错)
delete obj.id; // false(删除失败)

四个特性的详细说明

  1. **value**:属性的值

    1
    2
    Object.defineProperty(obj, 'x', { value: 10 });
    console.log(obj.x); // 10
  2. **writable**:是否可写

    1
    2
    3
    4
    5
    Object.defineProperty(obj, 'readonly', {
    value: '不可修改',
    writable: false
    });
    obj.readonly = '新值'; // 在严格模式下会报错,非严格模式下静默失败
  3. **enumerable**:是否可枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Object.defineProperty(obj, 'hidden', {
    value: '隐藏属性',
    enumerable: false
    });

    console.log(obj.hidden); // '隐藏属性'(可以访问)
    for (let key in obj) {
    console.log(key); // 不会打印 'hidden'
    }
    console.log(Object.keys(obj)); // 不包含 'hidden'
  4. **configurable**:是否可配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Object.defineProperty(obj, 'locked', {
    value: '锁定',
    configurable: false
    });

    // configurable: false 时,不能:
    // 1. 删除属性
    delete obj.locked; // false

    // 2. 修改描述符(但可以将 writable: true 改为 false)
    Object.defineProperty(obj, 'locked', {
    writable: false // 允许(writable 只能从 true 变 false)
    });

    // 3. 将属性改为访问器属性
    // Object.defineProperty(obj, 'locked', { get() {} }); // 报错

1.2.2 访问器属性(Accessor Property)

访问器属性不包含值,而是通过 getset 函数来控制读写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const user = {
_age: 0 // 约定:下划线开头表示私有属性(仅约定,非真正私有)
};

Object.defineProperty(user, 'age', {
get() {
console.log('读取 age');
return this._age;
},
set(value) {
console.log('设置 age');
if (value < 0 || value > 150) {
throw new Error('年龄必须在 0-150 之间');
}
this._age = value;
},
enumerable: true,
configurable: true
});

user.age = 25; // '设置 age'
console.log(user.age); // '读取 age' \n 25
user.age = 200; // 抛出错误

访问器属性的特点

  • 没有 valuewritable 特性
  • 只有 getset 函数
  • 可以实现数据验证、计算属性、副作用等

实际应用示例

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
30
31
// 示例1:计算属性
const rectangle = {
width: 10,
height: 20
};

Object.defineProperty(rectangle, 'area', {
get() {
return this.width * this.height;
},
enumerable: true
});

console.log(rectangle.area); // 200
rectangle.width = 15;
console.log(rectangle.area); // 300(自动重新计算)

// 示例2:数据验证
const form = {
_email: ''
};

Object.defineProperty(form, 'email', {
get() { return this._email; },
set(value) {
if (!value.includes('@')) {
throw new Error('邮箱格式不正确');
}
this._email = value;
}
});

1.2.3 批量定义属性

使用 Object.defineProperties() 可以一次性定义多个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const obj = {};

Object.defineProperties(obj, {
name: {
value: '张三',
writable: false,
enumerable: true
},
age: {
value: 25,
writable: true,
enumerable: true
},
id: {
get() { return Math.random().toString(36); },
enumerable: true
}
});

1.2.4 获取属性描述符

使用 Object.getOwnPropertyDescriptor() 查看属性的描述符:

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = { name: '张三' };
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');

console.log(descriptor);
// {
// value: '张三',
// writable: true,
// enumerable: true,
// configurable: true
// }

// 获取所有自有属性的描述符
const allDescriptors = Object.getOwnPropertyDescriptors(obj);

1.3 可枚举性与遍历

理解可枚举性对于正确遍历对象至关重要。不同的遍历方法有不同的行为。

1.3.1 什么是可枚举性?

可枚举性(enumerable)决定了属性是否会在某些遍历操作中出现。默认情况下,直接定义的属性都是可枚举的。

1
2
3
4
5
6
7
8
9
10
const obj = {
name: '张三', // 可枚举
age: 25 // 可枚举
};

// 不可枚举的属性
Object.defineProperty(obj, 'id', {
value: 1,
enumerable: false
});

1.3.2 各种遍历方法对比

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const parent = { parentProp: '父属性' };
const obj = Object.create(parent);

// 自有可枚举属性
obj.name = '张三';
obj.age = 25;

// 自有不可枚举属性
Object.defineProperty(obj, 'id', {
value: 1,
enumerable: false
});

// Symbol 属性
const sym = Symbol('symbolKey');
obj[sym] = 'symbol值';

console.log('=== for...in ===');
for (let key in obj) {
console.log(key);
// 'name', 'age', 'parentProp'
// 注意:会遍历继承的可枚举属性
}

console.log('=== Object.keys() ===');
console.log(Object.keys(obj));
// ['name', 'age']
// 只返回自有可枚举的字符串键

console.log('=== Object.getOwnPropertyNames() ===');
console.log(Object.getOwnPropertyNames(obj));
// ['name', 'age', 'id']
// 返回自有所有字符串键(包括不可枚举)

console.log('=== Object.getOwnPropertySymbols() ===');
console.log(Object.getOwnPropertySymbols(obj));
// [Symbol(symbolKey)]
// 返回自有所有 Symbol 键

console.log('=== Reflect.ownKeys() ===');
console.log(Reflect.ownKeys(obj));
// ['name', 'age', 'id', Symbol(symbolKey)]
// 返回自有所有键(字符串 + Symbol)

console.log('=== Object.values() / Object.entries() ===');
console.log(Object.values(obj));
// ['张三', 25]
// 只包含可枚举自有属性的值

console.log(Object.entries(obj));
// [['name', '张三'], ['age', 25]]

选择指南

方法 遍历范围 包含不可枚举 包含 Symbol 包含继承
for...in 可枚举
Object.keys() 自有可枚举
Object.getOwnPropertyNames() 自有所有字符串键
Object.getOwnPropertySymbols() 自有 Symbol 键 -
Reflect.ownKeys() 自有所有键

1.3.3 实际应用场景

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
30
31
// 场景1:只遍历自有属性(过滤继承属性)
function getOwnKeys(obj) {
const keys = [];
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
keys.push(key);
}
}
return keys;
}

// 更简洁的方式
const ownKeys = Object.keys(obj);

// 场景2:深拷贝时只拷贝可枚举属性
function deepClone(obj) {
const cloned = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = typeof obj[key] === 'object'
? deepClone(obj[key])
: obj[key];
}
}
return cloned;
}

// 场景3:获取所有属性(包括不可枚举和 Symbol)
function getAllKeys(obj) {
return Reflect.ownKeys(obj);
}

1.4 对象冻结/密封/扩展控制

JavaScript 提供了三个层次的对象保护机制,用于防止对象被意外修改。

1.4.1 Object.preventExtensions() - 禁止扩展

禁止向对象添加新属性,但可以修改和删除现有属性:

1
2
3
4
5
6
7
8
9
const obj = { name: '张三', age: 25 };

Object.preventExtensions(obj);

obj.name = '李四'; // ✅ 可以修改
delete obj.age; // ✅ 可以删除
obj.newProp = '新属性'; // ❌ 静默失败(严格模式下报错)

console.log(Object.isExtensible(obj)); // false(检查是否可扩展)

适用场景:防止意外添加属性,但允许修改现有属性。

1.4.2 Object.seal() - 密封对象

preventExtensions 的基础上,还将所有现有属性设为 configurable: false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = { name: '张三', age: 25 };

Object.seal(obj);

obj.name = '李四'; // ✅ 可以修改值
delete obj.age; // ❌ 不能删除
obj.newProp = '新属性'; // ❌ 不能添加

// 不能重新配置属性
Object.defineProperty(obj, 'name', {
enumerable: false // ❌ 报错:不能修改已密封对象的属性描述符
});

console.log(Object.isSealed(obj)); // true

特点

  • 不能添加新属性
  • 不能删除现有属性
  • 不能修改属性描述符
  • 可以修改属性值

1.4.3 Object.freeze() - 冻结对象

seal 的基础上,还将所有数据属性设为 writable: false

1
2
3
4
5
6
7
8
9
const obj = { name: '张三', age: 25 };

Object.freeze(obj);

obj.name = '李四'; // ❌ 不能修改值(静默失败)
delete obj.age; // ❌ 不能删除
obj.newProp = '新属性'; // ❌ 不能添加

console.log(Object.isFrozen(obj)); // true

注意:浅冻结

Object.freeze() 只冻结对象本身,不会冻结嵌套对象:

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
const obj = {
name: '张三',
address: {
city: '北京',
street: '某街道'
}
};

Object.freeze(obj);

obj.name = '李四'; // ❌ 失败
obj.address.city = '上海'; // ✅ 成功!因为 address 对象没有被冻结

// 深冻结需要递归
function deepFreeze(obj) {
Object.freeze(obj);
for (let key in obj) {
if (obj.hasOwnProperty(key) && typeof obj[key] === 'object' && obj[key] !== null) {
deepFreeze(obj[key]);
}
}
return obj;
}

deepFreeze(obj);
obj.address.city = '上海'; // ❌ 现在会失败

1.4.4 三种保护级别的对比

操作 正常对象 preventExtensions seal freeze
添加属性
删除属性
修改属性值
修改属性描述符

1.4.5 实际应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 场景1:配置对象(防止意外修改)
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
Object.freeze(config);

// 场景2:常量对象
const CONSTANTS = {
STATUS: {
PENDING: 'pending',
SUCCESS: 'success',
ERROR: 'error'
}
};
Object.freeze(CONSTANTS);
Object.freeze(CONSTANTS.STATUS); // 深冻结

// 场景3:不可变数据(函数式编程)
function createImmutableUser(name, age) {
const user = { name, age };
return Object.freeze(user);
}

2. 构造函数、实例与原型

这是 JavaScript 面向对象编程的核心。理解构造函数、实例和原型的关系,是掌握 JavaScript 继承机制的基础。

2.1 构造函数与实例关系

2.1.1 new 操作符的执行过程

当你使用 new 操作符调用函数时,JavaScript 会执行以下 4 个步骤:

1
2
3
4
5
6
function Person(name, age) {
this.name = name;
this.age = age;
}

const person = new Person('张三', 25);

详细步骤解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 步骤1:创建一个空对象
// 内部执行:const newObj = {};

// 步骤2:将新对象的 [[Prototype]] 指向构造函数的 prototype
// 内部执行:newObj.__proto__ = Person.prototype;
// 或者:Object.setPrototypeOf(newObj, Person.prototype);

// 步骤3:将构造函数内部的 this 绑定到新对象,并执行构造函数
// 内部执行:Person.call(newObj, '张三', 25);
// 此时 this.name = '张三', this.age = 25 会设置到 newObj 上

// 步骤4:判断返回值
// - 如果构造函数返回一个对象,则返回该对象
// - 否则返回新创建的对象

手动模拟 new 操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myNew(Constructor, ...args) {
// 步骤1:创建空对象
const obj = {};

// 步骤2:连接原型
Object.setPrototypeOf(obj, Constructor.prototype);
// 或者:obj.__proto__ = Constructor.prototype;

// 步骤3:执行构造函数,绑定 this
const result = Constructor.apply(obj, args);

// 步骤4:返回结果
return result instanceof Object ? result : obj;
}

// 使用
const person = myNew(Person, '张三', 25);

2.1.2 构造函数返回值的特殊情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 情况1:返回基本类型(忽略,返回新对象)
function Person1(name) {
this.name = name;
return '字符串'; // 被忽略
}
const p1 = new Person1('张三');
console.log(p1); // { name: '张三' }

// 情况2:返回对象(使用返回的对象)
function Person2(name) {
this.name = name;
return { custom: '自定义对象' };
}
const p2 = new Person2('张三');
console.log(p2); // { custom: '自定义对象' }(不是 { name: '张三' })

// 情况3:不返回(默认返回新对象)
function Person3(name) {
this.name = name;
// 没有 return
}
const p3 = new Person3('张三');
console.log(p3); // { name: '张三' }

最佳实践:构造函数通常不应该返回值,让 JavaScript 自动返回新对象。

2.1.3 判断函数是否作为构造函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name) {
// 判断是否使用 new 调用
if (!(this instanceof Person)) {
throw new Error('必须使用 new 调用');
}
// 或者使用 new.target(ES6)
if (new.target !== Person) {
throw new Error('必须使用 new 调用');
}

this.name = name;
}

// 错误调用
Person('张三'); // 抛出错误

// 正确调用
new Person('张三'); // 正常

2.2 原型对象(prototype)与实例原型([[Prototype]]/__proto__)

这是最容易混淆的概念。让我们详细区分它们。

2.2.1 三个关键概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name) {
this.name = name;
}

// 1. Person.prototype - 构造函数的原型对象
// 这是函数对象的一个属性,用于存储共享的方法和属性
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};

// 2. person.__proto__ - 实例的原型(已废弃,但广泛支持)
// 这是实例对象指向其构造函数的 prototype 的引用
const person = new Person('张三');
console.log(person.__proto__ === Person.prototype); // true

// 3. [[Prototype]] - 内部原型链(不可直接访问)
// 这是 JavaScript 引擎内部的属性,__proto__ 是它的访问器

关系图

1
2
3
4
5
6
7
8
9
Person (构造函数)
└── prototype (属性)
└── Person.prototype (原型对象)
├── sayHello (方法)
└── constructor (指向 Person)

person (实例)
└── [[Prototype]] (内部属性,通过 __proto__ 访问)
└── 指向 Person.prototype

2.2.2 推荐的原型操作方法

虽然 __proto__ 被广泛支持,但推荐使用标准方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const person = new Person('张三');

// ❌ 不推荐:直接使用 __proto__(已废弃)
person.__proto__ = someProto;

// ✅ 推荐:使用 Object.getPrototypeOf() 获取原型
const proto = Object.getPrototypeOf(person);
console.log(proto === Person.prototype); // true

// ✅ 推荐:使用 Object.setPrototypeOf() 设置原型
Object.setPrototypeOf(person, someProto);

// ✅ 推荐:创建时指定原型
const newObj = Object.create(Person.prototype);

为什么推荐标准方法?

  1. 性能Object.setPrototypeOf() 虽然也有性能问题,但 __proto__ 在某些引擎中可能被优化掉
  2. 可移植性:标准方法在所有环境中都可用
  3. 语义清晰:明确表达意图

2.2.3 prototype 属性的特殊性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person() {}

// 每个函数都有 prototype 属性(箭头函数除外)
console.log(typeof Person.prototype); // 'object'

// prototype 对象自动有一个 constructor 属性指向构造函数
console.log(Person.prototype.constructor === Person); // true

// 可以修改 constructor(但不推荐)
Person.prototype.constructor = function FakePerson() {};
// 这会导致 instanceof 判断出错

// 最佳实践:如果替换了 prototype,记得修复 constructor
function Animal() {}
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复 constructor

2.3 原型链查找规则

理解原型链查找是掌握 JavaScript 继承的关键。

2.3.1 属性读取的查找过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('你好');
};
Person.prototype.species = '人类';

const person = new Person('张三');

// 查找过程演示
console.log(person.name); // 1. 先在 person 自身查找 → 找到 '张三'
console.log(person.sayHello); // 2. person 自身没有,查找 person.__proto__ (Person.prototype) → 找到函数
console.log(person.species); // 3. person 自身没有,查找 Person.prototype → 找到 '人类'
console.log(person.toString); // 4. person 自身没有,Person.prototype 也没有
// 查找 Person.prototype.__proto__ (Object.prototype) → 找到 toString
console.log(person.unknown); // 5. 一直找到 null,返回 undefined

查找路径可视化

1
2
3
4
5
6
7
8
9
person
├── name: '张三' ✅ (找到,返回)
└── __proto__ → Person.prototype
├── sayHello: function ✅ (找到,返回)
├── species: '人类' ✅ (找到,返回)
└── __proto__ → Object.prototype
├── toString: function ✅ (找到,返回)
├── valueOf: function
└── __proto__ → null ❌ (停止,返回 undefined)

2.3.2 属性写入的特殊规则

重要:写入属性时,不会修改原型链上的属性,而是在实例自身创建新属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name) {
this.name = name;
}
Person.prototype.species = '人类';

const person1 = new Person('张三');
const person2 = new Person('李四');

// 读取:从原型链查找
console.log(person1.species); // '人类'(来自 Person.prototype)
console.log(person2.species); // '人类'(来自 Person.prototype)

// 写入:在实例自身创建属性(不会修改原型)
person1.species = '外星人';
console.log(person1.species); // '外星人'(自有属性)
console.log(person2.species); // '人类'(仍然是原型上的值)

// 验证
console.log(person1.hasOwnProperty('species')); // true(自有属性)
console.log(person2.hasOwnProperty('species')); // false(继承属性)
console.log(Person.prototype.species); // '人类'(原型未被修改)

特殊情况:访问器属性

如果原型上的属性是访问器属性(有 setter),写入时会调用 setter:

1
2
3
4
5
6
7
8
9
10
11
12
const parent = {
_value: 0,
get value() { return this._value; },
set value(v) {
console.log('设置值');
this._value = v;
}
};

const child = Object.create(parent);
child.value = 10; // '设置值'(调用父原型的 setter)
// 但 setter 中的 this 指向 child,所以 _value 被设置到 child 上

2.3.3 方法共享的优势

将方法定义在原型上可以节省内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 不好的做法:每个实例都有自己的方法
function Person1(name) {
this.name = name;
this.sayHello = function() { // 每个实例都创建新函数
console.log('你好');
};
}
const p1 = new Person1('张三');
const p2 = new Person1('李四');
console.log(p1.sayHello === p2.sayHello); // false(不同函数)

// ✅ 好的做法:方法定义在原型上
function Person2(name) {
this.name = name;
}
Person2.prototype.sayHello = function() { // 所有实例共享
console.log('你好');
};
const p3 = new Person2('张三');
const p4 = new Person2('李四');
console.log(p3.sayHello === p4.sayHello); // true(同一个函数)

内存对比

  • 1000 个实例,每个实例有 10 个方法
  • 方法在构造函数中:1000 × 10 = 10,000 个函数对象
  • 方法在原型上:10 个函数对象(所有实例共享)

2.4 判断关系

JavaScript 提供了多种方法来判断对象之间的关系。

2.4.1 hasOwnProperty() - 判断自有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name) {
this.name = name;
}
Person.prototype.species = '人类';

const person = new Person('张三');

console.log(person.hasOwnProperty('name')); // true(自有属性)
console.log(person.hasOwnProperty('species')); // false(继承属性)
console.log(person.hasOwnProperty('toString')); // false(继承属性)

// 注意:如果对象没有原型(Object.create(null)),需要这样调用
const cleanObj = Object.create(null);
cleanObj.name = 'test';
// cleanObj.hasOwnProperty('name'); // 报错:hasOwnProperty 不存在

// 安全的方式
Object.prototype.hasOwnProperty.call(cleanObj, 'name'); // true
// 或者
Object.hasOwn(cleanObj, 'name'); // ES2022 新方法

2.4.2 in 操作符 - 判断属性是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person(name) {
this.name = name;
}
Person.prototype.species = '人类';

const person = new Person('张三');

console.log('name' in person); // true(自有)
console.log('species' in person); // true(继承)
console.log('toString' in person); // true(继承)
console.log('unknown' in person); // false(不存在)

// 结合 hasOwnProperty 区分自有和继承
function getOwnProperties(obj) {
const own = [];
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
own.push(key);
}
}
return own;
}

2.4.3 instanceof - 判断实例关系

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
30
function Person(name) {
this.name = name;
}
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

const student = new Student('张三', 3);

// instanceof 检查原型链
console.log(student instanceof Student); // true
console.log(student instanceof Person); // true(Student 继承自 Person)
console.log(student instanceof Object); // true(所有对象都继承自 Object)

// instanceof 的原理(简化版)
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
const prototype = Constructor.prototype;

while (proto !== null) {
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}

instanceof 的陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 陷阱1:基本类型
console.log('string' instanceof String); // false(字面量不是对象)
console.log(new String('string') instanceof String); // true

// 陷阱2:跨 realm(iframe、Web Worker)
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
const arr = new iframeArray();

console.log(arr instanceof Array); // false(不同 realm 的 Array)
console.log(Array.isArray(arr)); // true(推荐方式)

// 陷阱3:修改 prototype
function Person() {}
const person = new Person();
Person.prototype = {}; // 修改了 prototype
console.log(person instanceof Person); // false(原型链断了)

2.4.4 isPrototypeOf() - 判断原型关系

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
this.name = name;
}
const person = new Person('张三');

// 检查 Person.prototype 是否在 person 的原型链上
console.log(Person.prototype.isPrototypeOf(person)); // true
console.log(Object.prototype.isPrototypeOf(person)); // true

// 更灵活:可以检查任意对象
const proto = { x: 1 };
const obj = Object.create(proto);
console.log(proto.isPrototypeOf(obj)); // true

isPrototypeOf vs instanceof

1
2
3
4
5
6
7
8
9
10
// isPrototypeOf 更灵活
const proto = { x: 1 };
const obj = Object.create(proto);
console.log(proto.isPrototypeOf(obj)); // true
// 但 obj instanceof ??? 无法使用,因为没有构造函数

// instanceof 更语义化
function Person() {}
const person = new Person();
console.log(person instanceof Person); // 更直观

3. 内建对象与原型

理解 JavaScript 内建对象的原型链结构,有助于理解整个语言的设计。

3.1 内建构造器的原型链

所有内建构造器(Object、Array、Function 等)本质上都是函数,它们都有自己的 prototype 属性。

3.1.1 数组的原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const arr = [1, 2, 3];

// 原型链结构
console.log(arr.__proto__ === Array.prototype); // true
console.log(Array.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

// 完整的链:arr → Array.prototype → Object.prototype → null

// 数组的方法来自 Array.prototype
console.log(arr.push === Array.prototype.push); // true
console.log(arr.map === Array.prototype.map); // true

// 对象的方法来自 Object.prototype
console.log(arr.toString === Object.prototype.toString); // true(被 Array.prototype.toString 覆盖)
console.log(arr.hasOwnProperty === Object.prototype.hasOwnProperty); // true

可视化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arr [1, 2, 3]
├── 0: 1
├── 1: 2
├── 2: 3
├── length: 3
└── __proto__ → Array.prototype
├── push: function
├── pop: function
├── map: function
├── filter: function
└── __proto__ → Object.prototype
├── toString: function
├── hasOwnProperty: function
└── __proto__ → null

3.1.2 函数的原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fn() {}

// 函数的特殊性:函数既是对象,也是函数
console.log(fn.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true

// Function.prototype 本身也是函数!
console.log(typeof Function.prototype); // 'function'
console.log(Function.prototype.__proto__ === Object.prototype); // true

// 函数的方法
console.log(fn.call === Function.prototype.call); // true
console.log(fn.apply === Function.prototype.apply); // true
console.log(fn.bind === Function.prototype.bind); // true

有趣的事实

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 所有函数都继承自 Function.prototype
function Person() {}
const Arrow = () => {};
class MyClass {}

console.log(Person.__proto__ === Function.prototype); // true
console.log(Arrow.__proto__ === Function.prototype); // true
console.log(MyClass.__proto__ === Function.prototype); // true

// 甚至 Function 和 Object 本身也是函数
console.log(typeof Function); // 'function'
console.log(typeof Object); // 'function'
console.log(Function.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true

3.1.3 其他内建对象的原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 日期对象
const date = new Date();
// date → Date.prototype → Object.prototype → null

// 正则表达式
const regex = /test/;
// regex → RegExp.prototype → Object.prototype → null

// Map 和 Set(ES6)
const map = new Map();
// map → Map.prototype → Object.prototype → null

const set = new Set();
// set → Set.prototype → Object.prototype → null

3.2 修改内建原型(猴子补丁)

修改内建对象的原型被称为”猴子补丁”(Monkey Patching),虽然可以实现功能扩展,但需要非常谨慎。

3.2.1 为什么需要谨慎?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ❌ 危险示例:全局修改 Array.prototype
Array.prototype.last = function() {
return this[this.length - 1];
};

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

// 问题1:可能与其他库冲突
// 如果另一个库也定义了 Array.prototype.last,会相互覆盖

// 问题2:for...in 遍历会包含新添加的方法
for (let key in arr) {
console.log(key); // 0, 1, 2, 'last'(意外!)
}

// 问题3:未来标准可能添加同名方法
// 如果未来 ES 标准添加了 Array.prototype.last,你的代码会失效

3.2.2 安全的做法

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:使用工具函数(推荐)
function arrayLast(arr) {
return arr[arr.length - 1];
}
console.log(arrayLast([1, 2, 3])); // 3

// ✅ 方式2:使用 Symbol(避免命名冲突)
const last = Symbol('last');
Array.prototype[last] = function() {
return this[this.length - 1];
};
const arr = [1, 2, 3];
console.log(arr[last]()); // 3

// ✅ 方式3:检查是否存在(幂等性)
if (!Array.prototype.last) {
Array.prototype.last = function() {
return this[this.length - 1];
};
}

// ✅ 方式4:使用类扩展(ES6+)
class MyArray extends Array {
last() {
return this[this.length - 1];
}
}
const myArr = new MyArray(1, 2, 3);
console.log(myArr.last()); // 3

3.2.3 实际应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 场景:为旧浏览器添加新方法(Polyfill)
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
// 实现 includes 的逻辑
const O = Object(this);
const len = parseInt(O.length) || 0;
if (len === 0) return false;
const n = parseInt(fromIndex) || 0;
let k = n >= 0 ? n : Math.max(len + n, 0);

function sameValueZero(x, y) {
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}

for (; k < len; k++) {
if (sameValueZero(O[k], searchElement)) {
return true;
}
}
return false;
};
}

4. 继承方式详解

JavaScript 有多种实现继承的方式,每种都有其适用场景和优缺点。

4.1 原型式继承(基于对象)

这是最简单的继承方式,直接以某个对象为原型创建新对象。

4.1.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 父对象
const parent = {
name: '父对象',
sayHello() {
console.log(`你好,我是${this.name}`);
},
hobbies: ['读书', '运动'] // 引用类型属性
};

// 创建子对象(以 parent 为原型)
const child = Object.create(parent);
child.name = '子对象';

child.sayHello(); // '你好,我是子对象'(继承的方法)
console.log(child.hobbies); // ['读书', '运动'](继承的属性)

4.1.2 共享引用属性的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const parent = {
hobbies: ['读书', '运动'] // 引用类型
};

const child1 = Object.create(parent);
const child2 = Object.create(parent);

// 修改引用类型属性会影响所有实例
child1.hobbies.push('编程');
console.log(child1.hobbies); // ['读书', '运动', '编程']
console.log(child2.hobbies); // ['读书', '运动', '编程'](也被修改了!)
console.log(parent.hobbies); // ['读书', '运动', '编程'](原型也被修改了!)

// 解决方案:在子对象中创建新数组
child1.hobbies = [...child1.hobbies, '编程']; // 创建新数组
// 或者
child1.hobbies = child1.hobbies.slice();
child1.hobbies.push('编程');

4.1.3 适用场景

  • 快速创建相似对象
  • 不需要构造函数的简单场景
  • 对象组合而非类继承
1
2
3
4
5
6
7
8
9
10
11
12
// 示例:配置对象继承
const defaultConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};

const devConfig = Object.create(defaultConfig);
devConfig.apiUrl = 'https://dev-api.example.com';

const prodConfig = Object.create(defaultConfig);
// prodConfig 使用默认配置

4.2 构造函数继承(借用构造函数)

通过在子类型构造函数中调用父类型构造函数来实现继承。

4.2.1 基本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Parent(name) {
this.name = name;
this.hobbies = ['读书', '运动'];
}

function Child(name, age) {
// 借用父构造函数,继承实例属性
Parent.call(this, name); // 关键:在子实例上调用父构造函数
this.age = age;
}

const child1 = new Child('张三', 20);
const child2 = new Child('李四', 25);

// 每个实例都有独立的属性
child1.hobbies.push('编程');
console.log(child1.hobbies); // ['读书', '运动', '编程']
console.log(child2.hobbies); // ['读书', '运动'](不受影响)

4.2.2 优点和缺点

优点

  • ✅ 每个实例都有独立的属性副本(解决引用类型共享问题)
  • ✅ 可以向父构造函数传递参数

缺点

  • ❌ 无法继承父原型上的方法
  • ❌ 方法无法复用(每个实例都要重新创建)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};

function Child(name, age) {
Parent.call(this, name);
this.age = age;
}

const child = new Child('张三', 20);
// child.sayHello(); // 报错:sayHello 不存在
// 因为 Child 的原型链没有连接到 Parent.prototype

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

组合继承结合了构造函数继承和原型继承的优点,是最常用的 ES5 继承方式。

4.3.1 实现步骤

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
30
31
32
33
34
35
36
function Parent(name) {
this.name = name;
this.hobbies = ['读书', '运动'];
}
Parent.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};

function Child(name, age) {
// 步骤1:借用构造函数,继承实例属性
Parent.call(this, name);
this.age = age;
}

// 步骤2:继承原型方法
Child.prototype = Object.create(Parent.prototype);

// 步骤3:修复 constructor 指向
Child.prototype.constructor = Child;

// 步骤4:添加子类特有方法
Child.prototype.sayAge = function() {
console.log(`我${this.age}岁`);
};

// 使用
const child1 = new Child('张三', 20);
const child2 = new Child('李四', 25);

child1.sayHello(); // '你好,我是张三'(继承的方法)
child1.sayAge(); // '我20岁'(子类方法)

// 实例属性独立
child1.hobbies.push('编程');
console.log(child1.hobbies); // ['读书', '运动', '编程']
console.log(child2.hobbies); // ['读书', '运动'](不受影响)

4.3.2 存在的问题

问题:父构造函数被调用了两次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Parent(name) {
this.name = name;
console.log('Parent 构造函数被调用');
}

function Child(name, age) {
Parent.call(this, name); // 第1次调用
this.age = age;
}

Child.prototype = new Parent(); // 第2次调用(旧写法,不推荐)
// 或者
Child.prototype = Object.create(Parent.prototype); // 不会调用,但需要手动设置

const child = new Child('张三', 20);
// 输出:'Parent 构造函数被调用'(只调用一次,如果使用 Object.create)

旧写法的问题

1
2
3
4
5
6
7
// ❌ 不推荐:直接 new Parent()
Child.prototype = new Parent();
// 问题:会执行 Parent 构造函数,在 Child.prototype 上创建不必要的属性

// ✅ 推荐:使用 Object.create()
Child.prototype = Object.create(Parent.prototype);
// 优点:只建立原型链,不执行构造函数

4.3.3 完整示例

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
function Animal(name) {
this.name = name;
this.species = '动物';
}
Animal.prototype.eat = function() {
console.log(`${this.name}在吃东西`);
};
Animal.prototype.sleep = function() {
console.log(`${this.name}在睡觉`);
};

function Dog(name, breed) {
Animal.call(this, name); // 继承实例属性
this.breed = breed;
}

// 继承原型方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 添加子类方法
Dog.prototype.bark = function() {
console.log(`${this.name}在叫:汪汪!`);
};

const dog = new Dog('旺财', '金毛');
dog.eat(); // '旺财在吃东西'(继承)
dog.bark(); // '旺财在叫:汪汪!'(自有)

4.4 寄生组合式继承(推荐的 ES5 写法)

这是组合继承的优化版本,解决了父构造函数被调用两次的问题。

4.4.1 核心实现

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
30
31
// 封装继承函数
function inherit(Sub, Sup) {
// 创建父原型的副本,避免直接修改父原型
Sub.prototype = Object.create(Sup.prototype);
// 修复 constructor 指向
Sub.prototype.constructor = Sub;
}

function Parent(name) {
this.name = name;
console.log('Parent 被调用');
}
Parent.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};

function Child(name, age) {
Parent.call(this, name); // 只调用一次
this.age = age;
}

inherit(Child, Parent); // 建立原型链,不调用构造函数

Child.prototype.sayAge = function() {
console.log(`我${this.age}岁`);
};

const child = new Child('张三', 20);
// 输出:'Parent 被调用'(只调用一次)
child.sayHello(); // '你好,我是张三'
child.sayAge(); // '我20岁'

4.4.2 为什么这是最佳实践?

  1. 只调用一次父构造函数:性能更好
  2. 原型链正确:子类可以访问父类方法
  3. 实例属性独立:每个实例有自己的属性副本
  4. 可以传递参数:支持向父构造函数传参

4.4.3 完整模板

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
30
// 继承工具函数
function inherit(Sub, Sup) {
Sub.prototype = Object.create(Sup.prototype);
Sub.prototype.constructor = Sub;
}

// 父类
function Animal(name) {
this.name = name;
}
Animal.prototype.say = function() {
return `I am ${this.name}`;
};

// 子类
function Dog(name, color) {
Animal.call(this, name); // 继承实例属性
this.color = color;
}
inherit(Dog, Animal); // 继承原型方法

// 子类方法
Dog.prototype.bark = function() {
return 'woof';
};

// 使用
const dog = new Dog('旺财', '黄色');
console.log(dog.say()); // 'I am 旺财'
console.log(dog.bark()); // 'woof'

4.5 ES6 类与 extends

ES6 的 classextends 是语法糖,本质仍然是构造函数和原型链,但语法更清晰。

4.5.1 基本语法

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 父类
class Animal {
constructor(name) {
this.name = name;
}

say() {
return `I am ${this.name}`;
}

// 静态方法(属于类本身)
static kind() {
return 'animal';
}
}

// 子类
class Dog extends Animal {
constructor(name, color) {
super(name); // 必须先调用 super()
this.color = color;
}

bark() {
return 'woof';
}

// 覆盖父类方法
say() {
return `${super.say()}, and I am a dog`; // 调用父类方法
}

// 覆盖静态方法
static kind() {
return 'dog';
}
}

const dog = new Dog('旺财', '黄色');
console.log(dog.say()); // 'I am 旺财, and I am a dog'
console.log(dog.bark()); // 'woof'
console.log(Dog.kind()); // 'dog'
console.log(Animal.kind()); // 'animal'

4.5.2 extends 建立的原型链

extends 会自动建立两条原型链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Parent {}
class Child extends Parent {}

// 1. 实例原型链(Child 的实例 → Parent 的实例)
const child = new Child();
console.log(child.__proto__ === Child.prototype); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true

// 2. 构造函数原型链(Child 构造函数 → Parent 构造函数)
console.log(Child.__proto__ === Parent); // true(注意:不是 Parent.prototype)

// 这允许子类继承父类的静态方法
Parent.staticMethod = function() { return 'static'; };
console.log(Child.staticMethod()); // 'static'

可视化

1
2
3
4
5
6
7
8
9
10
Child (构造函数)
└── __proto__ → Parent (构造函数)
└── staticMethod

Child.prototype (原型对象)
└── __proto__ → Parent.prototype
└── instanceMethod

child (实例)
└── __proto__ → Child.prototype

4.5.3 super 关键字详解

super 在不同上下文中有不同的含义:

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
30
31
32
33
34
class Parent {
constructor(name) {
this.name = name;
}

sayHello() {
return `Hello, I'm ${this.name}`;
}

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

class Child extends Parent {
constructor(name, age) {
// 1. 在构造函数中:调用父类构造函数
super(name); // 等价于 Parent.call(this, name)
this.age = age;
// 注意:必须先调用 super() 才能使用 this
}

sayHello() {
// 2. 在实例方法中:调用父类原型方法
return `${super.sayHello()}, age ${this.age}`;
// 等价于:Parent.prototype.sayHello.call(this)
}

static staticMethod() {
// 3. 在静态方法中:调用父类静态方法
return `${super.staticMethod()} - Child`;
// 等价于:Parent.staticMethod.call(this)
}
}

super 的规则

  1. 在派生类构造函数中必须先调用 super()

    1
    2
    3
    4
    5
    6
    7
    class Child extends Parent {
    constructor() {
    // this.name = 'test'; // ❌ 报错:在 super() 之前不能使用 this
    super();
    this.name = 'test'; // ✅ 正确
    }
    }
  2. super 不是变量,是关键字

    1
    2
    3
    4
    5
    // ❌ 错误
    const superAlias = super;

    // ✅ 正确:直接使用 super
    super.method();

4.5.4 类字段(Class Fields)

ES2022 引入了类字段语法:

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
class Person {
// 类字段(实例字段)
name = '默认名称';
age = 0;

// 私有字段(ES2022)
#privateField = '私有';

// 静态字段
static count = 0;

constructor(name, age) {
this.name = name; // 可以覆盖类字段
this.age = age;
Person.count++;
}

// 访问私有字段
getPrivate() {
return this.#privateField;
}
}

const person = new Person('张三', 25);
console.log(person.name); // '张三'
console.log(Person.count); // 1
console.log(person.getPrivate()); // '私有'
// console.log(person.#privateField); // ❌ 语法错误:私有字段外部不可访问

4.5.5 class 的本质

class 是语法糖,可以转换为 ES5 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ES6 class
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, ${this.name}`);
}
static create(name) {
return new Person(name);
}
}

// 等价的 ES5 代码
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, ${this.name}`);
};
Person.create = function(name) {
return new Person(name);
};

但 class 有一些特殊之处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. class 必须用 new 调用
class Person {}
Person(); // ❌ 报错:Class constructor Person cannot be invoked without 'new'

// 2. class 不会提升(存在 TDZ)
const p = new Person(); // ❌ 报错
class Person {}

// 3. class 内部默认严格模式
class Person {
constructor() {
undeclaredVar = 1; // ❌ 报错(严格模式下)
}
}

4.6 super 的含义(详细补充)

4.6.1 super 在构造函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Parent {
constructor(name) {
this.name = name;
console.log('Parent 构造函数');
}
}

class Child extends Parent {
constructor(name, age) {
// super() 做了两件事:
// 1. 调用父类构造函数 Parent.call(this, name)
// 2. 初始化 this(在派生类中,this 必须通过 super 初始化)
super(name);
this.age = age;
console.log('Child 构造函数');
}
}

const child = new Child('张三', 20);
// 输出:
// 'Parent 构造函数'
// 'Child 构造函数'

4.6.2 super 在实例方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Parent {
getName() {
return this.name;
}
}

class Child extends Parent {
getName() {
// super.getName() 等价于 Parent.prototype.getName.call(this)
return `Child: ${super.getName()}`;
}
}

const child = new Child();
child.name = '张三';
console.log(child.getName()); // 'Child: 张三'

4.6.3 super 在静态方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Parent {
static getType() {
return 'Parent';
}
}

class Child extends Parent {
static getType() {
// super.getType() 等价于 Parent.getType.call(this)
return `Child of ${super.getType()}`;
}
}

console.log(Child.getType()); // 'Child of Parent'

4.7 混入(Mixin)

混入是一种组合模式,通过对象复制来扩展功能,不改变原型链。

4.7.1 基本混入

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
// 定义可混入的功能
const Flyable = {
fly() {
console.log(`${this.name}在飞`);
}
};

const Singable = {
sing() {
console.log(`${this.name}在唱歌`);
}
};

// 混入到类
class Bird {
constructor(name) {
this.name = name;
}
}

// 使用 Object.assign 混入
Object.assign(Bird.prototype, Flyable, Singable);

const bird = new Bird('小鸟');
bird.fly(); // '小鸟在飞'
bird.sing(); // '小鸟在唱歌'

4.7.2 混入函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建混入函数
function mixin(target, ...sources) {
Object.assign(target, ...sources);
}

// 或者更高级的混入(支持方法合并)
function mixinAdvanced(target, source) {
Object.getOwnPropertyNames(source).forEach(name => {
const descriptor = Object.getOwnPropertyDescriptor(source, name);
Object.defineProperty(target, name, descriptor);
});
}

// 使用
class Dog {
constructor(name) {
this.name = name;
}
}

mixin(Dog.prototype, Flyable, Singable);

4.7.3 混入的优势

  • ✅ 不改变原型链(横向扩展)
  • ✅ 可以组合多个功能
  • ✅ 灵活,可以动态添加
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
30
31
32
33
34
35
// 示例:游戏角色系统
const Movable = {
move(x, y) {
this.x = x;
this.y = y;
console.log(`移动到 (${x}, ${y})`);
}
};

const Attackable = {
attack(target) {
console.log(`${this.name}攻击${target.name}`);
}
};

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

class Warrior extends Character {}
class Mage extends Character {}

// 不同角色混入不同能力
Object.assign(Warrior.prototype, Movable, Attackable);
Object.assign(Mage.prototype, Movable);

const warrior = new Warrior('战士');
warrior.move(10, 20); // ✅
warrior.attack({ name: '敌人' }); // ✅

const mage = new Mage('法师');
mage.move(5, 15); // ✅
// mage.attack(...); // ❌ Mage 没有攻击能力

5. 常见坑与排错

5.1 误用 proto

问题描述

1
2
3
4
5
6
7
8
// ❌ 不推荐:直接使用 __proto__
const obj = {};
obj.__proto__ = someProto;

// 问题:
// 1. __proto__ 已废弃(虽然仍被广泛支持)
// 2. 性能问题:某些引擎可能优化掉
// 3. 可移植性:不是所有环境都支持

解决方案

1
2
3
4
5
6
7
8
// ✅ 推荐:使用标准方法
const obj = Object.create(someProto);

// 或者
Object.setPrototypeOf(obj, someProto);

// 获取原型
const proto = Object.getPrototypeOf(obj);

5.2 共享引用属性

问题描述

1
2
3
4
5
6
7
8
9
10
11
function Person(name) {
this.name = name;
}
Person.prototype.hobbies = ['读书']; // ❌ 危险:引用类型在原型上

const p1 = new Person('张三');
const p2 = new Person('李四');

p1.hobbies.push('运动');
console.log(p1.hobbies); // ['读书', '运动']
console.log(p2.hobbies); // ['读书', '运动'](也被修改了!)

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ✅ 方案1:在构造函数中初始化
function Person(name) {
this.name = name;
this.hobbies = ['读书']; // 每个实例独立
}

// ✅ 方案2:使用 Object.defineProperty 定义不可写属性
Object.defineProperty(Person.prototype, 'defaultHobbies', {
value: ['读书'],
writable: false,
enumerable: true
});

// ✅ 方案3:提供方法而不是直接暴露属性
Person.prototype.addHobby = function(hobby) {
if (!this.hobbies) {
this.hobbies = [];
}
this.hobbies.push(hobby);
};

5.3 忘记重设 constructor

问题描述

1
2
3
4
5
6
7
8
function Parent() {}
function Child() {}

Child.prototype = Object.create(Parent.prototype);
// ❌ 忘记设置 constructor

const child = new Child();
console.log(child.constructor); // Parent(错误!应该是 Child)

解决方案

1
2
3
4
5
6
7
8
function Parent() {}
function Child() {}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // ✅ 修复 constructor

const child = new Child();
console.log(child.constructor); // Child(正确)

5.4 super 调用顺序

问题描述

1
2
3
4
5
6
class Child extends Parent {
constructor(name, age) {
this.age = age; // ❌ 报错:在 super() 之前不能使用 this
super(name);
}
}

解决方案

1
2
3
4
5
6
class Child extends Parent {
constructor(name, age) {
super(name); // ✅ 必须先调用 super()
this.age = age;
}
}

5.5 修改内建原型

问题描述

1
2
3
4
5
6
7
8
9
// ❌ 危险:全局修改
Array.prototype.last = function() {
return this[this.length - 1];
};

// 问题:
// 1. 可能与其他库冲突
// 2. for...in 会遍历到
// 3. 未来标准可能添加同名方法

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ✅ 方案1:使用工具函数
function arrayLast(arr) {
return arr[arr.length - 1];
}

// ✅ 方案2:使用 Symbol
const last = Symbol('last');
Array.prototype[last] = function() {
return this[this.length - 1];
};

// ✅ 方案3:检查是否存在
if (!Array.prototype.last) {
Array.prototype.last = function() {
return this[this.length - 1];
};
}

// ✅ 方案4:使用类扩展
class MyArray extends Array {
last() {
return this[this.length - 1];
}
}

5.6 instanceof 与多环境

问题描述

1
2
3
4
5
6
7
// 跨 realm(iframe)时 instanceof 失效
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
const arr = new iframeArray();

console.log(arr instanceof Array); // false(不同 realm)

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 方案1:使用 Array.isArray()
console.log(Array.isArray(arr)); // true

// ✅ 方案2:使用 Object.prototype.toString
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}

// ✅ 方案3:鸭子类型检查
function isArrayLike(obj) {
return obj != null &&
typeof obj.length === 'number' &&
typeof obj[Symbol.iterator] === 'function';
}

6. 代码片段(直接可用)

6.1 属性描述符与只读属性

1
2
3
4
5
6
7
8
9
10
11
const user = {};
Object.defineProperty(user, 'id', {
value: 1,
writable: false, // 不可修改
enumerable: true, // 可枚举
configurable: false // 不可删除、不可重新配置
});

// 使用
user.id = 2; // 静默失败(严格模式下报错)
delete user.id; // false

6.2 寄生组合继承模板

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
30
// 继承工具函数
function inherit(Sub, Sup) {
Sub.prototype = Object.create(Sup.prototype);
Sub.prototype.constructor = Sub;
}

// 父类
function Animal(name) {
this.name = name;
}
Animal.prototype.say = function () {
return `I am ${this.name}`;
};

// 子类
function Dog(name, color) {
Animal.call(this, name); // 继承实例属性
this.color = color;
}
inherit(Dog, Animal); // 继承原型方法

// 子类方法
Dog.prototype.bark = function () {
return 'woof';
};

// 使用
const dog = new Dog('旺财', '黄色');
console.log(dog.say()); // 'I am 旺财'
console.log(dog.bark()); // 'woof'

6.3 ES6 类继承示例

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
30
31
32
// 父类
class Animal {
constructor(name) {
this.name = name;
}
say() {
return `I am ${this.name}`;
}
static kind() {
return 'animal';
}
}

// 子类
class Dog extends Animal {
constructor(name, color) {
super(name); // 必须先 super
this.color = color;
}
bark() {
return 'woof';
}
static kind() {
return 'dog';
} // 覆盖静态方法
}

// 使用
const dog = new Dog('旺财', '黄色');
console.log(dog.say()); // 'I am 旺财'
console.log(dog.bark()); // 'woof'
console.log(Dog.kind()); // 'dog'

6.4 混入示例

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
// 定义可混入的功能
const Flyable = {
fly() {
console.log('fly');
}
};
const Singable = {
sing() {
console.log('sing');
}
};

// 类定义
class Bird {
constructor(name) {
this.name = name;
}
}

// 混入
Object.assign(Bird.prototype, Flyable, Singable);

// 使用
const bird = new Bird('小鸟');
bird.fly(); // 'fly'
bird.sing(); // 'sing'

6.5 深冻结函数

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
function deepFreeze(obj) {
// 获取所有属性名(包括不可枚举和 Symbol)
Object.getOwnPropertyNames(obj).forEach(name => {
const value = obj[name];
// 如果是对象且不为 null,递归冻结
if (value && typeof value === 'object') {
deepFreeze(value);
}
});
// 冻结对象本身
return Object.freeze(obj);
}

// 使用
const obj = {
name: '张三',
address: {
city: '北京',
street: '某街道'
}
};

deepFreeze(obj);
obj.name = '李四'; // 失败
obj.address.city = '上海'; // 失败(深冻结)

6.6 安全的属性检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 安全地检查自有属性(适用于没有原型的对象)
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}

// 或者使用 ES2022 新方法
function hasOwn(obj, prop) {
return Object.hasOwn(obj, prop);
}

// 使用
const cleanObj = Object.create(null);
cleanObj.name = 'test';
console.log(hasOwnProperty(cleanObj, 'name')); // true
console.log(hasOwn(cleanObj, 'name')); // true

6.7 手动实现 new 操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function myNew(Constructor, ...args) {
// 1. 创建空对象
const obj = {};

// 2. 连接原型
Object.setPrototypeOf(obj, Constructor.prototype);

// 3. 执行构造函数
const result = Constructor.apply(obj, args);

// 4. 返回结果
return result instanceof Object ? result : obj;
}

// 使用
function Person(name) {
this.name = name;
}
const person = myNew(Person, '张三');
console.log(person.name); // '张三'

6.8 手动实现 instanceof

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
function myInstanceof(obj, Constructor) {
// 基本类型返回 false
if (obj === null || typeof obj !== 'object') {
return false;
}

let proto = Object.getPrototypeOf(obj);
const prototype = Constructor.prototype;

// 沿着原型链向上查找
while (proto !== null) {
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}

return false;
}

// 使用
function Person() {}
function Student() {}
Student.prototype = Object.create(Person.prototype);

const student = new Student();
console.log(myInstanceof(student, Student)); // true
console.log(myInstanceof(student, Person)); // true
console.log(myInstanceof(student, Object)); // true

7. 实战应用场景

7.1 实现一个简单的类库系统

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
30
31
32
// 基础类
class Component {
constructor(name) {
this.name = name;
this.children = [];
}

add(child) {
this.children.push(child);
return this;
}

render() {
return `<${this.name}>${this.children.map(c => c.render()).join('')}</${this.name}>`;
}
}

// 叶子节点
class TextNode {
constructor(text) {
this.text = text;
}
render() {
return this.text;
}
}

// 使用
const div = new Component('div');
div.add(new TextNode('Hello'));
div.add(new Component('span').add(new TextNode('World')));
console.log(div.render()); // '<div>Hello<span>World</span></div>'

7.2 实现事件系统

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
30
31
32
33
34
35
36
37
38
39
// 事件发射器基类
class EventEmitter {
constructor() {
this.events = {};
}

on(event, handler) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
}

emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(handler => handler(...args));
}
}

off(event, handler) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(h => h !== handler);
}
}
}

// 使用
class User extends EventEmitter {
login() {
console.log('用户登录');
this.emit('login', { time: new Date() });
}
}

const user = new User();
user.on('login', (data) => {
console.log('登录事件触发', data);
});
user.login();

7.3 实现插件系统

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 插件基类
class Plugin {
constructor(name) {
this.name = name;
}
install(app) {
// 子类实现
}
}

// 应用类
class App {
constructor() {
this.plugins = [];
}

use(plugin) {
if (plugin instanceof Plugin) {
this.plugins.push(plugin);
plugin.install(this);
}
}

run() {
console.log('应用运行中...');
}
}

// 具体插件
class LoggerPlugin extends Plugin {
install(app) {
const originalRun = app.run.bind(app);
app.run = function() {
console.log(`[${this.name}] 开始运行`);
originalRun();
};
}
}

// 使用
const app = new App();
app.use(new LoggerPlugin('Logger'));
app.run();

8. 速记与总结

8.1 核心概念速记

  • “读属性先看自己,再爬链;找不到就是 undefined。”

    • 读取属性时,先在对象自身查找,找不到则沿原型链向上查找,直到 null
  • “构造四步:建对象、连原型、绑 this、看返回。”

    • new 操作符的四个步骤:创建空对象、连接原型、绑定 this 执行构造函数、返回结果。
  • “共享要小心:引用属性放构造,方法放原型。”

    • 引用类型属性(数组、对象)应放在构造函数中,避免共享;方法应放在原型上,节省内存。
  • “extends 仍是原型链 + super 初始化 this。”

    • ES6 的 classextends 本质仍是原型链,super() 用于初始化 this
  • “别乱改内建原型,想复用用 mixin/工具函数。”

    • 修改内建原型(如 Array.prototype)风险大,应使用工具函数或混入模式。

8.2 继承方式选择指南

场景 推荐方式 原因
ES6+ 项目 class + extends 语法清晰,自动处理细节
ES5 项目 寄生组合继承 性能好,只调用一次父构造
简单对象继承 Object.create() 快速,适合配置对象等
功能组合 Mixin 灵活,不改变原型链
需要多继承 Mixin 组合 JavaScript 不支持多继承,用 Mixin 模拟

8.3 常见错误检查清单

  • 是否在派生类构造函数中先调用了 super()
  • 是否修复了 constructor 指向(使用 Object.create 后)?
  • 是否将引用类型属性放在了构造函数中而非原型上?
  • 是否使用了 Object.getPrototypeOf 而非 __proto__
  • 是否避免了直接修改内建原型?
  • 是否在跨环境场景使用了 instanceof 的替代方案?

8.4 性能优化建议

  1. 方法放在原型上:避免每个实例都创建新函数
  2. 避免深度原型链:过深的原型链会影响查找性能
  3. 使用 Object.create(null) 创建纯净对象:没有原型链,查找更快
  4. 避免频繁修改原型:会影响引擎优化
  5. 使用类字段初始化:比在构造函数中赋值稍快

8.5 学习路径建议

  1. 基础:理解对象字面量、属性描述符
  2. 进阶:掌握构造函数、原型、原型链
  3. 高级:理解各种继承方式、ES6 类的本质
  4. 实战:在项目中应用,理解设计模式

结语:JavaScript 的原型系统虽然复杂,但一旦理解其本质,就能灵活运用。记住,class 只是语法糖,底层仍然是原型链。多实践,多思考,才能真正掌握。