经典的继承法有何问题
/**
* 经典的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 ()
按照原型链回溯规则,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());
-
正常继承的情况如下:
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原型上的方法
ES6中的步骤和本文中取巧继承Date的方法一模一样,不同的是ES6是语言底层的做法,有它的底层优化之处,而本文中的直接修改proto容易影响性能
ES6中在super中构建this的好处?
因为ES6中允许我们继承内置的类,如Date,Array,Error等。如果this先被创建出来,在传给Array等系统内置类的构造函数,这些内置类的构造函数是不认这个this的。 所以需要现在super中构建出来,这样才能有着super中关键的[[Class]]标志,才能被允许调用。(否则就算继承了,也无法调用这些内置类的方法)
构造函数与实例对象
new MyClass()中,都做了些什么工作
-
函数对象
所有引用类型(函数,数组,对象)都拥有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