1

JS和Java中虽然都有对象的概念,但这两种对象却大有不同。Java的对象是基于类创建的,JS的对象却是基于一个特殊的对象——原型对象——创建的,之前看到一个盖房子的比喻,在Java中盖房子是先画好图纸再盖房子,JS中盖房子却是先盖一个样板房再盖其他房子,觉得也挺贴切。

所以JS中的继承和Java中的继承就大有不同了,是基于原型对象的,如果两个对象形成继承关系,那必然是其中一个对象的原型链上存在一个指针指向另一个对象。即使JS中的两个类声明了继承关系,也是表现在原型对象上。比如:

class A {
    say() {
        console.log('say: hello!');
    }
}

class B extends A {
    constructor() {
        super();
    }
}

console.log(A.prototype); // {constructor: ƒ, say: ƒ}
console.log(B.__proto__); // class A {}
console.log(B.prototype); // A {constructor: ƒ}

首先,类是JS中函数的语法糖,并且在JS中函数本身也是对象,也就是说A和B是两个对象,所以extends操作使得B自身的原型属性__proto__指向了A,相当于const B = Object.create(A);

其次,类的继承关系也影响其生成的实例,众所周知,函数本身存在一个特殊的对象属性:prototype,函数经过构造调用产生的实例的原型属性__proto__是指向这个对象的,而extends操作修改了B的prototype对象,所以B实例上的原型属性__proto__也就被修改了,通过B实例的原型属性__proto__能找到A的prototype,即在B实例的原型链上能找到A的prototype。

const b = new B();
console.log(b.__proto__); // A {constructor: ƒ} 即B.prototype
console.log(b.__proto__.__proto__); // {constructor: ƒ, say: ƒ} 即A.prototype

在JS中使用字面量定义的对象时,其默认的原型属性__proto__指向Object的prototype对象,相当于默认继承自Object,所以字面量对象可以调用Object的实例方法。
可以使用isPrototypeOf来判断一个对象是否在另一个对象的原型链上。

由上述可知,JS中的继承关系与原型对象密切相关,为了达到继承的关联关系(共享某些属性和方法),就要从原型对象着手:

  1. 使用Object.create的方式创建对象,使两个对象直接产生继承关系
const o1 = {
    name: 'o1',
    age: 18,
    walk() {
        console.log('walking...')
    }
};
const o2 = Object.create(o1);
console.log(o2.__proto__); // {name: 'o1', age: 18}
console.log(o2.walk()); // walking...
console.log(o1.isPrototypeOf(o2)); // true
  1. 使用new操作创建对象,使产生的实例和类(或函数)的原型对象产生继承关系
const b = new B();
console.log(B.prototype); // A {constructor: ƒ}
console.log(b.__proto__); // A {constructor: ƒ} 即B.prototype
console.log(B.prototype.isPrototypeOf(b)); // true
  1. 使用extends关键字使类形成继承关系,扩展类实例的原型链
class A {
    say() {
        console.log('say: hello!');
    }
}

class B extends A {
    constructor() {
        super();
    }
}

console.log(A.prototype); // {constructor: ƒ, say: ƒ}
const b = new B();
console.log(b.__proto__.__proto__); // {constructor: ƒ, say: ƒ} 即A.prototype
console.log(A.isPrototypeOf(B)); // true
console.log(A.isPrototypeOf(b)); // false
console.log(A.prototype.isPrototypeOf(b)); // true
  1. 修改函数的prototype属性使函数形成继承关系,扩展函数实例的原型链
function C() {
    this.name = 'c';
    this.operation = function() { return 'printing...'};
}
function D() {}
D.prototype = new C();
const d = new D();
console.log(d.__proto__.__proto__ === C.prototype); // true
console.log(C.prototype.isPrototypeOf(d)); // true
console.log(D.prototype.isPrototypeOf(d)); // true

这里存在一个问题,就是子类实例化时无法向父类的构造函数传参

  1. 盗用父类构造函数

在函数内部通过call或apply调用父类函数(非构造调用),可继承父类实例自身(非原型对象)的属性和方法(相当于把子类实例(即this)传递进父类函数,对这个this做了一遍操作),虽然可在初始化时传递参数给父类,但无法形成原型链

function E() {
    C.call(this);
    this.do = function () { return 'do homework'; }
}
const e = new E();
console.log(E.prototype.isPrototypeOf(e)); // true
console.log(C.prototype.isPrototypeOf(e)); // false
console.log(e); // E {name: 'c', operation: ƒ, do: ƒ}
console.log(e.do()); // do homework

子类产生的实例无法对父类及其原型对象应用instanceof和isPrototypeOf方法。

此时如果父类想共享方法给子类,必须把方法直接在定义在函数内部,绑定到实例上,而无法通过父类的prototype对象共享。

  1. 结合4和5,使得子类实例可继承父类原型对象的属性和方法,且能形成原型链
function E() {
    C.call(this);
    this.do = function () { return 'do homework'; }
}
E.prototype = new C();
const e = new E();
console.log(E.prototype.isPrototypeOf(e)); // true
console.log(C.prototype.isPrototypeOf(e)); // true
console.log(e); // E {name: 'c', operation: ƒ, do: ƒ}
console.log(e.do()); // do homework
  1. 用Object.create()替换new父类实例来重写子类的原型对象
function inheritatePrototype(subT, superT) {
  let proto = Object.create(superT.prototype);
  proto.constructor = subT;
  subT.prototype = proto;
}

inheritatePrototype(E, C);

可以舍去new中不需要的操作

  1. 通过工厂方式共享属性和方式

类似工厂函数,但不是用裸的Object,以某种方式取得对象(如new等返回新对象的函数),对此对象加属性或方法以增强功能,并返回对象。

function createAnother(original) {
  let clone = Object.create(original);
  clone.xx = xxx;
  return clone;
}

适合主要关注对象,而不在乎类型和构造函数的场景

存在的问题: 必须在构造函数中定义方法(属于实例非原型对象的方法),函数不能重用


beckyyyy
550 声望414 粉丝

工作多年的一只前端菜鸟