原型与继承
# 原型与原型链
# 函数
- 每一个函数都有一个 prototype 属性,指向它的原型对象
- 原型对象上有一个 constructor 属性,指回构造器函数
- 函数也是对象,所以函数也有隐式原型对象
__proto__
,指向该函数的构造器的原型对象
let fn = function () {}
console.log(fn.prototype) // Object{}
fn.prototype.contructor === fn // true
fn.__proto__ === Function.ptototype // true
2
3
4
# 隐式原型 & 显式原型
- 每个实例上都有个
__proto__
属性,该属性指向构造函数的原型对象,这个属性称为隐式原型
let Fn = function () {}
let fn = new Fn()
fn.__proto__ === Fn.prototype // true
2
3
# 注意
- 重写原型对象的问题
通过对象字面量的方式重写原型,会导致原型对象的 constructor 属性丢失
function Person() {}
Persion.prototype = {
constructor: Person, // 这样会导致constructor属性变为可枚举的属性,使用Object.defineProperty()方法定义
name: 'Person',
say() {},
}
let p = new Person()
p instanceof Person // true
p.__proto__.constructor === Person // false
2
3
4
5
6
7
8
9
- 原型的动态性
实例的[[Prototype]]
指针是在调用构造函数时自动赋值的,重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型
function Person() {}
// 实例化之后,__proto__指针指向的原型对象不变
let friend = new Person()
// 实例化之后又重写原型对象,修改的只是Person.ptototype指针的指向,而不是原型对象本身
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
// 所以friend的原型对象仍然是之前的原型
friend.sayName() // 错误
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 继承
# 原型链继承常用
function Father() {
this.name = 'father'
// 引用类型变量,多个子类实例之间将共享这个变量,一个修改,其他都会被改变
this.friends = ['tom']
}
Father.prototype.sayName = function () {
console.log(this.name)
}
function Son() {
this.name = 'son'
}
// 子类的原型对象指向父类实例
Son.prototype = new Father()
let s1 = new Son()
let s2 = new Son()
s1.friends.push('lili')
s1.sayName() // 'son'
console.log(s1.friends) // ['tom', 'lili']
console.log(s2.friends) // ['tom', 'lili']
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 存在的问题
- 子类在实例化的时候无法给父类构造函数传参
- 引用类型的变量会被多个子类实例共享
# 盗用构造函数继承
function Father(name) {
this.name = name
this.friends = ['tom']
}
Father.prototype.sayName = function () {
console.log(this.name)
}
function Son(age, name) {
this.age = age
// 解决了向父类构造函数传参的问题
Father.call(this, name)
}
let s1 = new Son(10, 'a')
let s2 = new Son(11, 'b')
s1.friends.push('lili')
console.log(s1.friends) // ['tom','lili']
// 解决了引用类型变量共享的问题
console.log(s2.friends) // ['tom']
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 存在的问题
- 访问不了父类原型链上的方法,所以所有属性必须在父类构造函数中声明
- 方法必须定义在父类构造函数中,因此无法复用
# 组合继承
组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式
function Father(name) {
this.name = name
}
Father.prototype.sayName = function () {
console.log(this.name)
}
function Son(name, age) {
this.age = age
// 第二次调用父类构造函数
Father.call(this, name)
}
// 第一次调用父类构造函数
Son.prototype = new Father()
let s1 = new Son('a', 10)
s1.sayName() // a
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 存在的问题
- 多次调用父类构造函数
第一次调用,会在原型对象上存在 name 属性,第二次调用会在实例上创建 name 属性,然后实例上的 name 遮蔽了原型上的,其实是创建了两遍。有性能损耗。
# 原型式继承
原型式继承适用于这种情况:你有一个对象,想在它的基础上再创建一个新对象。 你需要把这个对象先传给 object(),然后再对返回的对象进行适当修改。 原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住, 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
function object(o) {
function F() {}
F.prototype = o
return new F()
}
2
3
4
5
ECMAScript 5 通过增加 Object.create()
方法将原型式继承的概念规范化了
// Object.create(proto, property)
// proto - 作为新对象原型的对象
// property - 给新对象定义额外属性的对象(可选)
let person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van'],
}
let anotherPerson = Object.create(person, {
name: { value: 'Greg' },
})
console.log(anotherPerson.name) // "Greg"
2
3
4
5
6
7
8
9
10
11
12
另外,在 Vue.extend (opens new window) 方法的实现中,就是使用了这种方式实现子组件构造器对 Vue
构造器的继承
# 存在的问题
- 引用类型的值在多个实例之间共享的问题
# 寄生继承
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。
function createAnother(original) {
let clone = object(original)
// 扩展方法
clone.sayHi = function () {
console.log('hi')
}
return clone // 返回这个对象
}
let person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van'],
}
let anotherPerson = createAnother(person)
anotherPerson.sayHi() // "hi"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 存在的问题
- 通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
# 组合寄生继承 完美
寄生式组合继承可以算是引用类型继承的最佳模式。
// 不会调用父类的构造函数
function inheritPrototype(Sub, Super) {
// 寄生
let proto = Object.create(Super.prototype)
proto.constructor = Sub
Sub.prototype = proto
}
function Father(age) {
this.name = 'Father'
this.age = age
}
Father.prototype.sayAge = function () {
console.log(this.age)
}
function Son(age) {
this.name = 'Son'
Father.call(this, age)
}
// inheritPrototype(Son, Father)
Son.prototype = Object.create(Father.prototype, {
constructor: {
value: Son,
enumerable: false,
writable: true,
configurable: true,
},
})
let s = new Son(10)
s.sayAge() // 10
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 类(class)
# 定义
// 类声明
class Person {}
// 类表达式
let Person = class {}
2
3
4
# 实例化
# new 操作符
使用 new 调用类的构造函数会执行如下操作: (1) 在内存中创建一个新对象。 (2) 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。 (3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。 (4) 执行构造函数内部的代码(给新对象添加属性)。 (5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
# 构造函数
默认情况下,类构造函数会在执行之后返回 this 对象。如果返回的不是 this 对 象,而是其他对象,那么这个对象不会通过 instanceof 操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改
class Person {
constructor() {
this.name = 'persion'
if (override) {
return {
name: 'override',
}
}
}
}
let p = new Person(true)
p.name // 'override'
p instanceof Persion // false
2
3
4
5
6
7
8
9
10
11
12
13
14
# 原型属性、静态属性、实例属性
class A {
NAME = 'a' // 定义在实例上
static NAME = 'A' // 定义在类上
constructor() {
this.age = 20 // 定义在实例上
}
get name() {
// this指向实例
return this.NAME
}
static get name() {
// this指向类A本身
return this.NAME
}
// 定义在原型上
getName() {
// this指向类的实例
return this.name
}
}
let a = new A()
a.name // 'a'
A.name // 'A'
A.NAME // 'A'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# extends
- 可以继承类也可以继承构造函数
class A {}
function B() {}
class C extends A {}
class D extends B {}
2
3
4
5
- 派生类都会通过原型链访问到类和原型上定义的方法
class Father {
static getAge() {
return 40
}
}
class Son extends Father() {}
console.log(Son.getAge()) // 40
2
3
4
5
6
7
# extends 的实现原理
首先我们声明两个类
class A {}
class B extends A {}
2
然后通过 babel 进行编译得到以下结果(省略部分代码)
var A = function A() {
_classCallCheck(this, A)
}
var B = (function (_A) {
_inherits(B, _A)
var _super = _createSuper(B)
function B() {
_classCallCheck(this, B)
// 调用父类构造器
return _super.apply(this, arguments)
}
return B
})(A)
// 继承的核心
function _inherits(subClass, superClass) {
if (typeof superClass !== 'function' && superClass !== null) {
throw new TypeError('Super expression must either be null or a function')
}
// 原型式
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true,
},
})
// 让子类继承父类的静态成员
if (superClass) _setPrototypeOf(subClass, superClass)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# super
super 关键字用于访问和调用一个对象的父对象上的函数 ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。super 始终会定义为[[HomeObject]]的原型。
- super 只能在派生类构造函数/实例方法/静态方法中使用
- 如果子类中有自定义的 constructor 函数,则在子类构造函数中访问 this 之前,必须首先调用 super
- 不能使用 delete 操作符 (opens new window) 加 super.prop 或者 super[expr] 去删除父类的属性
- 不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法
- 如果派生类中没有定义类构造函数,在实例化派生类时会调用 super() ,而且会传入所有传给派生类的 参数。
- 如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回 一个对象。
class Person {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}
class Man extends Person {
constructor(name) {
// 1、构造方法
super(name) // 相当于调用super.constructor(name)
this.age = 10
// 单独引用super会报错
console.log(super)
}
// 2、实例方法
sayName() {
return super.sayName()
}
// 3、静态方法
static getSuperName() {
return super.name // Person
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 抽象基类 - new.target
new.target 保存通过 new 关键字调 用的类或函数。如果一个函数是通过 new 调用的,则返回构造器;否则返回 undefined
- 抽象类不能被实例化
// 抽象基类
class Vehicle {
constructor() {
console.log(new.target)
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated')
}
}
}
// 派生类
class Bus extends Vehicle {}
new Bus() // class Bus {}
new Vehicle() // class Vehicle {}
// Error: Vehicle cannot be directly instantiated
2
3
4
5
6
7
8
9
10
11
12
13
14
- 要求派生类必须定义某个方法
因为原型方法在 调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法
// 抽象基类
class Vehicle {
constructor() {
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated')
}
if (!this.foo) {
throw new Error('Inheriting class must define foo()')
}
console.log('success!')
}
}
// 派生类
class Bus extends Vehicle {
foo() {}
}
// 派生类
class Van extends Vehicle {}
new Bus() // success!
new Van() // Error: Inheriting class must define foo()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25