原型、继承

原型

在规范里,prototype 被定义为:给其它对象提供共享属性的对象。

JS是通过函数来模拟类,当我们创建一个函数的时候,js会给这个函数自动添加prototype属性,值是一个包含constructor属性的对象,不是空对象。当我们把函数当成构造函数通过new关键词调用的时候,js会帮我们创建该构造函数的实例,实例继承构造函数prototype的所以属性和方法(实例通过设置自己的__proto__指向构造函数的prototype来实现继承)

constructor

constructor属性也是对象所独有的,它是一个对象指向一个函数,这个函数就是该对象的构造函数。

所有 object 对象都有一个隐式引用

什么叫隐式引用?
所谓的隐式,是指不是由开发者(你和我)亲自创建/操作。__proto__

原型链

原型链的概念,仅仅是在原型这个概念基础上所作的直接推论。
既然 prototype 只是恰好作为另一个对象的隐式引用的普通对象。那么,它也是对象,也符合一个对象的基本特征。
也就是说,prototype 对象也有自己的隐式引用,有自己的 prototype 对象。

如此,构成了对象的原型的原型的原型的链条,直到某个对象的隐式引用为 null,整个链条终止。

继承

所谓的原型继承,就是指设置某个对象为另一个对象的原型(塞进该对象的隐式引用位置)。
在 JavaScript 中,有两类原型继承的方式:显式继承和隐式继承。

显式原型继承

所谓的显式原型继承,就是指我们亲自将某个对象设置为另一个对象的原型。
通过调用Object.setPrototypeOf 方法

1
Object.setPrototypeOf(obj1, obj2) //将obj2设置为obj1的原型对象

还有另一种途径。即是通过 Object.create 方法,直接继承另一个对象。
Object.setPropertyOf 和 Object.create 的差别在于:

  • 1)Object.setPropertyOf,给我两个对象,我把其中一个设置为另一个的原型。
  • 2)Object.create,给我一个对象,它将作为我创建的新对象的原型。

隐式原型继承

通过 new 去创建 user 对象,可以通过 user.consturctor 访问到它的构造函数。

函数对象和普通对象

通过字面量或构造函数new的方式来创建对象

将对象分为函数对象和普通对象,所谓的函数对象,其实就是 JavaScript 的用函数来模拟的类实现

ES5 中的继承实现方法

new 关键字

手写new
1
2
3
4
5
6
7
8
var a = myFunction('Deng', 'yongling')

function myFunction() {
var obj = {}
obj.__proto__ = myFunction.prototype
var result = myFunction.call(obj, ...arguments)
return typeof result === 'object' ? result : obj
}
  • 创建一个空对象
  • 将该对象的隐式原型指向构造函数的显式原型
  • 使用call改变this指向
  • 如果无返回值或者返回值是非对象,则返回这个新建的obj,否则直接返回该返回值对象。

继承的多种方式和优缺点

1、原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
function Parent() {
this.name = 'YoLin'
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child() {

}

Child.prototype = new Parent()
var child = new Child()
child.getName() // YoLin

存在问题:

  • 1、引用类型得属性被所有实例共享
  • 2、在创建Child实例得时候,不能向Parent传参

2、借用构造函数(经典继承)

1
2
3
4
5
6
7
8
9
10
11
funtion Parent() {
this.names = ['YoLin','Deng']
}

function Child() {
Parent.call(this)
}
var child1 = new Child()
child1.names.push('123')// ['YoLin', 'Deng', '123']
var child2 = new Child()
child2.names // ['YoLin', 'Deng']

优点:

  • 1、避免了引用类型的属性被所有实例共享
  • 2、可以在Child中向Parent传参
    1
    2
    3
    4
    5
    6
    7
    8
    function Parent(name) {
    this.name = name
    }
    function Child(name) {
    Parent.call(this, name)
    }
    var child = new Child('YoLin')
    child.name // YoLin
    缺点:
    方法都在构造函数中定义,每次创建实例都会创建一遍方法

3、组合继承

原型链继承和经典继承的结合

1
2
3
4
5
6
7
8
9
10
11
12
13
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue, green']
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
Child.prototype = new Parent()
Child.prototype.constructor = Child

优点:融合原型链继承和构造函数继承的优点,是javascript中最常用的继承模式

组合继承最大的缺点是会调用两次父构造函数。

  • 一次是设置子类型实例的原型时
  • 一次在创建子类型实例的时候

4、原型式继承

1
2
3
4
5
function createObj(o) {
funtion F() {}
F.prototype = o
return new F()
}

就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
缺点:包含引用类型的属性始终都会共享相应的值

5、寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

1
2
3
4
5
6
7
function createObj(o) {
var clone = Object.create(o)
clone.sayName = fucntion() {
console.log('hi')
}
return clone
}

缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

6、寄生组合式继承

为了避免重复调用父构造函数

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
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name)
}

function Child (name, age) {
Parent.call(this, name);
this.age = age;
}


function object(o) {
function F() {}
F.prototype = o
return new F()
}
function prototpe(child, parent){
const prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
prototype(Child, Parent)

将组合式继承中的

1
Child.prototype = new Parent()

变成调用object重新创建一个新的Parent的原型实例,并赋值给Child的prototype来实现

1
prototype(Child, Parent)

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。