logo头像
Snippet 博客主题

js原型、原型链、继承相关学习心得

本文于995天之前发表,文中内容可能已经过时。

前置知识点:

  • js基本数据类型和引用数据类型(基本数据类型有: null, undefined, Boolen, Number, String, Symbol
  • 计算机堆、栈内存(栈内存存的是基本类型和引用类型的地址,堆内存由于树状结构可以延伸适合引用类型如Array, Object
  • js中this指向及bindcallapply方法使用,参阅call、apply和bind的实现

引生思考点

  • js语言原型和原型链为什么这么设计,好处与不好之处,与Java语言最大的区别是什么?
  • 浅拷贝与深拷贝如何实现?
  • js闭包知识点

js原型、原型链基本概念

ES5 和 ES6 继承的区别

ES5原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Super() {
this.xxx = 'hello world!';
}

Super.prototype.sayHello = function() {
console.log(this.xxx);
}

function Son() {
// ...
}

Son.prototype = new Super(); // 原型链式继承
// Son.prototype = Object.create(Super.prototype);

Son.prototype.constructor = Son; // 修复constructor

var sonInstance = new Son();
console.log(sonInstance.xxx); // 'hello world!';

特点:

  • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例instanceof方法通过层层查找原型链判断的

缺点:

  • 来自原型对象的所有属性被所有实例共享
  • 创建子类实例时,无法向父类构造函数传参

ES6继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Super {
constructor() {
this.xxx = 'hello world!';
}

sayHello() {
console.log(this.xxx);
}
}

class Son extends Super {
// ...
}

var sonInstance = new Son();
console.log(sonInstance.xxx); // 'hello world!';
以上图片引用自keenwon.com

实现ES6的class语法(2019-06-26·补充)

1
2
3
4
5
6
7
8
9
10
11
12
function inherit(subType, superType) {
subType.prototype = Object.create(superType.prototype, {
constructor: {
enumerable: false,
configurable: true,
writable: true,
value: subType,
}
});

Object.setPrototypeOf(subType, superType);
}
  • ES6 的 class 内部是基于寄生组合式继承,它是目前最理想的继承方式,通过 Object.create 方法创造一个空对象,并将这个空对象继承 Object.create 方法的参数,再让子类(subType)的原型对象等于这个空对象,就可以实现子类实例的原型等于这个空对象,而这个空对象的原型又等于父类原型对象(superType.prototype)的继承关系
  • 而 Object.create 支持第二个参数,即给生成的空对象定义属性和属性描述符/访问器描述符,我们可以给这个空对象定义一个 constructor 属性更加符合默认的继承行为,同时它是不可枚举的内部属性(enumerable:false)
  • 而 ES6 的 class 允许子类继承父类的静态方法和静态属性,而普通的寄生组合式继承只能做到实例与实例之间的继承,对于类与类之间的继承需要额外定义方法,这里使用 Object.setPrototypeOf 将 superType 设置为 subType 的原型,从而能够从父类中继承静态方法和静态属性

ES5实现继承的其它方式

调用父类构造函数继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Super(xxx) {
this.xxx = xxx;
}

Super.prototype.sayHello = function() {
console.log(this.xxx);
}

function Son() {
Super.call(this, 'hello world!'); // 调用父类构造函数
}

var sonInstance = new Son();
console.log(sonInstance.xxx); // 'hello world!';
sonInstance.sayHello(); // TypeError: sonInstance.sayHello is not a function

特点:

  • 创建子类实例时,可以向父类传递参数

缺点:

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Super(xxx) {
this.xxx = xxx;
}

Super.prototype.sayHello = function() {
console.log(this.xxx);
}

function Son() {
Super.call(this, 'hello world!'); // 调用父类构造函数
}

Son.prototype = new Super();

Son.prototype.constructor = Son; // 修复constructor

var sonInstance = new Son();
console.log(sonInstance.xxx); // 'hello world!';
sonInstance.sayHello(); // 'hello world!';

特点:

  • 创建子类实例时,可以向父类传递参数
  • 既是子类的实例,也是父类的实例
  • 实例拥有与父类同名的自己的属性(亦是缺点)
  • 既能继承父类的实例属性和方法,又能继承原型属性/方法

缺点:

  • 调用了两次父类构造函数,子类实例上属性将子类原型上的那份屏蔽了

寄生组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Super(xxx) {
this.xxx = xxx;
}

function Son(xxx) {
Super.call(this, xxx); // 调用父类构造函数
}

// 创建一个空方法,避免再次调用父类的构造函数
(function () {
var Temp = function() {};
Temp.prototype = Super.prototype;
Son.prototype = new Temp();
})();

Son.prototype.constructor = Son; // 修复constructor

var sonInstance = new Son('hello world!');
console.log(sonInstance.xxx); // 'hello world!';

特点:

  • 结合以上所有继承方式的优点

完整的原型图

参考文章:

微信打赏

赞赏是不耍流氓的鼓励