目录
- 原型
- 隐式原型
- 显式原型
- constructor
- new操作符
- 重写原型对象
- 原型链
- 继承
- 原型链继承
- 借用构造函数继承
- 组合构造继承
- 原型继承
- 寄生继承
- 组合寄生继承
- 原型继承关系
原型
在JavaScript
中,每个对象都有一个内置属性[[prototype]]
,这个属性指向一个另一个对象
当我们访问
对象中的属性时,会触发[[GET]]
操作
这个操作会现在自己对象内部寻找对应的值
,如果找不到就会在[[prototype]]
中所指向的对象中寻找
可以通过__proto__
和Object.getPrototypeOf
两个属性来访问
这个对象
可以通过__proto__
和Object.setPrototypeOf
两个属性来设置
这个对象
注意,__proto__
是早期浏览器自行添加
的属性,而Object.getPrototypeOf
和Object.setPrototypeOf
是标准添加
的
如下代码所示
var obj = {}console.log(obj.__proto__)console.log(Object.getPrototypeOf(obj))console.log(obj.__proto__ === Object.getPrototypeOf(obj))var obj2 = {}var obj3 = {a: 1}obj2.__proto__ = obj3console.log(obj2.__proto__)console.log(obj2.a)Object.setPrototypeOf(obj2, obj)console.log(obj2.__proto__)
控制台结果如下
隐式原型
每个对象都会有一个__proto__
属性,这个属性不建议直接访问或修改,是只在JavaScript内部使用的属性
,因此被称之为隐式原型
显式原型
函数也是一个特殊的对象
,是对象也就意味着也拥有隐式原型
但与普通对象不同的是,函数同时也拥有显式原型
和隐式原型
不同的是,显式原型可以直接访问,并且经常使用
显示原型的作用就是用来构造对象
函数的显式原型
可以通过prototype
属性来访问
如下代码
function foo() {}var obj = {}console.log(foo.prototype)console.log(obj.prototype)
控制台结果如下
constructor
在说明显式原型
的用处之前需要先知道一个函数constructor
constructor
在函数的显式原型
上
constructor
也被称之为构造函数
这个constructor
指向函数本身
function foo() {}console.log(foo.prototype.constructor)console.log(foo === foo.prototype.constructor)
控制台结果
new操作符
在之前的this
绑定规则一文中new
关键字做了以下操作
- 创建一个
空对象
- 将
空对象
的this
绑定到这个空对象
- 执行函数体里的代码
其实还有第四步
即将函数的显式原型
赋值到空对象中的隐式原型
上
这意味着如果我们通过某一个函数来构建一个对象,这个对象的隐式原型指向的是函数的显式原型
function Person() {}var obj = new Person()console.log(obj.__proto__)console.log(obj.__proto__ === Person.prototype)
控制台结果如下
我们说new
关键字会执行函数体里的代码,这句话不能说错
但更精确的说法是new
关键字会执行显式原型中constructor
函数里的代码
重写原型对象
如果我们需要在显式原型
上添加许多属性,通常我们会重写整个显式原型
function Person() {}Person.prototype = {a: 1,b: 2,foo: function () {console.log(this.a)}}var obj = new Person()console.log(obj.b)obj.foo()console.log(Person.prototype.constructor)
控制台结果如下
可以看到,如果我们重写显式原型
的话constructor
会指向Object
Person.prototype.constructor = Person
我们可以通过这种方式来修改Person
的constructor
,但这样修改得到的constructor
的[[Enumerable]]
被设置成了true
默认情况下的constructor
的[[Enumerable]]
是false
如果想要解决这个问题,可以通过Object.defineProperty
函数
Object.defineProperty(Person.prototype, "constructor", {enumerable: false,value: Person})
这样得到的constructor
就是不可枚举的了
关于对象的属性描述符
可以看我这篇文章
(未动笔,未来可寄)
原型链
在JavaScript
中,如果要实现继承
,就必须要理解一个重要概念,即原型链
当我们从一个对象获取一个属性时,如果在当前对象中没有获取到对应的值时就会通过对象的隐式原型来寻找
如果也没有找到的话就会一直向上寻找
所有对象的顶层原型为 [Object: null prototype] {}
所有通过Object
创建出来的对象其隐式原型
都指向这个
这个原型其实也有对应的隐式原型
,但指向的是null
综上所述,在JavaScript
中所有类的父类是Object
原型链
的顶层对象就是Object
的隐式原型
在理解了原型链
之后我们就能实现继承
继承
以下是几种继承
的实现方式
原型链继承
原型链继承
是通过JavaScript
对象属性查找规则
实现的一种继承
方式
function Person() {this.age = 18}var p = new Person()function Student() {this.id = "101"}Student.prototype = pvar stu = new Student()console.log(stu.age)console.log(stu.id)
控制台结果如下
这个方法需要构造一个父类的实例对象
,再将子类
的显式原型
指向父类构造的实例对象
,子类
在构造实例对象
时生成的对象其隐式原型
就指向了父类构造的实例对象
这个方法也有自己的缺点
- 某些属性其实是
存储在父类的实例对象上
的,直接打印子类的实例对象是看不到这些属性的 - 这个属性会
被多个对象共享
这个属性的值是唯一的
借用构造函数继承
借用构造函数继承
的关键就在于子类中直接调用父类的构造函数
function Person() {this.age = 18}function Student() {Person.call(this)this.id = "101"}var stu = new Student()console.log(stu)
控制台结果如下
可以看到此时父类的属性也已经继承过来了
但这只是属性的继承
,如果想要调用父类的方法的话还需要和原型链继承
一起使用
组合构造继承
function Person() {this.age = 18}Person.prototype.foo = function () {console.log(this.age)}var p = new Person()function Student() {Person.call(this)this.id = "101"}Student.prototype = pvar stu = new Student()console.log(stu.age)console.log(stu.id)stu.foo()
控制台结果如下
这样我们就实现了属性和方法的一起继承
这种方法其实也有一些问题
- 这个方法会调用
两次构造函数
- 一次在生成子类实例对象时调用了父类的构造函数
- 一次在创建子类原型的时候
- 所有的子类实例对象会拥有
两份父类属性
一份在自己这里,一份在自己的隐式原型中
默认访问时优先访问自己本身有的属性
原型继承
在2006年时道格拉斯·克罗克福德
提出了一种新的继承方式
这种方法并不依靠constructor
来实现
function Person() {this.age = 18}Person.prototype.foo = function () {console.log("this function")}function Student() {this.id = "101"}var obj = {}Object.setPrototypeOf(obj, Person.prototype)Student.prototype = objvar newObj = new Student()
我们使用借用构造函数继承
的目的就是要一个新对象
,新对象
的隐式原型
指向父类的显式原型
,最后子类
的显式原型
再指向这个新对象
通过Object.setprototypeOf
方法来设置新的obj
对象的隐式原型
指向父类
的显式原型
,子类
的显式原型
指向obj
这样就绕过了constructor
还有其他几种实现方法
function Person() {this.age = 18}Person.prototype.foo = function () {console.log("this function")}function Student() {this.id = "101"}var obj = {}function F() { }F.prototype = Person.prototypeStudent.prototype = new F()var newObj = new Student()
定义一个新函数,使新函数的显式原型直接指向父类的显式原型
在构造这个新函数的对象时实际上是构造了一个指向父类的空的新对象
再将子类的显式原型指向这个新对象
这也是道格拉斯·克罗克福德
提出来的方法
function Person() {this.age = 18}Person.prototype.foo = function () {console.log("this function")}function Student() {this.id = "101"}var obj = Object.create(Person.prototype)Student.prototype = objvar newObj = new Student()
这里使用了Object.create
方法,这个方法会创建一个空对象并将这个空对象的隐式原型指向你传入的对象
可能存在一些兼容性问题
寄生继承
最后我们将原型继承封装成一个函数
function inherit(Subtype, Supertype) {function F() { }F.prototype = Supertype.prototypevar obj = new F()Subtype.prototype = objObject.defineProperty(Subtype.prototype, "constructor", {enumerable: false,value: Subtype})}
这个inherit
就是寄生继承
的实现方法
这种方法同样由道格拉斯·克罗克福德
提出
组合寄生继承
此时寄生继承
已经能解决原型继承
和借用构造函数继承
中的绝大部分问题,剩下的一个最大的问题就是还需要继承父类中的属性
为了解决这个问题我们需要综合上面所有的方法来得到最终的解决方案
function inherit(Subtype, Supertype) {function F() { }F.prototype = Supertype.prototypevar obj = new F()Subtype.prototype = objObject.defineProperty(Subtype.prototype, "constructor", {enumerable: false,value: Subtype})}function Person() {this.age = 18}Person.prototype.foo = function () {console.log("this function")}function Student() {Person.call(this)this.id = "101"}inherit(Student, Person)var newObj = new Student()console.log(newObj.id)console.log(newObj.age)newObj.foo()
控制台结果如下
这也是目前在ES6
以前使用最多的继承解决方案
原型继承关系
最后我们再来梳理一下在JavaScript
中的原型继承关系
Object
是所有类的父类
Object的显式原型的隐式原型指向null
因为Object
也是一个函数,也同样拥有隐式原型
,它的隐式原型
指向Function的显式原型
Function
的隐式原型
同样指向自己的显式原型
因为Function
的显式原型
是通过new Object
创建出来的,所以它的隐式原型
指向Object的显式原型
我们创建的函数的显式原型
指向函数自己的显式原型
我们的函数本质上是通过new Function
创建出来的
所以函数的隐式原型
则指向Function的显式原型
我们通过foo
创建出来的对象
的隐式原型
指向foo的显式原型
通过new Object
创建出来的对象
的隐式原型
指向Object的显式原型
以上就是在JavaScript
中的原型继承关系图
最后附带一张更加形象的示例图