原型与继承

JS


# 原型与原型链

# 函数

  • 每一个函数都有一个 prototype 属性,指向它的原型对象
  • 原型对象上有一个 constructor 属性,指回构造器函数
  • 函数也是对象,所以函数也有隐式原型对象 __proto__,指向该函数的构造器的原型对象
let fn = function () {}
console.log(fn.prototype) // Object{}
fn.prototype.contructor === fn // true
fn.__proto__ === Function.ptototype // true
1
2
3
4

# 隐式原型 & 显式原型

  • 每个实例上都有个__proto__属性,该属性指向构造函数的原型对象,这个属性称为隐式原型
let Fn = function () {}
let fn = new Fn()
fn.__proto__ === Fn.prototype // true
1
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
1
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() // 错误
1
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']
1
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']
1
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
1
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()
}
1
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"
1
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"
1
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
1
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 {}
1
2
3
4

# 实例化

# new 操作符

使用 new 调用类的构造函数会执行如下操作: (1) 在内存中创建一个新对象。 (2) 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。 (3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。 (4) 执行构造函数内部的代码(给新对象添加属性)。 (5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

手写实现 new

# 构造函数

默认情况下,类构造函数会在执行之后返回 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
1
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'
1
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 {}
1
2
3
4
5
  • 派生类都会通过原型链访问到类和原型上定义的方法
class Father {
  static getAge() {
    return 40
  }
}
class Son extends Father() {}
console.log(Son.getAge()) // 40
1
2
3
4
5
6
7

# extends 的实现原理

首先我们声明两个类

class A {}
class B extends A {}
1
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)
}
1
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
  }
}
1
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
1
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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Last Updated: 10/21/2024, 4:15:17 PM