一、预备知识
1、构造函数的属性
function A(name) {
this.name = name; // 实例基本属性
this.arr = [1]; // 实例引⽤属性
this.say = function () { // 实例引⽤属性
console.log('hello')
}
}
注意:数组和⽅法都属于实例引⽤属性,但是数组强调私有、不共享的。⽅法需要复⽤、共享。
在构造函数中,⼀般很少有数组形式的引⽤属性,⼤部分情况都是:基本属性(定义在构造函数内) + ⽅法(定义在构造函数的原型上)。
2、什么是原型对象
简单来说,每个函数都有prototype属性,它就是原型对象,通过函数实例化出来的对象有个 __proto__属性,指向原型对象。
let a = new A()
a.__proto__ == A.prototype
// prototype的结构如下
A.prototype = {
constructor: A,
...其他的原型属性和⽅法
}
3、原型对象的作用
原型对象的⽤途是为每个实例对象存储共享的⽅法和属性,它仅仅是⼀个普通对象⽽已。并且所有的实例是共享同⼀个原型对象,因此有别于实例⽅法或属性,原型对象仅有⼀份。⽽实例有很多份,且实例属性和⽅法是独⽴的。在构造函数中:为了属性(实例基本属性)的私有性、以及⽅法(实 例引⽤属性)的复⽤、共享。因此提倡:
- 将属性封装在构造函数中
- 将⽅法定义在原型对象上
function A(name) {
this.name = name; // (该属性,强调私有,不共享)
}
A.prototype.say = function () {
// 定义在原型对象上的⽅法 (强调复⽤,需要共享)
console.log('hello')
}
二、JS继承方式
继承指子类继承父类的属性和方法,目的是让子类的实例可以享有父类的属性和方法
1、原型链继承
核⼼:让父类的属性和方法在子类实例的原型链上
-
child.prototype = new Parent()
-
Child.prototype.constructor = Child (修正constructor指向 )
优点:
- ⽅法复⽤,由于⽅法定义在⽗类的原型上,复⽤了⽗类构造函数的⽅法。⽐如say⽅法。
缺点:
-
创建⼦类实例的时候,不能传⽗类的构造函数传参数(⽐如name)。
-
⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性。
function Parent(x) {
this.x = x || 'parent' //基本属性
this.arr = [1]; // 引用属性
}
Parent.prototype.getX = function(){
console.log('hello parent')
}
function Child(y) {
this.y = y
}
//原型继承,如果new A的时候不传参数,那么newB的时候不能传父类的参数了,对应缺点第一点
Child.prototype = new Parent()
// 修正constructor指向
Child.prototype.constructor = B
//B原型上添加新方法getY
Child.prototype.getY = function(){
console.log('hello child')
}
let b1 = new Child(100)
let b2 = new Child(200)
// 优点:共享了⽗类构造函数原型的getX⽅法
console.log(b1.getX(), b2.getX()); //hello parent
console.log(b1.getX === b2.getX) //true
// 缺点1:不能向⽗类构造函数传参
console.log(b1.x, b2.x, b1.x===b2.x); // parent,parent,true
// 缺点2: ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性
b1.arr.push(2); // 修改了b1的arr属性,b2的arr属性,也会变化,因为两个实例的原型上(Child.prototype)有了⽗类构造函数的实例属性arr; 所以只要修改了b1.arr,b2.arr的属性也会变化。
console.log(b2.arr); // [1,2]
//注意:修改b1的x属性(基本属性),是不会影响到b2.x。因为设置b1.x相当于在⼦类实例新增了x属性。
2、通过call()实现继承
优点:实例之间独⽴。
-
创建⼦类实例,可以向⽗类构造函数传参数。
-
⼦类实例不共享⽗类构造函数的引⽤属性。如arr属性
-
可实现多继承(通过多个call或者apply继承多个⽗类)
缺点:
- ⽗类的⽅法不能复⽤
由于⽅法在⽗构造函数中定义,导致⽅法不能复⽤(因为每次创建⼦类实例都要创建⼀遍⽅法)。
- ⼦类实例,继承不了⽗类原型上的属性(因为没有⽤到原型)
//2.CALL继承
function Parent(name) {
this.name = name || 'parent' //基本属性
this.arr = [1]; // 引用属性
this.say = function(){
console.log('父类构造函数上的方法')
}
}
Parent.prototype.getX = function(){
console.log('hello parent')
}
function Child(x,y) {
Parent.call(this,x) //call继承
this.y = y
}
Child.prototype.getY = function(){
console.log('hello child')
}
let boy1 = new Child(100,200)
let boy2 = new Child(300,400)
// 优点1:可向⽗类构造函数传参
console.log(boy1.x, boy2.x); // 100,300
// 优点2:不共享⽗类构造函数的引⽤属性
boy1.arr.push(2); console.log(boy1.arr,boy2.arr);// [1,2] [1]
// 缺点1:⽅法不能复⽤
console.log(boy1.say === boy2.say) // false (说明,boy1和boy2的say⽅法是独⽴,不是共享的)
// 缺点2:不能继承⽗类原型上的⽅法
console.log(boy1.getX) //undefined
3、组合继承(CALL继承+原型继承)
核⼼:通过调⽤⽗类构造函数,继承⽗类的属性并保留传参的优点;然后通过将⽗类实例作为
⼦类原型,实现函数复⽤。
优点:
-
保留构造函数的优点:创建⼦类实例,可以向⽗类构造函数传参数。
-
保留原型链的优点:⽗类的⽅法定义在⽗类的原型对象上,可以实现⽅法复⽤。
-
不共享⽗类的引⽤属性。⽐如arr属性
缺点:
- 由于调⽤了2次⽗类的构造函数,会存在⼀份多余的⽗类实例属性。
第⼀次
Parent.call(this);从⽗类拷⻉⼀份⽗类实例属性,作为⼦类的实例属性第⼆次
Child.prototype = new Parent();创建⽗类实例作为⼦类原型,Child.prototype中的⽗类属性和⽅法会被第⼀次拷⻉来的实例属性屏蔽掉,所以多余。
注意:组合继承这种⽅式,要记得修复Child.prototype.constructor指向
function Parent(name) {
this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
this.arr = [1]; // 实例的引用属性(该属性,强调私有)
}
Parent.prototype.say = function () { // 将需要复⽤、共享的⽅法定义在⽗类原型上
console.log('hello')
}
function Child(name,like) {
Parent.call(this,name) // 核⼼ 第⼆次
this.like = like;
}
Child.prototype = new Parent() // 核⼼ 第⼀次
Child.prototype.constructor = Child // 修正constructor指向
let boy1 = new Child('⼩红','apple')
let boy2 = new Child('⼩明','orange')
// 优点1:可以向⽗类构造函数传参数
console.log(boy1.name,boy1.like); // ⼩红,apple
// 优点2:可复⽤⽗类原型上的⽅法
console.log(boy1.say === boy2.say) // true
// 优点3:不共享⽗类的引⽤属性,如arr属性
boy1.arr.push(2)
console.log(boy1.arr,boy2.arr); // [1,2] [1] 可以看出没有共享arr属性。
// 缺点1:由于调⽤了2次⽗类的构造⽅法,会存在⼀份多余的⽗类实例属性
4、寄生组合继承(核心:Object.create)
function Parent(name) {
this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
this.arr = [1]; // 实例的引用属性(该属性,强调私有)
}
Parent.prototype.say = function () { // 将需要复⽤、共享的⽅法定义在⽗类原型上
console.log('hello')
}
function Child(name,like) {
Parent.call(this,name,like) // 核⼼
this.like = like;
}
// 核⼼ 通过创建中间对象,⼦类原型和⽗类原型,就会隔离开,不是同⼀个啦
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
let boy1 = new Child('⼩红','apple')
let boy2 = new Child('⼩明','orange')
let p1 = new Parent('⼩爸爸')
5、ES6继承(extends)
//5.ES6的继承
class Parent {
constructor(name) {
this.name = name
}
say() {
console.log('parent prototype method')
}
}
class Child extends Parent {
constructor(name, like) {
super(name) //继承Parent,给Parent传参
this.like = like
}
favourite() {
console.log('child prototype method')
}
}
let boy = new Child('piano')
console.log(boy)
console.log(boy.say())
console.log(boy.favourite())
三、补充Object.create
1、基本概念
Object.create()可以帮我们生成一个对象,通过传参,可以将生成的对象的原型指向第一个参数,
⽅法还有第⼆个可选参数,是添加到新创建对象的属性,写法如下。
const a = Object.create(Person.prototype, {
age: {
value: 12,
writable: true,
configurable: true,
}
})
2、new 与 Object.create() 的区别?
-
new 产⽣的实例,优先获取构造函数上的属性;构造函数上没有对应的属性,才会去原型上查找; 如果构造函数中以及原型中都没有对应的属性,就会报错。
-
Object.create() 产⽣的对象,只会在原型上进⾏查找属性,原型上没有对应的属性,就会报错。