JS 中面向对象的编程——原型对象和方法

三葉Leaves Author

三种不同的方法

这个问题非常核心,彻底搞懂它,对于理解 JS 的面向对象编程、性能优化和代码组织至关重要。

先来看我写的这个例子,如果能看懂,本章可以直接过了。

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
// ================ 定义部分

function Girl(name, age) {
this.name = name;
// 对象自带方法
this.report = () => {
console.log(`${name}, ${age} years old`.toUpperCase());
};
}
// 静态方法
Girl.isGirl = function (param) {
return param.constructor === this;
};
// 实例方法
Girl.prototype.sayName = function () {
console.log(`Hi, my name is ${this.name}`);
};

const xiaohua = new Girl("小花", 18);

// ================ 使用部分

// 使用对象自带方法
xiaohua.report();

// 使用静态方法
console.log(Girl.isGirl(xiaohua));

// 使用实例方法
xiaohua.sayName();

// ================ 关于 constructor 指向

// 下面这三句指向了同一个东西:Girl 这个构造函数本身
console.log(Girl.prototype.constructor);
console.log(xiaohua.constructor);
// 这是上面第二句的隐式写法,注意前后都有两个 _
      // 这就说明了实例的 __proto__ 实际指向了对象的 prototype
console.log(xiaohua.__proto__.constructor);

下面就假设我们要创建一个“小狗”的“类”,来演示三种方法。

1. 实例方法 (挂在 prototype 上) —— 共享的“种族天赋” 🐕‍🦺

代码示例

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

// 这就是实例方法,挂在 Dog.prototype 上
bark() {
console.log(`${this.name} 汪汪汪!`);
}

run() {
console.log(`${this.name} 正在飞快地跑...`);
}
}

const d1 = new Dog('旺财');
const d2 = new Dog('小强');

d1.bark(); // 输出: 旺财 汪汪汪!
d2.bark(); // 输出: 小强 汪汪汪!

console.log(d1.bark === d2.bark); // true,证明两个实例共享同一个方法函数,非常节省内存
等价的构造函数语法
1
2
3
4
5
6
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() {
console.log(this.name + ' 汪汪汪!');
};

这是最常用、最高效的方式。你可以把它想象成所有小狗都与生俱来的、写在基因里的通用技能,比如“吠叫”、“奔跑”。我们不需要为每一只新出生的小狗都单独教一遍,它们天生就会。

  • 工作原理:这个方法只在内存中存在一份,位于 Dog.prototype 这个“技能原型”对象上。当你创建一个新的小狗实例(如 d1)时,d1 内部有一个特殊的 [[Prototype]] 链接指向 Dog.prototype。当你调用 d1.bark() 时,JS 引擎在 d1 自身上找不到 bark 方法,就会顺着链接去 Dog.prototype 上找,并执行它。

  • this 指向this 永远指向调用该方法的实例(谁调用,this 就是谁)。比如 d1.bark() 里的 this 就是 d1

✅ 总结

  • 内存占用:极低,所有实例共享一个方法。
  • 适用场景95% 的情况都应该用它。所有与实例状态(如 this.name)相关、且所有实例都通用的行为,都应该定义为实例方法。

2. 对象自带方法 (写在 constructor 里) —— “专属定制技能” 🎓

代码示例

ES6 class 写法
1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog {
// ES6 写法:在构造函数中给 this 添加方法
constructor(name) {
this.name = name;
this.greet = function() {
console.log(`你好,我是 ${this.name}`);
};
}
}

// 调用方式
const dog = new Dog('旺财');
dog.greet(); // 你好,我是 旺财
等价的构造函数语法
1
2
3
4
5
6
7
8
9
10
11
12
function Dog(name) {
this.name = name;

// ES5 写法:在构造函数中给 this 添加方法
this.greet = function() {
console.log('你好,我是 ' + this.name);
};
}

// 调用方式
const dog = new Dog('旺财');
dog.greet(); // 你好,我是 旺财

这种方法是为每一只小狗单独定制的技能。比如,我们给一只小狗请了个一对一的私教,教了它一个只有它自己会、而且和它“签约”时(new 的时候)的某些特定信息相关的特殊技能。

  • 工作原理:每执行一次 new Dog(...),构造函数内部的代码就会运行一次。这意味着每次都会创建一个新的函数,并把它作为属性直接挂载到新生成的实例上。

  • this 指向this 同样指向调用该方法的实例。

代码示例

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
class Dog {
constructor(name, secretWord) {
this.name = name;

// 这是写在构造函数里的方法
this.whisperSecret = function() {
// 这个方法可以访问构造函数作用域内的变量,比如 secretWord
// 这是原型方法做不到的(除非把secretWord也挂在this上)
console.log(`${this.name} 悄悄说: ${secretWord}`);
};
}

// 实例方法 (对比用)
bark() {
console.log(`${this.name} 汪汪汪!`);
}
}

const d1 = new Dog('旺财', '骨头最好吃');
const d2 = new Dog('小强', '猫咪是笨蛋');

d1.whisperSecret(); // 输出: 旺财 悄悄说: 骨头最好吃
d2.whisperSecret(); // 输出: 小强 悄悄说: 猫咪是笨蛋

console.log(d1.whisperSecret === d2.whisperSecret); // false,证明每个实例都有一个独立的方法副本,占用更多内存
console.log(d1.bark === d2.bark); // true, 作为对比,原型方法是共享的

✅ 总结

  • 内存占用:高,每个实例都有一份方法的副本。

  • 适用场景

    1. 当方法需要访问构造函数作用域中的私有变量(形成闭包)时。这是它最主要的用途。
    2. 当你想为不同实例提供不同版本的方法实现时(虽然通常有更好的设计模式)。
    • 在现代开发中,除非有明确的闭包需求,否则应避免使用这种方式,以优化性能。

3. 静态方法 (挂在构造函数上) —— “犬类研究中心”的工具 🔬

ES6 class 写法
1
2
3
4
5
6
7
8
9
class Dog {
// ES6 写法:使用 static 关键字
static getBreedInfo() {
console.log('犬类是人类最忠实的朋友。');
}
}

// 调用方式:通过类本身调用
Dog.getBreedInfo(); // 犬类是人类最忠实的朋友。
等价的构造函数语法
1
2
3
4
5
6
7
8
9
10
11
function Dog() {
// 构造函数可以为空,或者有其他逻辑
}

// ES5 写法:将方法直接挂载为构造函数的属性
Dog.getBreedInfo = function() {
console.log('犬类是人类最忠实的朋友。');
};

// 调用方式:通过构造函数本身调用
Dog.getBreedInfo(); // 犬类是人类最忠实的朋友。

静态方法不属于任何一只具体的小狗,而是属于 Dog 这个“物种”或“犬类研究中心”本身。它通常是一些辅助函数或工厂函数,与单个小狗的状态无关。比如,“创建一个随机名字的小狗”或者“比较两只狗的年龄”,这些操作不需要一只已经存在的小狗来发起。

  • 工作原理:方法直接附加在 Dog 构造函数这个对象上,而不是在它的 prototype 上。因此,实例无法访问到它。

  • this 指向:在静态方法内部,this 指向构造函数本身(即 Dog),而不是任何实例。

代码示例 (现代 ES6 Class 语法 - 推荐)

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
class Dog {
constructor(name) {
this.name = name;
}

// 这是静态方法
static createRandomDog() {
const names = ['豆豆', '花花', '小白'];
const randomName = names[Math.floor(Math.random() * names.length)];
// 在静态方法里,this 指向 Dog 这个类
return new this(randomName);
}

// 实例方法 (对比用)
bark() {
console.log(`${this.name} 汪汪汪!`);
}
}

// 直接通过类来调用,而不是实例
const randomDog = Dog.createRandomDog();
console.log(`我们领养了一只新狗,叫${randomDog.name}`);
randomDog.bark(); // 新实例当然可以使用实例方法

const d1 = new Dog('旺财');
// d1.createRandomDog(); // TypeError: d1.createRandomDog is not a function
// 实例无法调用静态方法!

✅ 总结

  • 内存占用:极低,和类本身绑定在一起,只有一份。
  • 适用场景:当你需要一个不依赖于实例状态的工具函数时。比如:
    • 工厂方法(如 Dog.create...
    • 配置(如 Dog.config = {...}
    • 纯工具函数(如 Math.max()Array.from() 都是典型的静态方法)

终极对比表格

特性 实例方法 (prototype) 对象自带方法 (constructor) 静态方法 (static)
定义位置 class 的顶级作用域内 / Constructor.prototype constructor 内部 class 内用 static / Constructor
调用方式 实例.方法() 实例.方法() 类.方法()
this 指向 调用它的实例 调用它的实例 类(构造函数)本身
内存占用 极低 (所有实例共享) (每个实例一份拷贝) 极低 (类本身持有一份)
核心用途 实例的核心、通用行为 (主要使用方式) 需要访问构造函数闭包变量的特殊方法 工具函数、工厂函数,与实例状态无关
比喻 种族天赋 (Shared) 私教定制技能 (Per-Instance) 犬类研究中心的工具 (Utility)

现代前端开发实践建议

  1. 首选实例方法 (prototype):这是定义对象行为的默认和最佳方式。它性能好,符合面向对象的直觉。
  2. 慎用构造函数内方法:只在你确定需要利用闭包来封装私有状态时才使用。否则,它会造成不必要的内存浪费。
  3. 善用静态方法:将所有与类相关但与具体实例无关的功能(如创建、转换、通用计算)组织为静态方法,这会让你的代码结构更清晰。
  • 标题: JS 中面向对象的编程——原型对象和方法
  • 作者: 三葉Leaves
  • 创建于 : 2025-06-06 00:00:00
  • 更新于 : 2025-06-07 19:46:13
  • 链接: https://blog.oksanye.com/8826998fb3d7/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论