关于面向对象,原型链,继承

关于 JS 如何实现面向对象


先看看官方如何解释:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain >

对于使用过基于类的语言 (如 Java 或 C++) 的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并且本身不提供一个 class 实现。(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype *)。该原型对象也有一个自己的原型对象( *proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

尽管这种原型继承通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。

原型模式的实现

  • 使用原型模式,基于对象实现经典类
    • 成员属性:this.xx
    • 私有属性:function 内的 var, let const
    • 静态属性:类.xxx
    • 原型属性和方法:类.prototype.xx
  • new 一个对象,新对象的proto指向构造函数的 prototype
  • 调用对象 this.xx 如果找不到成员属性,会往proto去找,一直到原型链的终点
  • 关于成员属性和原型属性的选择,一般方法绑定在原型上,属性在成员上。

什么是原型和原型链


  • 说下原型:构造函数有一个 prototype 对象,每 new 一个实例对象时,所有实例会共享prototype 对象上的方法和属性,这就叫原型。
  • 说下构造函数:实例的 constructor 指向它的构造函数。
  • 说下原型链:实例的proto是一个指针,访问一个属性时,找不到就往proto查找,最终会找到对象的 prototype Object.prototype,再往下 Object.prototype.proto就是 null

原型链


其实就是指实例的proto,如果一个实例 a,构造函数是 A,继承了 B, B 继承了 C,那么

  • a.proto === B.prototype
  • B.prototype.__proto === C.prototype
  • C.prototype.__proto === Object.prototype
  • 最后 a.proto.proto.proto === Object.prototype

继承

ES5 模拟继承的原理

1. 原型链继承 ,即继承原型方法

1
Son.prototype = new Father();


缺点:Father 的所有属性被 son 的所有实例共享,并且还能访问,因为 new 的时候,Father 的 this 上的属性和原型方法都被指向子类的 prototype。

2. 函数继承, 即继承成员属性

1
2
3
4
5

function Son(name) {
Father.call(this, "我是传给父类的参数");
this.name = name || "son";
}

组合继承


使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承

1
2
3
4
5
6
7
function Son(name) {
// 第一次调用父类构造器 子类实例增加父类实例
Father.call(this, '我是传给父类的参数')
this.name = name || 'son'
}
// 经过new运算符 第二次调用父类构造器 子类原型也增加了父类实例
Son.prototype = new Father()

优点

  1. 弥补了构造继承的缺点,现在既可以继承实例的属性和方法,也可以继承原型的属性和方法
  2. 既是子类的实例,也是父类的实例
  3. 可以向父类传递参数
  4. 函数可以复用

缺点

  1. 调用了两次父类构造函数,生成了两份实例
  2. constructor 指向问题

寄生组合继承


通过寄生方式,砍掉父类的实例属性,避免了组合继承生成两份实例的缺点

1
2
3
4
5
6
7
8
function Son(...args) {
// 实例属性继承
Father.call(this, ...args)
}
// 此步实际作用是 Son.prototype.__proto = Father.prototype
Son.prototype = Object.create(Father.prototype)
// 修复构造函数指向, 继承后指向回自身
Son.prototype.constructor = Son

ES6 Class 和 Function 实现类的区别

ES6 的 Class 实现类只是语法糖,与经典的面向对象类是有区别的,JS 采用的是原型模式的设计,即构造函数,Prototype,原型链。

  • class 内部属性不可枚举
  • class 不存在变量提升
  • class 默认严格模式
  • function 用 this.xx 代表成员属性(外部可访问),内部变量默认就是私有属性。
  • 往原型上添加属性,初始化后会在proto中找到,可通过实例.name4访问
1
Person.prototype.name4 = 'name4'

class 继承和寄生组合继承区别

  • class 使用 extends 实现原型的继承
  • 使用 super()实现实例属性和方法的继承

模拟 new 的实现

1
2
3
4
5
6
7
8
9
10
// 模拟new的实现
function _new(fn, ...args) {
// 拿到fn的原型对象:创建对象实例,这样,原型指向fn.prototype
// 即obj.__proto__ = fn.prototype
const obj = Object.create(fn.prototype)
// 传入原型对象作为this,执行构造函数,参数透传
const ret = fn.apply(obj, args)
// 没有返回值就返回实例
return ret || obj
}

寄生组合继承和 new 过程的共同点

这两个知识点其实都是基于原型的设计模式

  • 创建对象指向构造函数的原型:Object.create(fn.prototype)
  • 调用构造函数:fn.apply(this, args)