构造函数与原型链
OOP 的特性:封装、抽象、继承和多态。
区分JS与其他语言中的OOP
典型的面向对象编程:
- 对象/实例是基于一个类来初始化的,这个类就像一个蓝图一样。
- 所有的方法都会从类中复制一份到对象中。
JS 中的面向对象编程:
- 对象会和一个原型对象连接(link)起来。
- 原型继承:对象可以使用原型对象中的所有方法。
- 这也叫做事件委托。
一个具体的例子:
考虑一些问题🙋:
- 如何创建原型对象?
- 如何将对象和原型对象连接起来?(原型继承/委托)
- 如何创建没有原型对象的对象?
带着这些问题,我们继续以下的概念。
- 构造函数
- 从一个函数中创建对象。
- 这就是 JS 内置对象,如Array, Map 和 Set 等使用的方法。
- ES6 的 class
- 现代构造函数。
- 原始构造函数的语法糖。
Object.create()
- 直接讲对象和原型连接起来的方法。
构造函数
TIP
构造函数与普通函数唯一的区别就是构造函数可以使用 new
关键字。
那么, 当使用 new
来调用一个构造函数的时候,会发生什么?
- 创建一个新对象。
- 调用该函数,函数中的
this
关键字指向上面这个刚创建的新对象。 - 新对象连接到其自身的原型,即为新对象创建
__proto__
属性,并将其指向构造函数的prototype
属性指向的对象Person.prototype
。 - 该函数返回刚刚创建的新对象,也是返回
this
。
const Person = function(firstName, birthYear){
this.name = firstName; // 为新对象设置属性
this.birth = birthYear;
// 使用 new 调用时自动返回 `this`
}
const jonas = new Person('jonas', 1991);
console.log(jonas); // {name: 'jonas', birth: 1991}
对象的属性名与给构造函数传入的参数的名字没必要一样(如上面所示),但一般约定俗成是相同的。
TIP
this
指向的是实例化的那个对象。
验证一个对象是否是另一个对象的实例 instanceof
:
console.log(jonas instanceof Person); // true
如何为一个对象创建方法:
const Person = function(firstName, birthYear){
this.firstName = firstName;
this.birthYear = birthYear;
// 虽然可以,但,在生产场景下,永远不要在构造函数内部创建方法
// 如果在构造函数内部定义方法,JS 引擎会为每个对象单独创建一个新的函数实例,影响性能
this.calAge = function(){
console.log(2024 - this.birthYear);
}
}
解决上述问题的方法就是使用原型链。
原型与原型链
首先要明确的几点:
- JS 中所有函数自动地拥有一个名为
prototype
的属性。 - 通过构造函数创建的所有对象,拥有构造函数的
prototype
上的所有属性和方法。
const Person = function(firstName, birthYear){
this.name = firstName; // 为新对象设置属性
this.birth = birthYear;
// 使用 new 调用时自动返回 `this`
}
const jonas = new Person('jonas', 1991);
console.log(jonas); // {name: 'jonas', birth: 1991}
也就是说,在上面这段代码中,jonas
可以使用 Person.prototype
上的所有方法和属性。
通过原型继承的方法让对象拥有可以调用的方法:
Person.prototype.calAge = function(){
console.log(2024 - this.birthYear);
}
const jiaqi = new Person('jiaqi', 1999);
jiaqi.calAge(); // 25
我们打印出 jiaqi
这个对象看看它的样子:
可以看出,这个对象只有两个属性,calAge
这个方法是顺着原型链找到的。
TIP
任何对象总是可以访问它的构造函数的 prototype
中的方法和属性。
这是由于,对象内部有一个特殊的属性:__proto__
,这一属性与其构造函数中的 prototype
属性相同:
console.log(jiaqi.__proto__ === Person.prototype); // true
这里有一个非常令人困惑的点,需要仔细思考:Person.prototype
并不是 Person
的原型,而是通过 Person
这个构造函数 new
出来的所有对象的原型。
仔细理解上面这句话,JS 内置对象也可以印证上面这句话:
let arrayInstance = [];
console.log(arrayInstance.__proto__ === Array.prototype); // true
从上面的图中可以看出对象和生成对象的构造函数之间的关系。
注意⚠️:这一关系在构造函数和 ES6 的 class 中都是相同的,但在 Object.create()
中是不同的。
原型链:
TIP
每个对象都有原型(__proto__
属性),这一原型是通过相应构造函数 new
的时候,通过构造函数的 prototype
属性找到的。
上面👆图中的例子就是:jonas 找到了它的原型 Person.prototype
,而 Person.prototype
本身也是个对象,也有自己的原型,即 Object.prototype
,此时就到了原型链的最顶层,Object.prototype.__proto__
指向了 null
。
console.log(Object.prototype.__proto__); // null
用内置函数理解一下原型链:
const func = function(){
}
console.log(func.__proto__ === Function.prototype); // true
console.log(func.__proto__.__proto__ === Object.prototype); // true
console.log(func.__proto__.__proto__.__proto__); // null
正是由于原型链这种继承方式,使得当我们为构造函数的 prototype
属性加上任意方法时,所有实例都可以调用它,并且不占用自身内存。
类之间的继承
TIP
手动创建原型链的过程。
用例:创建一个 Student
类来继承 Person
类。
继承属性
首先:子类继承父类的所有属性,
const Person = function(firstName, birthYear){
this.firstName = firstName;
this.birthYear = birthYear;
};
Person.prototype.calAge = function(){
console.log(2024-this.birthYear);
};
const Student = function(firstName, birthYear, course){
// 继承 Person 的 firstName 和 birthYear
Person.call(this, firstName, birthYear);
this.course = course;
}
注意,这里是通过 call
方法将子实例的 this
绑定到父类的属性上。
给子类绑定方法:
Student.prototype.introduce = function(){
console.log(`hi, my name is ${this.firstName} and I study ${this.course}🖐️`);
}
调用该方法:
const jiaqi = new Student('jiaqi', 1999, 'cs');
jiaqi.introduce(); // hi, my name is jiaqi and I study cs🖐️
通过输出可以看出,到目前为止,子类的实例已经可以使用父类中定义的属性了。
TIP
这里并不是引用,而是创建了副本。
继承方法
除此之外,我们还希望子类能够调用父类 prototype
上的方法。
为此,我们需要手动创建一个原型链,具体如下图所示:
Object.create() 中讲到过,Object.create()
可以手动设置任何对象的原型对象,这正是我们这里需要的功能。
// 继承方法
Student.prototype = Object.create(Person.prototype); // 放在添加其他方法的前面
Student.prototype.introduce = function(){
console.log(`hi, my name is ${this.firstName} and I study ${this.course}🖐️`);
}
const jiaqi = new Student('jiaqi', 1999, 'cs');
jiaqi.calAge(); // 25
WARNING
注意,这里不能使用 Student.prototype = Person.prototype
这种方法,因为我们希望的是能够顺着原型链找到那个方法,而非直接指向那个方法。