JS继承-如何继承Date对象

经典的继承法有何问题

/**
 * 经典的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());

看一下报错信息

TypeError: this is not a Date object.
    at MyDate.getTime ()
JS继承-如何继承Date对象_第1张图片

按照原型链回溯规则,Date的所有原型方法都可以通过MyDate对象的原型链往上回溯到。 仔细看看,发现它的关键并不是找不到方法,而是this is not a Date object.

由于调用的对象不是Date的实例,所以不允许调用,就算是自己通过原型继承的也不行。

为什么无法被继承?

  • JavaScript的日期对象只能通过JavaScript Date作为构造函数来实例化。
  • v8引擎底层代码中有限制,如果调用对象的[[Class]]不是Date,则抛出错误。
调用Date上方法的实例对象必须通过Date构造出来,否则不允许调用Date的方法

该如何实现继承?

  • 暴力混合法
function MyDate() {
  var _d = new Date();

  function init(that) {
    var arr = ['getDate', 'getDay', 'getFullYear'];
    arr.forEach(key => {
      that[key] = _d[key]
    });
  }
  init(this);

  // this.getDay = function () {
  //   return _d.getDay()
  // }
}

let date = new MyDate();

console.log(date.getDay());
内部生成一个Date对象,然后此类暴露的方法中,把原有Date中所有的方法都代理一遍。
但是实际测试过程中还是遇到了同样的问题。
  • ES5黑魔法
// 需要考虑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();

// 正常输出,譬如1515638988725
console.log(date.getTest());
JS继承-如何继承Date对象_第2张图片
原型链关系
  • 正常继承的情况如下:

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

  • 关键点在于:

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

2.构造函数里的Date对象的[[ptototype]](对外,浏览器中可通过proto访问)指向MyDate.prototype,然后MyDate.prototype再指向Date.prototype。 所以最终的实例对象仍然能进行正常的原型链回溯,回溯到原本Date的所有原型方法

  • ES6大法
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的,所以效果完全不一样。

ES6继承与ES5继承的区别

ES5中继承的实质是:(那种经典寄生组合式继承法)

先由子类(SubClass)构造出实例对象this

然后在子类的构造函数中,将父类(SuperClass)的属性添加到this上,SuperClass.apply(this, arguments)

子类原型(SubClass.prototype)指向父类原型(SuperClass.prototype)

所以instance是子类(SubClass)构造出的(所以没有父类的[[Class]]关键标志)

所以,instance有SubClass和SuperClass的所有实例属性,以及可以通过原型链回溯,获取SubClass和SuperClass原型上的方法

ES6中继承的实质是:

先由父类(SuperClass)构造出实例对象this,这也是为什么必须先调用父类的super()方法(子类没有自己的this对象,需先由父类构造)

然后在子类的构造函数中,修改this(进行加工),譬如让它指向子类原型(SubClass.prototype),这一步很关键,否则无法找到子类原型(注,子类构造中加工这一步的实际做法是推测出的,从最终效果来推测)

然后同样,子类原型(SubClass.prototype)指向父类原型(SuperClass.prototype)

所以instance是父类(SuperClass)构造出的(所以有着父类的[[Class]]关键标志)

所以,instance有SubClass和SuperClass的所有实例属性,以及可以通过原型链回溯,获取SubClass和SuperClass原型上的方法

JS继承-如何继承Date对象_第3张图片
ES5+ES6.png
ES6中的步骤和本文中取巧继承Date的方法一模一样,不同的是ES6是语言底层的做法,有它的底层优化之处,而本文中的直接修改proto容易影响性能
ES6中在super中构建this的好处?

因为ES6中允许我们继承内置的类,如Date,Array,Error等。如果this先被创建出来,在传给Array等系统内置类的构造函数,这些内置类的构造函数是不认这个this的。 所以需要现在super中构建出来,这样才能有着super中关键的[[Class]]标志,才能被允许调用。(否则就算继承了,也无法调用这些内置类的方法)

构造函数与实例对象

new MyClass()中,都做了些什么工作
JS继承-如何继承Date对象_第4张图片
梳理图.png
  • 函数对象

所有引用类型(函数,数组,对象)都拥有proto属性(隐式原型)

所有函数拥有prototype属性(显式原型)(仅限函数)

原型对象:拥有prototype属性的对象,在定义函数时就被创建

  • 构造函数
//创建构造函数
        function Word(words){
            this.words = words;
        }
        Word.prototype = {
            alert(){
                alert(this.words);
            }
        }
        //创建实例
        var w = new Word("hello world");
        w.print = function(){
            console.log(this.words);
            console.log(this);  //Person对象
        }
        w.print();  //hello world
        w.alert();  //hello world

print()方法是w实例本身具有的方法,所以w.print()打印hello world;alert()不属于w实例的方法,属于构造函数的方法,w.alert()也会打印hello world,因为实例继承构造函数的方法。

实例w的隐式原型指向它构造函数的显式原型,指向的意思是恒等于
w.__proto__ === Word.prototype
当调用某种方法或查找某种属性时,首先会在自身调用和查找,如果自身并没有该属性或方法,则会去它的proto属性中调用查找,也就是它构造函数的prototype中调用查找。

w本身没有alert()方法,所以会去Word()的显式原型中调用alert(),即实例继承构造函数的方法。

  • 原型和原型链
        Function.prototype.a = "a";
        Object.prototype.b = "b";
        function Person(){}
        console.log(Person);    //function Person()
        let p = new Person();
        console.log(p);         //Person {} 对象
        console.log(p.a);       //undefined
        console.log(p.b);       //b

p.a打印结果为undefined,p.b结果为b

解析: p是Person()的实例,是一个Person对象,它拥有一个属性值proto,并且proto是一个对象,包含两个属性值constructor和proto

        //Function
        function Function(){}
        console.log(Function);  //Function()
        console.log(Function.prototype.constructor);    //Function()
        console.log(Function.prototype.__proto__);      //Object.prototype
        console.log(Function.prototype.__proto__.__proto__);    //NULL
        console.log(Function.prototype.__proto__.constructor);  //Object()
        console.log(Function.prototype.__proto__ === Object.prototype); //true

总结:
1.查找属性,如果本身没有,则会去 proto 中查找,也就是构造函数的显式原型中查找,如果构造函数中也没有该属性,因为构造函数也是对象,也有 proto ,那么会去它的显式原型中查找,一直到null,如果没有则返回undefined
2.通过 proto 形成原型链而非protrotype

JS继承-如何继承Date对象_第5张图片
原型和原型链

你可能感兴趣的:(JS继承-如何继承Date对象)