面向对象
# 05.面向对象
# 目录介绍
# 5.1 创建对象
在 JavaScript 中,可以通过以下方式创建对象:
# 5.1.1 对象字面量
直接使用 {} 创建对象。
const person = {
name: "Alice",
age: 25,
greet() {
console.log(`Hello, my name is ${this.name}`);
},
};
person.greet(); // Hello, my name is Alice
2
3
4
5
6
7
8
9
# 5.1.2 构造函数
使用 new 关键字和构造函数创建对象。
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
}
const person = new Person("Alice", 25);
person.greet(); // Hello, my name is Alice
2
3
4
5
6
7
8
9
10
构造函数的特点有两个。
- 函数体内部使用了
this关键字,代表了所要生成的对象实例。 - 生成对象的时候,必须使用
new命令。
# 5.1.3 class类
使用 class 关键字定义类。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
const person = new Person("Alice", 25);
person.greet(); // Hello, my name is Alice
2
3
4
5
6
7
8
9
10
11
12
13
# 5.1.4 new命令原理
使用new命令时,它后面的函数依次执行下面的步骤。
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的
prototype属性。 - 将这个空对象赋值给函数内部的
this关键字。 - 开始执行构造函数内部的代码。
也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。
如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。
var Vehicle = function () {
this.price = 1000;
return 1000;
};
(new Vehicle()) === 1000
// false
2
3
4
5
6
7
上面代码中,构造函数Vehicle的return语句返回一个数值。这时,new命令就会忽略这个return语句,返回“构造”后的this对象。
但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意。
var Vehicle = function (){
this.price = 1000;
return { price: 2000 };
};
(new Vehicle()).price
// 2000
2
3
4
5
6
7
new命令简化的内部流程,可以用下面的代码表示。
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
// 将 arguments 对象转为数组
var args = [].slice.call(arguments);
// 取出构造函数
var constructor = args.shift();
// 创建一个空对象,继承构造函数的 prototype 属性
var context = Object.create(constructor.prototype);
// 执行构造函数
var result = constructor.apply(context, args);
// 如果返回结果是对象,就直接返回,否则返回 context 对象
return (typeof result === 'object' && result != null) ? result : context;
}
// 实例
var actor = _new(Person, '张三', 28);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5.1.5 对象拷贝
如果要拷贝一个对象,需要做到下面两件事情。
- 确保拷贝后的对象,与原对象具有同样的原型。
- 确保拷贝后的对象,与原对象具有同样的实例属性。
function copyObject(orig) {
var copy = Object.create(Object.getPrototypeOf(orig));
copyOwnPropertiesFrom(copy, orig);
return copy;
}
function copyOwnPropertiesFrom(target, source) {
Object
.getOwnPropertyNames(source)
.forEach(function (propKey) {
var desc = Object.getOwnPropertyDescriptor(source, propKey);
Object.defineProperty(target, propKey, desc);
});
return target;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
另一种更简单的写法,是利用 ES2017 才引入标准的Object.getOwnPropertyDescriptors方法。
function copyObject(orig) {
return Object.create(
Object.getPrototypeOf(orig),
Object.getOwnPropertyDescriptors(orig)
);
}
2
3
4
5
6
# 5.1.6 对象属性的底层存储
疑惑:JavaScript 对象的属性是如何在内存中存储的?
答疑:V8 引擎使用隐藏类(Hidden Class / Map) 来优化对象属性的访问。当你创建一个对象时,V8 会为其分配一个隐藏类。每次添加属性,V8 会创建一个新的隐藏类,并建立从旧隐藏类到新隐藏类的转换链(Transition Chain)。
论证:
// V8 内部为 a 和 b 创建相同的隐藏类(属性添加顺序相同)
const a = {};
a.x = 1;
a.y = 2;
const b = {};
b.x = 3;
b.y = 4;
// c 与 a、b 的隐藏类不同(属性添加顺序不同)
const c = {};
c.y = 5; // 先添加 y
c.x = 6; // 再添加 x
2
3
4
5
6
7
8
9
10
11
12
13
结果展示:相同结构(属性名和添加顺序相同)的对象共享隐藏类,V8 可以通过**内联缓存(Inline Cache)**直接用偏移量访问属性,速度接近 C++ 的结构体访问。因此,保持对象属性初始化顺序一致是重要的性能优化技巧。
# 5.2 原型
# 5.2.1 原型对象
如果我创建1000个对象,那岂不是内存中就有1000个相同的方法,那要是有10000个,那对内存的浪费可不是一点半点的。
我们可以把函数抽取出来,作为全局函数,在构造函数中直接引用就可以了,上代码:
// 使用构造函数来创建对象
function Person(name, age) {
// 设置对象的属性
this.name = name;
this.age = age;
// 设置对象的方法
this.sayName = sayName
}
// 抽取方法为全局函数
function sayName() {
console.log(this.name);
}
var person1 = new Person("孙悟空", 18);
var person2 = new Person("猪八戒", 19);
var person3 = new Person("沙和尚", 20);
person1.sayName();
person2.sayName();
person3.sayName();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在全局作用域中定义函数却不是一个好的办法,因为,如果要是涉及到多人协作开发一个项目,别人也有可能叫sayName这个方法,这样在工程合并的时候就会导致一系列的问题,污染全局作用域。
那该怎么办呢?有没有一种方法,我只在Person这个类的全局对象中添加一个函数,然后在类中引用?答案肯定是有的,这就需要原型对象了,我们先看看怎么做的,然后在详细讲解原型对象。
// 使用构造函数来创建对象
function Person(name, age) {
// 设置对象的属性
this.name = name;
this.age = age;
}
// 在Person类的原型对象中添加方法
// 每个构造函数都有一个 prototype 属性,指向一个对象。
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person("孙悟空", 18);
var person2 = new Person("猪八戒", 19);
var person3 = new Person("沙和尚", 20);
person1.sayName();
person2.sayName();
person3.sayName();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 5.2.2 原型对象概念
什么是原型对象?
- 每个 JavaScript 对象都有一个原型对象(
[[Prototype]])。 - 原型对象也是一个对象,它包含可以被其他对象共享的属性和方法。
- 当访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找。
那原型(prototype)到底是什么呢?
我们所创建的每一个函数,解析器都会向函数中添加一个属性prototype,这个属性对应着一个对象,这个对象就是我们所谓的原型对象,即显式原型,原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将对象中共有的内容,统一设置到原型对象中。
如果函数作为普通函数调用prototype没有任何作用,当函数以构造函数的形式调用时,它所创建的对象中都会有一个隐含的属性,指向该构造函数的原型对象,我们可以通过__proto__(隐式原型)来访问该属性。当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果找到则直接使用。
以后我们创建构造函数时,可以将这些对象共有的属性和方法,统一添加到构造函数的原型对象中,这样不用分别为每一个对象添加,也不会影响到全局作用域,就可以使每个对象都具有这些属性和方法了。
# 5.2.3 原型链
访问一个对象的属性时,先在自身属性中查找,找到返回, 如果没有,再沿着__proto__这条链向上查找,找到返回,如果最终没找到,返回undefined,这就是原型链,又称隐式原型链,它的作用就是查找对象的属性(方法)。
原型链的底层原理:在 V8 引擎中,每个对象内部都有一个 [[Prototype]] 隐藏属性(可通过 __proto__ 或 Object.getPrototypeOf() 访问)。当访问对象属性时,引擎先在对象自身的隐藏类(Hidden Class / Map) 中查找属性的偏移量;如果未找到,则沿着 [[Prototype]] 链逐级查找。V8 使用内联缓存(Inline Cache, IC) 优化属性查找——首次查找时记录属性所在对象的 Hidden Class 和偏移量,后续访问直接命中缓存,避免重复遍历原型链。原型链的终点是 Object.prototype,其 [[Prototype]] 为 null。
- 原型链是由对象的
[[Prototype]]链接起来的链式结构。 - 当访问一个对象的属性或方法时,JavaScript 会依次在对象本身、对象的原型、原型的原型……上查找,直到找到该属性或方法,或者到达原型链的顶端(
null)。
# 5.2.4 原型链验证
instanceof:检查对象是否是某个构造函数的实例。isPrototypeOf():检查一个对象是否在另一个对象的原型链上。
function Person(name) {
this.name = name;
}
const alice = new Person("Alice");
console.log(alice instanceof Person); // 输出: true
console.log(Person.prototype.isPrototypeOf(alice)); // 输出: true
2
3
4
5
6
7
8
# 5.2.5 原型链的完整图谱
null
↑ [[Prototype]]
Object.prototype
(toString, valueOf 等)
↑ ↑
Person.prototype Function.prototype
(sayName 等) (call, apply, bind)
↑ ↑
person1 Person(构造函数)
(name, age) (也是一个对象)
2
3
4
5
6
7
8
9
10
核心关系:
- 每个构造函数都有
prototype属性,指向原型对象 - 每个实例都有
[[Prototype]](__proto__),指向构造函数的prototype - 原型对象有
constructor属性,指向构造函数 Object.prototype.__proto__ === null(原型链终点)
function Person(name) { this.name = name; }
const p = new Person("Alice");
// 核心等式
p.__proto__ === Person.prototype; // true
Person.prototype.constructor === Person; // true
Person.__proto__ === Function.prototype; // true(构造函数也是 Function 的实例)
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true(终点)
2
3
4
5
6
7
8
9
# 5.3 封装
封装可以通过以下方式实现:
- 1.使用闭包隐藏私有属性。
- 2.使用
#定义私有字段(ES2022)。
# 5.3.1 闭包
function Person(name, age) {
let _age = age; // 私有属性
this.name = name;
this.getAge = function () {
return _age;
};
}
const person = new Person("Alice", 25);
console.log(person.name); // Alice
console.log(person.getAge()); // 25
console.log(person._age); // undefined
2
3
4
5
6
7
8
9
10
11
12
13
# 5.3.2 私有字段
私有字段(ES2022)
class Person {
#age; // 私有字段
constructor(name, age) {
this.name = name;
this.#age = age;
}
getAge() {
return this.#age;
}
}
const person = new Person("Alice", 25);
console.log(person.name); // Alice
console.log(person.getAge()); // 25
console.log(person.#age); // Error: Private field '#age' must be declared in an enclosing class
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5.3.3 封装的技术演变
技术演变过程:
// 阶段1:命名约定(最早期)—— 下划线前缀表示"私有"
function Person(name) {
this._name = name; // 约定"私有",但实际可访问
}
// 阶段2:闭包封装(ES3 时代)
function Person(name) {
this.getName = function() { return name; }; // 真正的私有
}
// 阶段3:Symbol 作为键(ES6)—— 半私有
const _name = Symbol('name');
class Person {
constructor(name) {
this[_name] = name; // 外部无法通过字符串键访问
}
}
// 阶段4:WeakMap 存储(ES6)—— 真正的私有
const privateData = new WeakMap();
class Person {
constructor(name) {
privateData.set(this, { name });
}
getName() {
return privateData.get(this).name;
}
}
// 阶段5:# 私有字段(ES2022)—— 语言原生支持
class Person {
#name;
constructor(name) { this.#name = name; }
getName() { return this.#name; }
}
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
# 5.4 继承
继承可以通过以下方式实现:
- 原型链继承。
- 使用
extends关键字(ES6)。
# 5.4.1 原型链继承
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
function Student(name, major) {
Person.call(this, name); // 调用父类构造函数
this.major = major;
}
Student.prototype = Object.create(Person.prototype); // 继承原型
Student.prototype.constructor = Student;
const student = new Student("Alice", "Computer Science");
student.greet(); // Hello, my name is Alice
console.log(student.major); // Computer Science
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
缺点描述:
- 原型链继承多个实例的引用类型属性指向相同,一个实例修改了原型属性,另一个实例的原型属性也会被修改
- 不能传递参数
- 继承单一
# 5.4.2 extends
使用 extends(ES5)
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
class Student extends Person {
constructor(name, major) {
super(name); // 调用父类构造函数
this.major = major;
}
}
const student = new Student("Alice", "Computer Science");
student.greet(); // Hello, my name is Alice
console.log(student.major); // Computer Science
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 5.4.3 组合继承
核心思想: 原型链+借用构造函数的组合继承
基本做法:
- 利用原型链实现对父类型对象的方法继承
- 利用super()借用父类型构建函数初始化相同属性
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.setName = function (name) {
this.name = name;
};
function Student(name, age, price) {
Person.call(this, name, age); // 为了得到父类型的实例属性和方法
this.price = price; // 添加子类型私有的属性
}
Student.prototype = new Person(); // 为了得到父类型的原型属性和方法
Student.prototype.constructor = Student; // 修正constructor属性指向
Student.prototype.setPrice = function (price) { // 添加子类型私有的方法
this.price = price;
};
var s = new Student("孙悟空", 24, 15000);
console.log(s.name, s.age, s.price);
s.setName("猪八戒");
s.setPrice(16000);
console.log(s.name, s.age, s.price);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
缺点:父类中的实例属性和方法既存在于子类的实例中,又存在于子类的原型中,不过仅是内存占用,因此,在使用子类创建实例对象时,其原型中会存在两份相同的属性和方法 。
# 5.4.4 寄生组合继承
寄生组合继承是目前最理想的继承方式(ES6 class extends 的底层实现基本等同于此):
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};
function Student(name, age, grade) {
Person.call(this, name, age); // 借用构造函数
this.grade = grade;
}
// 关键:使用 Object.create 创建干净的原型
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.study = function() {
console.log(`${this.name} is studying`);
};
const s = new Student("Alice", 20, "A");
s.greet(); // Hello, I'm Alice
s.study(); // Alice is studying
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
优势:只调用一次父构造函数,原型链上没有多余属性。
# 5.4.5 继承方式对比总结
| 继承方式 | 优点 | 缺点 |
|---|---|---|
| 原型链继承 | 简单 | 引用类型共享、无法传参 |
| 借用构造函数 | 可传参、不共享引用 | 方法不复用 |
| 组合继承 | 属性独立、方法复用 | 调用两次父构造函数 |
| 寄生组合继承 | 完美解决上述问题 | 写法稍复杂 |
| ES6 class extends | 语法简洁、语义清晰 | 本质是寄生组合继承的语法糖 |
# 5.5 多态
# 5.5.1 多态使用
多态允许子类重写父类的方法。
class Animal {
speak() {
console.log("Animal speaks");
}
}
class Dog extends Animal {
speak() {
console.log("Dog barks");
}
}
class Cat extends Animal {
speak() {
console.log("Cat meows");
}
}
const animals = [new Animal(), new Dog(), new Cat()];
animals.forEach((animal) => animal.speak());
// Animal speaks
// Dog barks
// Cat meows
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 5.5.2 Mixin模式实现多态
JavaScript 没有传统的多重继承,但可以通过 Mixin 模式实现类似效果:
// Mixin 工具函数
const Serializable = (superclass) => class extends superclass {
serialize() {
return JSON.stringify(this);
}
static deserialize(json) {
return Object.assign(new this(), JSON.parse(json));
}
};
const Validatable = (superclass) => class extends superclass {
validate() {
for (const [key, value] of Object.entries(this)) {
if (value == null) throw new Error(`${key} is required`);
}
return true;
}
};
// 使用 Mixin
class User extends Serializable(Validatable(Object)) {
constructor(name, email) {
super();
this.name = name;
this.email = email;
}
}
const user = new User("Alice", "alice@example.com");
user.validate(); // true
console.log(user.serialize()); // {"name":"Alice","email":"alice@example.com"}
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
# 5.6 静态方法和属性
静态方法和属性属于类本身,而不是类的实例。
class MathUtils {
static PI = 3.14;
static square(x) {
return x * x;
}
}
console.log(MathUtils.PI); // 3.14
console.log(MathUtils.square(5)); // 25
2
3
4
5
6
7
8
9
10
静态方法的设计原理:静态方法和属性存储在构造函数对象本身上,而不是 prototype 上,因此实例无法直接访问。这与 Java 的 static 关键字语义一致。
class Counter {
static count = 0;
constructor() {
Counter.count++;
}
static getCount() {
return Counter.count;
}
// 静态方法中的 this 指向类本身
static create() {
return new this(); // this === Counter
}
}
new Counter();
new Counter();
console.log(Counter.getCount()); // 2
const c = new Counter();
// c.getCount(); // TypeError: c.getCount is not a function
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 5.7 Object方法
JavaScript 在Object对象上面,提供了很多相关方法,处理面向对象编程的相关操作。
# 5.7.1 getPrototypeOf
Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法。
var F = function () {};
var f = new F();
Object.getPrototypeOf(f) === F.prototype // true
2
3
下面是几种特殊对象的原型。
// 空对象的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true
// Object.prototype 的原型是 null
Object.getPrototypeOf(Object.prototype) === null // true
// 函数的原型是 Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // true
2
3
4
5
6
7
8
9
# 5.7.2 setPrototypeOf
Object.setPrototypeOf方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。
var a = {};
var b = {x: 1};
Object.setPrototypeOf(a, b);
Object.getPrototypeOf(a) === b // true
a.x // 1
2
3
4
5
6
# 5.7.3 Object.create()
描述:创建一个新对象,使用现有对象作为新对象的原型。
生成实例对象的常用方法是,使用new命令让构造函数返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构建函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢?
JavaScript 提供了Object.create()方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。
// 原型对象
var A = {
print: function () {
console.log('hello');
}
};
// 实例对象
var B = Object.create(A);
Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true
2
3
4
5
6
7
8
9
10
11
12
13
# 5.7.4 isPrototypeOf
实例对象的isPrototypeOf方法,用来判断该对象是否为参数对象的原型。
var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);
o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true
2
3
4
5
6
# 5.8 面向对象总结
JavaScript 的面向对象编程可以通过以下方式实现:
- 对象字面量:简单直接。
- 构造函数:适合创建多个相似对象。
- 类(ES6):更接近传统面向对象语言。
- 封装:通过闭包或私有字段隐藏实现细节。
- 继承:通过原型链或
extends实现代码复用。 - 多态:子类重写父类方法。
- 静态方法和属性:属于类本身。