js继承

192 阅读4分钟

在从前端小白进阶的过程中,最难也最让我困惑的,就是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()

  1. 作用:通过改变函数内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总是指向调用这个函数的对象。

  1. 区别: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继承的认识你有没有稍微清楚一点呢?希望这篇文章可以带给你一些帮助。