面向对象
this 绑定规则、面向对象三大特点、new 关键字原理、构造函数与原型链、ES6 class 语法
目录
| 章节 | 核心内容 |
|---|---|
一、this 的四种绑定规则 |
默认绑定、隐式绑定、显式绑定、new 绑定 |
| 二、优先级 | 四种规则的优先级判断 |
| 三、箭头函数 | 箭头函数的 this 规则 |
| 四、普通函数 vs 箭头函数对比 | 两种函数的特性对比 |
| 五、特殊场景 | 类中的 this、事件处理器中的 this |
六、判断 this 的实战流程 |
实战判断流程 |
| 七、类比理解 | 理解 this 的类比方法 |
| 速查总结 | 快速查询表格 |
| 面向对象 | |
| 一、什么是对象 | 对象的定义与特性 |
| 二、什么是面向对象(OOP) | 面向对象编程思想 |
| 三、面向对象的三大特点 | 封装、继承、多态 |
四、new 关键字到底做了什么 |
new 的四个步骤 |
| 五、构造函数、实例、原型对象的关系网 | 三角关系与原型链 |
| 六、与 Python 类的对比 | JS 与 Python 的对比 |
| 七、知识点总结 | 核心知识点汇总 |
| 八、练习题 | 实战练习 |
| ES6 中的面向对象 | |
| 1. Classes(类)和 Object(对象) | 类与对象的概念 |
| 2. Constructor(构造函数) | 构造函数的作用 |
| 3. Inheritance(继承) | 继承的实现 |
| 4. 重写(Override) | 方法重写 |
| 5. 重载(Overload) | 方法重载 |
| 6. 访问修饰符 | public/private/protected |
| 7. 静态方法 | static 方法 |
| 8. Getter 和 Setter | 属性访问器 |
| 9. instanceof | 类型判断 |
| 10. 封装 | 封装思想 |
| 11. 多态 | 多态实现 |
| 12. 抽象 | 抽象类 |
| 13. Proxy 与 Reflect | ES6 代理机制、Vue 3 响应式原理 |
| 14. Object.create() 与属性检测 | 原型式继承、hasOwnProperty、for...in |
| 📌 总结 | 知识点总结表格 |
| 例题 | 实战练习 |
JavaScript 中 this
this的值不是在函数定义时决定的,而是在函数被调用时才确定的。理解
this的关键就一句话:看函数是怎么被调用的。
一、this 的四种绑定规则
1. 默认绑定(独立函数调用)
函数被独立调用时,this 指向全局对象(浏览器中是 window,Node 中是 global)。在严格模式下,this 是 undefined。
function showThis() {
console.log(this);
}
showThis(); // 浏览器中: window;严格模式下: undefined
function strictMode() {
'use strict';
console.log(this); // undefined
}
strictMode();
2. 隐式绑定(作为对象方法调用)
当函数作为对象的方法被调用时,this 指向调用它的那个对象(即点号前面的对象)。
const user = {
name: 'Shea',
greet() {
console.log(`Hello, ${this.name}`);
}
};
user.greet(); // Hello, Shea —— this 指向 user
⚠️ 坑点:隐式绑定丢失
const greetFn = user.greet;
greetFn(); // Hello, undefined —— this 变成了全局对象/undefined
// 函数作为参数传递时也会丢失
setTimeout(user.greet, 1000); // Hello, undefined
原因:
greetFn()是独立调用,不是通过user.调用的,所以走默认绑定。
3. 显式绑定(call / apply / bind)
可以用这三个方法强制指定 this 的值。
| 方法 | 调用方式 | 是否立即执行 | 参数传递 |
|---|---|---|---|
call |
fn.call(thisArg, arg1, arg2) |
立即执行 | 逐个传参 |
apply |
fn.apply(thisArg, [arg1, arg2]) |
立即执行 | 数组传参 |
bind |
fn.bind(thisArg, arg1) |
返回新函数,不执行 | 逐个传参,可预设部分参数 |
function introduce(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
const person = { name: 'Shea' };
introduce.call(person, 'Hi', '!'); // Hi, I'm Shea!
introduce.apply(person, ['Hello', '.']); // Hello, I'm Shea.
const boundFn = introduce.bind(person, 'Hey');
boundFn('?'); // Hey, I'm Shea?
bind 特别有用,可以解决前面隐式绑定丢失的问题:
const safeGreet = user.greet.bind(user);
setTimeout(safeGreet, 1000); // Hello, Shea ✓
4. new 绑定(构造函数调用)
用 new 调用函数时,this 指向新创建的实例对象。
function Post(title) {
this.title = title;
this.createdAt = new Date();
}
const p = new Post('My First Post');
console.log(p.title); // My First Post
new做了四件事:
- 创建新对象
- 把
this指向它- 执行函数体
- 返回这个对象
二、优先级:四种规则同时存在时怎么办?
优先级从高到低:
new绑定 > 显式绑定(bind)> 隐式绑定 > 默认绑定
function show() {
console.log(this.name);
}
const obj1 = { name: 'obj1', show };
const obj2 = { name: 'obj2' };
obj1.show(); // obj1 (隐式)
obj1.show.call(obj2); // obj2 (显式 > 隐式)
const bound = show.bind(obj1);
bound(); // obj1 (显式)
bound.call(obj2); // obj1 (bind 后再 call 无效)
const instance = new bound(); // undefined (new > bind,this 指向新实例)
三、箭头函数:完全不一样的规则
箭头函数没有自己的
this。它的this在定义时就被词法作用域决定了 —— 也就是它外层函数的this,而且永远无法改变。
const obj = {
name: 'Shea',
// 普通函数:this 指向 obj
regular() {
console.log(this.name); // Shea
},
// 箭头函数:this 指向定义时的外层(这里是全局)
arrow: () => {
console.log(this.name); // undefined
}
};
obj.regular(); // Shea
obj.arrow(); // undefined
箭头函数最经典的用途:解决回调中的 this 丢失问题
// 普通函数版本 —— 有问题
const counter = {
count: 0,
start() {
setInterval(function() {
this.count++; // this 指向全局,不是 counter
console.log(this.count); // NaN
}, 1000);
}
};
// 箭头函数版本 —— 完美
const counter2 = {
count: 0,
start() {
setInterval(() => {
this.count++; // this 继承自 start(),即 counter2
console.log(this.count); // 1, 2, 3, ...
}, 1000);
}
};
箭头函数无视 call / apply / bind
const arrowFn = () => console.log(this);
arrowFn.call({ name: 'whatever' }); // 仍然是外层 this,call 失效
箭头函数也不能用 new
const Foo = () => {};
new Foo(); // TypeError: Foo is not a constructor
四、普通函数 vs 箭头函数对比
| 特性 | 普通函数 | 箭头函数 |
|---|---|---|
自己的 this |
有,调用时决定 | 没有,继承外层 |
call/apply/bind 改变 this |
可以 | 不可以 |
能否用 new |
可以 | 不可以 |
arguments 对象 |
有 | 没有(用剩余参数 ...args) |
| 适合做对象方法 | ✓ | ✗(this 指不到对象) |
| 适合做回调 | 需要 bind 或 that=this |
✓(自动继承) |
五、特殊场景
1. 类(class)中的 this
类的方法默认是隐式绑定。但如果方法被作为回调传递,同样会丢失 this。
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(this.name);
}
}
const u = new User('Shea');
u.greet(); // Shea
const fn = u.greet;
fn(); // TypeError: 类方法默认严格模式,this 是 undefined
解决方法:在构造函数里 bind,或者用箭头函数定义方法。
// 方法一:构造函数中 bind
class User {
constructor(name) {
this.name = name;
this.greet = this.greet.bind(this);
}
greet() {
console.log(this.name);
}
}
// 方法二:类字段 + 箭头函数
class User2 {
constructor(name) {
this.name = name;
}
greet = () => {
console.log(this.name);
}
}
2. 事件处理器中的 this
DOM 事件中,this 默认指向触发事件的元素。但箭头函数会改变这个行为:
button.addEventListener('click', function() {
console.log(this); // <button> 元素
});
button.addEventListener('click', () => {
console.log(this); // 外层 this(通常是 window)
});
六、判断 this 的实战流程
遇到 this 不知道指向哪,按这个顺序问自己:
- 是箭头函数吗? → 看外层定义处的
this,结束。 - 是用
new调用的吗? →this是新实例。 - 是用
call/apply/bind调用的吗? →this是第一个参数。 - 是作为某个对象的方法调用的(
obj.fn())吗? →this是那个对象。 - 以上都不是 → 默认绑定:严格模式
undefined,否则全局对象。
七、类比理解
可以把 this 想象成一个代词「我」:
「我」指谁,取决于谁在说这句话,而不是这句话写在哪里。
- 普通函数:就像演员念台词,谁来念,「我」就是谁。
- 箭头函数:像剧本里写死的旁白,「我」永远是写剧本的那个人,不管谁来读都不会变。
速查总结
| 调用方式 | this 指向 |
|---|---|
fn() |
全局对象 / undefined(严格模式) |
obj.fn() |
obj |
fn.call(ctx) / fn.apply(ctx) |
ctx |
fn.bind(ctx)() |
ctx |
new Fn() |
新创建的实例 |
() => {} |
外层词法作用域的 this |
面向对象
搞懂 JS 中的对象、面向对象思想、
new的工作原理,以及构造函数、实例、原型对象之间的关系网。
一、什么是对象
在 JavaScript 中,对象是键值对的无序集合。值可以是任何类型——基本类型、其他对象、甚至函数。当函数作为对象的属性出现时,我们称它为方法。
const person = {
name: "Shea", // 属性(基本类型值)
age: 25, // 属性
hobbies: ["coding"], // 属性(对象值)
sayHi() { // 方法(函数值)
console.log("Hi, I'm " + this.name);
}
};
person.name; // 访问属性
person.sayHi(); // 调用方法
JS 中的"一切皆对象"
除了 7 种原始类型(string、number、boolean、null、undefined、symbol、bigint),其他都是对象:
- 数组(Array)是对象
- 函数(Function)是对象——可以有属性
- 日期、正则、Map、Set 都是对象
💡 类比 Python:JS 对象类似 Python 的
dict,但 JS 对象的属性可以是函数,这让它能承担"类的实例"的角色。
二、什么是面向对象(OOP)
面向对象编程(Object-Oriented Programming)是一种编程范式,核心思想是把现实世界的事物抽象成"对象",让对象自己携带数据(属性)和行为(方法)。
面向过程 vs 面向对象
以"博客文章管理"为例:
| 维度 | 面向过程 | 面向对象 |
|---|---|---|
| 组织方式 | 数据和函数分离 | 数据和方法绑在对象上 |
| 代码示例 | createPost(title) updatePost(id) |
post.update() post.publish() |
| 思维方式 | 我要执行哪些步骤? | 谁来负责做这件事? |
| 适用场景 | 简单脚本、流程化任务 | 复杂业务、多状态实体 |
JS 的面向对象很特殊
- Java、C++ 是基于类(class-based) 的——先有类,再有实例
- JS 本质上是基于原型(prototype-based) 的——对象直接从其他对象"继承"
- ES6 引入的
class关键字只是语法糖,底层依然是原型机制
三、面向对象的三大特点
1. 封装(Encapsulation)
把数据和操作数据的方法包裹在一起,对外暴露必要接口,隐藏内部细节。
class BankAccount {
#balance = 0; // # 表示私有字段,外部访问不到
deposit(amount) {
if (amount > 0) this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
2. 继承(Inheritance)
子类复用父类的属性和方法,避免重复代码。JS 通过原型链实现继承。
class Animal {
eat() { console.log("eating"); }
}
class Dog extends Animal {
bark() { console.log("woof"); }
}
const d = new Dog();
d.eat(); // 继承自 Animal
d.bark(); // 自己的
3. 多态(Polymorphism)
同一个方法,不同对象有不同表现——子类可以重写父类方法。
class Animal {
speak() { console.log("some sound"); }
}
class Dog extends Animal {
speak() { console.log("woof"); } // 重写
}
class Cat extends Animal {
speak() { console.log("meow"); } // 重写
}
[new Dog(), new Cat()].forEach(a => a.speak());
// woof
// meow
四、new 关键字到底做了什么
这是 JS 面试的经典必考题。当你写 const p = new Person("Shea") 时,JS 引擎在背后做了 4 件事:
// 模拟 new 的内部实现
function myNew(Constructor, ...args) {
// 第 1 步:创建一个全新的空对象
const obj = {};
// 第 2 步:把新对象的 __proto__ 指向构造函数的 prototype
// (这一步建立了实例和原型对象的链接)
obj.__proto__ = Constructor.prototype;
// 第 3 步:把构造函数里的 this 绑定到新对象,执行构造函数
// (这一步给实例添加自有属性)
const result = Constructor.apply(obj, args);
// 第 4 步:如果构造函数显式返回了对象,就用那个对象;
// 否则返回我们创建的 obj
return (typeof result === "object" && result !== null) ? result : obj;
}
记忆口诀:创建、链接、绑定、返回
| 步骤 | 动作 | 作用 |
|---|---|---|
| 1. 创建 | const obj = {} |
生成一个空对象作为实例 |
| 2. 链接 | obj.__proto__ = Fn.prototype |
建立原型链,让实例能访问原型上的方法 |
| 3. 绑定 | Fn.call(obj, ...args) |
让构造函数里的 this 指向新实例,初始化属性 |
| 4. 返回 | return obj |
把新实例返回给调用者 |
验证一下
function Person(name) {
this.name = name; // 第 3 步:this 就是 new 出来的新对象
}
const p = new Person("Shea");
console.log(p.name); // "Shea" — 第 3 步设置的
console.log(p.__proto__ === Person.prototype); // true — 第 2 步建立的链接
console.log(p instanceof Person); // true
五、构造函数、实例、原型对象的关系网
三角关系图
![[JS构造函数、实例、原型对象关系网.svg]]
三个核心链接
口诀:构造函数有
prototype,实例有__proto__,原型对象有constructor
| 关系 | 方向 | 代码表达 | 谁创建的? |
|---|---|---|---|
| 构造函数 → 原型对象 | Person.prototype |
函数声明时 JS 自动创建 | JS 引擎 |
| 原型对象 → 构造函数 | Person.prototype.constructor |
原型对象上的回指属性 | JS 引擎 |
| 实例 → 原型对象 | p.__proto__ |
隐藏的内部链接 | new 的第 2 步 |
完整代码验证
function Person(name) {
this.name = name; // 实例自有属性
}
Person.prototype.sayHi = function() { // 共享方法放在原型上
console.log("Hi, I'm " + this.name);
};
const p1 = new Person("Shea");
const p2 = new Person("Gavin");
// ===== 三角关系全部成立 =====
p1.__proto__ === Person.prototype // true
Person.prototype.constructor === Person // true
p1.constructor === Person // true(通过原型链找到)
// ===== 共享 vs 独立 =====
p1.name === p2.name // false(自有属性,各自独立)
p1.sayHi === p2.sayHi // true(共享同一个原型方法)
原型链的查找过程
当你调用 p.sayHi() 时,JS 会沿着原型链向上查找:
p (实例)
↓ 自己身上有 sayHi 吗?没有
↓ 通过 __proto__ 上溯
Person.prototype (原型对象)
↓ 这里有 sayHi 吗?有!✅ 调用它
↓ 如果还没有,继续上溯
Object.prototype (终极原型)
↓ 这里有 sayHi 吗?没有
↓ 继续上溯
null ← 链的尽头,如果还没找到就返回 undefined
这条向上查找的链路就是 原型链(Prototype Chain),也是 JS 实现继承的底层机制。
为什么要把方法放在原型上?
// ❌ 不好:每次 new 都创建一份方法,1000 个实例 = 1000 份相同函数
function Person(name) {
this.name = name;
this.sayHi = function() { console.log(this.name); };
}
// ✅ 好:方法只在原型上定义一份,所有实例共享
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() { console.log(this.name); };
结论:实例自有的数据(name、age)放在构造函数里用 this 设置;所有实例共享的方法(sayHi、update)放在原型上。
六、与 Python 类的对比
为方便理解,这里把 JS 原型机制和 Python 类机制做个对照:
| 概念 | Python | JavaScript |
|---|---|---|
| 定义"模板" | class Person: |
function Person() 或 class Person {} |
| 实例自有属性 | self.name = name |
this.name = name |
| 共享方法的存放位置 | 类对象本身 | 构造函数的 prototype 对象 |
| 创建实例 | p = Person("Shea") |
const p = new Person("Shea") |
| 方法查找 | MRO(方法解析顺序) | 原型链 |
| 继承 | class Dog(Animal): |
class Dog extends Animal {} |
核心差异:Python 的"类"是一种特殊的对象,实例和类是两种不同的东西;JS 里**"类"本质上还是函数**,实例通过 __proto__ 链接到一个普通对象(prototype),没有真正的"类"概念。
七、知识点总结
- 对象 = 键值对集合,值可以是任何东西包括函数
- 面向对象 = 把数据和行为绑在对象上的编程思想
- 三大特点 = 封装(藏细节)、继承(复用)、多态(重写)
new做的 4 件事 = 创建空对象 → 链接原型 → 绑定 this 执行 → 返回对象- 三角关系 = 构造函数
.prototype↔ 原型对象.constructor,实例.__proto__指向原型对象 - 原型链 = 实例找不到属性时,沿着
__proto__一路向上查找,直到null
八、练习题
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(this.name + " is eating");
};
const cat = new Animal("Tom");
// 以下输出是什么?为什么?
console.log(cat.name); // Tom
console.log(cat.eat === Animal.prototype.eat); // true
console.log(cat.__proto__ === Animal.prototype); // true
console.log(cat.constructor === Animal); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
点击查看答案
"Tom" // 自有属性
true // 方法在原型上,实例通过原型链访问的是同一个函数
true // new 的第 2 步建立的链接
true // cat 自身没有 constructor,从 Animal.prototype 找到
true // Animal.prototype 本身也是一个对象,它的原型是 Object.prototype
ES6 中的面向对象
ES6 中面向对象编程的核心概念,包括类与对象、构造函数、继承、重写重载、访问修饰符、静态方法、Getter/Setter、instanceof、封装、多态与抽象等。
- Classes(类)和 Object(对象)
- Constructor(构造函数)
- Inheritance(继承)
- 重写(Override)
- 重载(Overload)
- 访问修饰符
- 静态方法
- Getter 和 Setter
- instanceof
- 封装
- 多态
- 抽象
1. Classes(类)和 Object(对象)
核心概念:类是对象的模板,对象是类的实例。
// 定义一个类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}
// 创建对象(实例化)
const p1 = new Person('Shea', 25);
p1.sayHello(); // Hello, I'm Shea
类 vs 对象
| 项目 | 类(Class) | 对象(Object) |
|---|---|---|
| 定义 | 抽象的模板/蓝图 | 类的具体实例 |
| 关键字 | class |
new |
| 内存 | 不占用实例内存 | 每次实例化分配新内存 |
| 用途 | 描述事物的特征与行为 | 实际操作和使用 |
2. Constructor(构造函数)
作用:在 new 一个对象时自动调用,用于初始化实例的属性。
class Car {
constructor(brand, color) {
this.brand = brand; // 实例属性
this.color = color;
}
}
const myCar = new Car('Toyota', 'red');
console.log(myCar.brand); // Toyota
⚠️ 注意:每个类只能有一个构造函数,否则会抛出
SyntaxError。
3. Inheritance(继承)
关键字:使用 extends 实现继承,使用 super 调用父类。
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
bark() {
console.log(`${this.name} is barking`);
}
}
const d = new Dog('Buddy', 'Labrador');
d.eat(); // Buddy is eating ← 继承自 Animal
d.bark(); // Buddy is barking
super 的两种用法
| 用法 | 场景 | 示例 |
|---|---|---|
super(...) |
在子类构造函数中调用父类构造函数 | super(name) |
super.method() |
调用父类的方法 | super.eat() |
4. 重写(Override)
定义:子类重新定义父类中已有的方法。
class Animal {
speak() {
console.log('Some sound...');
}
}
class Cat extends Animal {
// 重写父类的 speak 方法
speak() {
console.log('Meow!');
}
}
new Animal().speak(); // Some sound...
new Cat().speak(); // Meow!
💡 Tip:重写时可通过 super.speak() 在子类中调用父类原始方法。
5. 重载(Overload)
说明:JavaScript 不直接支持传统意义上的方法重载(同名不同参数),但可通过参数判断来模拟。
class Calculator {
add(...args) {
if (args.length === 2) {
return args[0] + args[1];
} else if (args.length === 3) {
return args[0] + args[1] + args[2];
}
return args.reduce((a, b) => a + b, 0);
}
}
const c = new Calculator();
console.log(c.add(1, 2)); // 3
console.log(c.add(1, 2, 3)); // 6
console.log(c.add(1, 2, 3, 4)); // 10
重写 vs 重载对比
| 维度 | 重写(Override) | 重载(Overload) |
|---|---|---|
| 发生位置 | 父子类之间 | 同一个类中 |
| 方法签名 | 完全相同 | 同名,参数不同 |
| JS 原生支持 | ✅ 支持 | ❌ 不直接支持,需模拟 |
| 目的 | 改变父类行为 | 提供多种调用方式 |
6. 访问修饰符
ES2022 起,JS 引入了真正的私有字段语法 #。
class BankAccount {
#balance = 0; // 私有字段(# 开头)
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const acc = new BankAccount();
acc.deposit(100);
console.log(acc.getBalance()); // 100
// console.log(acc.#balance); // ❌ SyntaxError
访问级别对照
| 修饰符 | 写法 | 访问范围 |
|---|---|---|
| public(默认) | this.name |
任何地方都可访问 |
| private | this.#name |
仅类内部可访问 |
| protected(约定) | this._name(下划线约定) |
约定子类可访问,无强制 |
💡 TypeScript 中提供了完整的
public/private/protected关键字。
7. 静态方法
关键字:static,属于类本身而非实例。
class MathUtil {
static PI = 3.14159; // 静态属性
static square(n) { // 静态方法
return n * n;
}
}
console.log(MathUtil.PI); // 3.14159
console.log(MathUtil.square(5)); // 25
const m = new MathUtil();
// m.square(5); // ❌ TypeError:实例不能调用静态方法
8. Getter 和 Setter
作用:用像访问属性的方式来调用方法,常用于数据校验和计算属性。
class User {
#age = 0;
get age() { // getter
return this.#age;
}
set age(value) { // setter
if (value < 0) {
throw new Error('Age cannot be negative');
}
this.#age = value;
}
}
const u = new User();
u.age = 25; // 触发 setter
console.log(u.age); // 触发 getter,输出 25
// u.age = -5; // ❌ Error: Age cannot be negative
9. instanceof
作用:检测一个对象是否是某个类的实例(包括继承链)。
class Animal {}
class Dog extends Animal {}
const d = new Dog();
console.log(d instanceof Dog); // true
console.log(d instanceof Animal); // true(继承链)
console.log(d instanceof Object); // true(所有对象都是 Object 的实例)
10. 封装
核心思想:隐藏内部实现,对外只暴露接口。
class Temperature {
#celsius = 0; // 隐藏内部存储
setCelsius(c) { // 暴露接口
this.#celsius = c;
}
getFahrenheit() { // 暴露接口
return this.#celsius * 9 / 5 + 32;
}
}
const t = new Temperature();
t.setCelsius(25);
console.log(t.getFahrenheit()); // 77
✨ 封装的好处:
- 🔒 保护数据完整性
- 🛠 降低耦合,便于维护
- 🧪 便于测试
11. 多态
定义:同一个方法在不同对象上有不同的表现。
class Shape {
area() {
return 0;
}
}
class Circle extends Shape {
constructor(r) { super(); this.r = r; }
area() { return Math.PI * this.r ** 2; }
}
class Square extends Shape {
constructor(s) { super(); this.s = s; }
area() { return this.s ** 2; }
}
// 同一调用,不同结果 —— 这就是多态
const shapes = [new Circle(5), new Square(4)];
shapes.forEach(s => console.log(s.area()));
// 78.539...
// 16
12. 抽象
说明:JS 没有原生 abstract 关键字,但可通过抛出错误来模拟抽象类。
class AbstractAnimal {
constructor() {
if (new.target === AbstractAnimal) {
throw new Error('Cannot instantiate abstract class');
}
}
// 抽象方法 —— 子类必须实现
speak() {
throw new Error('Method "speak()" must be implemented');
}
}
class Dog extends AbstractAnimal {
speak() {
console.log('Woof!');
}
}
// new AbstractAnimal(); // ❌ Error
new Dog().speak(); // Woof!
💡 TypeScript 中可以直接使用
abstract class和abstract method。
13. Proxy 与 Reflect
Proxy(代理)是 ES6 引入的元编程特性,可以拦截并自定义对象的基本操作,是 Vue 3 响应式系统的核心基础。
Proxy 基本语法
// new Proxy(目标对象, 处理器对象)
const proxy = new Proxy(target, handler);
常用 handler trap(陷阱):
| trap | 拦截的操作 | 示例 |
|---|---|---|
get |
读取属性 obj.key |
实现默认值、只读属性 |
set |
写入属性 obj.key = v |
数据校验、响应式触发 |
has |
in 操作符 |
隐藏内部属性 |
deleteProperty |
delete obj.key |
阻止删除 |
apply |
函数调用 fn() |
函数日志、权限控制 |
construct |
new 操作符 |
单例模式 |
实际用例
// 用例 1:数据校验
const person = new Proxy({}, {
set(target, key, value) {
if (key === 'age' && typeof value !== 'number') {
throw new TypeError('age 必须是数字');
}
target[key] = value;
return true; // set trap 必须返回 true 表示成功
}
});
person.age = 25; // ✅
person.age = '25'; // ❌ TypeError: age 必须是数字
// 用例 2:默认值(访问不存在的属性返回默认值)
const withDefault = new Proxy({}, {
get(target, key) {
return key in target ? target[key] : `[${key} 不存在]`;
}
});
withDefault.name = 'Shea';
console.log(withDefault.name); // 'Shea'
console.log(withDefault.missing); // '[missing 不存在]'
// 用例 3:简化版 Vue 3 响应式原理
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log(`读取 ${key}`); // 依赖收集(track)
return Reflect.get(target, key);
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
console.log(`${key} 变更为 ${value}`); // 触发更新(trigger)
return result;
}
});
}
const state = reactive({ count: 0 });
state.count++; // 读取 count → count 变更为 1
Reflect
Reflect 是与 Proxy 配套的工具对象,提供操作对象的标准方法,与 Proxy 的 trap 一一对应:
// Reflect 的方法是对象操作的"标准写法"
Reflect.get(obj, 'name') // 相当于 obj.name
Reflect.set(obj, 'name', 'Shea') // 相当于 obj.name = 'Shea'
Reflect.has(obj, 'name') // 相当于 'name' in obj
Reflect.deleteProperty(obj, 'name') // 相当于 delete obj.name
Reflect.ownKeys(obj) // 获取所有自有属性 key(含 Symbol)
为什么在 Proxy handler 里用 Reflect 而不是直接操作 target?
// ❌ 直接操作 target,this 指向可能错误
get(target, key) {
return target[key]; // 如果是子类调用,this 可能错误
}
// ✅ 用 Reflect 保证 receiver(即 proxy 本身)正确传递
get(target, key, receiver) {
return Reflect.get(target, key, receiver); // receiver = proxy
}
Proxy vs Object.defineProperty(Vue 2 vs Vue 3)
| 对比 | Object.defineProperty(Vue 2) |
Proxy(Vue 3) |
|---|---|---|
| 拦截范围 | 只能拦截已知属性 | 拦截整个对象,包括新增属性 |
| 数组监听 | 需要 hack 数组方法 | 原生支持数组变化 |
| 嵌套对象 | 递归遍历,初始化慢 | 惰性代理,用到时才代理 |
| 性能 | 初始化开销大 | 按需代理,更轻量 |
14. Object.create() 与属性检测
Object.create()
创建一个以指定对象为原型的新对象,无需构造函数:
const animal = {
eat() { console.log(`${this.name} is eating`); }
};
// 以 animal 为原型创建 dog
const dog = Object.create(animal);
dog.name = 'Buddy';
dog.bark = function() { console.log('Woof!'); };
dog.eat(); // Buddy is eating(继承自 animal)
dog.bark(); // Woof!(自有方法)
Object.getPrototypeOf(dog) === animal; // true
dog.__proto__ === animal; // true(等价)
与 new 的对比:
// new:需要构造函数,且实例原型 = 构造函数.prototype
function Animal(name) { this.name = name; }
const a1 = new Animal('Cat');
// a1.__proto__ === Animal.prototype
// Object.create:直接以对象为原型,更灵活
const a2 = Object.create(Animal.prototype);
// a2.__proto__ === Animal.prototype(等价)
// 但 Animal 函数体没有执行,name 未设置
Object.create(null): 创建无原型的纯净对象,常用作字典/Map:
const dict = Object.create(null);
dict.key = 'value';
// 没有 toString、hasOwnProperty 等原型方法,不会有属性名污染
console.log('toString' in dict); // false ✅
console.log(dict.constructor); // undefined
hasOwnProperty vs in 操作符
function Person(name) { this.name = name; }
Person.prototype.greet = function() {};
const p = new Person('Shea');
// in:检查自有属性 + 原型链上的所有属性
'name' in p; // true(自有)
'greet' in p; // true(原型链上)
// hasOwnProperty:只检查自有属性
p.hasOwnProperty('name'); // true
p.hasOwnProperty('greet'); // false
// ES2022 推荐:Object.hasOwn()(更安全,不怕 hasOwnProperty 被重写)
Object.hasOwn(p, 'name'); // true
Object.hasOwn(p, 'greet'); // false
for...in vs Object.keys() 的区别
function Person(name) { this.name = name; }
Person.prototype.greet = function() {};
const p = new Person('Shea');
// for...in:遍历所有可枚举属性(含原型链)
for (const key in p) {
console.log(key); // name, greet(两个都输出)
}
// 配合 hasOwnProperty 只遍历自有属性(老写法)
for (const key in p) {
if (Object.hasOwn(p, key)) console.log(key); // 只有 name
}
// Object.keys():只返回自有可枚举属性(推荐)
Object.keys(p); // ['name']
// Object.getOwnPropertyNames():自有属性(含不可枚举,不含 Symbol)
Object.getOwnPropertyNames(p); // ['name']
// Reflect.ownKeys():自有属性(含不可枚举 + Symbol)
Reflect.ownKeys(p); // ['name']
📌 总结
| 概念 | 关键字/语法 | 一句话理解 |
|---|---|---|
| 类与对象 | class / new |
模板 vs 实例 |
| 构造函数 | constructor |
实例化时的初始化 |
| 继承 | extends / super |
子类复用父类 |
| 重写 | 同名方法 | 子类改写父类方法 |
| 重载 | 参数判断模拟 | 同名方法不同参数 |
| 访问修饰符 | # / _ |
控制可见性 |
| 静态方法 | static |
类本身的方法 |
| Getter/Setter | get / set |
像属性一样调用方法 |
| instanceof | instanceof |
类型判断 |
| 封装 | 私有 + 接口 | 隐藏实现细节 |
| 多态 | 重写 + 父类引用 | 同方法多形态 |
| 抽象 | 模拟实现 | 不可实例化的模板 |
| Proxy | new Proxy(target, handler) |
拦截对象操作,响应式核心 |
| Reflect | Reflect.get/set/has... |
对象操作的标准方法,配合 Proxy |
| Object.create() | Object.create(proto) |
以指定对象为原型创建实例 |
| 属性检测 | Object.hasOwn() / in |
区分自有属性与原型属性 |
课后例题
![[面向对象课后例题.png]]
// todo 面向过程
function Hero(name, hp, [attack, spellPower], defense) {
let hero = {
name,
hp,
gj: (attack ? attack : spellPower),
defense,
level: 1
}
const move = () => console.log('move')
const attackEnemy = gj => {
return gj
}
const levelUp = () => {
hero.level++;
hero.gj *= 1.1
}
hero = { ...hero, move, attackEnemy, levelUp }
return hero
}
------------------------------------------------------------
// todo 面向对象
class Hero {
#hp; // ← 私有字段声明,告诉类"我有一个叫 #hp 的私有属性" /// ES2022+ (2021年发布)
constructor(name, hp, attack, spellPower, defense) {
this.name = name;
this.#hp = hp;
this.attack = attack;
this.spellPower = spellPower;
this.defense = defense;
this.level = 1;
}
get hp() {
return this.#hp;
}
takeDamage(damage) {
this.#hp = Math.max(0, this.#hp - damage);
}
move() {
console.log(`${this.name} 正在移动`);
}
attackEnemy(enemy) {
let damage = this.attack > 0 ? this.attack : this.spellPower;
damage = Math.max(1, damage - enemy.defense * 0.5);
enemy.takeDamage(damage);
console.log(`${this.name} 攻击 ${enemy.name},造成 ${damage.toFixed(1)} 点伤害`);
}
levelUp() {
this.level++;
if (this.attack > 0) {
this.attack *= 1.1;
}
if (this.spellPower > 0) {
this.spellPower *= 1.1;
}
console.log(`${this.name} 升级到 ${this.level} 级`);
}
}```