《JavaScript高级程序设计(第三版)》学习笔记
笔记目录:
JavaScript学习(1) – JavaScript历史回顾
关键词:实现继承、原型链的原理与本质、原型继承、组合继承、寄生继承、寄生组合式继承
本章节学习路线:
- 通过原型链实现继承
- 原型链本身存在的问题:同原型对象一样,引用类型作为共享属性会导致互相干扰
- 解决原型链问题的方法:借用构造函数
1. 实现继承
1.1 JavaScript只支持实现继承
由于JavaScript中不具备类似Java中的interface这种方法,所以其继承的方法仅提供实现继承,具体是通过原型链来实现的
1.2 补充知识:实现继承与引用继承(Java说明)
- 实现继承:继承实际的方法
- 引用继承:继承方法和签名
为更清楚的解释这两个概念,下面通过Java来具体说明:
1.2.1 实现继承:继承实际的方法
// 1. 实现继承 - 继承实际的方法
// 定义一个bird类,其中包含公共方法fly()
public class bird {
public void fly()
}
// 通过extends使eagle类继承实现bird类,并重写fly()
// 注意:extends后只能继承一个类
public class eagle extends bird {
@override
public void fly()
}
复制代码
1.2.2 引用继承:继承方法和签名
// 2. 引用继承 - 继承方法和签名
// 定义接口1
public interface ServeBall {
public void ServeBalls();
}
// 定义接口2
public interface GiveBirth {
public void HaveBaby();
}
// 接口继承方法1:通过implements使新建的类实现上述两个接口,并对其中方法进行重写(继承多个接口)
public class WomenBasketballPlayer implements ServeBall,GiveBirth {
@Override
public void HaveBaby() {
// TODO Auto-generated method stub
}
@Override
public void ServeBalls() {
// TODO Auto-generated method stub
}
}
// 接口继承方法2:接口继承接口:
public interface GiveBirth extends ServeBall{
public void HaveBaby();
}
复制代码
1.2.3 额外补充知识:Java中的方法签名
Definition:
Two of the components of a method declaration comprise the method signature—the method’s name and the parameter types.
-
方法签名的组成:方法名称+参数类型;例如:calculate(value1, value2)
-
意义:用于重载 – 方法签名是可以唯一的确定一个method,与method的返回值一点关系都没有,这是判断重载重要依据
// 不允许的写法:方法签名完全相同,无法区分进行重载
public long calculate() {}
public int calculate() {}
// 允许的写法:方法签名不同
public long calculate(value1, value2, value3) {}
public long calculate2(value1, value2, value3)
public long calculate2(value1, value2) {}
复制代码
-
备注:如前文(基本概念)中提过,javascript中并不支持重载,因为后定义的函数会覆盖之前名称相同的函数
-
参考文档:
1.3 JavaScript不支持重载
后定义的函数会覆盖之前名称相同的函数。而并不能有java的重载的效果
function foo(num) {
return num + 100
}
function foo(num) {
return num + 200
}
var result = foo(100) //300
复制代码
2. 原型链
2.1 基本概念
- 原型链的本质:将父类属性继承到子类的prototype中,然后层层嵌套继承
- 一个对象,继承父类的属性时,会把父类的属性(包括实例属性和原型属性)放在自己的prototype中;
- 父类继承上一层(例如:Object)的属性时,会把上一层的属性放在父类自己的prototype中;
- 子类可以通过层层嵌套的原型链,向上溯源,直接使用父类或者Object的属性
- 继承注意事项: 不要使用对象字面量方法重写prototype,否则将切断继承关系
2.2 通过代码说明继承
代码目标:定义父类SuperType,定义子类SubType并继承父类的实例属性和原型属性
// 1. 定义一个父类型 SuperType
// 1.1 创建SuperType的构造函数,其中包含属性值property: true
function SuperType() {
this.property = true
}
// 1.2 设置SuperType的原型对象,添加方法属性getSuperValue()
SuperType.prototype.getSuperValue = function () {
return this.property
}
// 2. 定义一个子类型 SubType - 创建SubType的构造函数,其中包含属性值subProperty: false
function SubType() {
this.subProperty: false
}
// 3. 继承:使子类SubType继承父类SuperType的方法和原型(通过构造函数SuperType)
SubType.prototype = new SuperType()
// 4. 为子类SubType的原型对象添加方法getSubValue()
/* 注意:
* 添加方法尽量避免使用对象字面量方法重写
* 原因:再次重写整个prototype,使其仅包含一个object的实例,所以再也无法调用SuperType中的属性和方法
* 导致结果:会切断SubType和SuperType之间的联系
*/
SubType.prototype.getSubValue = function () {
return this.subProperty
}
// 3. 创建SubType的实例,该实例继承了SuperType的方法和属性,所以该实例可以直接使用父类的方法
var subInstance = new SubType()
alert(subInstance.getSuperValue()) // true - 子类实例可以使用父类的方法获得父类属性
alert(subInstance.getSubValue()) // false - 子类实例可以使用自身的方法获得自身的属性值
// 注意:本质上,SuperType作为引用类型,也继承了Object,所以Object的属性和方法存在于SuperTye的原型对象中。从而构成原型链。
复制代码
图片说明:

2.3 判断原型与实例的关系:instanceof和isPrototypeOf()
// 方法1: instanceof
alert(subInstance instanceof Object) // true
alert(subInstance instanceof SubType) // true
alert(subInstance instanceof SuperType) // true
// 方法2:isPrototypeOf()
alert(Object.prototype.isPrototypeOf(subInstance)) // true
alert(SubType.prototype.isPrototypeOf(subInstance)) // true
alert(SuperType.prototype.isPrototypeOf(subInstance)) // true
复制代码
2.4 原型链的问题 – 引用类型放在原型属性中引发的问题
问题的本质:引用类型存在于prototype中,因为prototype具有共享特性,导致的问题
参考:JavaScript学习(3) – 聊聊原型链- 2. 对象与原型 – 2.3.6节:成也原型,败也原型
- 包含引用类型的值的原型 – 父类实例属性中如果包含一个引用类型值,则通过继承后,会变成子类的原型属性
- 无法向父类构造函数传参 – 创建子类时,无法在不影响所有对象实例的情况下,向父类的构造函数中传参
结论:很少单独使用原型链(解决方法:借用构造函数)
// 创建父类,其中包含实例属性colors
function SuperType () {
this.colors = ['red', 'green', 'blue']
}
// 创建子类
function SubType() {}
// 子类继承父类 - 父类中的实例属性colors会变成原型属性,成为共享属性
SubType.prototype = new SuperType()
var instance1 = new SubType()
var instance2 = new SuperType()
// 由于引用对象是原型属性,所以对子类继承到的引用对象进行改变,会影响到父类自身的实例属性
instance1.colors.push('black')
alert(instance1.colors) // 'red, green, blue, black'
alert(instance2.colors) // 'red, green, blue, black'
复制代码
3. 借用构造函数 constructor stealing
- 基本思想:在子类构造函数的内部调用父类构造函数完成继承 – 用
call()或apply()实现 - 优点:可以向父类构造函数传参
- 缺点:function都在构造函数中定义,因此缺乏函数复用性(构造函数的问题)- 解决方案:组合继承
// 创建父类,父类构造函数需传参
function SuperType (name) {
this.name = name
}
// 创建子类(注意:先继承,再添加或修改属性)
function SubType () {
// 继承SuperType,同时进行传参
SuperType.call(this, 'Nicholas')
// 创建自身的实例属性
this.age = 29
}
var instance = new SubType()
alert(instance.name) // 'Nicholas'
alert(instance.age) // 29
复制代码
4. 组合继承 combination inheritance
- 基本思想:使用原型链实现对原型属性和方法的集成,通过借用构造函数来实现对实例属性的集成,从而保证原型中的方法可以复用,又能保证每个实例有自己单独的属性
- 缺点: 调用两次父类构造函数
- 解决方案:寄生组合式继承(但要了解该方法,需要先学习原型继承和寄生式继承)
// 创建父类,包含实例属性和原型方法
function SuperType(name) {
this.name = name
this.colors = ['red', 'green', 'blue']
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
// 创建子类
function SubType (name, age) {
// 继承实例属性(第一次调用父类构造函数)
SuperType(this, name)
// 创建子类实例属性
this.age = age
}
// 子类继承父类原型方法
// 1. 继承父类(第二次调用父类构造函数)
SubType.prototype = new SuperType()
// 2. 将继承后子类原型中的constructor重写,指回SubType本身的constructor,而不是让其默认指向父类SuperType的constructor
SubType.prototype.constructor = SubType
// 3. 给继承后的原型添加方法sayAge
SubType.prototype.sayAge = function () {
alert(this.age)
}
// 创建一个子类型的实例instance1,其具备独立的实例属性:name、age、colors,其中name和colors继承于父类
var instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
alert(instance1.colors) // 'red, green, blue, black'
instance1.sayName() // Nicholas
instance1.sayAge() // 29
// 创建另一个子类型实例,其他同类实例的改变不会对该实例造成影响,各个实例具备独立的实例属性
var instance2 = new SubType('Cindy', 27)
alert(instance2.colors) // 'red, green, blue'
instance2.sayName() // Cindy
instance2.sayAge() // 27
复制代码
5. 原型继承 Prototypal Inheritance
- 基本思想:浅拷贝原型及属性
- 缺点:该方法同样存在引用类型的问题,故不会直接使用,但是给了解决上面组合继承的问题的一个思路
// 将对象进行浅拷贝的方法:
function object(o) {
function F () // 1. 创建临时构造函数
F.prototype = o // 2. 将传入的对象o作为这个临时构造函数的原型
return new F() // 3. 返回该临时类型的实例
}
// 使用样例:
// 1. 创建一个普通对象,其中存在引用类型属性
var obj = {
name: 'showColor',
colors: ['red', 'green', 'blue']
}
// 2. 使用object()方法对obj对象进行浅拷贝 - obj对象作为新的实例的原型对象,故obj中原有属性现在均为原型属性进行共享
var obj2 = object(person)
obj2.name = 'anotherColor'
obj2.colors.push('black') // colors现在属于原型对象,所以colors现在是共享状态的,任何改变都会对原本的对象进行改变
alert(obj.colors) // 'red, green, blue, black'
复制代码
- 补充知识:ES5提供了
Object.create()方法,可以传入两个参数
/*
* Object.create(arg1, arg2)
* 参数1:用作新对象原型的对象
* 参数2:为新对象定义定义额外属性的对象(可重写覆盖原有同名属性)
*/
// 1. 创建一个普通对象
var obj = {
name: 'showColor',
colors: ['red', 'green', 'blue']
}
// 2. 只传第一个参数:使用官方的Object.create()方法对obj对象进行浅拷贝,此时obj的内容存在于obj2.prototype中
var obj2 = Object.create(obj)
obj2.name = 'anotherColor' // 为obj2添加实例属性name
obj2.colors.push('black') // 为obj2添加属性,因colors存在于prototype中,且未引用类型(Array),所以是对引用对象修改
/* obj2 - 输出结果:
{
name: 'anotherColor',
_proto_: {
name: 'showColor',
colors: ["red", "green", "blue", "black"]
}
}
*/
console.log(obj1.colors) // ["red", "green", "blue", "black"]
// 3. 传两个参数,对同名属性进行覆盖重写
var obj3 = Object.create(person, {
name: {
value: 'color2'
}
})
alert(obj3.name) // color2
复制代码
6. 寄生式继承 Parasitic Inheritance
- 基本思路:创建一个仅用于封装继承过程的函数,该函数内部以某种方式增强对象,然后按照工厂模式的思路,返回整个对象
- 本质:寄生构造函数 + 工场模式 + 原型继承
- 缺点:实例属性中增强的函数无法复用,问题类似构造函数模式
- 解决方案:寄生组合式继承
// 寄生继承函数,可将对象进行增强
function createAnother (original) {
var clone = Object.create(original) // 1. 浅拷贝传入的对象
clone.sayHi = function () { alert('Hi') } // 2. 增强对象(例如:写入其他方法,放入实例属性中)
return clone // 3. 返回这个对象
}
// 创建对象
var obj = {
name: 'showColor',
colors: ['red', 'green', 'blue']
}
// 寄生继承上面的对象,新对象可以使用增强的内容
var obj2 = createAnother(obj)
obj2.sayHi() // hi
复制代码
7. 寄生组合式继承 – 最佳解决方案
7.1 简单总结上面几种继承方法的问题
| 继承方法 | 基本思路 | 问题 |
|---|---|---|
| 直接使用原型链 | 将父类对象直接继承到子类prototype中 | 1. 父类实例属性中的引用对象会被继承到子类原型对象中,对子类该属性的修改会影响父类的该实例属性 2. 无法向父类构造函数传参 解决方案:借用构造函数 |
| 借用构造函数 | 1. 将父类的属性继承到构造函数中作为子类的实例属性 2. 可以对父类构造函数传参 |
无法将需要共享的属性(例如function)继承到prototype中,缺乏函数复用性 解决方案:组合继承 |
| 组合继承 | 1. 构造函数部分继承父类的实例属性 2. 原型部分继承父类的原型属性 |
调用两次父类构造函数: 1. 创建子类构造函数时,使用第一次父类构造函数 2. 继承父类到子类prototype中 解决方案:寄生组合式继承(但须先了解原型继承+寄生继承) |
| 原型继承 | 浅拷贝原型属性到子类prototype中 | 存在同直接使用原型链一样的问题 |
| 寄生式继承 | 原型模式 + 寄生构造函数: 1. 浅拷贝父类对象到子类prototype中 2. 对拷贝后的副本对象写入实例属性 |
实例属性中增强的函数无法复用,问题类似构造函数模式 |
| 寄生组合式继承 | 组合继承 + 寄生继承 使用寄生式继承来继承父类原型,再将结果指定给子类原型 |
7.2 寄生组合式继承方法
- 基本思路:就是使用寄生式继承来继承父类原型,再将结果指定给子类原型
- 本质:组合继承 + 寄生继承
- 优点:仅调用一次父类构造函数、可以传参,使用寄生继承的方式将父类prototype指定给子类prototype
- 继承操作逻辑:
- 创建父类,包含实例属性和原型属性
- 创建子类构造函数,调用父类构造函数将父类的实例属性写入子类实例属性,并可添加自身实例属性
- 寄生继承父类原型对象:
- 浅拷贝父类prototype
- 增强浅拷贝副本,用子类构造函数覆盖副本的constructor(避免父类实例属性存在于子类原型中)
- 将增强后的父类prototype副本存入子类prototype
- 为子类prototype添加自身原型属性
// 寄生组合继承方法:
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype) // 1. 创建父类prototype的副本对象
prototype.constructor = subType // 2. 增强副本对象,将子类构造函数添加覆盖
subType.prototype = prototype // 3. 指定子类对象的原型为上面创建的副本对象
}
// 使用上述方法进行继承:
// 1. 创建父类,并添加原型方法sayName()
function SuperType(name) {
this.name = name
this.colors = ['red', 'green', 'blue']
}
SuperType.prototype.sayName = function() {
alert(this.name)
}
// 2. 创建子类 - 通过寄生继承的方式,继承原型对象
function SubType(name, age) {
// 调用一次父类构造函数,传入父类的实例属性
SuperType.call(this, name)
this.age = age
}
// 使用寄生继承的方式,通过浅拷贝父类的原型对象,直接将副本指定给子类的原型对象,避免在SubType.prototype上创建不必要的属性
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function () {
alert(this.age)
}
复制代码
8. 小结
- JavaScript:仅支持实现继承,且不支持重载(可用Java继承思想来理解)
- 原型链:父类被继承到子类的prototype中,层层溯源,直接使用
- 原型链的问题:引用对象放在protoype的共享问题
- 几种继承的方法:(基本思路 + 优缺点 + 解决方案)
- 直接使用原型链
- 借用构造函数
- 组合继承
- 原型继承
- 寄生式继承(寄生的本质:增强 – 在原有基础上添加自定义新属性)
- 寄生组合式继承(最佳)
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END





![在画布上绘图[入门canvas教程]-一一网](https://www.proyy.com/skycj/data/images/2021-05-17/c4a95a1c2898fef10457d10679b68685.jpg)
















![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)