← Frontend

面向对象

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 做了四件事:

  1. 创建新对象
  2. 把 this 指向它
  3. 执行函数体
  4. 返回这个对象

二、优先级:四种规则同时存在时怎么办?

优先级从高到低:

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 不知道指向哪,按这个顺序问自己:

  1. 是箭头函数吗? → 看外层定义处的 this,结束。
  2. 是用 new 调用的吗? → this 是新实例。
  3. 是用 call / apply / bind 调用的吗? → this 是第一个参数。
  4. 是作为某个对象的方法调用的(obj.fn())吗? → this 是那个对象。
  5. 以上都不是 → 默认绑定:严格模式 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),没有真正的"类"概念。


七、知识点总结

  1. 对象 = 键值对集合,值可以是任何东西包括函数
  2. 面向对象 = 把数据和行为绑在对象上的编程思想
  3. 三大特点 = 封装(藏细节)、继承(复用)、多态(重写)
  4. new 做的 4 件事 = 创建空对象 → 链接原型 → 绑定 this 执行 → 返回对象
  5. 三角关系 = 构造函数 .prototype ↔ 原型对象 .constructor,实例 .__proto__ 指向原型对象
  6. 原型链 = 实例找不到属性时,沿着 __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、封装、多态与抽象等。


  1. Classes(类)和 Object(对象)
  2. Constructor(构造函数)
  3. Inheritance(继承)
  4. 重写(Override)
  5. 重载(Overload)
  6. 访问修饰符
  7. 静态方法
  8. Getter 和 Setter
  9. instanceof
  10. 封装
  11. 多态
  12. 抽象

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} 级`);
    }
}```