一.类的概念
在JavaScript中,类是创建实例对象的模板。
在ES6之前,是用构造函数来创建实例对象的。
这里面最重要的就是继承。
二.构造函数
构造函数就是ES6之前的类。
1.类创建的实例对象有两种属性,一种是实例对象自己的,一种是类上公共的。
通过 hasOwnProperty方法,返回 true就是自己的属性,false就不是自己的。
function Animal() {
// 类中 this 指的都是创建的实例
this.type = '哺乳类'
}
// 创建 animal 这个实例对象
let animal = new Animal()
console.log(animal.type) // 哺乳类
console.log(animal.hasOwnProperty('type')) // true,type是实例上自己的属性
// 给构造函数的原型对象添加方法
Animal.prototype.eat = function () {
console.log('吃吃吃')
}
animal.eat() // 吃吃吃
console.log(animal.hasOwnProperty('eat')) // false,可以知道这不是实例上的属性
2.构造函数实现继承
如果我们再创建一个Tiger类。此时 Tiger类是要继承 Animal类的,而Animal类则不能被new,所以需要对Animal类进行处理。
function Animal() {
if (new.target === Animal) {
throw new Error('Animal类可以被继承但是不能被new')
}
this.type = '哺乳类'
}
function Tiger() {}
let animal = new Animal()
而Tiger类要继承Animal类的话,需要在自己里面去调用Animal类,并且上面说过,类里面的this指向的是实例对象,所以我们需要改变Animal类调用时候的this指向。通过call()方法。
function Animal() {
if (new.target === Animal) {
throw new Error('Animal类可以被继承但是不能被new')
}
this.type = '哺乳类'
}
function Tiger() {
// 通过call方法让Animal执行this.type的时候this指向Tiger的this,也就是Tiger创建的实例对象
Animal.call(this) // 继承了实例上的属性
}
let animal = new Tiger()
console.log(animal.type) // 哺乳类
此时还需要继承Animal类的原型上的属性,不可以像下面这么写,这样虽然可以获取Animal类原型对象的方法,但是在给Tiger类的原型添加属性的时候也会给Animal的原型上添加,这就不是继承而是两边相等了。
Tiger.prototype = Animal.prototype
我们应该做的是,tiger实例对象找不到属性的时候,先去Tiger的原型对象上找,也就是tiger.__proto__,如果还找不到就去Animal类的原型对象上面找,所以只需要将Tiger类的原型对象的原型链__proto__指向Animal的原型对象就好了。
Tiger.prototype.__proto__ = Animal.prototype
// tiger => tiger.__proto__(Tiger.prototype) => tiger.__proto__.__proto__(Tiger.prototype.__proto__)(也就是Animal.prototype)
所以全部应该是:
function Animal() {
if (new.target === Animal) {
throw new Error('Animal类可以被继承但是不能被new')
}
this.type = '哺乳类'
}
Animal.prototype.eat = function () {
console.log('吃吃吃')
}
function Tiger() {
Animal.call(this)
}
let tiger = new Tiger()
Tiger.prototype.__proto__ = Animal.prototype
console.log(tiger.type) // 哺乳类
tiger.eat() // 吃吃吃
ES6 更新写法:
Tiger.prototype.__proto__ = Animal.prototype
// ES6 换了个更简便的方法
Object.setPrototypeOf(Tiger.prototype, Animal.prototype)
或者使用 Object.create() 方法,不过该方法需要在new之前调用。
function Animal() {
if (new.target === Animal) {
throw new Error("Animal类可以被继承但是不能被new");
}
this.type = "哺乳类";
}
Animal.prototype.eat = function () {
console.log("吃吃吃");
};
function Tiger() {
Animal.call(this);
}
Tiger.prototype = Object.create(Animal.prototype)
let tiger = new Tiger();
console.log(tiger.type); // 哺乳类
tiger.eat(); // 吃吃吃
关于 create() 方法我们可以手动写一下。
function create(parentPrototype) {
let Fn = function () {} // 定义一个构造函数 Fn
Fn.prototype = parentPrototype // 将父类的原型对象赋值给Fn的原型对象,此时实例对象fn就可以通过__proto__找到父类的原型对象
return new Fn() // 再将fn赋值给Tiger的原型对象,此时 tiger就可以通过__proto__找到fn,从而同fn.__proto__找到父类的原型对象
} // 而且就算给 Tiger.prototype添加属性,也是添加在fn上,而不会影响到Animal.prototype
二.类的使用
ES6新增了类。
1.语法
class Test {
constructor() {}
}
- 使用class关键字声明类。
- 类名一般为名词,首字母大写。
- constructor:每个类都会有的特殊方法,是类的构造方法,用来实例化一个由class创建的对象,像上文讲构造函数的时候创建实例的代码就放在constructor里面,在new的时候,会自动执行constructor函数,若没有定义constructor()构造方法,JavaScript会自动声明一个空的constructor()方法。
2.类的属性
类的属性分为三种:实例属性、静态属性、私有属性。(还有一个是原型上的属性,其实是通过get方法去return出去的,不算在其中)
2.1 实例属性
通过实例去访问的属性(定义在实例上),在constructor方法里定义的属性就是实例属性,像上文说的,ES5构造函数创建实例的代码就放在constructor方法里面。
function Animal() {
this.type = "哺乳类"; // ES5 实例属性
}
class Animal {
constructor() {
this.type = "哺乳类"; // ES6 实例属性
}
}
let animal = new Animal();
console.log(animal.type); // 哺乳类
在constructor外面声明的变量,也算是实例属性。
class Animal {
constructor() {
this.type = "哺乳类";
// 相当于 this.a = 1
}
a = 1;
}
let animal = new Animal();
console.log(animal.hasOwnProperty("a")); // true
但是通过get方法去声明的变量就不是实例属性了,通过hsaOwnProperty() 方法可以知道这是声明在类的原型对象上的属性,其实说是属性,不如说是方法。
class Animal {
constructor() {
this.type = "哺乳类"; // ES6 实例属性
}
get a() {
return 100;
}
}
let animal = new Animal();
console.log(animal.hasOwnProperty("a")); // false
console.log(animal.__proto__.hasOwnProperty("a")); // true
2.2 静态属性
通过类去访问的属性(定义在类上),用static关键字声明。
class Animal {
constructor() {
this.type = "哺乳类"; // ES6 实例属性
}
static a = 1;
}
let animal = new Animal();
console.log(animal.a); // undefined
console.log(Animal.a); // 1
也可也通过类名.的方式去声明。
class Animal {
constructor() {
this.type = "哺乳类"; // ES6 实例属性
}
}
Animal.a = 2;
let animal = new Animal();
console.log(animal.a); // undefined
console.log(Animal.a); // 2
ES6是不支持通过关键字static声明静态属性的,ES7以后就可以,但是ES6支持声明静态方法,所以我们就可以这样。
class Animal {
constructor() {}
static get a() { // ES6中的静态属性
return 1;
}
}
console.log(Animal.a); // 1
console.log(Animal.hasOwnProperty("a")); // true
2.3 私有属性(ES2022引入)
通过#去声明的属性,只能在类的内部访问。
class Animal {
// 声明一个私有属性
#age;
constructor(age) {
// 使用私有属性 #age 存放年龄
this.#age = age;
}
getAge() {
return this.#age;
}
}
let tiger = new Animal(5);
console.log(tiger.getAge()); // 5
// 尝试直接访问私有字段会导致语法错误
// console.log(tiger.#age); // 这里会引发错误,因为#age是私有的
- 封装性增强:私有字段有效限制了对类内部状态的直接访问,促进了良好的封装实践。
- 安全性提升:防止外部代码无意或恶意修改类的内部数据,减少潜在的bug。
3.方法
类的方法分为四种:构造方法、静态方法、普通方法(也就是定义在类原型对象上的方法)、私有方法。
3.1 构造方法
constructor() 就是类的构造方法,在new的时候自动执行,如果没有定义该方法,js会自动声明一个空的constructor() 方法。
class Animal {
constructor(name, age) {
// 使用'this'关键字给实例绑定属性
// 'this'在此处指的是通过类Animal创建的实例
this.name = name; // 绑定name属性
this.age = age; // 绑定age属性
}
}
const tiger = new Animal("tiger", 5);
console.log(tiger.name); // 输出: tiger
- constructor方法名不可以改变。
- 构造函数可以接收任意数量的参数,这些参数用于初始化实例的属性。
- 在构造方法里,this指向要创建的实例对象。
- new的时候自动调用。
3.2 静态方法
通过类去访问的方法(定义在类上),用static关键字声明。
class Animal {
constructor() {}
static age = 5;
static getAge() {
return this.age; // 这里的this指的是类,constructor里的this才指向创建的实例
}
}
console.log(Animal.getAge());
console.log(Animal.hasOwnProperty("getAge")); // true
3.3 普通方法
定义在constructor外面的方法,也就是原型对象上的方法,这种方法叫做普通方法,可以被实例通过__proto__去访问到。
讲构造函数的时候,给类的原型对象上添加公共方法,是通过prototype。
Animal.prototype.eat = function () {
console.log("吃吃吃");
};
类也可以这样去写,但是这样感觉不像一个整体,我们可以直接把方法写constructor外面,在constructor外声明的方法,是原型对象上的方法,也就是普通方法。
class Animal {
constructor() {
this.type = "哺乳类";
}
// 这里声明的方法都是在当前类的原型对象上
eat() {
console.log("吃吃吃");
}
}
let animal = new Animal();
animal.eat(); // 吃吃吃
console.log(animal.hasOwnProperty("eat")); // false
console.log(animal.__proto__.hasOwnProperty("eat")); // true
3.4 私有方法(ES2020引入)
通过#去声明的方法,只能在类的内部访问。
class Calculator {
// 创建一个私有方法
#multiply(a, b) {
return a * b;
}
calculateProduct(a, b) {
// 类的内部可以访问私有方法
return this.#multiply(a, b);
}
}
const calc = new Calculator();
// 通过公共方法间接调用私有方法,得到正确结果
console.log(calc.calculateProduct(2, 3)); // 输出: 6
// 尝试直接访问或调用私有方法会导致语法错误
// console.log(calc.#multiply(2, 3)); // 这是不允许的,会导致编译或运行时错误