在从前端小白进阶的过程中,最难也最让我困惑的,就是js的继承问题。为了搞懂它,也让更多人搞懂它,所以总结了下面这篇文章。
首先需要申明的一点,也是非常重要的一点就是需要理解继承和实例化的关系:实例是继承了类的原型。
在介绍继承之前,我们需要先明确几个概念:
Object&&Function的关系
在Javascript中,一切都是对象。JS本来就是一门基于对象的编程语言。对于Object和Function的关系,很多网上的文章都是用鸡和蛋来描述两者的关系。看来很多相关文档,最有帮助的就是下面这句话。
先有Object.prototype(原型链顶端),Function.prototype继承Object.prototype而产生,最后,Function和Object和其它构造函数继承Function.prototype而产生。
Object.prototype -> Function.prototype -> Object/Array/Function
几乎所有JavaScript中的对象都是位于原型链顶端的Object的实例。
prototype && __proto__
prototype: 只有函数才具有的属性。
-
《JavaScript权威指南》中指出,每一个js对象一定有一个原型对象,并从原型对象上继承属性和方法。所以你可以认为
prototype是一个模板,新创建的自定义对象都是这个模版(prototype)的一个拷贝 (实际上不是拷贝而是链接,只不过这种链接是不可见,新实例化的对象内部有一个看不见的__proto__指针,指向原型对象)。 -
当你创建函数时,JS会为这个函数自动添加prototype属性,值是一个有 constructor属性的对象,不是空对象。而一旦你把这个函数当作构造函数(constructor)调用(即通过new关键字调用),那么JS就会帮你创建该构造函数的实例,实例继承构造函数prototype的所有属性和方法。
__proto__: 实例对象的原型
- 每个实例对象(object)都有一个私有属性(称之为__proto__)指向它的原型对象(prototype)。该原型对象也有一个自己的原型对象(
__proto__) ,层层向上直到一个对象的原型对象为 null。
call() && apply()
- 作用:通过改变函数内this指向的方式,扩充函数的作用域。
window.name = 'mac';
var person = { name: 'tom'};
function getName() {
console.log(this.name);
}
getName.call(); // mac 默认指向
getName.call(window); // mac
getName.call(person); // tom
注: 在函数中,this总是指向调用这个函数的对象。
- 区别:call()函数和apply()函数的的不同在于它们的第二个参数类型不同。call()和apply()中的第一个参数都表示想要this指向的对象,第二个参数都是可选参数,是这个函数中需要传入的参数。
call(Object, Object)
apply(Object, Array) //第二个参数必须是数组类型
继承方式
- 原型链继承(对象间的继承)
原型式继承是借助已有的对象创建新的对象,将子类的原型指向父类,就相当于加入了父类这条原型链。原型链的尽头(root)是Object.prototype。所有对象均从Object.prototype继承属性。
function Parent(){
this.name = 'parents';
}
function Child(){
this.childName = 'child';
}
Child.prototype = new Parent();//Child继承Parent,通过原型,形成链条
var child = new Child();
console.log(child.name); // parents 得到被继承的属性
//继续原型链继承
- 类式继承(构造函数间的继承) 类式继承是在子类型构造函数的内部调用超类型的构造函数。
function Parent(){
this.name = 'parent';
}
function Child(){
Parent.call(this);
}
ES6的class继承
在ES6中引入了class的概念,我们可以通过关键字extends来非常方便的实现继承,这对于习惯来面向对象编程的人来说是非常友好的。
class Parents{
constructor() {
this.name = 'parent';
}
}
class Children extends Parents{
constructor() {
super();
this.childName = 'child';
}
getName = () => {
console.log(this.name);
}
}
let child = new Children();
child.getName(); //parents
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
继承属性
对js的继承了解之后,我们还需要了解两个概念:实例属性和类属性。
- 实例属性: 可以继承的属性。在ES6中,我们在constructor()中声明的属性就是实例属性。
class Child {
constructor(){
this.name = 'child'; //实例属性
}
//在chorme canary中,支持直接定义属性
age = 12; //实例属性
}
let child = new Child();
console.log(child.name); //child
console.log(child.age); // 12
- 类属性: 不可以继承的属性。类属性是定义在类自身上的属性,子类和实例无法访问。
class Child {
constructor(){
this.name = 'child'; //实例属性
}
};
Child.age = 12;
let child = new Child();
console.log(child.name); // child
console.log(child.age); //undefined
什么情况下需要定义类属性?
一些变量或者静态方法,在我们没有还实例化的时候,我们就想要使用到这些值,那么我们可以将它们定义为类属性。
看到这里,对于js继承的认识你有没有稍微清楚一点呢?希望这篇文章可以带给你一些帮助。