文章目录
- 继承 :
- 为什么需要继承 ?
- 构造函数 :
- 常见的继承方案 :
- 1. 原型继承 : Student.prototype = new Person('Jack', 18)
- 2. call 继承 : Person.call(this, name, age)
- 3. 组合继承 :
- 4. ES6 的继承语法
- 原型继承方法代码
- call继承方法代码
- 组合继承方法代码
- ES6 的继承语法
继承 :
- 继承是一个关于 构造函数 的高阶应用
- 继承一定是出现在 两个构造函数 之间的关系
- 什么是 继承 :
=> 当 构造函数 A 的 实例 , 使用了 构造函数 B 原型 上书写的属性和方法
=> 此时, 我们就说 A 和 B 出现了 继承关系
=> 构造函数 A 继承自 构造函数 B
=> 构造函数 A 是 构造函数 B 的 子类
=> 构造函数 B 是 构造函数 A 的 父类
为什么需要继承 ?
- 我们面向对象开发的核心, 是 => 高内聚 低耦合 <=
- 来思考 ? 进行假设 :
=> 我书写一个 苹果类 :
~> 颜色, 品牌, 重量, 水分, 名称
~> 吃的方法: 削皮吃, 去核吃
=> 我书写一个 橘子类 :
~> 颜色, 品牌, 重量, 水分, 名称
~> 吃的方法: 剥皮吃, 吐籽吃
可以看到 : 苹果类 和 橘子类 有 共同的 部分
- 所以换一种书写方法 :
=> 水果类 :
~> 颜色, 品牌, 重量, 水分, 名称
=> 苹果类 :
~> 继承 水果类
~> 书写我独立的内容
~> 形状
=> 橘子类 :
~> 继承 水果类
~> 书写我独立的内容
~> 额外用途
构造函数 :
- 构造函数 :
=> 原型 ( prototype ) :
每一个函数 天生自带一个属性, 叫做 prototype , 是一个 对象数据类型
=> 为什么需要原型 :
~> 为了解决 构造函数 的不合理
~> 书写 构造函数 的时候
~> 属性 直接书写在 构造函数体内
~> 方法 书写在 构造函数 的 原型 上
=> 目的: 为了书写一些方法, 给到该 构造函数 的 实例 使用
~> 把每一个实例都会用到的方法, 提取出来放在了 构造函数 的 原型 上
A 继承自 C , A 是 子类 , C 是 父类
B 继承自 C , B 是 子类 , C 是 父类
继承的目的 , 就是为了能够使用 构造函数 C 身上的 属性 和 方法 ,
把公共的东西提取出来成为更公共的东西
常见的继承方案 :
1. 原型继承 : Student.prototype = new Person(‘Jack’, 18)
原型继承,就是在本身的原型链上加一层结构
function Student() {}
Student.prototype = new Person('Jack',20)
原型继承(原型链继承) :
- 核心: 让子类的 原型 指向 父类的 实例
- 语法:
子类.prototype = new 父类( )
优点:
- 父类的 构造函数体内的 属性 和 原型 上的 方法 都可以继承下来
缺点:
- 本该一个构造函数完成的所有参数, 需要分开两个地方传递
- 同样都是给 子类实例 使用的属性, 在两个位置传递参数
- 继承下来的属性不在自己身上, 子类的实例的所有属性分开了两部分书写
- 访问的属性需要去到
__proto__
上才能找到
// 父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
const p = new Person('Jack', 18)
/*
父类的实例 :
p === {
name: 'Jack',
age: 18,
__proto__(Person.prototype): {
sayHi: function () {},
constructor: Person,
__proto__(Object.prototype): {
__proto__: null
}
}
}
不管是谁继承了 Person 类
+ 要求继承的构造函数的实例, 可以使用 name 和 age 属性
+ 要求继承的构造函数的实例, 可以使用 sayHi 方法
*/
function Person(name, age) {
this.name = name
this.age = age
}
// 当 Person构造函数书写完毕以后
// 此时会伴生一个 Person.prototype 是一个对象数据类型
// 我就可以向 Person.prototype 上添加一个成员
// Person.prototype.a = 100
// 我也可可以把 Person.prototype 从新赋值
// 本身你的 Person.prototype 保存的是一个 对象数据类型 的地址
// 我给你赋值为一个新的 对象数据类型 的地址
// 就把你本身保存的地址覆盖了
const obj = {
message: '我是自定义的 obj 对象'
}
Person.prototype = obj
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
function Student(gender) {
this.gender = gender
}
const p = new Person('Jack', 18)
Student.prototype = p
// 父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 子类
function Student(gender) {
this.gender = gender
}
const p = new Person('Jack', 18)
// Student 的实例可以使用 Person 书写的属性和方法了
// 我们就说 Student 继承自 Person
// Student 是 Person 的 子类
// Person 是 Student 的 父类
Student.prototype = p
// s 是 Student 的实例
// s 所属的构造函数是 Student
// s.__proto__ 指向 所属构造函数的 Prototype
// 因为 Student.prototype === p
// s.__proto__ === p
const s = new Student('男')
console.log(s)
根据原型链的规则, 对象访问
当你需要访问 s 的 gender 成员的时候,s 自己本身就有, 直接使用
console.log(s.gender) // 自己的, 男
当你需要访问 s
的 name
成员的时候:
s
自己本身没有, 需要去到 自己的 __proto__
上查找,s
自己本身的 __proto__
是 p
, 所以就是在使用 p
身上的name
成员,p
是 Person
的实例,所以 Student
的实例使用的是 Person
构造函数体内书写的属性
当你需要访问 s
的 sayHi
成员的时候
s
自己本身没有, 需要去到 自己的 __proto__
上查找
因为自己的 __proto__
(就是p
) 上也没有, 再去到 p.__proto__
上查找,其实这时候就是去 Person.prototype
上查找的内容,此时 Student
的实例使用了 Person.protptype
上书写的 方法
总结:
// 因为这句代码的执行
// Student 的实例可以使用 Person 书写的属性和方法了
// 我们就说 Student 继承自 Person
// Student 是 Person 的 子类
// Person 是 Student 的 父类
Student.prototype = new Person('Jack', 18)
2. call 继承 : Person.call(this, name, age)
把父类构造函数体借用过来使用一下而已
function Student(name, age) {
Person.call(this, name, age)
}
call 继承 ( 借用构造函数继承 ) :
- 核心: 把父类构造函数体, 当做普通函数调用,
利用 call 方法调用父类构造函数 并 改变 this 指向 - 语法: 在子类构造函数体内书写
父类.call(this)
优点:
- 一个实例使用的属性可以在一个位置传递参数
- 可以把继承来的属性直接出现在 子类的实例 身上
缺点:
- 只能继承 父类构造函数体内 书写的内容, 父类构造函数 原型上 的内容不能继承(就是继承不了sayHi了)
// 父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 子类
function Student(gender, name, age) {
this.gender = gender
// 这里是 Student 的构造函数体内
// 这里的 this 是 Student 的每个实例对象
// 利用 call 调用 Person 构造函数, 把 Person 内的 this 修改为 Student 内的 this
// 把 Person 内的 this 修改为 Student 的每一个实例对象
// Person 构造函数体内书写成员就都添加到了 Student 的实例身上
Person.call(this, name, age)
}
Student.prototype.play = function () { console.log('你好 世界') }
const s = new Student('男', '张三', 20)
console.log(s)
从小例子开始分析:
A.call(B,x,y)
改变函数A的this指向,使之指向B;
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 既然 Person 可以当做普通函数调用
// call 方法就是用来调用函数, 并且改变 this 指向的
const obj = { message: '我是自定义的一个 obj 对象' }
// 在调用 Person 的时候
// 使用 call 方法, 改变Person的this指向,把 Person 内的 this 改成 obj
// Person 内书写的两个代码添加的成员, 加在 obj 身上了
Person.call(obj, 'Jack', 20)
console.log(obj)
obj身上就有name和age了
// 父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 子类
function Student(gender, name, age) {
this.gender = gender
}
Student.prototype.play = function () { console.log('你好 世界') }
const s = new Student('男', '张三', 20)
// 因为 call 方法调用 Person 的时候, 把 Person 内的 this 修改为 s 了
// 所以 name 和 age 成员添加到了 s 身上
Person.call(s, 'Rose', 20)
console.log(s)
// 父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 子类
function Student(gender, name, age) {
this.gender = gender
// 这里是 Student 的构造函数体内
// 这里的 this 是 Student 的每个实例对象
// 利用 call 调用 Person 构造函数, 把 Person 内的 this 修改为 Student 内的 this
// 把 Person 内的 this 修改为 Student 的每一个实例对象
// Person 构造函数体内书写成员就都添加到了 Student 的实例身上
Person.call(this, name, age)
}
Student.prototype.play = function () { console.log('你好 世界') }
const s = new Student('男', '张三', 20)
console.log(s)
const s2 = new Student('女', '李四', 22)
console.log(s2)
扩展:用apply写
3. 组合继承 :
就是把 原型继承 和 借用构造函数继承 两个方式组合在一起
function Student() {
Person.call(this)
}
Student.prototype = new Person('Jack',20)
- 核心: 把 原型继承 和 call 继承 合并在一起就做组合继承
- 优点:
=> 父类构造函数体内的属性和原型上的方法都可以继承
=> 继承下来的属性在子类实例对象自己的身上 - 缺点:
=> 原型上会多一套继承下来的属性名
原型继承 优缺点 :
call继承 优缺点:
两种方法的优缺点刚好互补
// 父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 子类
function Student(gender, name, age) {
this.gender = gender
// 实现 call 继承
// 目的: 为了把父类构造函数体内的内容放在子类实例自己身上
Person.call(this, name, age)
}
// 实现原型继承
// 目的: 为了把 Person 的 prototype 内的内容继承下来
Student.prototype = new Person('Jack',20)
Student.prototype.play = function () { console.log('你好 世界') }
// 创建子类的实例
const s = new Student('男', '张三', 20)
console.log(s)
Student.prototype = new Person()
可以不用传参数,因为Student自己有name,age,直接拿自己的了
// 父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 子类
function Student(gender, name, age) {
this.gender = gender
// 实现 call 继承
// 目的: 为了把父类构造函数体内的内容放在子类实例自己身上
Person.call(this, name, age)
}
// 实现原型继承
// 目的: 为了把 Person 的 prototype 内的内容继承下来
Student.prototype = new Person()
Student.prototype.play = function () { console.log('你好 世界') }
// 创建子类的实例
const s = new Student('男', '张三', 20)
console.log(s)
4. ES6 的继承语法
下面表示创造一个 Student 类,继承自 Person 类
class Student extends Person {
constructor (name, age) {
// 必须在 constructor 里面执行一下 super() 完成继承
super(name, age)
}
}
ES6 的继承方案 :
ES6 的继承语法:
- 书写子类的时候, 使用 extends 关键字
=> class 子类名 extends 父类 { ... }
- 在子类的 constructor 内书写
=> super( )
=> 必须要两个条件同时书写
=> 在 constructor 内书写 super 的时候, 必须写在所有 this 的 最前面
( 必须要先继承 : 你必须要继承传统 , 然后再创新 )
// ES6写法的父类
// class Person {
// constructor (name, age) {
// this.name = name
// this.age = age
// }
// sayHi () { console.log('hello world') }
// }
// ES5写法的父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// ES6 的类的继承
// 创建一个 继承自 Person 的 Student 类
// extends 关键字相当于原型继承
class Student extends Person {
constructor (gender, name, age) {
// 相当于在调用父类构造函数体, 把 name 和 age 传递过去
// 相当于 call 继承
super(name, age)
this.gender = gender
}
play () { console.log('你好 世界') }
}
const s = new Student('男', 'Jack', 20)
console.log(s)
总结:
原型继承方法代码
// 父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 子类
function Student(gender) {
this.gender = gender
}
// 原型继承方法核心代码
Student.prototype = new Person('Jack', 18)
const s = new Student('男')
console.log(s)
call继承方法代码
// 父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 子类
function Student(gender, name, age) {
this.gender = gender
// call 继承核心代码
Person.call(this, name, age)
}
Student.prototype.play = function () { console.log('你好 世界') }
const s = new Student('男', '张三', 20)
console.log(s)
组合继承方法代码
// 父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 子类
function Student(gender, name, age) {
this.gender = gender
// 实现 call 继承
// 目的: 为了把父类构造函数体内的内容放在子类实例自己身上
Person.call(this, name, age)
}
// 实现原型继承
// 目的: 为了把 Person 的 prototype 内的内容继承下来
Student.prototype = new Person()
Student.prototype.play = function () { console.log('你好 世界') }
// 创建子类的实例
const s = new Student('男', '张三', 20)
console.log(s)
ES6 的继承语法
// ES6写法的父类
// class Person {
// constructor (name, age) {
// this.name = name
// this.age = age
// }
// sayHi () { console.log('hello world') }
// }
// ES5写法的父类
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// ES6 的类的继承
// 创建一个 继承自 Person 的 Student 类
// extends 关键字相当于原型继承
class Student extends Person {
constructor (gender, name, age) {
// 相当于在调用父类构造函数体, 把 name 和 age 传递过去
// 相当于 call 继承
super(name, age)
this.gender = gender
}
play () { console.log('你好 世界') }
}
const s = new Student('男', 'Jack', 20)
console.log(s)
前端 JavaScript 之 继承 _ 原理版
JavaScript学习笔记(二十九)-- 闭包与继承