了解JavaScript对象
为了理解JavaScript的继承,我们首先要了解一下JS对象,主要了解JS对象的创建方式。
创建对象的方式
工厂模式
- 是什么?
工厂模式是一种软件领域应用广泛的设计模式,是一个抽象
创建特定对象的过程。 - 解决了什么问题?
- 解决创建多个类似对象的问题。
- 具体案例:
// 1. 创建特定对象(人)的过程
function createPerson(name,sex,age) {
let person = {}
person.name = name
person.sex = sex
person.age = age
person.sayName = function() {
console.log(this.name)
}
return person
}
// 2. 解决的问题:创建多个类似对象
let person1 = createPerson('小明','男','18')
let person2 = createPerson('小花','女','21')
// 3. 存在的问题,返回的都是Object类型
console.log(person1.constructor) // ƒ Object() { [native code] }
console.log(person2.constructor) // ƒ Object() { [native code] }
- 存在的问题?
- 它没有解决对象标识的问题,即新创建的对象是模式类型。这里的对象标识说的是实例的
constructor,这里总是返回Object类型,不够明确类型。 - 每创建一个对象的,都会进行一次对象方法
sayName的创建。
构造函数模式
- 是什么?
构造函数也是用于创建
特定对象的过程,像Objecr、Array就是原生构造函数,用于创建对象和数组。 - 解决的问题?
- 解决创建多个类似对象的问题
- 解决工厂模式中对象标识的问题
- 具体案例:
// 1. 创建特定对象(人)的过程
function Person(name,sex,age) {
this.name = name
this.sex = sex
this.age = age
this.sayName = function() {
console.log(this.name)
}
}
// 2. 解决的问题:创建多个类似对象,明确对象标识
let person1 = new Person('小明','男','18') // 这里可以思考下 new 的过程
let person2 = new Person('小花','女','21')
console.log(person1.constructor) // 确定对象标识,用 instanceof 会更好一点
console.log(person2.constructor)
// 2. 解决的问题:明确对象标识 —— Person
ƒ Person(name,sex,age) {
this.name = name
this.sex = sex
this.age = age
this.sayName = function() {
console.log(this.name)
}
}
- 存在的问题?
- 每创建一个对象的,都会进行一次对象方法
sayName的创建。因为都是做同一件事,所以没有必要创建多次 - 临时解决方案:将
sayName的创建放在外部,在内部提供一个引用。这种方案存在的问题,创建了全局变量,不好!!
let sayName = function() {
console.log(this.name)
}
function Person(name,sex,age) {
this.name = name
this.sex = sex
this.age = age
this.sayName = sayName
}
- 与工厂模式的区别?
其实没什么区别,构造函数的内层实现跟工厂模式是一样的,只是在内层实现通过
this来解决绑定对象实例,通过new来解决对象标识问题。
原型模式
- 是什么?
每个函数创建的时候都有一个
prototype属性,这是一个对象,可以包含特定实例的共享属性和方法 - 解决的问题?
- 只需要创建一次属性和方法
- 具体案例:
function Person() {}
// 只需要创建一次
Person.prototype.name = '小花'
Person.prototype.sayName = function() {
console.log(this.name)
}
let person1 = new Person()
let person2 = new Person()
console.log(person1.sayName === person2.sayName) // true
- 存在的问题:原型属性容易被实例更改,影响到其他实例
function Person() {}
// 只需要创建一次
Person.prototype.friends = [1,2,3]
let person1 = new Person()
let person2 = new Person()
console.log(person1.friends) // [1,2,3]
console.log(person2.friends) // [1,2,3]
// 数据被更改,影响其他实例
person1.friends.push(4)
console.log(person1.friends) // [1,2,3,4]
console.log(person2.friends) // [1,2,3,4]
继承
在了解完创建对象的几种方式后,我们来了解一下JavaScript中的继承。
很多面向对象的语言支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实际方法。接口继承在JavaScript中是不可能的,因为,函数没有签名,只能使用实现继承。在JavaScript中实现继承主要是通过原型链实现的
原型链
了解一下构造函数、原型和实例之间的关系:每一个构造函数都有一个prototype属性(是一个对象),原型对象有一个默认constructor属性,指向的是这个构造函数;实例内部有一个指针直接指向这个constructor,通过实例的constructor属性可以直接访问到这个构造函数,通过构造函数的prototype可以访问到原型对象。
原型对象可以定义一些实例共享的属性和方法
如果这个构造函数的prototype这个对象是另外一个构造函数的实例,这样就意味着另一个构造函数的prototype也有可能是另外一个构造函数的实例,这在构造函数的原型上就形成了一个引用链,这就成为原型链。原型链的定层是Object.prototype.proto: null
- 具体案例:
// 父类
function Parent(name) {
this.name = name
}
Parent.prototype.sayName = function() {
console.log(this.name)
}
// 子类
function Son() {}
// 重写子类的原型
Son.prototype = new Parent('小花')
let son = new Son()
son.sayName() // 小花
console.log(son.constructor) // Parent
- 解决的问题 如何继承其他引用类型的属性和方法。
- 存在的问题:
- 我们通常把属性定义在构造函数中,方法定义在原型上,当使用原型链实现继承的时候,就意味着原来的实例属性可能会变成原型属性,即变成了引用属性,这也是原型模式存在的问题。
- 无法在子类实例化的时候向父类传参
// 父类
function Parent() {
this.friends = [1,2,3]
}
// 子类
function Son() {}
// 重写子类的原型
Son.prototype = new Parent()
let son1 = new Son() // 无法向父类传参
let son2 = new Son()
console.log(son1.friends) // [1,2,3]
console.log(son2.friends) // [1,2,3]
son1.friends.push(4)
console.log(son1.friends) // [1,2,3,4]
console.log(son2.friends) // [1,2,3,4]
盗用构造函数模式
- 具体案例 基本思路:在子类构造函数中调用一次父类
// 父类
function Parent(name) {
this.name = name
this.friends = [1,2,3]
}
// 子类
function Son(name) {
Parent.call(this,name) // 可以向父类传参
}
let son1 = new Son('小明') // 无法向父类传参
let son2 = new Son('小花')
console.log(son1.friends,son1.name) // [1,2,3] 小明
console.log(son2.friends,son2.name) // [1,2,3] 小花
son1.friends.push(4)
console.log(son1.friends) // [1,2,3,4]
console.log(son2.friends) // [1,2,3]
- 解决的问题
- 子类可以向父类构造函数传参
- 保证子类单独维护一份父类的属性副本(friends)
- 存在的问题:
- 方法必须定义在父类的构造函数中
- 子类不能访问父类原型上的属性和方法
组合继承
思路:综合原型链模式和盗用构造函数的优点,使用原型链模式继承父类原型上的属性和方法,通过盗用构造函数模式继承实例属性。
- 具体案例
// 父类
function Parent(name) {
this.name = name
}
Parent.prototype.sayName = function() {
console.log(this.name)
}
// 子类
function Son(name) {
Parent.call(this,name) // 继承实例属性,保存一份副本
}
Son.prototype = new Parent() // 继承原型属性和方法
let son = new Son('小花')
son.sayName() // 小花
- 解决的问题
- 原型链模式继承父类原型上的属性和方法
- 盗用构造函数模式继承实例属性,保留各自的副本
- 存在的问题 对父类构造函数进行了两次调用
// 父类
function Parent(name) {
this.name = name
}
Parent.prototype.sayName = function() {
console.log(this.name)
}
// 子类
function Son(name) {
Parent.call(this,name) // 第一次调用
}
Son.prototype = new Parent() // 第二次调用
let son = new Son('小花')
son.sayName() // 小花
原型继承
不自定义类型,通过原型实现对象之间的信息共享 实现方案:
function object(obj) {
function f() {}
f.prototype = o;
return new f()
}
- 具体实现 这种模式适用于已经存在一个对象,你想在这个对象的基础上在创建一个对象。
function object(obj) {
function f() {}
f.prototype = o;
return new f()
}
// 已有对象
let student = {
name: '小明'
}
// 增强对象
let goodStudent = object(student) // 继承学生的名字
goodStudent.score = 90 // 增强分数属性 (外部增强-与寄生继承对比)
这种模式就是ES5中的Object.create(obj,des)
寄生式继承
思路: 原型继承+工厂模式
- 具体实现
// 原型继承
function object(obj) {
function f() {}
f.prototype = obj;
return new f()
}
// 工厂模式
function createAnother(o) {
let temp = object(o)
temp.sayName = function() { // 在内部增强
console.log(this.name)
}
return temp
}
let o = {
name: '小明'
}
let newO = createAnother(o)
console.log(newO.name) // 小明
寄生式组合继承
思路:不通过调用父类构造函数定义子类原型,而是取得父类原型的一个副本,通过寄生继承来增强这个副本,然后再将这个副本赋值给子类原型。
- 具体实现
function object(obj) {
function f() {}
f.prototype = obj;
return new f()
}
function init(Person,Son) {
let prototype = object(Person.prototype) // 取得父类原型
prototype.constructor = Son // 更子类构造函数
Son.prototype = prototype // 更改子类原型
}
- 具体案例
function object(obj) {
function f() {}
f.prototype = obj;
return new f()
}
function init(Person,Son) {
let prototype = object(Person.prototype) // 取得父类原型
prototype.constructor = Son // 更改子类构造函数
Son.prototype = prototype // 更改子类原型
}
// 父类
function Parent(name) {
this.name = name
}
Parent.prototype.sayName = function() { // 原型链继承
console.log(this.name)
}
// 子类
function Son(name,age) { // 盗用构造函数
Parent.call(this,name)
this.age = age
}
// 寄生式组合继承
init(Parent,Son)
let son = new Son('小明', 18)
console.log(son.name)
son.sayName() // 小明
- 解决的问题
- 解决了组合继承中调用两次父类构造函数的问题
- 原型链仍然保持不变
总结
个人总结继承主要通过两个思路实现:
- 通过原型链进行引用继承,主要实现有:原型链继承、盗用构造函数继承、组合继承
- 通过增强一个现有对象实现继承,这个现有对象可能是一个构造函数
prototype,也可以是一个单独的对象。主要实现有:原型继承(Object.create)、寄生式继承(内部增强对象)、寄生式组合继承(盗用构造函数、寄生继承)
以上如有不对,请批评指正!