从原型继承到类继承详解
构造函数的产生
-
对象是一个容器,封装了属性(property)和方法(method)
-
生成对象,需要一个模板.
-
JavaScript 语言使用构造函数(constructor)作为对象的模板。
-
所谓”构造函数”,就是专门用来生成实例对象的函数。
new 命令的原理
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的
prototype
属性。 - 将这个空对象赋值给函数内部的
this
关键字。 - 开始执行构造函数内部的代码。
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
// 将 arguments 对象转为数组
var args = [].slice.call(arguments);
// 取出构造函数
var constructor = args.shift();
// 创建一个空对象,继承构造函数的 prototype 属性
var context = Object.create(constructor.prototype);
// 执行构造函数
var result = constructor.apply(context, args);
// 如果返回结果是对象,就直接返回,否则返回 context 对象
return (typeof result === 'object' && result != null) ? result : context;
}
// 实例
var actor = _new(Person, '张三', 28);
⚠️如果构造函数内部有return
语句,而且return
后面跟着一个对象,new
命令会返回return
语句指定的对象;否则,就会不管return
语句,返回this
对象。
this的产生
var obj = { foo: 5 };
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
-
JavaScript 引擎会先在内存里面,生成一个对象
{ foo: 5 }
,然后把这个对象的内存地址赋值给变量obj
。也就是说,变量obj
是一个地址(reference)。后面如果要读取obj.foo
,引擎先从obj
拿到内存地址,然后再从该地址读出原始的对象,返回它的foo
属性。 -
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。
var obj = { foo: function () {} };
{
foo: {
[[value]]: 函数的地址
...
}
}
- 引擎会将函数单独保存在内存中,然后再将函数的地址赋值给
foo
属性的value
属性。
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 单独执行
f() // 1
// obj 环境执行
obj.f() // 2
- 由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,
this
就出现了。
注意点1
var obj ={
foo: function () {
console.log(this);
}
};
// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window
⚠️上面代码中,obj.foo
就是一个值。JavaScript 引擎内部,obj
和obj.foo
储存在两个内存地址,称为地址一和地址二。obj.foo()
这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this
指向obj
。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this
指向全局环境。
注意点2
var a = {
p: 'Hello',
b: {
m: function() {
console.log(this.p);
}
}
};
a.b.m() // undefined
⚠️如果this
所在的方法不在对象的第一层,这时this
只是指向当前一层的对象,而不会继承更上面的层。
Function.prototype.bind()
⚠️bind()
方法每运行一次,就返回一个新函数。
//错误的写法
element.addEventListener('click', o.m.bind(o));
// ...
element.removeEventListener('click', o.m.bind(o));
// 正确的写法
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
//bind()可以结合call()方法使用
[1, 2, 3].slice(0, 1) // [1]
// 等同于 :本质是在[1, 2, 3]上面调用Array.prototype.slice()方法
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
//等同于: call()实质上是调用Function.prototype.call()方法
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
原型链
-
JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
-
修改原型对象时,一般要同时修改
constructor
属性的指向。
构造函数的继承
// 继承
// 父类
function Super() {
this.x = 1;
this.y = 2;
this.z = 100;
this.xxx = function () {
console.log('父类实例享方法');
}
this.arr = [1,2,3];
}
Super.prototype.x = 8;
Super.prototype.fff = function () {
console.log('父类共享方法');
};
Super.hhh = function () {
console.log('父类静态方法');
};
// 子类
function Sub() {
Super.call(this); // 子类只具有父类实例的属性和方法 ,这样导致 Sub实例为Sub { x: 3, y: 4, z: 100, xxx: [Function] }
this.x = 3;
this.y = 4;
}
Sub.prototype = Object.create(Super.prototype); // 子类只具有父类原型的属性和方法
// Sub.prototype = new Super(); // 子类会具有父类原型的属性和方法 也会具有父类实例的属性和方法,但是 Sub实例依旧为Sub { x: 3, y: 4 }
// Sub.prototype.__proto__ = Super.prototype; 等价于 Sub.prototype = Object.create(Super.prototype); 和 Sub.prototype.constructor = Sub;
Sub.prototype.constructor = Sub;
Sub.prototype.sss = function () {
console.log('子类共享方法');
}
// 静态属性方法继承
Sub.__proto__ = Super;
// Object.create(Super.prototype) 返回一个实例,实例的原型是Super.prototype,也就是说实例继承自Super.prototype。
//Sub.prototype是子类的原型,要将它赋值为Object.create(Super.prototype),而不是直接等于Super.prototype。否则后面两行对Sub.prototype的操作,会连父类的原型Super.prototype一起修改掉。
const sub1 = new Sub();
console.log(sub1); //Sub { x: 3, y: 4 }
sub1.fff();
console.log(sub1.x); // 3
console.log(sub1.z); // undefined | 100
// sub1.xxx(); //sub1.xxx is not a function | 父类实例享方法
const sub2 = new Sub();
sub2.arr.push(4);
console.log(sub1.arr); // [ 1, 2, 3 ] | [ 1, 2, 3, 4 ]
Sub.hhh();
console.log(Sub.prototype.constructor);
/**
* 问题1:继承父类是要部分继承(只原型)还是整体继承? 觉得Super.call(this);和 Sub.prototype = Object.create(Super.prototype); 搭配的最好
* 问题2:先创造子类实例的this,再将父类的属性方法添加到this上(Super.call(this))
* 问题3:模拟了静态属性和方法的继承
*/
类
-
类的数据类型就是函数
-
类的所有方法都定义在类的
prototype
属性上
取值函数(getter)和存值函数(setter)
⚠️目的:拦截该属性的存取行为
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
类继承
-
子类必须在
constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。 -
ES5 的继承,实质是先创造子类的实例对象
this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。 -
在子类的构造函数中,只有调用
super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super
方法才能调用父类实例。
// 父类
class Super {
constructor(){
this.x = 1;
this.y = 2;
this.z = 100;
this.xxx = function () {
console.log('父类实例方法');
};
this.arr = [1,2,3];
}
fff() {
console.log(this); // 原型方法中的this 为 子类实例
console.log('父类共享方法');
};
static hhh() {
console.log(this); // 静态方法中的this 为 子类
console.log('父类静态方法');
};
}
/**
Sub.prototype = {
constructor: Sub
toString() {},
};
*/
// 子类
class Sub extends Super{
// 类似ES5 的构造函数Sub
// 类似ES5 constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
constructor(x, y) {
super(); // 继承 父类的 实例属性和方法 ; 即 Super.prototype.constructor.call(this)。
// 实例属性
this.x = x;
this.y = y;
}
// 实例属性 新写法
// 那么constructor方法还有什么用?1.调用super() 2.传入参数x,y ;新写法则无法接受
bar = 'hello';
// ES5 原型上的方法
sss() {
console.log(this); // 原型方法中的this 为 实例
super.fff(); // 在子类普通方法中通过`super`调用父类的方法时,方法内部的`this`指向当前的子类实例。
console.log('子类共享方法');
}
// 静态方法
static staticFun() {
console.log(this === Sub); // 静态方法中的this 为 类
super.hhh(); //
return 'hello';
}
// 静态属性
static myStaticProp = 42;
// 私有属性
#count = 0;
// 私有方法
#sum() {
return this.#count;
}
}
const sub1 = new Sub(3,4);
console.log(sub1); //{x: 3, y: 4, z: 100, #sum: ƒ, #count: 0,arr: (3) [1, 2, 3],bar: "hello"}
console.log(sub1.z);
sub1.xxx();
sub1.fff();
sub1.sss();
console.log(Sub.staticFun());
console.log(Sub.myStaticProp);
Sub.hhh(); // 父类的静态方法,可以被子类继承。
const sub2 = new Sub(33,44);
sub2.arr.push(55);
console.log(sub1.arr); // [1,2,3]
// 综上, 子类 继承了 父类的实例属性和方法 ,继承了 父类的共享属性和方法 ,还继承了 静态属性和方法
// 先将父类实例对象的属性和方法,加到`this`上面(所以必须先调用`super`方法),然后再用子类的构造函数修改`this`
判断一个类是否继承了另一个类
Object.getPrototypeOf(ColorPoint) === Point
super 关键字
-
super
虽然代表了父类A
的构造函数,但是返回的是子类B
的实例,即super
内部的this
指的是B
的实例,因此super()
在这里相当于A.prototype.constructor.call(this)
。 -
super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。 -
由于
super
指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super
调用的。 即ES5的Sub.prototype = Object.create(Super.prototype); -
在子类普通方法中通过
super
调用父类的方法时,方法内部的this
指向当前的子类实例。实际上执行的是super.print.call(this)
。 -
在子类普通方法中通过
super
调用父类的方法时,方法内部的this
指向当前的子类实例。 -
在子类的静态方法中通过
super
调用父类的方法时,方法内部的this
指向当前的子类,而不是子类的实例。
子类的__proto__
和子类的prototype
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
class A {
}
class B {
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性 (比ES5多的,也是(1)的解释)
Object.setPrototypeOf(B, A);
const b = new B();
⚠️这句话有意思:
子类(B
)的原型对象(prototype
属性)是父类的原型对象(prototype
属性)的实例。
【子类(`B`)的原型对象(`prototype`属性)】是【父类的原型对象(`prototype`属性)的实例】。
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B
// 等同于
子类(`B`)的原型对象(`prototype`属性)是 【父类的原型对象(`prototype`属性)】的实例。
B.prototype.__proto__ = A.prototype;
实例的__proto__
b.__proto__= B.prototype
B.prototype.__proto__ = A.prototype;
拓展
- Object.setPrototypeOf(A,B)方法为参数对象A设置原型B,返回该参数对象A
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
- 判断一个类是否继承了另外一个类
Object.getPrototypeOf(ColorPoint) === Point