> ## Content Index
> Fetch the complete content index at: https://iiong.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# 对 JavaScript  进阶学习
- URL: https://iiong.com/learn-more-about-javascript/
- Published: 2023-07-27T14:48:26.000Z
- Updated: 2026-06-22T15:26:57.000Z
- Description: 最近很长一段时间做项目比较轻松，导致进阶知识点大量丢失，虽然之前看过很多知识点但时间长还是忘了，趁最近有时间再次温习并且记录下来。
- Author: 淮城一只猫
- Tags: 编程技术, Web 前端

### 前言

最近很长一段时间做项目比较轻松，导致进阶知识点大量丢失，虽然之前看过很多知识点但时间长还是忘了，趁最近有时间再次温习并且记录下来。

### 对象原型

对象原型在之前一直处于半懵半懂的状态，所以这次搜了大量资料把心中存在的疑问一次性全部解决掉，按照我理解来看原型的概念类似面向对象 class 类的扩展方法，只需要理解它的原型链工作原理就基本理解原型的概念。比如下面的代码：

```javascript
const myObject = {
  city: "Madrid",
  greet() {
    console.log(`来自 ${this.city} 的问候`);
  },
};
myObject.greet(); // 来自 Madrid 的问候

```

如果在控制台打印输出 `myObject` 对象的时候，展开 `Prototype` 会发现有很多熟悉，这些属性可以直接调用并且执行输出内容，所以这些东西其实是所有对象的一个内置属性，也就是所谓的 `prototype` 原型，原型本身也是一个对象，既然是对象那肯定也有原型，一层一层走下去构成原型链，所有原型链的尽头都为 `null`。如果访问一个对象的属性时候，如果在对象本身找不到这个属性的时候，就会在它的原型中查找属性，如果仍然没有找到该属性，那么就搜索原型的原型，直到找到这个属性，这时候还没找到但已经到末端则会返回 `undefined`。比如调用 `myObject.toString()` 的时候它的过程是这样的：

- 在 `myObject` 中寻找 `toString` 属性
- `myObject` 中找不到 `toString` 属性，故在 `myObject` 的原型对象中寻找 `toString`
- 其原型对象拥有这个属性，然后调用它

如果查找一个对象的原型可以使用 `Object.getPrototypeOf()` 方法：

```javascript
Object.getPrototypeOf(myObject); // Object { }

```

如果要重写一个对象的属性方法，可以这么做：

```javascript
myObject.toString = function () {
    console.log('hello')
}

myObject.toString() // hello

```

除了重写对应的属性之外还可以在对象上添加新的原型对象：

```javascript
const personPrototype = {
  greet() {
    console.log("hello!");
  },
};

const carl = Object.create(personPrototype);
carl.greet(); // hello!

```

所有的函数都有一个名为 `prototype` 的属性。调用一个函数作为构造函数时，这个属性被设置为新构造对象的原型（在名为 `__proto__` 的属性中）。在设置一个构造函数的 `prototype`，可以保证所有用该构造函数创建的对象都被赋予该原型：

```javascript
const personPrototype = {
  greet() {
    console.log(`你好，我的名字是 ${this.name}！`);
  },
};

function Person(name) {
  this.name = name;
}

Object.assign(Person.prototype, personPrototype);
// 或
// Person.prototype.greet = personPrototype.greet;

```

这里解释了：

- 创建了一个 `personPrototype` 对象，它具有 `greet()` 方法
- 创建了一个 `Person()` 构造函数，它初始化了要创建人物对象的名字

使用 [Object.assign](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/assign?ref=iiong.com) 将 `personPrototype` 中定义的方法绑定到 `Person` 函数的 `prototype` 属性上：

```javascript
const reuben = new Person("Reuben");
reuben.greet(); // 你好，我的名字是 Reuben！

```

使用上面的 `Person` 构造函数创建的对象有两个属性：

- `name` 属性，在构造函数中设置，在 `Person` 对象中可以直接看到
- `greet()` 方法，在原型中设置

在方法是在原型上定义的，但数据属性是在构造函数中定义的。这是因为方法通常对我们创建的每个对象都是一样的，有时候需要希望每个对象的数据属性都有自己的值（就像这里每个人都有不同的名字）。直接在对象中定义的属性，如这里的`name`，被称为**自有属性**，你可以使用静态方法 [Object.hasOwn()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/hasOwn?ref=iiong.com) 检查一个属性是否是自有属性：

```javascript
const irma = new Person("Irma");

console.log(Object.hasOwn(irma, "name")); // true
console.log(Object.hasOwn(irma, "greet")); // false

```

### 面向对象编程

如果有后端 `OOP` 编程思想这一块应该很好理解的，比如可以做个`老师`的通用模板，然后根据这个模板继承扩展出不同的人种模板，例如语文老师、数学老师等：

```
class Professor
    properties
        name
        teaches
    methods
        grade(paper)
        introduceSelf()

```

`Professor` 类的定义包括如下内容：

- 两个属性：姓名 `name` 和所教的课程 `teaches`
- 两个方法：`grade()` 方法用于为学生的论文打分；`introduceSelf()` 方法用于介绍自己

定义构造函数后就可以创建出具体的老师了，编程语言通常使用 `new` 关键字来表示执行构造函数：

```javascript
walsh = new Professor("沃尔什", "心理学");
lillian = new Professor("丽莲", "诗歌");

walsh.teaches; // '心理学'
walsh.introduceSelf(); // '我是沃尔什，我是你们的心理学老师。'

lillian.teaches; // '丽莲'
lillian.introduceSelf(); // '我是丽莲，我是你们的诗歌老师'

```

如果在上面的基础上新增学生的类，那么需要提出和老师的共同属性和方法，例如：

```
class Student
    properties
        name
        year
    constructor
        Student(name, year)
    methods
        introduceSelf()

```

在某种层级上，二者实际上是*同种事物*，他们能够具有相同的属性也是合理的。继承（Inheritance）可以完成这一操作：

```
class Person
    properties
        name
    constructor
        Person(name)
    methods
        introduceSelf()

class Professor : extends Person
    properties
        teaches
    constructor
        Professor(name, teaches)
    methods
        grade(paper)
        introduceSelf()

class Student : extends Person
    properties
        year
    constructor
        Student(name, year)
    methods
        introduceSelf()

```

更具体的操作过程如下：

```javascript
walsh = new Professor("沃尔什", "心理学");
walsh.introduceSelf(); // '我是沃尔什，我是你们的心理学老师。'

summers = new Student("萨摩斯", 1);
summers.introduceSelf(); // '我是萨摩斯，我是一年级的学生。'

```

或者是普通人的调用：

```javascript
pratt = new Person("普拉特");
pratt.introduceSelf(); // '我是普拉特。'

```

当其他部分的代码想要执行对象的某些操作时，可以借助对象向外部提供的接口完成操作，借此，对象保持了自身的内部状态不会被外部代码随意修改。也就是说，对象的内部状态保持了私有性，而外部代码只能通过对象所提供的接口访问和修改对象的内部状态，不能直接访问和修改对象的内部状态。保持对象内部状态的私有性、明确划分对象的公共接口和内部状态，这些特性称之为**封装**（encapsulation）。封装的好处在于，当程序员需要修改一个对象的某个操作时，程序员只需要修改对象对应方法的内部实现即可，而不需要在所有代码中找出该方法的所有实现，并逐一修改。某种意义上来说，封装在对象内部和对象外部设立了一种特别的“防火墙”。

例如学生只有在二年级以后才能学习弓箭课，我们可以将学生的 `year` 属性暴露给外部，从而外部代码可以通过检查学生的 `year` 属性来确认该学生是否可以选修该课程：

```javascript
if (student.year > 1) {
  // 允许学生选修弓箭课
}

```

向 `Student` 类中添加一个 `canStudyArchery()` 方法（用于检查学生是否能够选修弓箭课），那么相应代码的实现逻辑就会集中在一个地方：

```
class Student : extends Person
    properties
       year
    constructor
        Student(name, year)
    methods
       introduceSelf()
       canStudyArchery() { return this.year > 1 }

```

```javascript
if (student.canStudyArchery()) {
  // 允许学生选修弓箭课课
}

```

这样一来，如果要修改选修弓箭课的规则，只需要更新 `Student` 类中的相应方法即可，而其他地方的代码无需修改，整个系统仍旧可以正常工作。

在许多面向对象编程语言中，可以使用 `private` 关键字标记对象的私有部分，也就是外部代码无法直接访问的部分。如果一个属性在被标记为 `private` 的情况下，外部代码依旧尝试访问该属性，那么通常来说，计算机会抛出一个错误。

```
class Student : extends Person
    properties
       private year
    constructor
        Student(name, year)
    methods
       introduceSelf()
       canStudyArchery() { return this.year > 1 }

student = new Student('Weber', 1)
student.year // 错误：'year'是学生类的私有属性

```

在之前开发汽车小组件我使用 `class` 对象去编写业务，使用过程中也没有遇到太大的坑，绝大部分和后端编程语言差不多，根据上面的 `Person` 类如下：

```javascript
class Person {
  name;

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

  introduceSelf() {
    console.log(`Hi! I'm ${this.name}`);
  }
}

```

在这个 `Person` 类的声明中，有：

- 一个 `name` 属性。
- 一个需要 `name` 参数的构造函数，这一参数用于初始化新的对象的 `name` 属性。
- 一个 `introduceSelf()` 方法，使用 `this` 引用了对象的属性。

`name;` 这一声明是可选的：你可以省略它，因为在构造函数中的 `this.name = name;` 这行代码会在初始化 `name` 属性前自动创建它。但是，在类声明中明确列出属性可以方便阅读代码的人更容易确定哪些属性是这个类的一部分。

```javascript
const giles = new Person("Giles");

giles.introduceSelf(); // Hi! I'm Giles

```

然后继承子类 `Professor`：

```javascript
class Professor extends Person {
  teaches;

  constructor(name, teaches) {
    super(name);
    this.teaches = teaches;
  }

  introduceSelf() {
    console.log(
      `My name is ${this.name}, and I will be your ${this.teaches} professor.`,
    );
  }

  grade(paper) {
    const grade = Math.floor(Math.random() * (5 - 1) + 1);
    console.log(grade);
  }
}

```

实例化此对象：

```javascript
const walsh = new Professor("Walsh", "Psychology");
walsh.introduceSelf(); // 'My name is Walsh, and I will be your Psychology professor'

walsh.grade("my paper"); // some random grade

```

对于子类特殊情况进行封装，例如 `Student` 的 `year` 属性变为私有的，我们可以在不破坏任何使用了 `Student` 类的代码的情况下，修改射箭课程的规则：

```javascript
class Student extends Person {
  #year;

  constructor(name, year) {
    super(name);
    this.#year = year;
  }

  introduceSelf() {
    console.log(`Hi! I'm ${this.name}, and I'm in year ${this.#year}.`);
  }

  canStudyArchery() {
    return this.#year > 1;
  }
}

```

执行如下：

```javascript
const summers = new Student("Summers", 2);

summers.introduceSelf(); // Hi! I'm Summers, and I'm in year 2.
summers.canStudyArchery(); // true

summers.#year; // SyntaxError

```