JS原型与继承
2023-09-24 02:22:04 #JS

原型对象

理解原型

  • 每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向函数的原型对象,使用原型对象可以让所有对象实例共享它所包含的属性和方法
  • 所有函数的原型对象都会自动获得一个 constructor(构造函数)属性,这个属性指向 prototype 属性所在函数(Person.prototype.constructor -> Person
  • 构造函数的原型对象只会默认取得 constructor 属性,其他方法都是从 Object 继承而来的
  • 当调用构造函数创建一个新实例后,该实例的内部包含一个指针(内置属性 [[prototype]]),使用 __proto__ 属性可以访问到这个内置属性,__proto__ 指向构造函数的原型对象(p1.__proto__ -> Person.prototype
1
2
3
4
5
6
7
8
9
10
11
function Person() {
}

Person.prototype.name = 'Max';
Person.prototype.age = 27;
Person.prototype.hobbies = ['swimming', 'jogging'];
Person.prototype.sayName = function() {
console.log(this.name);
}

const p1 = new Person();
  • Object.setPrototypeOf() 方法传入两个对象作为参数,将第二个对象设置为第一个对象的原型
1
2
3
4
5
6
7
8
9
10
11
const person = {
};
const someone = {
name: 'Albert',
sayHi: function() {
console.log('Hi~' + this.name);
}
};

// 将对象 someone 设置为对象 person 的原型
Object.setPrototypeOf(person, someone);
  • Object.getPrototypeOf() 方法用于获取对象的原型
1
2
3
Object.getPrototypeOf(p1).name               // 'Max'
Object.getPrototypeOf(person).name // 'Albert'
Object.getPrototypeOf(person).sayHi() // 'Hi~Albert'
  • 为实例添加一个与原型同名的属性,该属性会屏蔽(不是修改)原型中的同名属性
1
2
3
4
5
const p2 = new Person();
p2.name = 'Anthony'; // 为实例添加属性

console.log(p1.name); // 'Max' 来自原型
console.log(p2.name); // 'Anthony' 来自实例
  • 使用 hasOwnProperty() 方法可以判断一个属性是存在于实例中,还是存在于原型中,当给定属性存在于对象实例中时,返回 true,否则返回 false
1
2
console.log(p1.hasOwnProperty('name'));      // false
console.log(p2.hasOwnProperty('name')); // true

遍历对象属性的方法

有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用

  • 在单独使用 in 时,in 操作符只要通过对象能够访问给定属性就会返回 true,无论该属性存在于实例中还是原型中
1
2
3
4
5
6
7
// p1.name 来自原型
console.log(p1.hasOwnProperty('name')); // false
console.log('name' in p1); // true

// p2.name 来自实例
console.log(p2.hasOwnProperty('name')); // true
console.log('name' in p2); // true
  • 在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的属性(enumerable: true),其中既包括存在于实例中的属性,也包括存在于原型中的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person() {
this.name = 'Max';
}
Person.prototype.age = 27;

const p1 = new Person();
Object.defineProperty(p1, 'hobbies', {
value: ['swimming', 'jogging'],
enumerable: false
})

for(let prop in p1) {
console.log(prop); // name, age
}
  • 使用 Object.key() 方法遍历对象所有可枚举的实例属性(不遍历原型中的属性),返回一个包含所有可枚举属性的字符串数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person() {
this.name = 'Max';
}
Person.prototype.age = 27;

const p1 = new Person();
Object.defineProperty(p1, 'hobbies', {
value: ['swimming', 'jogging'],
enumerable: true
})
Object.defineProperty(p1, 'job', {
value: 'software engineer',
enumerable: false
})

let keys = Object.keys(p1);
console.log(keys); // ['name', 'hobbies']
  • 使用 Object.getOwnPropertyNames() 方法遍历所有实例属性(不遍历原型中的属性),无论实例属性是否可枚举,返回一个包含所有可枚举属性的字符串数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person() {
this.name = 'Max';
}
Person.prototype.age = 27;

const p1 = new Person();
Object.defineProperty(p1, 'hobbies', {
value: ['swimming', 'jogging'],
enumerable: true
})
Object.defineProperty(p1, 'job', {
value: 'software engineer',
enumerable: false
})

let keys = Object.getOwnPropertyNames(p1);
console.log(keys); // ['name', 'hobbies', 'job']

更简洁的原型语法

  • 通过使用 constructor 属性可以访问创建该对象时所用的函数
  • 所有实例对象都可以使用 constructor 属性验证其原始类型
1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {
}

const p = new Person();

// typeof 仅能检测出 p 的类型为一个对象
console.log(typeof p === 'object'); // true

// instanceof 检测出 p 是由 Person 构造而来的
console.log(p instanceof Person); // true

// constructor 属性验证 p 的原始类型为 Person
console.log(p.constructor === Person); // true
  • 为了精简代码,可以使用一个包含所有属性和方法的对象字面量来重写原型对象
1
2
3
4
5
6
7
8
9
10
11
function Person() {
}
Person.prototype = {
name: 'Max',
age: 27,
hobbies: ['swimming', 'jogging'],
sayName: function() {
console.log(this.name);
}
}
const p1 = new Person();
  • 但是!由于 Person.prototype 是一个以对象字面量形式创建的新对象,因此 constructor 属性不再指向 Person,而是指向了 Object
1
2
3
4
console.log(p1 instanceof Person);             // true
console.log(p1 instanceof Object); // true
console.log(p1.constructor === Person); // false
console.log(p1.constructor === Object); // true
  • 使用 Object.defineProperty() 方法修改 constructor 属性的配置
1
2
3
4
5
6
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})

console.log(p1.constructor === Person); // true

原型的动态性

  • 可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person() {
this.name = 'Max';
}

const p1 = new Person();

// 在创建完成实例后,为原型添加属性和方法
Person.prototype.age = 24;
Person.prototype.sayName = function () {
return this.name;
};

// 验证可以在对象创建完成之后修改该对象的原型
console.log(p1.age); // 24
console.log(p1.sayName()); // 'Max'
  • 但是,如果重写了整个原型对象,已经创建的实例仍然指向原先的原型
1
2
3
4
5
6
7
8
9
10
// 使用字面量对象完全重写原型对象,仅有一个 showHobbies 方法
Person.prototype = {
showHobbies: function () {
return ['swimming', 'jogging'];
}
};

// 已经创建的实例仍然指向原先的原型
console.log(p1.showHobbies); // undefined
console.log(p1.sayName()); // 'Max'
  • 新创建的实例则指向新的原型
1
2
3
4
const p2 = new Person();

console.log(p2.showHobbies()); // ['swimming', 'jogging']
console.log(p2.sayName); // undefined

原型对象的问题

  • 问题一:由于原型模式省略了为构造函数传递初始化参数,因此所有实例在默认情况下都将取得相同的属性值

  • 问题二:在实例上添加一个与原型属性同名的属性时,将隐藏原型中的对应属性,所以,当属性为引用类型时,原型中的引用值会在所有实例之间共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person() {
}
Person.prototype = {
constructor: Person,
name: 'Max',
age: 24,
hobbies: ['swimming', 'jogging']
}

const p1 = new Person();
p1.hobbies.push('singing');

const p2 = new Person();

console.log(p1.hobbies); // ['swimming', 'jogging', 'singing']
console.log(p2.hobbies); // ['swimming', 'jogging', 'singing']
console.log(p1.hobbies === p2.hobbies); // true

构造函数与原型

组合使用构造函数模式和原型模式可以解决上述原型对象的问题,其中

  • 构造函数模式用于定义实例属性
  • 原型模式用于定义方法和共享的属性

因此,每个实例都会有自己的一份实例属性副本,同时又共享着对方法的引用,节省了内存,还可以向构造函数传递参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Person(name, age) {
this.name = name;
this.age = age;
this.hobbies = ['swimming', 'jogging'];
}

Person.prototype = {
constructor: Person,
sayName: function() {
return this.name;
}
}

const p1 = new Person('Max', 27);
p1.hobbies.push('singing');

const p2 = new Person('Anthony', 24);

console.log(p1.hobbies); // ['swimming', 'jogging', 'singing']
console.log(p2.hobbies); // ['swimming', 'jogging']
console.log(p1.hobbies === p2.hobbies); // false
console.log(p1.__proto__ === Person.prototype); // true
console.log(p2.__proto__ === Person.prototype); // true
  • 把函数作为构造函数,通过 new 进行调用时,this 指向新创建的对象实例,所以在构造函数内部添加的属性直接赋给新创建的实例
  • 当通过实例访问构造函数内部的属性时,不需要遍历原型链
1
2
3
4
5
6
7
8
9
10
11
12
13
// 验证实例方法会重写与之同名的原型方法
function Person() {
this.flag = false;
this.getFlag = function () { // 实例方法,返回值为变量 flag 取反
return !this.flag;
}
}
Person.prototype.getFlag = function () { // 原型方法,返回值为变量 flag
return this.flag;
}

const person = new Person();
person.getFlag(); // true

实现继承

  • 由于 JS 中的函数没有签名,因此 JS 只支持实现继承方式,不支持接口继承方式
  • 原型链是实现继承的主要方法

原型链

  • 原型链的基本思想是,利用原型让一个引用类型继承另一个引用类型的属性和方法
  • 令原型对象等于另一个原型的实例(SubType.prototype = new SuperType()),则此时的原型对象将包含一个指向另一个原型的指针(SubType.prototype.__proto__ -> SuperType.prototype),相应地,另一个原型中也包含着一个指向另一个构造函数的指针(SuperType.prototype.constructor -> SuperType),以此类推,形成一个原型链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType() {
this.job = 'Artist';
}

SuperType.prototype.getJob = function() {
return this.job;
}

function SubType() {
this.name = 'Anthony';
}

// SubType 继承 SuperType,实现的本质是重写 SubType 的原型对象,为一个新类型的实例
SubType.prototype = new SuperType();

SubType.prototype.getName = function() {
return this.name;
}

const instance = new SubType();
console.log(instance.getJob()); // 'Artist'
  • 通用原型链实现继承时,查找特定属性将会被委托在整个原型链上,只有当没有更多的原型可以进行查找时,才会停止查找
  • 所有函数的默认原型都是 Object 的实例,默认原型内部都会指向 Object.prototype,因此所有自定义类型都会继承 toString()valueOf() 等默认方法

确定原型和实例的关系

  • 方法一:instanceof 运算符用于检测(右侧的)函数的原型是否存在于(左侧的)实例对象的原型链中
1
2
3
instance instanceof SubType                    // true
instance instanceof SuperType // true
instance instanceof Object // true
  • 方法二:isPrototypeOf() 方法用于检查一个对象是否存在于另一个对象的原型链中
1
2
3
SubType.prototype.isPrototypeOf(instance)      // true
SuperType.prototype.isPrototypeOf(instance) // true
Object.prototype.isPrototypeOf(instance) // true

谨慎地定义方法

  • 当子类重写超类中的某个方法,或者子类添加超类中不存在的某个方法时,给子类的原型添加方法的代码必须放在替换原型的语句之后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function SuperType() {
this.job = 'Artist';
}

SuperType.prototype.getJob = function() {
return this.job;
}

function SubType() {
this.name = 'Anthony';
}

// SubType 继承 SuperType,这一步必须在定义方法之前
SubType.prototype = new SuperType();

// 添加新方法
SubType.prototype.getName = function() {
return this.name;
}

// 重写超类中的方法
SubType.prototype.getJob = function() {
return 'singer';
}

const instance = new SubType();
console.log(instance.getJob()); // 'singer'

const instance2 = new SuperType();
console.log(instance2.getJob()); // 'Artist'
  • 通过原型实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function SuperType() {
this.job = 'Artist';
}

SuperType.prototype.getJob = function() {
return this.job;
}

function SubType() {
this.name = 'Anthony';
}

// SubType 继承 SuperType
SubType.prototype = new SuperType();

// 使用字面量添加新方法,导致原型链实现继承失效
// 此时原型包含的是另一个 Object 的实例,而非 SuperType 的实例
SubType.prototype = {
getName: function() {
return this.name;
},
getHobby: function() {
return 'singing';
}
}

const instance = new SubType();
console.log(instance.getJob()); // TypeError: instance.getJob is not a function
console.log(SubType.prototype.constructor === Object); // true

原型链的问题

  • 问题一:多个实例共享包含引用类型值的原型属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType() {
this.hobbies = ['swimming', 'jogging'];
}

function SubType() {
}
SubType.prototype = new SuperType();

const instance1 = new SubType();
instance1.hobbies.push('singing');
console.log(instance1.hobbies); // ['swimming', 'jogging', 'singing']

const instance2 = new SubType();
console.log(instance2.hobbies); // ['swimming', 'jogging', 'singing']
  • 问题二:在创建子类的实例时,不能向超类的构造函数传递参数
1
2
3
4
5
6
7
8
9
10
function SuperType(name) {
this.name = name;
}

function SubType() {
}
SubType.prototype = SuperType();

const instance = new SubType('Max');
console.log(instance.name); // undefined

鉴于以上问题,因此很少单独使用原型链,而是借助构造函数进行组合式继承。

组合继承

  • 通过原型链,即子类的原型指向超类的实例,实现对原型的属性和方法的继承(共享)
  • 借用构造函数,即在子类构造函数的内部使用 call() 或 apply() 方法调用超类构造函数,将超类的实例属性绑定到子类的 this 中,实现对实例属性的继承(独享)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function SuperType(name) {
this.name = name;
this.hobbies = ['swimming', 'jogging'];
this.sayHi = function() {
console.log('Hi~' + this.name);
}
}

SuperType.prototype.sayName = function() {
return this.name;
}

function SubType(name, age) {
SuperType.call(this, name); // 第二次调用 SuperType()
this.age = age;
}

SubType.prototype = new SuperType(); // 第一次调用 SuperType()

// 当通过设置 SuperType 的实例对象为 SubType 的原型时,已经丢失了 SubType 和 SubType 初始原型之间的关联,因此需要重新指定 SubType.prototype.constructor 的值为 SubType
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function() {
return this.age;
}

const instance1 = new SubType('Anthony', 23);
instance1.hobbies.push('singing');
instance1.sayHi(); // 'Hi~Anthony'
instance1.sayName(); // 'Anthony'
instance1.sayAge(); // 23
console.log(instance1.hobbies); // ['swimming', 'jogging', 'singing']

const instance2 = new SubType('Max', 24);
instance2.sayHi(); // 'Hi~Max'
instance2.sayName(); // 'Max'
instance2.sayAge(); // 24
console.log(instance2.hobbies); // ['swimming', 'jogging']

console.log(instance1.constructor === SubType); // true
console.log(SubType.prototype.constructor === SubType); // true

组合继承是最常用的继承模式,但是存在一个问题:无论在什么情况下,都会调用两次超类构造函数,一次在创建子类原型时调用,另一次在创建子类实例时又在子类构造函数内部调用,因此生成了两份实例,造成了不必要的内存开销,影响了性能。

寄生组合式继承

改进了原型链 + 借用构造函数的组合继承中的调用两次超类构造函数的问题,不必为了指定子类的原型而调用超类的构造函数,而是使用超类原型的一个副本。

1
2
3
4
5
6
// 用于替代子类原型赋值语句
function inheritPrototype(subType, superType) {
const prototype = Object.create(superType.prototype); // 创建超类原型对象(副本)
prototype.constructor = subType; // 为创建的副本添加 constructor 属性
subType.prototype = prototype; // 将创建的副本赋值给子类的原型
}

改写组合继承为寄生组合式继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function SuperType(name) {
this.name = name;
this.hobbies = ['swimming', 'jogging'];
this.sayHi = function() {
console.log('Hi~' + this.name);
}
}

SuperType.prototype.sayName = function() {
return this.name;
}

function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}

inheritPrototype(SubType, SuperType); // 替代 SubType.prototype = new SuperType()

SubType.prototype.sayAge = function() {
return this.age;
}

const instance1 = new SubType('Anthony', 23);
instance1.hobbies.push('singing');
instance1.sayHi(); // 'Hi~Anthony'
instance1.sayName(); // 'Anthony'
instance1.sayAge(); // 23
console.log(instance1.hobbies); // ['swimming', 'jogging', 'singing']

const instance2 = new SubType('Max', 24);
instance2.sayHi(); // 'Hi~Max'
instance2.sayName(); // 'Max'
instance2.sayAge(); // 24
console.log(instance2.hobbies); // ['swimming', 'jogging']

console.log(instance1.constructor === SubType); // true
console.log(SubType.prototype.constructor === SubType); // true

寄生组合式继承只调用一次 SuperType 构造函数,因此避免了在 SubType.prototype 上创建不必要的属性,同时原型链保持不变。寄生组合式继承是引用类型最理想的继承范式。

类继承

class 是语法糖

ES6 引入关键字 class,提供更为优雅的创建对象还实现继承的方式,但底层仍然是基于原型的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.hobbies = ['swimming', 'jogging'];
}
sayName() {
return `Hi~${this.name}`;
}
}

const p1 = new Person('Anthony', 24);
p1.hobbies.push('singing');

console.log(p1.hobbies); // ['swimming', 'jogging', 'singing']
console.log(p1.sayName()); // 'Hi~Anthony'

const p2 = new Person('Max', 21);
console.log(p2.hobbies); // ['swimming', 'jogging']
console.log(p2.age); // 21

静态方法

  • static 修饰的属性和方法是静态属性和方法
  • 静态属性和方法只能被类名调用,不能被实例对象调用,同时也不能被子类继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Person {
constructor(name, level) {
this.name = name;
this.level = level;
}

// 静态属性
static hi = 'Hi!';

// 普通方法,调用静态属性
sayName() {
return `${Person.hi} ${this.name}`;
}

// 静态方法
static compare(person1, person2) {
return person1.level - person2.level;
}
}

const p1 = new Person('Max', 5);
const p2 = new Person('Albert', 3);

p1.sayName(); // 'Hi! Max'
p2.sayName(); // 'Hi! Albert'
Person.compare(p1, p2); // 2

console.log('compare' in Person); // true 类可访问静态方法
console.log('compare' in p1 || 'compare' in p2); // false 实例不可访问静态方法
console.log('sayName' in Person); // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name, level){
this.name = name;
this.level = level;
this.sayName = function() {
console.log(Person.hi + this.name);
}
}

// 在构造函数上添加属性或方法模拟 ES6 中的静态属性和静态方法
Person.hi = 'Hi! ';
Person.compare = function(person1, person2) {
return person1.level - person2.level;
}

const p1 = new Person('Max', 5);
const p2 = new Person('Albert', 3);

p1.sayName(); // 'Hi! Max'
Person.compare(p1, p2); // 2
console.log('compare' in Person); // true
console.log('compare' in p1 || 'compare' in p2); // false

实现继承

  • 使用关键字 extends 实现继承
  • 使用关键字 super 调用超类构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class SuperType {
constructor(name) {
this.name = name;
this.hobbies = ['swimming', 'jogging'];
}

sayHi() {
console.log('hi~' + this.name);
}
}

class SubType extends SuperType {
constructor(name, age) {
super(name);
this.age = age;
}

sayAge() {
console.log(this.age);
}
}

const sub1 = new SubType('Anthony', 23);
sub1.hobbies.push('singing');
console.log(sub1.hobbies); // ['swimming', 'jogging', 'singing']
sub1.sayHi(); // 'hi~Anthony'
sub1.sayAge(); // 23

const sub2 = new SubType('Max', 24);
console.log(sub2.hobbies); // ['swimming', 'jogging']

const super1 = new SuperType('Albert');
super1.sayHi(); // 'Hi~Albert'
console.log(super1 instanceof SubType); // false
console.log('sayAge' in super1); // false