原型、class 继承

367 阅读11分钟

基于原型继承

原型继承概念

1. 原型

  • 原型是指为其他对象提供共享属性的对象
  • 所有对象都有一个隐式引用,指向这个对象的原型对象或者 null

2. 原型链

  • 每个实例对象都有一个私有属性(proto)指向它的构造函数的原型对象(prototype)
  • 该原型对象也有一个自己的原型对象(proto)
  • 层层向上直到一个对象的原型对象为null

3. 构造函数

  • 可以通过 new Constructor 创建对象
  • constructor 函数有 prototype 属性(它默认是以 Object.prototype 为原型的对象,包含 constructor 一个字段,指向构造函数)test.prototype.__proto__ === Object.prototype test.prototype.constructor === test
  • constructor 是一个函数,而所有函数都是 new Function 创建出来的,函数字面量可以看作是它的语法糖test.__proto__ === Function.prototype

4. __proto_

  • 通过 Object.getPrototypeOf(obj) 间接访问指定对象的 prototype 对象
  • 通过 Object.setPrototypeOf(obj, anotherObj) 间接设置指定对象的 prototype 对象
  • 部分浏览器提前开了 __proto_ 的口子,使得可以通过 obj.__proto__ 直接访问原型,通过 obj.__proto__ = anotherObj 直接设置原型
  • ECMAScript 2015 规范只好向事实低头,将 __proto__ 属性纳入了规范的一部分
  • 访问对象的 obj.__proto__ 属性,默认走的是 Object.prototype 对象上 __proto__ 属性的 get/set 方法
  • __proto__ 属性既不能被 for...in 遍历出来,也不能被 Object.keys(obj) 查找出来

总结

  • prototype 是函数的原型对象,即 prototype 是一个对象,它会被对应的 __proto__ 引用
  • 要知道自己的 __proto__ 引用了哪个 prototype ,只需要看看是哪个构造函数构造了你,那你的 __proto__ 就是那个构造函数的 prototype
  • 所有的构造函数的原型链最后都会引用 Object 构造函数的原型,即可以理解 Object 构造函数的原型是所有原型链的最底层,即 Object.prototype.__proto===null
  • 题目
    • Object.__proto__ === Function.prototype // Object是用来创建对象的构造函数
    • Function.__proto__ === Function.prototype // Function是用来创建函数的构造函数
    • true.__proto__ === Boolean.prototype // 是由构造函数Boolean创建出来的对象
    • Function.prototype.__proto__ === Object.prototype // prototype是一个对象, 对象的构造函数是Object
    • Object.prototype.__proto__ === null

原型继承分类

1. 原型继承就是指设置某个对象为另一个对象的原型(塞进该对象的隐式引用位置)。包括两种方式:

  • 显式原型继承 - 通过 Object.create 或者 Object.setPrototypeOf 显示继承另一个对象,将其设置为原型
    • 通过调用 Object.setPrototypeOf() 将某个对象设置为另一个对象的原型
    • 通过 Object.create(),直接继承另一个对象
  • 隐式原型继承 - 通过 constructor 构造函数,在使用 new 关键字实例化时,会自动继承 constructor 的 prototype 对象,作为实例的原型
    • 除箭头函数外的函数,都有 prototype 属性
    • 它默认是以 Object.prototype 为原型的对象test1.__proto__ === test.prototype
    • 包含 constructor 一个字段,指向构造函数

2. 隐式原型继承和显式原型继承的互操作性

  • 从隐式原型继承中剥离出 Object.create 方法
    • 将 constructor 设置成空函数
    • 关联原型
const create = (proto) => {
  function F () {}
  F.prototype = proto
  return new F()
}
  • 用显式原型继承的方式完成 constructor 初始化过程
    • 先将 Constructor.prototype 作为原型,创建一个空对象
    • 通过 Constructor.call 将构造函数内部的 this 指向 instance 变量,将 args 传入
const createInstance = (Constructor, ...args) => {
  let instance = Object.create(Constructor.prototype)
  Constructor.call(instance, ...args)
  return instance
}

原型继承的几种实现方式

1. 原型链继承: 子类型的原型为父类型的一个实例对象

function Person(name, age) {}
Person.prototype.setAge = function () { }
function Student(price) {}
Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象

2. 借用构造函数继承: 在子类型构造函数中通用call()调用父类型构造函数

function Person(name, age) {}
Person.prototype.setAge = function () { }
function Student(name, age, price) {Person.call(this, name, age)}

3. 原型链+借用构造函数的组合继承: 通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。

function Person(name, age) {}
Person.prototype.setAge = function () { }
function Student(name, age, price) {Person.call(this, name, age)}
Student.prototype = Object.create(Person.prototype)//核心代码 
Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的

4. 创建子类的典型方法: 定义子类,将其原型设置为超类的实例,然后在该实例上定义属性。这么写很不优雅,特别是对于 getters 和 setter 而言。 相反,您可以使用此代码设置原型:

function superclass() {}
superclass.prototype = {
  // 在这里定义方法和属性
};
function subclass() {}
subclass.prototype = Object.create(superclass.prototype, Object.getOwnPropertyDescriptors({
  // 在这里定义方法和属性
}));

基于类继承

ES6 的类完全可以看作构造函数的另一种写法(构造函数语法糖)

class 的概念

类的属性名,可以采用表达式

类与函数一样也可以使用表达式的形式定义 const MyClass = class Me {}。类的名字 Me 只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用 MyClass 引用。采用 Class 表达式,可以写出立即执行的

1. 类的属性和方法

  • constructor 方法
    • constructor 方法是类的默认方法,如果没有显式定义,一个空的 constructor 方法会被默认添加
    • 通过 new 命令生成对象实例时,自动调用类的 constructor 方法
  • 类的实例
    • 类必须使用 new 命令生成类的实例,不加 new 会报错;而普通构造函数不用 new 也可以执行
    • 与 ES5 一样,类的所有实例共享一个原型对象
    • 可以通过实例的__proto__属性为“类”添加方法
  • 取值函数(getter)和存值函数(setter)
    • 在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
    • 存值函数和取值函数是设置在属性的 Descriptor 对象上的
  • 类的方法
    • 类的方法都定义在类的 prototype 属性上面,使用 Object.assign 方法可以很方便地一次向类添加多个方法 Object.assign(Class.prototype, { ... })
    • 定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了
    • 类的内部所有定义的方法,都是不可枚举的(non-enumerable);而ES5 定义的是可枚举的
    • 方法之间不需要逗号分隔,加了会报错
  • 静态方法
    • 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”
    • 如果静态方法包含 this 关键字,这个 this 指的是类,而不是实例
    • 父类的静态方法,可以被子类继承
    • 静态方法也是可以从 super 对象上调用的
  • 实例属性
    • 定义在 constructor() 方法里面的 this 上面
    • 定义在类的最顶层
  • 静态属性
    • 静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性
    • 通过 ClassName.prop = 1 定义
    • 通过在实例属性的前面,加上 static 关键字定义
  • 私有方法和私有属性
    • 一种做法是在命名上加以区别,_bar方法前面的下划线,表示这是一个只限于内部使用的私有方法
    • 将私有方法移出模块
    • 利用Symbol值的唯一性,将私有方法的名字命名为一个 Symbol 值
    • 为class加了私有属性。方法是在属性名或方法之前,使用 # 表示
  • new.target 属性
    • 该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令或 Reflect.construct() 调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的
    • 子类继承父类时,new.target 会返回子类,利用这个特点,可以写出不能独立使用、必须继承后才能使用的类

2. 注意

  • 严格模式:类和模块的内部,默认就是严格模式
  • 不存在提升:类不存在变量提升(hoist),这一点与 ES5 完全不同
  • name 属性:name 属性总是返回紧跟在class关键字后面的类名
  • Generator 方法:如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数
  • this 的指向:类的方法内部如果含有 this,它默认指向类的实例。但是一旦单独使用该方法,很可能报错
  • 构造函数默认返回实例对象(this),可指定返回另一个对象

基于类的继承的概念

1. 简介

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错

  • 因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法
  • 然后再对其进行加工,加上子类自己的实例属性和方法,所以在子类的构造函数中,只有调用 super 之后才可以使用 this 关键字,否则会报错

2. super 关键字

super 关键字,既可以当作函数使用,也可以当作对象使用。由于对象总是继承其他对象的,所以可以在任意一个对象中,使用 super 关键字。使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

  • super 作为函数调用时
    • 代表父类的构造函数,只能用在子类的构造函数之中,子类的构造函数必须执行一次 super 函数
    • super 返回的是子类的实例,即 super 内部的 this 指的是子类的实例,相当于 A.prototype.constructor.call(this)
  • super 作为对象时
    • 在子类普通方法中 super 指向父类的原型对象(Parent.prototype),所以定义在父类实例上的方法或属性,是无法通过 super 调用
      • 在子类的普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类实例,即相当于 super.function.call(this)
      • 在子类的普通方法中通过 super 对某个属性赋值,这时 super 就是 this,赋值的属性会变成子类实例的属性
    • 在静态方法中 super 作为对象,将指向父类,而不是父类的原型对象
      • 在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例
// super.x = 3,这时等同于对this.x赋值为3
// 读取super.x的时候,读的是A.prototype.x,所以返回undefined
class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

3. 类的 prototype 属性和__proto__属性

ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的 prototype 属性

Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链

  • 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类
  • 子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性
class A {}
class B extends A {}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

这样的结果是因为,类的继承是按照下面的模式实现的

class A {}
class B {}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
const b = new B();

Object.setPrototypeOf 方法的实现如下:

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

这两条继承链,可以这样理解:

  • 作为一个对象,子类(B)的原型(__proto__属性)是父类(A)
  • 作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例
B.prototype = Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

4. 实例的 __proto__ 属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性,即子类的原型的原型,是父类的原型

通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为

基于类的继承的实现原理

  • 通过构造一个新的 constructor 函数,将 SuperConstructor 和 properties 里的 constructor 里的属性初始化行为合并到一起
  • 通过 Object.setPrototypeOf 将 Super 和 Sub 的原型关联起来
const inherit = (SuperConstructor, properties) => {
  let { constructor } = properties
  let SubConstructor = function(...args) {
    SuperConstructor.call(this, ...args)
    constructor.call(this, ...args)
  }
  SubConstructor.prototype = {
    ...properties,
    constructor: SubConstructor
  }
  Object.setPropertypeOf(
    SubConstructor.prototype,
    SuperConstructor.prototype
  )
  return SubConstructor
}

基于 class 继承和基于 prototype 继承的区别

  • 可继承的能力
    • 基于 class 的继承,继承的是行为和结构,但没有继承数据
      • 无论通过 class fields 语法,还是在 constructor 里面声明数据,最后,它们都将出现在实例对象上,而非原型对象上
      • data 数据是由 instance 承载,而 methods 行为/方法则在 class 里
    • 基于 prototype 的继承,可以继承数据、结构和行为三者
  • object 创建的方式
    • class -> class 之间存在继承关系,object 基于某个已完成继承关系的 class 模板所创建
    • object -> object 之间存在继承关系,object 可以由各种方式创建。可以在创建时设置继承对象,也可以在创建后修改继承对象
  • this 的创建过程
    • class 是先将父类实例对象的属性和方法,加到 this 上面 (所以必须先调用 super 方法),然后再用子类的构造函数修改 this
    • prototype 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))

继承的本质

  • 对象如何创建,由谁创建?
  • 对象如何跟其它对象或者方法,关联起来,由谁关联起来?
  • 对象的属性/数据如何初始化/填充,由谁填充?