Skip to content

js 面向对象的实质是 基于原型 的对象系统,而不是基于类

一、new 的实现原理

new 关键字具体操作如下

  • 首先创建一个空对象,这个对象将作为执行构造函数后返回的对象实例
  • 使创建的空对象的原型(__proto__)指向构造函数的prototype属性
  • 将空对象赋值给构造函数内部的 this, 并执行构造函数逻辑
  • 根据构造函数逻辑,返回第一步创建的对象或者构造函数显式返回值

实现 newFunc 函数来模拟 new 关键字的操作

js
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: 决定该属性能否被删除或者改变特征值

默认给对象设置属性,都会产生数据属性,其中的 writableenumerableconfigurable 默认都为 true

  • Object.getOwnPropertyDescriptor: 查看数据属性
  • Object.definedProperty: 设置修改数据属性

2.2 访问器属性

访问器属性(getter/setter)的 4 个特征

  • getter: 函数或 undefined, 在取属性值时被调用
  • setter: 函数或 undefined, 在设置属性值时被调用
  • enumerable: 决定 for in 能否枚举该属性
  • configurable: 决定该属性能否被删除或者改变特征值

三、实现继承的解决方案

3.1 原型链继承

将子类 SubType 的原型对象替换成父类 SuperType 的实例。

TIP

父类中私有和公有的属性方法,最后都变成子类实例公有的

关键代码

js
SubType.prototype = new SuperType();

缺点

  • 原型中存在引用值,一个实例的修改会影响其它实例
  • 基类 SubType 在实例化时不能给超类 SuperType 的构造函数传参

3.2 借用构造函数继承

为了解决原型中包含引用值无法传参数等导致的继承问题, 在子类的构造函数中,调用父类的构造函数

关键代码

js
function SubType(name){
  SuperType.call(this, name)
}

缺点

  • 子类只能继承父类中的私有属性,不能继承父类原型上的公有方法

3.3 组合继承

综合了原型链和借用构造函数,将二者优点进行结合

关键代码

js
function SubType(name, age){
   // 继承属性
   SuperType.call(this, name);
   this.age = age;
}

// 继承方法
SubType.prototype = new SuperType();

缺点

  • 父类构造函数 SuperType 调用了两次,影响性能
  • 调用俩次,导致子类的实例,以及子类的原型对象上都存在 父类实例的属性, 而子类的原型上并不需要的父类实例的属性

3.4 原型式继承

创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型, 然后返回这个临时类型的一个实例

关键代码

js
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

本质及适应场景 该继承方法本质是对传入的对象进行一次浅拷贝,ES6 通过 Object.create() 方法将原型式继承的概念规范化

TIP

可以看出 Object.create() 的本质就是创建某个类的空实例

适应场景主要是:适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合

缺点

  • 引用值始终在相关对象之间共享

3.5 寄生式继承

寄生构造函数和工厂模式:创建一个实现继承的函数,以某种 方式增强对象,然后返回这个对象

关键代码

js
function createAnother(original) {
  let clone = object(original);   // 通过调用函数创建一个新对象
  clone.sayHi = function() {     // 以某种方式增强这个对象
    console.log("hi");
  };
  return clone;     // 返回这个对象
}

适应场景

只关注对象,不关注类型和构造函数的场景

缺点

  • 增强函数无法复用

3.6 寄生式组合继承

使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。解决组合继承中父类构造函数调用多次

关键代码

js
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);

这样,子类原型上就不存在父类实例属性了