关于 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 |
|
组合继承
使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承
1 | function Son(name) { |
优点
缺点
- 调用了两次父类构造函数,生成了两份实例
- constructor 指向问题
寄生组合继承
通过寄生方式,砍掉父类的实例属性,避免了组合继承生成两份实例的缺点
1 | function Son(...args) { |
ES6 Class 和 Function 实现类的区别
ES6 的 Class 实现类只是语法糖,与经典的面向对象类是有区别的,JS 采用的是原型模式的设计,即构造函数,Prototype,原型链。
- class 内部属性不可枚举
- class 不存在变量提升
- class 默认严格模式
- function 用 this.xx 代表成员属性(外部可访问),内部变量默认就是私有属性。
- 往原型上添加属性,初始化后会在proto中找到,可通过
实例.name4
访问
1 | Person.prototype.name4 = 'name4' |
class 继承和寄生组合继承区别
模拟 new 的实现
1 | // 模拟new的实现 |
寄生组合继承和 new 过程的共同点
这两个知识点其实都是基于原型的设计模式
- 创建对象指向构造函数的原型:Object.create(fn.prototype)
- 调用构造函数:fn.apply(this, args)