使用javascript实现Date对象的继承

  最近在处理时间的时候发现一个问题,不能继承Date上面的方法,之前使用其它的对象都没出现过问题,我们先看下面这个例子,使用经典的寄生组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 经典的js寄生组合式继承
*/
function MyDate() {
Date.apply(this, arguments);
this.abc = 1;
}

function inherits(subClass, superClass) {
function Inner() {}

Inner.prototype = superClass.prototype;
subClass.prototype = new Inner();
subClass.prototype.constructor = subClass;
}

inherits(MyDate, Date);

MyDate.prototype.getTest = function() {
return this.getTime();
};


let date = new MyDate();

console.log(date.getTest());

运行之后,可以在控制台里面看到如下输出:

1
2
3
4
5
VM2356:17 Uncaught TypeError: this is not a Date object.
at MyDate.getTime (<anonymous>)
at MyDate.getTest (<anonymous>:17:17)
at <anonymous>:23:18us>:20:17)
at <anonymous>:26:18

这是什么原因呢,MDN中有提到,JavaScript的日期对象只能通过JavaScript Date作为构造函数来实例化,那么,是不是就无法使用Date来实现继承来呢?

通过在网上查找,发现了网友给出的很多有趣的答案,现将网上的答案整理如下,不外乎有三种:

  • 1、比较暴力的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyDate(){
var d = new Date();
function init(that){
var i;
var methods = ['getDate','getTime','getYear',/***/,'toString'];
for (i = 0; i < methods.length; i++) {
that[methods[i]] = d[methods[i]]
}
}
init(this)
this.MyTest() = function() {
console.log('dd')
}
}

这种方法使用了方法代理,并不能称之为继承

  • 2、使用es5来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 需要考虑polyfill情况
Object.setPrototypeOf = Object.setPrototypeOf ||
function(obj, proto) {
obj.__proto__ = proto;

return obj;
};

/**
* 用了点技巧的继承,实际上返回的是Date对象
*/
function MyDate() {
// bind属于Function.prototype,接收的参数是:object, param1, params2...
var dateInst = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();

// 更改原型指向,否则无法调用MyDate原型上的方法
// ES6方案中,这里就是[[prototype]]这个隐式原型对象,在没有标准以前就是__proto__
Object.setPrototypeOf(dateInst, MyDate.prototype);

dateInst.abc = 1;

return dateInst;
}

// 原型重新指回Date,否则根本无法算是继承
Object.setPrototypeOf(MyDate.prototype, Date.prototype);

MyDate.prototype.getTest = function getTest() {
return this.getTime();
};

let date = new MyDate();

// 正常输出,譬如1525744465718
console.log(date.getTest());

可以看到,用的是非常巧妙的一种做法:

正常继承的情况如下:

new MyDate()返回实例对象date是由MyDate构造的
原型链回溯是: date(MyDate对象)->date.__proto__->MyDate.prototype->MyDate.prototype.__proto__->Date.prototype

这种做法的继承的情况如下:

new MyDate()返回实例对象date是由Date构造的
原型链回溯是: date(Date对象)->date.__proto__->MyDate.prototype->MyDate.prototype.__proto__->Date.prototype

可以看出,关键点在于:

构造函数里返回了一个真正的Date对象(由Date构造,所以有这些内部类中的关键[[Class]]标志),所以它有调用Date原型上方法的权利

构造函数里的Date对象的[[ptototype]](对外,浏览器中可通过__proto__访问)指向MyDate.prototype,然后MyDate.prototype再指向Date.prototype

所以最终的实例对象仍然能进行正常的原型链回溯,回溯到原本Date的所有原型方法

  • 3、使用ES6的class来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyDate extends Date {
constructor() {
super();
this.abc = 1;
}
getTest() {
return this.getTime();
}
}

let date = new MyDate();

// 正常输出,譬如1515638988725
console.log(date.getTest());

这里的正常输出环境是直接用ES6运行,不经过babel打包,打包后实质上是转化成ES5的,所以效果完全不一样