同样,分析一段代码,一个函数声明,通过new Xiaolu()创建一个xiaolu1的对象,然后进行相关打印
function Xiaolu() {
this.name = "小鹿"
}
Xiaolu.prototype.code = function () {
return this.name + '在敲代码'
}
var xiaolu1 = new Xiaolu()
//访问xiaolu1的name属性
//打印结果:小鹿
console.log(xiaolu1.name)
//调用xiaolu1的code方法
//打印结果:小鹿在敲代码
console.log(xiaolu1.code())
//查看xiaolu1的[[prototype]]的指向
//打印结果:看图
console.log(xiaolu1.__proto__)
//调用xiaolu1.toString()方法
//打印结果:[object Object]
console.log(xiaolu1.toString())
分析四个打印结果
第一行
第二行:
两行代码都是一样的原理,当构造函数创建对象后,创建的对象的[[prototype]]指向原型对象。
查找xiaolu1.name 和 xiaolu1.code(),都会触发[[Get]]操作:在当前的对象中查找是否有对应的属性和方法,如果找不到,就会去[[prototype]]指向的对象中查找。
这里就是在原型对象中找到了name属性和code方法。
通过这个现象,可以提出一个疑问:
当一个对象在触发[[Get]]操作时,在当前对象以及[[prototype]]指向的对象中都没查找到想要的属性,这时[[prototype]]指向的对象中恰好也有一个[[prototype]]指向另一个对象,那么[[Get]]操作会继续去那个对象中去寻找属性吗?
这个疑问先留在这,分析完后两行打印结果后,一切将会清晰
第三行:
proto__在上篇文章中提过,用来检测[[prototype]]的指向。
在打印结果中,看到了code()方法和指向构造函数的constructor,以及一个__proto__:Object
这个__proto__: Object是不是就代表着在xiaolu1这个对象[[prototype]]指向的原型对象中也有一个[[prototype]]并指向Object,这里就出现了我们之前提出疑问中的那个恰巧:
[[prototype]]指向的对象中有[[prototype]]指向另一个对象。
先不要急着得出答案,进入到第四行打印结果的分析
第四行:
从打印结果的[object Object],可以得出必定是一个对象调用了toString方法(涉及强制类型转换),但在xiaolu1这个对象甚至它的[[prototype]]指向的原型对象中,我们都没看到toString方法。
结合第三行:在xiaolu1中[[prototype]]指向的原型对象中有[[prototype]]指向Object
那么,是不是一切疑问都集中到了这个Object身上?
这样问题就变简单了,打印一下Object.prototype,不就能解决疑问了吗
function Xiaolu() {
this.name = "小鹿"
}
Xiaolu.prototype.code = function () {
return this.name + '在敲代码'
}
var xiaolu1 = new Xiaolu()
console.log(Object.prototype)
果然!在Objct.prototype中有toString方法,那么前面几个疑问是不是全部解决了?
分析一下整个[[Get]]操作的过程:
在xiaolu1中查找,没找到toString,去到[[prototype]]指向的原型对象查找,又没有找到,然后去向[[prototype]]指向的Object中查找,然后终于找到。(没找到会返回undefined)
这里连接着的[[prototype]],就把它叫做原型链,并且可以得出所有原型链的顶端都是Obejct.prototype,这也是为什么对象创建出来可以使用hasOwnProperty()、valueOf()、toString()等方法的原因了,都是通过原型链找到最顶端的Object.prototype
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
class 父类 {
}
class 子类 extends 父类 {
}
在java中的继承是这样式的
回到js,es6新增了class之后,可以通过class进行面向对象操作了,继承自然也是通过extends和super来完成
但在es6前,想要实现继承,我们大多通过原型链的方式来实现,不过都是对象与对象之间的关联,我倒更愿意叫它为“委托”
在原型链中,我想用你的方法和属性,就发出委托,让你把方法和属性给我使用,我觉得这样理解更好,并不一定要说是继承,毕竟原型和继承放在那就感觉像互斥的。
function Xiaolu() {
this.name = "小鹿"
this.work = ['工作', '代码']
}
// 通过关联原型,创建原型链实现继承
function Xiaolu1() {
}
//手动更改prototype指向
Xiaolu1.prototype = new Xiaolu()
var xiaolu1 = new Xiaolu1()
var xiaolu2 = new Xiaolu1()
// 一个更改基本类型
xiaolu2.name = "小鹿1"
// 一个更改引用类型
xiaolu1.work.push('敲代码')
// 看看对基本类型的更给是否会对另一个对象造成影响
// 打印结果:小鹿
console.log(xiaolu1.name)
// 看看对引用类型的更改是否会对另一个对象造成影响
// 打印结果:["工作", "代码", "敲代码"]
console.log(xiaolu2.work)
原型链中通过创建原型关联,使子辈可以访问父类的属性和方法,变相的实现了继承
缺点也显而易见:对于父类中的引用类型来说,子类对其进行一些改变操作时,也会改变其他子类对这个引用类型的引用。
另外还有一点,传参,全篇只有new Xiaolu()一次可以传参的机会,这样对子类内部属性不是非常的友好。
原型链式继承两个缺点:
function Xiaolu() {
this.name = '小鹿'
}
function Xiaolu1() {
//这里的this指向xiaolu1
//call将Xiaolu的this绑定到xiaolu1
//然后在这调用this.name = '小鹿'
//也就是xiaolu1.name = '小鹿'
Xiaolu.call(this)
}
var xiaolu1 = new Xiaolu1()
console.log(xiaolu1.name)
要想知道这个借用是什么意思,得理解这段代码
第一步:调用Xiaolu1时,就会执行 Xiaolu.call(this)
第二步:分析this指向,代码中 var xiaolu1 = new Xiaolu1(),因此触发了new绑定,这时this指向xiaolu1这个新对象
第三步:Xiaolu.call(xiaolu1),call是将this绑定向第一个参数,也就是将Xiaolu()中的this绑定向xiaolu1
第四步:执行Xiaolu(),xiaolu1.name = “小鹿”
这样就完成了继承
理解了上面的代码后,让我们看看借用构造函数的优缺点
function Xiaolu(name) {
// 构造函数的属性
this.name = name
// 构造函数上的方法
this.write = function() {
console.log(this.name + '在工作')
}
}
// 原型对象上的方法
Xiaolu.prototype.code = function() {
console.log(this.name + '在敲代码')
}
// 原型对象上的属性
Xiaolu.prototype.height = 180
// 子类,借用构造函数
function Xiaolu1(name, age) {
// 子类的属性age
this.age = age
// 这里借用实现属性的传值
Xiaolu.call(this, name)
}
var xiaolu1 = new Xiaolu1("小鹿", 18)
var xiaolu2 = new Xiaolu1("20岁的小鹿", 20)
// 看传参是否成功
// 打印结果:小鹿
console.log(xiaolu1.name)
// 看传参是否成功
// 打印结果:18
console.log(xiaolu1.age)
// 看在构造函数内部创建的函数是否共用
// 打印结果:false
console.log(xiaolu1.write === xiaolu2.write)
// 看是否能访问原型链上的height属性
// 打印结果:undefined
console.log(xiaolu1.height)
// 看是否能访问原型链上的code方法
// 打印结果:is not a function
xiaolu1.code()
直接给出优缺点,结合代码去理解一下
优点:可从子类构造函数中向父类构造函数传参
缺点:写在构造函数内部的方法不共用,不符合函数复用的原则。并且访问不到原型对象上的属性和方法。
函数借调的方式还有别的实现方式,但是原理都是一样的。但是有一点要记住,这里其实并没有真的继承,仅仅是调用了父类构造函数而已。也就是说,子类对象和父类构造函数没有任何的关系。
组合继承就是将原型链和借用构造函数结合在一起
简单理解就是使用借用构造函数继承属性,原型链继承方法,形成了一个挺适合大众的继承方式
上代码
// 父类构造函数
function Xiaolu(name) {
// 父类属性
this.name = name
this.arr = ['123', '123123']
// 在构造函数内部 在原型上添加方法
// 先检测,这样只会生成一次方法
if(typeof this.code !== 'function') {
Xiaolu.prototype.code = function() {
console.log(this.name + '敲代码')
}
}
}
// 子类构造函数
function extXiaolu(name, age) {
// 子类属性
this.age = age
// 借用构造函数
Xiaolu.call(this, name)
}
// 原型链是继承,继承原型上方法
extXiaolu.prototype = new Xiaolu()
var xiaolu1 = new extXiaolu("小鹿", 18)
var xiaolu2 = new extXiaolu("小鹿2", 20)
// 看传参是否成功
console.log(xiaolu1.name)
// 看是否会对引用类型共用
xiaolu1.arr.push('123123')
console.log(xiaolu1.arr)
console.log(xiaolu2.arr)
// 看是否可以使用原型对象上的方法
xiaolu1.code()
可以看出,组合继承可从子类构造函数中向父类构造函数传参,也可访问原型上的方法,并且将属性用借用构造函数方式继承,使得其引用类型不会被共用
组合继承是我们最常用的一种方式了,其他继承方式这里就不提了,之后会专门写一篇
参考自:《javascript高级程序设计》《你不知道的javascript上卷》