Object和Function的关系
Object、Function和其他对象的关系可以归纳为四点:
- 一切对象最终都是继承自Object对象,Object对象直接继承自根对象Null
- 一切对象的原型链最终都是.... → Object.prototype → null。例如定义一个num变量var num = 1,则num的原型链为x → Number.prototype → Object.prototype → null; 定义一个函数对象fnfunction fn() {},则fn的原型链为fn → Function.prototype → Object.prototype → null;等等...
- 一切对象都包含有Object的原型方法,Object的原型方法包括了toString、valueOf、hasOwnProperty等等,在js中不管是普通对象,还是函数对象都拥有这些方法,下面列出了几个例子,大家可以自行去举例验证:
- 一切函数对象(包括Object对象)都直接继承自Function对象
函数对象包括了Function、Object、Array、String、Number,还有正则对象RegExp、Date对象等等,它们在js中的构造源码都是function xxx() {[native code]);,Function其实不仅让我们用于构造函数,它也充当了函数对象的构造器,甚至它也是自己的构造器。
- 一切对象都继承自Object对象是因为一切对象的原型链最终都是.... → Object.prototype → null,包括Function对象,只是Function的原型链稍微绕了一点,Function的原型链为Function → Function.prototype → Object.prototype → null,它与其它对象的特别之处就在于它的构造器为自己,即直接继承了自己,最终继承于Object,上面的原型链可以在浏览器验证:
- Object继承自Function,Object的原型链为 Object → Function.prototype → Object.prototype → null,原型链又绕回来了,并且跟第一点没有冲突。可以说Object和Function是互相继承的关系。
- Object对象直接继承自Function对象
- Function对象直接继承自己,最终继承自Object对象
继承的实现方式
在实现继承之前,让我们回顾一下原型链的概念:每一个实例对象都有一个__proto__属性(隐式原型),在js内部用来查找原型链,对象有一个prototype属性(显式原型),用来显示修改对象的原型,实例.proto = 对象.prototype = 原型。原型链的特点就是通过实例的__proto__属性去查找原型的属性,从子类一直向上查找对象原型的属性,继而形成一个查找链即原型链。
原型链继承
核心:将父类的实例作为子类的原型,即sub.prototype = new super,这样连通了子类-子类原型-父类。
function Super() {
this.flag = true;
this.arr = ['1'];
}
// 为了提高复用性,方法绑定在父类原型属性上
Super.prototype.getFlag = function() {
return this.flag;
}
function Sub(subFlag) {
this.subFlag = subFlag;
}
// 实现继承
Sub.prototype = new Super(); // 缺陷1,这虽然实现了继承,但是切断了Sub.prototype.constructor与Sub的关系,相当于给Sub.prototype重新赋值了
Sub.prototype.constructor = Sub; // 解决缺陷1
// 给子类添加子类持有的方法,注意顺序要在继承之后
Sub.prototype.getSubFlag = function() {
return this.subFlag;
}
// 构造实例
let sub1 = new Sub(false);
let sub2 = new Sub(true);
sub1.arr.push('2');
console.log(sub1.getSubFlag());
console.log(sub1.getFlag());
console.log(sub1.arr);
console.log(sub2.getSubFlag());
console.log(sub2.getFlag());
console.log(sub2.arr);
// 执行结果
// false
// true
// [ '1', '2' ]
// true
// true
// [ '1', '2' ]
构造函数继承
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(在构造子类构造函数内部使用call或apply来调用父类的构造函数)
function Super() {
this.flag = true;
this.arr = ['1'];
this.getFlag = function() {
return this.flag;
}
}
Super.prototype.sayFlag = function() {
return 'hi,' + this.flag;
}
function Sub() {
Super.call(this); // 如果父类可以接收参数,这里也可以直接传递
// Super.apply(this, arguments); // 这个地方的继承可以使用apply和call来实现
}
let obj1 = new Sub();
obj1.flag = false;
obj1.arr.push('2');
let obj2 = new Sub();
console.log(obj1.flag);
console.log(obj2.flag);
console.log(obj1.arr);
console.log(obj2.arr);
console.log(obj1.getFlag());
console.log(obj2.getFlag());
// console.log(obj2.sayFlag());
// 执行结果
// false
// true
// [ '1', '2' ]
// [ '1' ]
// false
// true
// 报错:TypeError: obj2.sayFlag is not a function
组合继承
利用构造函数和原型链的方法,可以比较完美的实现继承
// 第一个例子
function Super() {
this.flag = true;
}
Super.prototype.getFlag = function() {
return this.flag; // 继承方法
}
function Sub() {
this.subFlag = false;
Super.call(this); // 继承属性
}
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
let obj = new Sub();
Super.prototype.getSubFlag = function() {
return this.subFlag;
}
console.log(obj.flag);
console.log(obj.getFlag());
console.log(obj.getSubFlag());
//执行结果:
// true
// true
// false
// 第二个例子
function Fu(name) {
this.name = name;
this.arr = ['1'];
this.sayName = function() {
console.log(this.name);
}
}
Fu.prototype.sayHi = function() {
console.log('hi, ', this.name);
}
function Zi(name) {
Fu.apply(this, arguments);
// this.name = name; // 如果父构造函数没有参数name,则这里需要赋值
}
Zi.prototype = new Fu();
Zi.prototype.constructor = Zi;
let z1 = new Zi('儿子1');
z1.sayName();
z1.sayHi();
z1.arr.push('2');
let z2 = new Zi('儿子2');
console.log(z1.arr);
console.log(z2.arr);
// 执行结果
// 儿子1
// hi, 儿子1
// [ '1', '2' ]
// [ '1' ]
原型式继承
核心:直接利用es5的object.create方法
object.create原理
- 创建一个构造函数,构造函数的原型指向对象
- 调用new操作符创建实例,并返回这个实例,
- 本质上是一个浅拷贝
let parent = {
name: 'parent',
share: [1, 2, 3], // 父类的属性全部被子类共享
log: function() {
return this.name;
}
}
let child = Object.create(parent);
寄生继承
核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免组合继承的缺点。
function Fu(name) {
this.name = name;
this.arr = ['1'];
this.sayName = function() {
console.log(this.name);
};
}
Fu.prototype.sayHi = function() {
console.log('Hi, ', this.name);
};
function Zi(name) {
Fu.apply(this, arguments);
this.name = name;
}
Zi.prototype = Object.create(Fu.prototype); // Object.create(Fu.prototype)是对Fu.prototype的浅拷贝,而不是调用Fu的构造函数
// Zi.prototype = create(Fu.prototype);
Zi.prototype.constructor = Zi;
let z1 = new Zi('儿子1');
console.log(z1);
z1.sayName();
z1.sayHi();
// Object.create(Fu.prototype)相当于下面的函数,先初始化一个F构造函数,F的原型指向obj原型,返回F的构造函数
function create(obj) {
let F = function() {};
F.prototype = obj;
return new F();
}
// 执行结果
// Zi { name: '儿子1', arr: [ '1' ], sayName: [Function (anonymous)] }
// 儿子1
// Hi, 儿子1
寄生组合式继承
揉杂了原型链式、构造函数式、组合式、原型式、寄生式而形成的一种方式:主要是解决了组合继承的唯一缺点:多次调用Parent
function Parent(name, friends) {
this.name = name;
this.friends = friends;
}
Parent.prototype = {
constructor: Parent, // 需要手动绑定conatructor
share: [1, 2, 3],
log: function() {
return this.name
}
}
function Child(name, friends, gender) {
Parent.call(this, name, friends);
this.gender = gender;
}
function proto(child, parent) {
let clonePrototype = Object.create(parent.prototype);
child.prototype = clonePrototype;
child.prototype.constructor = child;
}
proto(Child, Parent);
es6 class
class Parent {
constructor(name, friends) { // 该属性在构造函数上,不共享
this.name = name;
this.friends = friends;
}
log() { // 该方法在原型上,共享
return this;
}
}
Parent.prototype.share = [1, 2, 3]; // 原型上的属性,共享
class Child extends Parent {
constructor(name, friends, gender) {
super(name, friends);
this.gender = gender;
}
}
各自的优缺点
结合上面实现方式,很容易看出他们各自的优缺点。
原型链继承
优点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/原有属性,子类都能访问到
缺点: - 切断了Sub.prototype.constructor与Sub的关系
- 原型链上的引用类型的数据会被所有实例共享
构造函数继承
优点:
- 解决了原型继承中的子类实例共享父类引用属性的问题。
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
缺点: - 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性和方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
组合继承
优点:
- 弥补了构造函数继承的缺陷,可以继承实例属性/方法,也可以继承原型原型/方法
- 既是子类的实例也是父类的实例
- 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点: - 调用了两次父类的继承,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
原型式继承
优点:父类方法可以复用
缺点:
- 父类引用属性全部被共享
- 子类不可传递参数给父类
寄生继承
特点:使用到了Object.create(Fu.prototype)实现原型链的浅拷贝
优点:解决了原型链继承和构造函数继承的缺点
继承的应用场景
JS继承的话主要用于面向对象的编程中,试用场景的话还是以单页面应用或者JS为主的开发里,因为如果只是在页面级的开发中很好会用到JS继承的方式,与其说继承,还不如直接写个函数来的简单有效一些。
想用继承的话最好是那种主要以JS为主开发的大型项目,比如说单页面的应用或者写JS框架,前台的所有的东西都用JS来完成,整个站的跳转,部分逻辑,数据处理等大部分使用JS来做,这样面向对象的编程才有存在的意义和价值。
开源项目(如node)中应用原理继承的案例
参考开源项目的源码