对 JavaScript 进阶学习
前言
最近很长一段时间做项目比较轻松,导致进阶知识点大量丢失,虽然之前看过很多知识点但时间长还是忘了,趁最近有时间再次温习并且记录下来。
对象原型
对象原型在之前一直处于半懵半懂的状态,所以这次搜了大量资料把心中存在的疑问一次性全部解决掉,按照我理解来看原型的概念类似面向对象 class 类的扩展方法,只需要理解它的原型链工作原理就基本理解原型的概念。比如下面的代码:
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()
方法:
Object.getPrototypeOf(myObject); // Object { }
如果要重写一个对象的属性方法,可以这么做:
myObject.toString = function () {
console.log('hello')
}
myObject.toString() // hello
除了重写对应的属性之外还可以在对象上添加新的原型对象:
const personPrototype = {
greet() {
console.log("hello!");
},
};
const carl = Object.create(personPrototype);
carl.greet(); // hello!
所有的函数都有一个名为 prototype
的属性。调用一个函数作为构造函数时,这个属性被设置为新构造对象的原型(在名为 __proto__
的属性中)。在设置一个构造函数的 prototype
,可以保证所有用该构造函数创建的对象都被赋予该原型:
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 将 personPrototype
中定义的方法绑定到 Person
函数的 prototype
属性上:
const reuben = new Person("Reuben");
reuben.greet(); // 你好,我的名字是 Reuben!
使用上面的 Person
构造函数创建的对象有两个属性:
name
属性,在构造函数中设置,在Person
对象中可以直接看到greet()
方法,在原型中设置
在方法是在原型上定义的,但数据属性是在构造函数中定义的。这是因为方法通常对我们创建的每个对象都是一样的,有时候需要希望每个对象的数据属性都有自己的值(就像这里每个人都有不同的名字)。直接在对象中定义的属性,如这里的name
,被称为自有属性,你可以使用静态方法 Object.hasOwn()
检查一个属性是否是自有属性:
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
关键字来表示执行构造函数:
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()
更具体的操作过程如下:
walsh = new Professor("沃尔什", "心理学");
walsh.introduceSelf(); // '我是沃尔什,我是你们的心理学老师。'
summers = new Student("萨摩斯", 1);
summers.introduceSelf(); // '我是萨摩斯,我是一年级的学生。'
或者是普通人的调用:
pratt = new Person("普拉特");
pratt.introduceSelf(); // '我是普拉特。'
当其他部分的代码想要执行对象的某些操作时,可以借助对象向外部提供的接口完成操作,借此,对象保持了自身的内部状态不会被外部代码随意修改。也就是说,对象的内部状态保持了私有性,而外部代码只能通过对象所提供的接口访问和修改对象的内部状态,不能直接访问和修改对象的内部状态。保持对象内部状态的私有性、明确划分对象的公共接口和内部状态,这些特性称之为封装(encapsulation)。封装的好处在于,当程序员需要修改一个对象的某个操作时,程序员只需要修改对象对应方法的内部实现即可,而不需要在所有代码中找出该方法的所有实现,并逐一修改。某种意义上来说,封装在对象内部和对象外部设立了一种特别的“防火墙”。
例如学生只有在二年级以后才能学习弓箭课,我们可以将学生的 year
属性暴露给外部,从而外部代码可以通过检查学生的 year
属性来确认该学生是否可以选修该课程:
if (student.year > 1) {
// 允许学生选修弓箭课
}
向 Student
类中添加一个 canStudyArchery()
方法(用于检查学生是否能够选修弓箭课),那么相应代码的实现逻辑就会集中在一个地方:
class Student : extends Person
properties
year
constructor
Student(name, year)
methods
introduceSelf()
canStudyArchery() { return this.year > 1 }
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
类如下:
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
属性前自动创建它。但是,在类声明中明确列出属性可以方便阅读代码的人更容易确定哪些属性是这个类的一部分。
const giles = new Person("Giles");
giles.introduceSelf(); // Hi! I'm Giles
然后继承子类 Professor
:
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);
}
}
实例化此对象:
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
类的代码的情况下,修改射箭课程的规则:
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;
}
}
执行如下:
const summers = new Student("Summers", 2);
summers.introduceSelf(); // Hi! I'm Summers, and I'm in year 2.
summers.canStudyArchery(); // true
summers.#year; // SyntaxError