共计 6124 个字符,预计需要花费 16 分钟才能阅读完成。
写在前面
此文只涉及基于原型的继承,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 同理(略)