面向对象系统中的继承方法
-
在Java语言中,有一套完整面向对象系统。类与类之间是通过extends关键词实现继承的。继承之后的效果是子类可以之间可以共享、覆盖父类的变量或方法。如果要使用方法,或者访问变量,就需要去实例化一个类,然后再去执行相关的动作。
-
在JavaScript中,对象与对象之间也有这样的继承关系,例如普通对象和Object之间的关系,就是子与父的关系。子对象之间共享Object的方法,像toString, getOwnProtopertyNames, getOwnProtopertySymbols,isPrototypeOf等属性。子对象本身并没有定义这样属性,但能够使用这些方法,是因为父对象Object定义了这些方法。
-
OK,那Javascript是以什么样的方式来实现对象之间的继承关系呢?很遗憾我们并没有像Java这样的继承方式,但是我们有更简洁的继承方式,这就是原型继承
我们要注意,Java是基于类的编程。而在JavaScript中,更提倡程序员去关注一系列对象实例的行为,而后才去关系如何将这些对象按照使用方式的相似性来分类,而不是分成一个个的“类”
不要见到这么多文字就头疼啊,我的文字还是很好懂的😁
什么是原型
简单的来说,原型就是两点:
- 任何对象都有私有字段[[prototype]], 其指向的就是对象的原型
- 在对象上面查找属性的时候,没有查到,就会去原型对象上面找,直到原型对象为空,或者找到为止。
使用原型来实现下继承吧
我们先创建一个函数,用来被继承。并且在函数prototype上面挂上一个方法
function Animal(name){
this.name = name;
}
Animal.prototype.logName = function(){
console.log('i am Animal: '+ this.name);
}
var animal = new Animal('Cat');
animal.logName(); //i am Animal: Cat
通过Animal函数来创建一个animal对象,然后用让animal对象打印出自己的名字
prototype和私有[[prototype]],是两个不同字段哦
我们再来创建一个函数,用来继承上面创建Animal
function Tiger(name){
this.name = name;
}
Tiger.prototype = new Animal();
var tiger = new Tiger('tiger');
tiger.logName(); //i am Animal: tiger
首先改变Tiger的prototype,让其指向一个Animal实例对象。
然后通过Tiger函数实例化一个tiger对象,然后用让tiger对象打印出自己的名字。我们知道,tiger并没有定义自己的logName方法,显然这个方法是从Animal中继承过来
注意我的单词大小哦,大写开头的一般是构造函数,小写开头的一般是实例对象
是不是有点懵
没关系,我们来一步一步拆解上面的代码
1. 构造函数,实例对象,对象的原型,这三者之间是什么关系
上面的例子中,Animal函数new了一个animal对象出来。这个过程,Animal函数就是充当构造函数,而animal对象就是实例对象。在Animal.prototype上面挂了一个函数,animal对象竟然可以访问到。
你心中是不是会想,Animal.prototype就是animal的原型对象啊,所以animal对象可以访问得到。
嗯,恭喜你,猜对了。哈哈哈哈 我们来看下面这张图
这幅图里面就是我们要讨论的三个东西,构造函数,实例化对象,原型对象
- 构造函数里面有一个prototype属性,指向原型对象。实例对象中也有一个属性指向原型对象,这个属性是__proto__,prototype和__proto__是两个含义完全不同的属性
- __proto__是所有对象都有的一个隐藏属性。作用是,在对象上查找属性查找不到的时候,就会在__proto__所指向的对象里面接着找。这也是为什么animal能够调用logName方法。
你在浏览器控制台里面可能会看到[[prototype]]。 别怕,这个就是__proto__属性
- prototype是构造函数特有的属性,非隐藏属性。作用是,实例化出来的对象,它的__proto__属性会指向prototype所指向的对象
- 是不是有点绕?没关系,我刚开始学也是
把这个图背下来吧,这三者形成一个三角形
2. 构造函数实例化对象的过程
我们先看实例化出来的对象,和构造函数的写法有什么联系
var animal = new Animal('Cat');
console.log(animal);
我把实例对象animal打印出来了,可以看到其中有个name属性,还有一个[[prototype]]属性
再打开[[prototype]]看看
[[prototype]]里面有个logName
到这里,你应该知道点什么了吧
我再具体化一下构造函数实例化对象的过程:
-
首先,JS引擎会创建一个对象A,并且A对象的
__proto__指向构造函数prototype所指向的对象。该过程类似于A = Object.create(Animal.prototype);,如果不清楚create的作用,可以查查文档 -
调用构造函数,并且将构造函数里面的this指向刚创建的A对。该过程类似于,
Animal.call(A, name);call的作用就是改变Animal函数执行过程中的this指向。关于this指向,讲清楚要花点时间,我会专门放在一篇文章里讲清楚。这里你只要知道会这么做就行了
-
调用了Animal函数之后,A对象就有name属性。构造函数执行结束后,我们就得到了最终的A对象,也就是Animal函数实例化的对象
-
如果在构造函数中,return了另外一个对象B,那用户得到的就是这个被return的B对象,否则得到的就是上面的A对象
function Animal(){
return {
name: 'dog'
}
}
var dog = new Animal();
这样得到的dog对象,和Animal原型对象一点关系也没有
console.log( dog.__proto__ === Animal.prototype ); // false
- 我把实例化对象的过程,用代码总结一下
function initialAnimal(name){
var A = Object.create(Animal.prototype);
var B = Animal.call(A, name);
if(typeof B === 'object'){
return B;
}
return A;
}
围绕着原型对象再说一说
1. constructor
每个原型对象都有一个constructor的属性,指向自己的构造函数。
这是Animal.prototype的打印结果:
我们可以看到,除了logName的属性,还是有个不可枚举属性--constructor。这个constructor指向Animal构造函数
这样的话,上面的三角图就有了变化:
这里我实例了两个对象,animal1、animal2,它们的__proto__都指向了同一个对象,也就是Animal.prototype指向的对象
我们可以看到原型对象中有个constructor属性,指向了构造函数。这个属性有什么用呢,当你拿到一个原型对象的时候,就可以通过constructor来判断该原型对象是哪个构造函数的原型对象。到后面讲到原型链尽头的时候,非常有用
console.log(Animal.prototype.constructor === Animal); //true
2. instanceof
instanceof的用处是:用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
额,TMD好学术啊
不过,别慌。理解这句话有两个角度
-
站在构造函数的角度去看,如果你是Animal构造函数,你的prototype所指的对象在另一个对象的原型链上,那么instanceof的操作结果就是true
-
站在实例对象的角度去看。我们已经知道JS引擎在查找属性时,会顺着__proto__属性查找一个又一个原型对象地找。如果你是animal实例对象,在查找属性的过程,有个构造函数的prototype属性,指向了你可能要查找的原型对象,那么instanceof的操作结果就是true
假设你要调用logName这个属性。这个过程就是,首先会在自己身上找,如果找不到,就去__proto__所指向的对象去找,正好你只找一个原型对象就找到了logName。
假设你要调用toString这个属性。这个过程是,首先会在自己身上找,如果找不到,就去__proto__所指向的原型对象去找;如果在原型对象还找不到,就去原型对象的__proto__所指向的原型对象去找,也就是原型的原型;就这样一直找啊找啊,到最后,在Object的原型对象上找到了这个方法
我们很容易就感知到,查找属性的过程,像顺着一个链条查找。我们这个链条叫做原型链
我们来看用法
console.log(tiger instanceof Tiger); //true
console.log(tiger instanceof Animal); //true
console.log(tiger instanceof Object); // true
现在,你再理解这句话:“instanceof的用处是:用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上” ,是不是很清晰了
理解了instanceof,那对下面的这个结果就不难理解了
var tiger2 = new Tiger('tiger2');
console.log(tiger2 instanceof Tiger); //true
console.log(tiger2 instanceof Object); // true
console.log(Tiger.prototype instanceof Object); //true
3. isPrototypeOf
这是一个确定一个对象是否为另一个对象的原型的方法,很好理解的一个方法,没有instanceof这么绕
我们来看具体用法
var tiger1 = new Tiger('tiger');
var tiger2 = new Tiger('tiger');
console.log(Tiger.prototype.isPrototypeOf(tiger1)); // true
console.log(Tiger.prototype.isPrototypeOf(tiger2)); //true
4. Object.getPrototypeOf,Object.setPrototypeOf()
如果我们要判断一个对象是否为另一个对象的原型,方法不止isPrototypeOf()
Object.getPrototypeOf()的作用是返回一个对象的__proto__属性指向的对象,也就是直接获取一个对象的原型对象
我们来看具体用法
console.log(Object.getPrototypeOf(tiger1) === Tiger.prototype); //true
console.log(Object.getPrototypeOf(tiger2) === Tiger.prototype) // true
是不是超简单😁
Object.setPrototypeOf()的作用是设置一个对象的属性
我们来看具体用法
Object.setPrototypeOf(tiger, Animal.prototype);
console.log(Object.getPrototypeOf(tiger) === Tiger.prototype); //false
console.log(Object.getPrototypeOf(tiger) === Animal.prototype); //true
我们知道,“=== ”比较对象的时候,是比较引用的。如果比较结果是true,那就说明是同一个对象
下一讲,我们再来研究下原型链
好吧,这一讲的东西有点多,多看几遍,不难😁
总结:
- 不同编程语言对象系统的继承方法
- 用两句话概括原型
- 手动用原型实现一个继承
- 构造函数、原型对象、实例对象的关系
- 再说一说和原型对象有关的细节和API
- 如果有哪里讲的不明白,留言告诉我,谢谢🙏