乐趣区

JavaScript-原型继承

写在前面

此文只涉及基于原型的继承,ES6 之后基于 Class 的继承请参考相关文献。

知识储备

构造函数的两种调用方式(结果完全不同)

  • 通过关键字 new 调用:
function Person(name) {
    this.name = name;
    this.age = 18;
}
var o = new Person('hx');
console.log(o.name, o.age);
// hx 18
console.log(window.name, window.age);
// '' undefined
  • 直接调用:
function Person(name) {
    this.name = name;
    this.age = 18;
}
var o = Person('hx');
console.log(o);
// undefined
console.log(window.name, window.age);
// hx 18

由此可见:

  • 构造函数与普通函数无异,可直接调用,无返回值,this 指向 Window;
  • 通过 new 调用的话,返回值为一个对象,且 this 指向该对象

new 到底做了什么?

new 关键字会进行如下操作:

  • 创建一个空对象;
  • 链接该对象到另一个对象(即:设置该对象的构造函数);
  • 将第一步创建的空对象作为 this 的上下文(this 指向该空对象);
  • 执行构造函数(为对象添加属性),并返回该对象
function Person(name) {
    this.name = name;
    this.age = 18;
}
var o = new Person('hx');

上述代码对应的四步操作是:

  • var obj = {};
  • obj.__proto__ = Person.prototype;
  • Person.call(obj,'hx');
  • return obj;

JavaScript 实现继承的几种方式

1. 原型链继承

function Parent(name) {
    this.name = name;
    this.age = 18;
    this.arr = ['hello','world']
}
Parent.prototype.sayAge = function() {console.log(this.age)
}

function Child(gender) {this.gender = gender;}
Child.prototype = new Parent();

var child1 = new Child('male');
child1.arr.push('js')
console.log(child1.name); // undefined
console.log(child1.age); // 18
console.log(child1.arr); // ['hello','world','js']
console.log(child1.gender); // male
child1.sayAge(); // 18

var child2 = new Child('female');
console.log(child2.name); // undefined
console.log(child2.age); // 18
console.log(child2.arr); // ['hello','world','js']
console.log(child2.gender); // female
child2.sayAge(); // 18

优点:

  • Parent 原型对象上的方法可以被 Child 继承

缺点:

  • Parent 的引用类型属性会被所有 Child 实例共享,互相干扰
  • Child 无法向 Parent 传参

2. 构造函数继承(经典继承)

function Parent(name) {
    this.name = name;
    this.age = 18;
    this.arr = ['hello','world'];
    this.sayName = function() {console.log(this.name)
    }
}
Parent.prototype.sayAge = function() {console.log(this.age)
}

function Child(name,gender) {Parent.call(this,name); // this 由 Window 指向待创建对象
    this.gender = gender;
}

var child1 = new Child('lala','male');
child1.arr.push('js');
console.log(child1.name); // lala
console.log(child1.age); // 18
console.log(child1.arr); // ['hello','world','js']
console.log(child1.gender); // male
child1.sayName(); // 18
child1.sayAge(); // Uncaught TypeError: child1.sayAge is not a function

var child2 = new Child('fafa','female');
console.log(child2.name); // fafa
console.log(child2.age); // 18
console.log(child2.arr); // ['hello','world']
console.log(child2.gender); // female
child2.sayName(); // 18
child2.sayAge(); // Uncaught TypeError: child1.sayAge is not a function

优点:

  • 避免了引用类型属性被所有 Child 实例共享
  • Child 可以向 Parent 传参

缺点:

  • Parent 原型对象上的方法无法被 Child 继承
  • 每次创建 Child 实例都会创建 sayName 方法,造成内存资源的浪费

3. 组合继承

function Parent(name,age) {
    this.name = name;
    this.age = age;
    this.arr = ['hello','world']
}
Parent.prototype.sayName = function() {console.log(this.name)
}

function Child(name,age,gender) {Parent.call(this,name,age);
    this.gender = gender
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constuctor = Child;
Child.prototype.sayAge = function() {console.log(this.age)
}

var child1 = new Child('lala',18,'male');
child1.arr.push('js');
child1.name; // 'lala'
child1.age; // 18
child1.arr; // ['hello','world','js']
child1.gender; // 'male'
child1.sayName(); // lala
child1.sayAge(); // 18

var child2 = new Child('fafa',28,'female');
child1.name; // 'fafa'
child1.age; // 28
child1.arr; // ['hello','world']
child1.gender; // 'female'
child1.sayName(); // fafa
child1.sayAge(); // 28

组合继承是 JavaScript 继承的最佳实践

  • 属性使用构造函数继承 – 避免了 Parent 引用属性被多个 Child 实例影响,同时支持传参
  • 方法使用原型链继承 – 支持 Child 继承 Parent 原型对象方法,避免了多实例中方法的重复拷贝

补充 1

对于组合继承代码中的Child.prototype = Object.create(Parent.prototype),还有两种类型的方法:

Child.prototype = Parent.prototype或者Child.prototype = new Parent()

  • Child.prototype = Parent.prototype:这样肯定不行,给 Child.prototype 添加方法或影响到 Parent;
  • Child.prototype = new Parent():这种方式有一个缺点,在 new 一个 Child 实例时会调用两次 Parent 构造函数(一次是new Parent(),另一次是Parent.call(this,name)),浪费效率,且如果 Parent 构造函数有副作用,重复调用可能造成不良后果。

对于第二种情况,除了使用 Object.create(Parent.prototype) 这种方法外,还可以借助一个桥接函数实现。实际上,不管哪种方法,其实现思路都是调整原型链:

由:
new Child() ----> Child.prototype ----> Object.prototype ----> null

调整为:
new Child() ----> Child.prototype ----> Parent.prototype ----> Object.prototype ----> null

function Parent(name) {this.name = name}
Parent.prototype.sayName = function() {console.log(this.name)
}

function Child(name,age) {Parent.call(this,name);
    this.age = age;
}

function F() {}

F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constuctor = Child;

Child.prototype.sayAge = function() {console.log(this.age)
}

可见,通过一个桥接函数 F,实现了只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性

// 封装一下上述方法
function object(o) {function F() {}
    F.prototype = o;
    return new F();}

function prototype(child, parent) {var prototype = object(parent.prototype);
    child.prototype = prototype;
    prototype.constructor = child;
}

// 当我们使用的时候:prototype(Child, Parent);

补充 2

什么是最优的继承方式?

其实不管是改良的 组合继承 (使用 Object.create 也好,还是使用 Object.setPrototypeOf 也好),还是所谓的 寄生组合继承(使用桥接函数F),都不是回答该问题的关键。

最优的继承方式体现的是一种设计理念:
不分静态属性还是动态属性,其维度的划分标准是:是否可共享

  • 对于每个 子类都有 ,但子类实例 相互独立 的属性(非共享):应该 ++ 放到父类的构造方法上 ++,然后通过子类调用父类构造方法来实现初始化;
  • 对于每个 子类都有 ,且子类实例 可以共享 的属性(不管是静态属性还是动态属性):应该 ++ 放到父类的原型对象上 ++,通过原型链获得;
  • 对于每个 子类独有 ,且子类实例 相互独立 的属性(非共享):应该 ++ 放到子类的构造方法上 ++ 实现;
  • 对于每个 子类独有 ,但子类实例 可以共享 的属性:应该 ++ 放到子类的原型对象上 ++,通过原型链获得;

从文字上不容易理解,看代码:

function Man(name,age) {
    // 每个子类都有,但相互独立(非共享)this.name = name;
    this.age = age;
}

Man.prototype.say = function() {
    // 每个子类都有,且共享的动态属性(共享)console.log(`I am ${this.name} and ${this.age} years old.`)
}
// 每个子类都有,且共享的静态属性(共享)Man.prototype.isMan = true;

function Swimmer(name,age,weight) {Man.call(this,name,age);
    // Swimmer 子类独有,且各实例独立(非共享)this.weight = weight;
}

function BasketBaller(name,age,height) {Man.call(this,name,age);
    // BasketBaller 子类独有,且各实例独立(非共享)this.height = height;
}

// 使用 ES6 直接设置原型关系的方法来构建原型链
Object.setPrototypeOf(Swimmer.prototype, Man.prototype)
// 等同于 Swimmer.prototype = Object.create(Man.prototype); Swimmer.prototype.constructor = Swimmer;
Object.setPrototypeOf(BasketBaller.prototype, Man.prototype)
// 等同于 BasketBaller.prototype = Object.create(Man.prototype); BasketBaller.prototype.constructor = BasketBaller;

// 继续扩展子类原型对象
Swimmer.prototype.getWeight = function() {
    // Swimmer 子类独有,但共享的动态属性(共享)console.log(this.weight);
}
// Swimmer 子类独有,但共享的静态属性(共享)Swimmer.prototype.isSwimmer = true;

var swimmer1 = new Swimmer('swimmer1',11,100);
var swimmer2 = new Swimmer('swimmer2',21,200);

swimmer1; // Swimmer {name: "swimmer1", age: 11, weight: 100}
swimmer1.isMan; // ture
swimmer1.say(); // I am swimmer1 and 11 years old.
swimmer1.isSwimmer; // ture
swimmer1.getWeight(); // 100

swimmer2; // Swimmer {name: "swimmer2", age: 21, weight: 200}
swimmer2.isMan; // ture
swimmer2.say(); // I am swimmer2 and 21 years old.
swimmer2.isSwimmer; // ture
swimmer2.getWeight(); // 200

// BasketBaller 同理(略)
退出移动版