作为 Js 新学习者,最近好好整理了下 Js 中的原型链和继承机制。结合文档和网课,我把这个话题相关的文字给码了下。
对象 Object
在了解继承和原型链之前,首先我们需要掌握一个对象 Object
是如何被创建的。
总的来说,在 JavaScript 中我们有以下的几种方式来创建一个新的 Object
。
- 原生的 Object 方法
- 对象字面量
- 工厂模式
- 构造函数
- Prototype 原型
原生 Object
1 | // 使用 Object 方式创建用户对象 |
对象字面量
- 对象定义的一种简写形式
- 简化创建包含大量属性的对象的过程
- 在为函数传递大量可选参数时,可考虑使用对象字面量
1 | // 使用对象字面量方式,创建用户对象 |
除此之外,
var arr = []
其实是 var arr = new Array()
的语法糖,
function Foo() {...}
其实是 var Foo = new Function {...}
的语法糖
工厂模式
- 软件工程领域的一种设计模式
- 抽象了创建对象的过程
- 通过函数封装创建对象的细节
1 | // 使用工厂模式创建用户对象 |
构造函数 (!)
1 | // 使用构造函数方式创建用户对象 |
构造函数也就是一个函数,只不过它于普通的函数又有点不同:
- 没有显示的创建对象;
- 直接将属性和方法赋给
this
; - 没有
return
语句;
原型 prototype
- 每个函数都有一个 prototype (原型) 属性
- 其是一个指针,指向一个对象 (Object)
- 这个对象的用途是包含可以由特定类型的所有实例的属性和方法
1 | // 使用构造函数方式创建用户对象 |
上述用构造函数+prototype这种方法构造对象,也叫做用混合方式构造对象,即
- 构造函数:来写函数的属性
- 原型 prototype:来写函数的方法
继承
关于 prototype 和继承
上一节里我们用到了 prototype,那么 prototype 到底是什么呢?
- prototype 属性包含一个对象 (prototype 对象)
- 所有实例对象需要共享的属性和方法,都放在这个对象里
- 实例对象一旦创建,将自动引用 (共享) prototype 对象的属性和方法
- prototype 对象好像是实例对象的原型,而实例对象则好像“继承”了 prototype 对象一样
几种继承的方式
更改 prototype 指向
常见的方法
Student.prototype = new Person()
Student
的prototype
指向一个Person
的实例,所有“学生”的实例,就能继承Person
了
1 | function Person() { |
注意
如果替换了 prototype
对象,即 o.prototype = {};
要为新的 prototype
对象加上 constuctor
属性,并将这个属性指回原来的构造函数,即 o.prototype.constructor = o;
直接继承 prototype
1 | function Person() {} |
这样继承的优点在于效率比较高 (不用执行和建立 Person 的实例了),
而缺点是 Student.prototype
和 Person.prototype
现在指向了同一个对象,任何对 Student.prototype
的修改,都会反映到 Person.prototype
。
利用空对象作为中介
为了解决上述更改 Student.prototype
都会反映到 Person.prototype
的问题,我们可以利用空对象作为中介来进行继承。其优点在于:
- 空对象,几乎不占用内存
- 修改
Student
的prototype
对象,不会影响到Person
的prototype
对象 - 可以封装成函数
1 | // F 为空对象,几乎不占用内存 |
将上述代码封装成一个函数,即
1 | function extend(Child, Parent) { |
(有 ES6 的 extends
那味儿了)
等到需要用到继承的时候,我们便可以直接通过 extend(Student, Person)
来实现了,省去了不少工夫。
构造函数绑定来进行继承
为什么要绑定?
假设有以下代码,提问下面的 alert
会弹出什么结果?
1 | function Person() { |
答案为 red, yellow, blue
,而不是 red, yellow
,为什么会这样呢?
原因是在这里的所有实例即 stu1
、stu2
,共享了父函数的非基本类型属性,如数组或对象,一个实例对属性的修改会影响所有实例。
构造函数绑定
- 在子类型构造函数的内部调用父类型构造函数
- 通过
call()
或apply()
方法
代码如下
1 | function Person() { |
组合继承
上述代码只能实现属性的继承,那么如何对方法也进行继承呢?我们可以用组合继承的方法。
组合继承也叫伪经典继承:
- 其将原型链继承和构造函数继承组合在一块
- 原型链实现对原型属性和方法的继承
- 借用构造函数实现对实例属性的继承
1 | function Person() { |
拷贝继承
通过对父类原型的属性遍历拷贝,也能把父类的属性放到子类上。简单且暴力。
1 | function Student(name, no) { |
原型链
原型规则
了解以上继承知识之后,我们便能将原型链的概念好好梳理下了。原型有以下几点规则:
- 所有的引用类型,即数组、对象、函数,都具有对象特性,即可自由扩展属性 (除了
null
)。 - 所有的引用类型,都有一个
__proto__
属性,属性值是一个普通的对象。 - 所有的函数,都有一个
prototype
属性,属性值也是一个普通的对象。 - 所有的引用类型,
__proto__
属性值都指向它的构造函数的prototype
属性值。 - 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的
__proto__
(即它的构造函数的prototype
) 中寻找。 - 这样为了得到某个属性,沿着其
__proto__
属性进行寻找的链条,我们叫做原型链。
instanceof
f instanceof Foo
可用来判断元素 f
是否为 Foo
的原型,
其判断逻辑就是:对 f
的 __proto__
一层一层往上,看是否能对应到 Foo.prototype
,即
f.__proto__ = Foo.prototype
或 f.__proto__.__proto__... = Foo.prototype