基于原型继承
原型继承概念
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是一个对象, 对象的构造函数是ObjectObject.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 定义的是可枚举的
- 方法之间不需要逗号分隔,加了会报错
- 类的方法都定义在类的 prototype 属性上面,使用 Object.assign 方法可以很方便地一次向类添加多个方法
- 静态方法:
- 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 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 调用父类的方法时,方法内部的 this 指向当前的子类实例,即相当于
- 在静态方法中 super 作为对象,将指向父类,而不是父类的原型对象
- 在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例
- 在子类普通方法中 super 指向父类的原型对象(Parent.prototype),所以定义在父类实例上的方法或属性,是无法通过 super 调用
// 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 的继承,可以继承数据、结构和行为三者
- 基于 class 的继承,继承的是行为和结构,但没有继承数据
- object 创建的方式
- class -> class 之间存在继承关系,object 基于某个已完成继承关系的 class 模板所创建
- object -> object 之间存在继承关系,object 可以由各种方式创建。可以在创建时设置继承对象,也可以在创建后修改继承对象
- this 的创建过程
- class 是先将父类实例对象的属性和方法,加到 this 上面 (所以必须先调用 super 方法),然后再用子类的构造函数修改 this
- prototype 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))
继承的本质
- 对象如何创建,由谁创建?
- 对象如何跟其它对象或者方法,关联起来,由谁关联起来?
- 对象的属性/数据如何初始化/填充,由谁填充?