面向对象
面对对象的考察方式无非是两种
第一种:类与实例
- 类的声明
// 类的声明两种方式
// 方式一:传统的构造函数类的方式
function Animal() {
this.name = 'name'
}
// 方式二:ES6中的class的声明
// class后跟的是类名,constructor是构造函数,构造函数里跟ES5中的写法是一样的
class Animal2 {
constructor(){
this.name = 'name2'
}
}
- 生成实例
// 类的实例化,无论是哪种方式声明的类,实例化都是通过new来实现的
// 实例化的时候,如果构造函数没有参数,那么new的时候,后边的括号是可以不要的
console.log(new Animal(),new Animal2()) //Animal {name: "name"} Animal2 {name: "name2"}
console.log(new Animal, new Animal2) // Animal {name: "name"} Animal2 {name: "name2"}
第一种:类与继承
- 记重点:继承的本质是原型链
- 继承的方式一
// 方式一:借助构造函数实现继承,步骤如下:
// 先声明一个父类的构造函数
function Parent1(){
this.name = 'parent1'
}
Parent1.prototype.say = function(){}
// 再声明一个子类的构造函数
function Child1(){
// 既然是要继承,所以要关联父类,要在子类的构造函数里执行父类构造函数里的代码
// call和apply方法,都是用来改变函数运行时的上下文(context),换句话说,也就是为了改变函数体内部this的指向
// Parent1.call(this) 会执行Parent1里的代码,并把Parent1里this指向了现在的Child1
Parent1.call(this)
// 再给子类增加一个自己的属性type
this.type = 'child1'
}
console.log(new Child1) // Child1 {name: "parent1", type: "child1"}
console.log(new Child1().say()) //(intermediate value).say is not a function
通过构造函数实现继承的缺点:因为是通过改变父级构造函数内部的this的指向实现的,所以子类能继承父类的所有的属性和方法,但是也仅限于父类内部的属性和方法,如果是父类的原型链上的东西并没有被继承
- 继承的方式二:为了弥补构造函数实现继承的不足,出现了借助原型链来实现继承的方式
// 借助原型链实现继承
function Parent2(){
this.name = 'parent2'
}
function Child2(){
this.type = 'child2'
}
Child2.prototype = new Parent2()
console.log(new Child2) // Child2 {type: "child2"}
console.log(new Child2().__proto__ === Child2.prototype) // true
console.log(new Child2().__proto__.name) // parent2
在学原型链的时候我们知道,任何一个函数,都有一个prototype属性,这个属性的作用就是为了让这个构造函数的实例,能访问到它的原型对象上,这是原型链的基本原理。
正常情况下,当我们实例化了Child2后,这个实例化对象上会有一个proto属性。
这个属性指向了 构造函数Child2的原型对象,也就是Child2.prototype。
现在我们把Parent2的实例化对象,赋值给了Child2.prototype。
也就造成了 Child2的实例化对象的原型对象变成了Parent2的实例化对象。
当我们去Child2的实例化对象里去找name属性的时候 是找不到的,我们要到Child2.prototype指向的原型对象里去找。
Child2.prototype现在指向了Parent2的实例化对象,它里边是有name属性的,这就是原型链继承
通过原型链实现继承也是有缺点的,我们来看一下代码
function Parent2(){
this.name = 'parent2';
this.friends = [1,2,3];
}
function Child2(){
this.type = 'child2'
}
Child2.prototype = new Parent2()
console.log(new Child2) // Child2 {type: "child2"}
var s1 = new Child2();
var s2 = new Child2();
console.log(s1.friends,s2.friends) // [1, 2, 3] [1, 2, 3]
s1.friends.push(4)
console.log(s1.friends, s2.friends) //[1, 2, 3, 4] [1, 2, 3, 4]
console.log(s1.__proto__ === s2.__proto__) // true
我们实例化了两个Child2对象,这两个对象也都继承了Parent2的friends属性,所以s1和s2属性都可以访问到friends属性。
但是,当我们通过s1去修改friends属性的时候,我们发现s2这个对象的friends属性也受到了影响。
这是因为friends属性是s1和s2原型链上的属性,这他们俩的原型对象是引用的同一个对象(Parent2的实例对象),当s1修改friends属性的时候,实际上就是修改了Parent2的实例对象的属性,所以通过s2去访问friends属性的时候,才发现也跟着发生了变化
- 继承的方式三:组合式(继承比较常用的方式)
为了继承上边两种方式的优点,弥补它们的不足,出现了组合式的继承方法,看代码:
function Parent3() {
this.name = 'parent3';
this.friends = [1, 2, 3];
}
function Child3() {
Parent3.call(this);
this.type = 'child3';
}
Child3.prototype = new Parent3()
var s3 = new Child3();
var s4 = new Child3();
console.log(s3.friends,s4.friends); // [1, 2, 3] [1, 2, 3]
s3.friends.push(4);
console.log(s3.friends, s4.friends); // [1, 2, 3, 4] [1, 2, 3]
这种组合式的继承也是有缺点的,看代码我们可以知道,当我们new一个Child3实例对象的时候,会执行一次Child3构造函数,而Child3构造函数体内部,通过call的调用会执行一次Parent3构造函数。然后当我们把Parent3的实例赋值给Child3的原型对象的时候,又执行了一次Parent3构造函数,也就是说,这种组合方式会执行两次父类的构造函数。这是没有必要的。
我们要怎么优化呢?
- 组合继承的优化一
之前为了拿到父类原型对象上的属性和方法,我们把父类的实例对象赋值给了子类的原型对象。但是这造成了父类构造函数多执行了一次。
现在我们直接把父类的原型对象的引用,直接赋值给子类的原型对象,也可以实现拿到父类的原型对象上的属性和方法的目的,而且父类构造函数并没有再执行一次。
function Parent4() {
this.name = 'parent3';
this.friends = [1, 2, 3];
}
function Child4() {
Parent3.call(this);
this.type = 'child3';
}
Child4.prototype =Parent4.prototype
var s5 = new Child4();
var s6 = new Child4();
console.log(s5.friends, s6.friends); // [1, 2, 3] [1, 2, 3]
s5.friends.push(4);
console.log(s5.friends, s6.friends); // [1, 2, 3, 4] [1, 2, 3]
这种组合优化的方式也并非没有缺点,缺点就是,无法区分实例是由子类直接直接创建的还是由父类直接创建的。因为此时子类的原型对象和父类的原型对象是
之前通过学习原型链,我们得知要判断一个对象是否是一个类的实例,可以通过instanceof
// s5即是Child4类的实例,又是Parent4类的实例
console.log(s5 instanceof Child4,s5 instanceof Parent4) // true true
但是通过instanceof
我们无法区分,这个实例化对象s5是由子类Child4直接实例化的对象,还是由父类Parent4直接实例化的对象呢?这个时候我们需要借助另外一个办法那就是 constructor属性
,
这个属性也可以判断一个对象是否是一个类的实例
console.log(s5.constructor) //Parent4
这个时候,我们发现,s5竟然是通过Parent4直接实例化的,但是我们明明是通过new Child4得到的s5啊,显示这不是我们想要的结果。
分析原因,我们可以得知,子类Child4的原型对象,已经被赋值为了父类Parent4的原型对象,而父类Parent4的原型对象里是有constructor
属性的,而这个属性就指向了构造函数Parent4本身,所以s5实例的原型对象的constructor属性当然就是Parent4啦
所以,如果我们有了组合继承的优化二
- 组合继承的优化二
// 组合式继承优化2
function Parent5() {
this.name = 'parent3';
this.friends = [1, 2, 3];
}
function Child5() {
Parent3.call(this);
this.type = 'child3';
}
Child5.prototype = Object.create(Parent5.prototype)
Child5.prototype.constructor = Child5
var s7 = new Child5();
console.log(s7 instanceof Child5, s7 instanceof Parent5) // true true
console.log(s7.constructor) // Child5
我们知道 通过Object.create创建的对象的原型对象,就是Object.create的参数。
通过上边代码可知,我们通过Object.create创建了一个新对象,然后把这个新对象赋值给了子类Child5的原型对象。
通过创建中间对象的方式,就把父类和子类的的原型对象区分开了,但是因为我们是通过Object.create创建的的这个新对象,所以我们又把 子类和父类在原型链上又连接起来了。
但是这个时候,我们并没有解决问题,s7的直接构造函数,还是Parent5,因为s7的原型对象是Object.create创建的新对象,而这个新对象是没有自己的constructor属性的,不过因为新对象的原型对象是Parent5的原型对象,而Parent5的原型对象是有constructor属性的,而且这个constructor属性指向的就是Parent5。
所以,我们还需要再加一步,那就是给Child5的原型对象的constructor属性重新赋值,也就是修改Child5的原型对象的constructor属性的指向,让它重新指向Child5,这个时候,我们就能正常区分父类和子类的实例的构造函数了
这里可能会有人有疑问,既然就是改一下constructor属性的指向,那直接在组合继承优化1里加不就行了吗?也就是和像下边这样
Child4.prototype =Parent4.prototype
Child4.prototype.constructor = Child4
其实是不行的,因为这个时候Child4的原型对象和Parent4的原型对象就是一个对象,修改了子类的原型对象的constructor属性,就是修改了父类的原型对象的constructor属性,当我们把Child4.prototype.constructor = Child4的时候,我们就不能区分父类的实例的构造函数了
所以,这就是组合继承方式的完美写法
第二种问法 ,一个对象继承了某个类,问它的原型链。
- 在一个对象上找不到某个属性,会通过原型链一级一级往上找,这个就完全可以把它理解为是一种继承关系。