js 面向对象的实质是 基于原型 的对象系统,而不是基于类
一、new 的实现原理
new
关键字具体操作如下
- 首先创建一个空对象,这个对象将作为执行构造函数后返回的对象实例
- 使创建的空对象的原型(
__proto__
)指向构造函数的prototype
属性 - 将空对象赋值给构造函数内部的
this
, 并执行构造函数逻辑 - 根据构造函数逻辑,返回第一步创建的对象或者构造函数显式返回值
实现 newFunc
函数来模拟 new
关键字的操作
function Person(name){
this.name = name
}
const person = new newFunc(Person, 'tom')
person.name // 'tom'
function newFunc(...args){
// 取出构造函数
const constructor = args.shift()
// 实现 obj.__proto__ = constructor.prototype
const obj = Object.create(constructor.prototype)
// 执行构造函数逻辑,this = obj
const result = constructor.apply(obj, args)
// 判断函数执行结果是否为对象类型
return (typeof result === 'object' && result !== null) ? result : obj
}
二、JS对象的两类属性
为了提高抽象能力,JS
的属性被设计成更加复杂的形式,它提供了数据属性和访问器属性(getter/setter
)两类。
JS 中的属性并非简单的名称和值,JS 用一组 特征(attribute
) 来描述属性 (property
)
2.1 数据属性
数据属性的 4 个特征
value
: 属性值writable
: 决定属性能否被赋值enumerable
: 决定 for in 能否枚举该属性configurable
: 决定该属性能否被删除或者改变特征值
默认给对象设置属性,都会产生数据属性,其中的 writable
,enumerable
,configurable
默认都为 true
Object.getOwnPropertyDescriptor
: 查看数据属性Object.definedProperty
: 设置修改数据属性
2.2 访问器属性
访问器属性(getter/setter
)的 4 个特征
getter
: 函数或 undefined, 在取属性值时被调用setter
: 函数或 undefined, 在设置属性值时被调用enumerable
: 决定 for in 能否枚举该属性configurable
: 决定该属性能否被删除或者改变特征值
三、实现继承的解决方案
3.1 原型链继承
将子类 SubType
的原型对象替换成父类 SuperType
的实例。
TIP
父类中私有和公有的属性方法,最后都变成子类实例公有的
关键代码
SubType.prototype = new SuperType();
缺点
- 原型中存在引用值,一个实例的修改会影响其它实例
- 基类
SubType
在实例化时不能给超类SuperType
的构造函数传参
3.2 借用构造函数继承
为了解决原型中包含引用值、无法传参数等导致的继承问题, 在子类的构造函数中,调用父类的构造函数
关键代码
function SubType(name){
SuperType.call(this, name)
}
缺点
- 子类只能继承父类中的私有属性,不能继承父类原型上的公有方法
3.3 组合继承
综合了原型链和借用构造函数,将二者优点进行结合
关键代码
function SubType(name, age){
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
缺点
- 父类构造函数 SuperType 调用了两次,影响性能
- 调用俩次,导致子类的实例,以及子类的原型对象上都存在 父类实例的属性, 而子类的原型上并不需要的父类实例的属性
3.4 原型式继承
创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型, 然后返回这个临时类型的一个实例
关键代码
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
本质及适应场景 该继承方法本质是对传入的对象进行一次浅拷贝,ES6 通过 Object.create()
方法将原型式继承的概念规范化
TIP
可以看出 Object.create()
的本质就是创建某个类的空实例
适应场景主要是:适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合
缺点
- 引用值始终在相关对象之间共享
3.5 寄生式继承
寄生构造函数和工厂模式:创建一个实现继承的函数,以某种 方式增强对象,然后返回这个对象
关键代码
function createAnother(original) {
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
适应场景
只关注对象,不关注类型和构造函数的场景
缺点
- 增强函数无法复用
3.6 寄生式组合继承
使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。解决组合继承中父类构造函数调用多次
关键代码
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
这样,子类原型上就不存在父类实例属性了