JS中的继承和原型链

作为 Js 新学习者,最近好好整理了下 Js 中的原型链和继承机制。结合文档和网课,我把这个话题相关的文字给码了下。

对象 Object

在了解继承和原型链之前,首先我们需要掌握一个对象 Object 是如何被创建的。

总的来说,在 JavaScript 中我们有以下的几种方式来创建一个新的 Object

  • 原生的 Object 方法
  • 对象字面量
  • 工厂模式
  • 构造函数
  • Prototype 原型

原生 Object

1
2
3
4
5
6
7
8
9
10
11
// 使用 Object 方式创建用户对象
var user = new Object();
// 定义对象
user.name = "张三";
user.pwd = "123456";
// 定义对象的方法
user.showInfo = function () {
document.write(this.name + "-" + this.pwd + "<br />")
}
// 调用对象
user.showInfo();

对象字面量

  • 对象定义的一种简写形式
  • 简化创建包含大量属性的对象的过程
  • 在为函数传递大量可选参数时,可考虑使用对象字面量
1
2
3
4
5
6
7
8
9
10
// 使用对象字面量方式,创建用户对象
// 事实上 var user = {};
// 即为 var user = new Object();
var user = {
name: "张三",
pwd: "123456",
showInfo: function() {
document.write(this.name + "-" + this.pwd + "<br />");
}
}

除此之外,

var arr = [] 其实是 var arr = new Array() 的语法糖,

function Foo() {...} 其实是 var Foo = new Function {...} 的语法糖

工厂模式

  • 软件工程领域的一种设计模式
  • 抽象了创建对象的过程
  • 通过函数封装创建对象的细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用工厂模式创建用户对象
function createUser(name, pwd) {
var user = new Object();
// 定义对象的属性
user.name = name;
user.pwd = pwd;
// 定义对象的方法
user.showInfo = function() {
document.write(this.name + "-" + this.pwd + "<br />");
}
return user;
}

// 创建用户对象
// 弊端1: 看不出来数据类型
var user1 = createUser("张三", "123456");
var user2 = createUser("李四", "654321");

构造函数 (!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用构造函数方式创建用户对象
function User(name, pwd) {
// 定义对象的属性
this.name = name;
this.pwd = pwd;
// 定义对象的方法
this.showInfo = function() {
document.write(this.name + "-" + this.pwd + "<br />");
}
// 构造函数这里默认有一个 return this; 只不过我们不写了
}

// 创建用户对象
// 弊端1: 看不出数据类型
var user1 = new User("张三", "123456");
var user2 = new User("李四", "654321");

构造函数也就是一个函数,只不过它于普通的函数又有点不同:

  • 没有显示的创建对象;
  • 直接将属性和方法赋给this
  • 没有return语句;

原型 prototype

  • 每个函数都有一个 prototype (原型) 属性
  • 其是一个指针,指向一个对象 (Object)
  • 这个对象的用途是包含可以由特定类型的所有实例的属性和方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用构造函数方式创建用户对象
function User(name, pwd) {
// 定义对象的属性
this.name = name;
this.pwd = pwd;
}

// 原型对象
User.prototype.showInfo = function() {
document.write(this.name + "-" + this.pwd + "<br />");
}

// 创建用户对象(同上)
// 弊端1: 看不出数据类型
var user1 = new User("张三", "123456");
var user2 = new User("李四", "654321");

上述用构造函数+prototype这种方法构造对象,也叫做用混合方式构造对象,即

  • 构造函数:来写函数的属性
  • 原型 prototype:来写函数的方法

继承

关于 prototype 和继承

上一节里我们用到了 prototype,那么 prototype 到底是什么呢?

  • prototype 属性包含一个对象 (prototype 对象)
  • 所有实例对象需要共享的属性和方法,都放在这个对象里
  • 实例对象一旦创建,将自动引用 (共享) prototype 对象的属性和方法
  • prototype 对象好像是实例对象的原型,而实例对象则好像“继承”了 prototype 对象一样

几种继承的方式

更改 prototype 指向

常见的方法

  1. Student.prototype = new Person()
  2. Studentprototype 指向一个 Person 的实例,所有“学生”的实例,就能继承 Person
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person() {
this.foot = 2;
this.head = 1;
}

function Student(name, no) {
this.name = name;
this.no = no;
}

// 通过 prototype 属性实现继承
Student.prototype = new Person();
// Student 的 prototype 属性指向一个 Person 实例的写法,
// 相当于完全删除了 prototype 原先的值,赋了一个新值

Student.prototype.constructor = Student;
// 每个实例的 prototype 对象都有一个 constructor 属性
// 直接通过原型链继承后,会发生“继承链紊乱”,需要手动理顺

注意

如果替换了 prototype 对象,即 o.prototype = {};

要为新的 prototype 对象加上 constuctor 属性,并将这个属性指回原来的构造函数,即 o.prototype.constructor = o;

直接继承 prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {}
Person.prototype.foot = 2;
Person.prototype.head = 1;

function Student(name, no) {
this.name = name;
this.no = no;
}

// 通过直接继承 prototype 属性来继承
Student.prototype = Person.prototype
// 手动理顺一下继承链
Student.prototype.constructor = Student;

这样继承的优点在于效率比较高 (不用执行和建立 Person 的实例了),

缺点Student.prototypePerson.prototype 现在指向了同一个对象,任何对 Student.prototype 的修改,都会反映到 Person.prototype

利用空对象作为中介

为了解决上述更改 Student.prototype 都会反映到 Person.prototype 的问题,我们可以利用空对象作为中介来进行继承。其优点在于:

  • 空对象,几乎不占用内存
  • 修改 Studentprototype 对象,不会影响到 Personprototype 对象
  • 可以封装成函数
1
2
3
4
5
6
7
// F 为空对象,几乎不占用内存
var F = function () {};
F.prototype = Person.prototype;
Student.prototype = new F();
// 由于 Student 的 constructor 是 Person,则还需要手动理顺继承链
var stu1 = new Student("张三", "s001");
Student.prototype.constructor = Student;

将上述代码封装成一个函数,即

1
2
3
4
5
6
function extend(Child, Parent) {
var F = function() {}
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}

(有 ES6 的 extends 那味儿了)

等到需要用到继承的时候,我们便可以直接通过 extend(Student, Person) 来实现了,省去了不少工夫。

构造函数绑定来进行继承

为什么要绑定?

假设有以下代码,提问下面的 alert 会弹出什么结果?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person() {
this.foot = 2;
this.head = 1;
this.favorColor = ["red", "yellow"]; // **
}
function Student(name, no) {
this.name = name;
this.no = no;
}

Student.prototype = new Person();
var stu1 = new Student("张三", "s001");
stu1.favorColor.push("blue");

// 以下代码弹出什么结果?
var stu2 = new Student("李四", "s002");
alert(stu2.favorColor);

答案为 red, yellow, blue,而不是 red, yellow,为什么会这样呢?

原因是在这里的所有实例即 stu1stu2,共享了父函数的非基本类型属性,如数组对象,一个实例对属性的修改会影响所有实例。

构造函数绑定
  • 在子类型构造函数的内部调用父类型构造函数
  • 通过 call()apply() 方法

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person() {
this.foot = 2;
this.head = 1;
this.favorColor = ["red", "yellow"];
}
function Student(name, no) {
// 将父对象的构造函数,绑定在子对象上
// 借调父类的构造函数
Person.call(this);
this.name = name;
this.no = no;
}

var stu1 = new Student("张三", "s001");
stu1.favorColor.push("blue");
alert(stu1.foot);
alert(stu1.head);
alert(stu1.favorColor); // red, yellow, blue

var stu2 = new Student("李四", "s002");
alert(stu2.favorColor); // red, yellow

组合继承

上述代码只能实现属性的继承,那么如何对方法也进行继承呢?我们可以用组合继承的方法。

组合继承也叫伪经典继承:

  • 其将原型链继承和构造函数继承组合在一块
  • 原型链实现对原型属性和方法的继承
  • 借用构造函数实现对实例属性的继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person() {
this.foot = 2;
this.head = 1;
this.favorColor = ["red", "yellow"];
}
Person.prototype.sayColor = function() {
alert("Hello, 我最爱的颜色是: " + this.favorColor);
}

function Student(name, no) {
// 使用构造函数绑定,实现了对父类属性的继承
Person.call(this);
this.name = name;
this.no = no;
}

//原型链继承: 继承对象的属性和方法
Student.prototype = new Person();
Student.prototype.constructor = Student;

拷贝继承

通过对父类原型的属性遍历拷贝,也能把父类的属性放到子类上。简单且暴力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Student(name, no) {
this.name = name;
this.no = no;
}

// 拷贝继承
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
}

extend2(Student, Person);
var stu1 = new Student("张三", "s001");

原型链

原型规则

了解以上继承知识之后,我们便能将原型链的概念好好梳理下了。原型有以下几点规则:

  1. 所有的引用类型,即数组对象函数,都具有对象特性,即可自由扩展属性 (除了 null)。
  2. 所有的引用类型,都有一个 __proto__ 属性,属性值是一个普通的对象。
  3. 所有的函数,都有一个 prototype 属性,属性值也是一个普通的对象。
  4. 所有的引用类型__proto__ 属性值都指向它的构造函数prototype 属性值。
  5. 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的 __proto__ (即它的构造函数的 prototype) 中寻找。
  6. 这样为了得到某个属性,沿着其 __proto__ 属性进行寻找的链条,我们叫做原型链

instanceof

f instanceof Foo 可用来判断元素 f 是否为 Foo 的原型,

其判断逻辑就是:对 f__proto__ 一层一层往上,看是否能对应到 Foo.prototype,即

f.__proto__ = Foo.prototypef.__proto__.__proto__... = Foo.prototype