js继承从入门到理解

开场白

大三下学期结束时候,一我的跑到帝都来参加各厂的面试,免不了的面试过程当中常常被问到的问题就是JS中如何实现继承,当时的本身也是背熟了实现继承的各类方法,回过头来想一想殊不知道__proto__是什么,prototype是什么,以及各类继承方法的优势和缺点,想必有好多刚入坑的小伙伴有着跟我同样的体验,这篇文章将从基础概念出发,进一步说明js继承,以及各类继承方法的优缺点,但愿对看这篇文章的你有所帮助,若是你是见多识广的大佬,既然看到这里了,不妨继续看下去,指点一二,让新入坑的小伙伴更好的成长。(若是你都看到这了,透露一下文末有彩蛋嗷!)下面,咱们进入正题:javascript

设计思想

若是你没看过,也会听别人说JavaScript的继承不一样于Java和c++,js中没有“类”和“实例”的区分,而是靠一种原型链的一级一级的指向来实现继承。那么当时的创造JavaScript这种的语言的人为何要这样实现js独有的继承,你们能够阅读阮一峰老师的Javascript继承机制的设计思想,就像讲故事同样,从古代至现代说明了js继承这种设计模式的原因。html

prototype对象

了解了js继承的设计思想后,咱们须要学习原型链上的第一个属性prototype,这个属性是一个指针,指向的是原型对象的内存堆。从阮一峰老师的文章中,咱们能够知道prototype是为了解决构造函数的属性和方法不能共享的问题而提出的,下面咱们先实现一个简单的继承:java

function constructorFn (state, data) {
    this.data = data;
    this.state = state;
    this.isPlay = function () {
        return this.state + ' is ' + this.data;
    }
}
var instance1 = new constructorFn ('1', 'doing');
var instance2 = new constructorFn ('0', 'done');
console.log(instance1.isPlay()); // 1 is doing
console.log(instance2.isPlay()); // 0 is done

此时,实例1 和实例2 都有本身的data属性、state属性、isPlay方法,形成了资源的浪费,既然两个实例都须要调用isPlay方法,即可以将isPlay方法挂载到构造函数的prototype对象上,实例便有了本地属性方法和引用属性方法,以下:c++

function constructorFn (state, data) {
    this.data = data;
    this.state = state;
}
constructorFn.prototype.isPlay = function () {
    return this.state + ' is ' + this.data;
}
constructorFn.prototype.isDoing = 'nonono!';
var instance1 = new constructorFn ('1', 'doing');
var instance2 = new constructorFn ('0', 'done');
console.log(instance1.isPlay()); // 1 is doing
console.log(instance2.isPlay()); // 0 is done
console.log(instance1.isDoing); // nonono!
console.log(instance2.isDoing); // nonono!

咱们将isPlay方法挂载到prototype对象上,同时增长isDoing属性,既然是共享的属性和方法,那么修改prototype对象的属性和方法,实例的值都会被修改,以下:面试

constructorFn.prototype.isDoing = 'yesyesyes!';
console.log(instance1.isDoing); // yesyesyes!
console.log(instance2.isDoing); // yesyesyes!

问题来了,为何实例会取到prototype对象上的属性和方法,别急,没多久就会结合其余问题综合解答。chrome

同时,你可能会问,若是修改实例1的isDoing属性的原型,实例2的isDoing会不会受到影响?设计模式

instance1.isDoing = 'yesyesyes!';
console.log(instance1.isDoing); // yesyesyes!
console.log(instance2.isDoing); // nonono!

问题又来了,能够看到修改实例1的isDoing属性,实例2的实例并未受到影响。这是为何呢?浏览器

那若是修改实例1的isDoing属性的原型属性,实例2的isDoing会不会受到影响?以下:网络

instance1.__proto__.isDoing = 'yesyesyes!';
console.log(instance1.isDoing); // yesyesyes!
console.log(instance2.isDoing); // yesyesyes!

问题又又来了,为何修改实例1的__proto__属性上的isDoing的值就会影响到构造函数的原型对象的属性值?app

咱们先整理一下,未解决的三个问题:

  1. 为何实例会取到prototype对象上的属性和方法?
  2. 为何修改实例1的isDoing属性,实例2的实例没有受到影响?
  3. 为何修改实例1的__proto__属性上的isDoing的值就会影响到构造函数的原型对象的属性值?

这时候不得不背后真正的操做者搬出来了,就是new操做符,一样是面试最火爆的问题之一,new操做符干了什么?相信有人也是跟我同样,已经背的倒背如流了,以 Var instance1 = new constructorFn();为例,就是下面三行代码:

var obj = {};
obj.__proto__ =  constructorFn.prototype;
constructorFn.call(obj);

第一行声明一个空对象,由于实例自己就是一个对象。
第二行将实例自己的__proto__属性指向构造函数的原型,obj新增了构造函数prototype对象上挂载的属性和方法。
第三行将构造函数的this指向替换成obj,再执行构造函数,obj新增了构造函数本地的属性和方法。

理解了上面三行代码的含义,那么三个问题也就迎刃而解了。<br/>
问题1:实例在新建的时候,自己的__ptoto__指向了构造函数的原型。<br/>
问题2:实例1和实例2 在新建后,有了各自的this,修改实例1的isDoing属性,只是修改了当前对象的isDoing的属性值,并无影响到构造函数。<br/>
问题3:修改实例1的__proto__,即修改了构造函数的原型对象的共享属性<br/>

到此处,涉及到的内容你们能够再回头捋一遍,理解了就会以为醍醐灌顶。

__proto__

同时,你可能又会问,__proto__是什么?<br/>
简单来讲,__proto__是对象的一个隐性属性,同时也是一个指针,能够设置实例的原型。
实例的__proto__指向构造函数的原型对象。

须要注意的是,

每一个对象都有内置的__proto__属性,函数对象才会有prototype属性。

用chrome和FF均可以访问到对象的__proto__属性,IE不能够。

咱们继续用上面的例子来讲明:

function constructorFn (state, data) {
    this.data = data;
    this.state = state;
}
constructorFn.prototype.isPlay = function () {
    return this.state + ' is ' + this.data;
}
constructorFn.prototype.isDoing = 'nonono!';
var instance1 = new constructorFn ('1', 'doing');
console.log(instance1.__proto__ === constructorFn.prototype); // true

构造函数的原型对象也是对象,那么constructor.prototype.__proto__指向谁呢?

定义中说对象的__proto__指向的是构造函数的原型对象,下面咱们验证一下constructor.prototype.__proto__的指向:

console.log(instance1.__proto__ === constructorFn.prototype); // true
console.log(constructorFn.prototype.__proto__ === Object.prototype) // true

用图形表示的话,以下:

<img src="./images/1_1.png" />

能够看出,constructor.prototype.__proto__的指向是Object的原型对象。<br/>
那么,Object.prototype.__proto__的指向呢?

console.log(instance1.__proto__ === constructorFn.prototype); // true
console.log(constructorFn.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__); // null

用图形表示的话,以下:

<img src="./images/1_2.png" />

能够发现,Object.prototype.__proto__ === null;
这样也就造成了原型链。经过将实例的原型指向构造函数的原型对象的方式,连通了实例-构造函数-构造函数的原型,原型链的特色就是逐层查找,从实例开始查找一层一层,找到就返回,没有就继续往上找,直到全部对象的原型Object.prototype。

继承的方法

了解了上面的基础概念,就要将学到的用在实际当中,到底要怎么实现继承呢?实现的方式有哪些?下面主要说明实现继承最经常使用的三用方式,能够知足基本的开发需求,想要更深刻的了解,能够参考阮一峰老师的网络博客

原型链继承

实现原理:将父类的实例做为子类的原型

function Animal (name) {
    this.name = name;
}
Animal.prototype = {
    canRun: function () {
        console.log('it can run!');
    }
}
function Cat () {
    this.speak = '喵!';
} 
Cat.prototype = new Animal('miao');
Cat.prototype.constructor = Cat;

注:

  1. 这种继承方式须要将子类的构造函数指回自己,由于从父类继承时同时也继承了父类的构造函数。
  2. 简单的使用Cat.prototype = Animal.prototype将会致使两个对象共享相同的原型,一个改变另外一个也会改变。
  3. 不要使用Cat.prototype = Animal,由于不会执行Animal的原型,而是指向函数Animal。所以原型链将会回溯到Function.prototype,而不是Animal.prototype,所以canRun将不会在Cat的原型链上。
使用call、apply方法实现

实现原理:改变函数的this指向

function Animal (name) {
    this.name = name;
}
Animal.prototype = {
    canRun: function () {
        console.log('it can run!');
    }
}
function Cat (name) {
    Animal.call(this, name);
    this.speak = '喵!';
}

注:

  1. 该方法将子类Cat的this指向父类Animal,可是并无拿到父类原型对象上的属性和方法
使用混合方法实现

实现原理:原型链能够继承原型对象的属性和方法,构造函数能够继承实例的属性且能够给父类传参

function Animal (name) {
    this.name = name;
}
Animal.prototype = {
    canRun: function () {
        console.log('it can run!');
    }
}
function Cat (name, age) {
    Animal.call(this, name);
    this.speak = '喵!';
    this.age = age;
} 
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat = new Cat('tom', '12');

每一种继承方式都有本身的优势和不足,读者能够根据实际状况选择相应的方法。为了在实际开发中更方便的使用继承,能够封装一个继承的方法,以下:

function extend (child, parent) {
    var F = function () {};
    F.prototype = parent.prototype;
    child.prototype = new F();
    child.prototype.construtor = child;
    child.superObj = parent.prototype;
    //修正原型的constructor指向
    if(!parent.prototype.contrucotor == Object.prototype.constructor){
        parent.prototype.constructor = parent;
    }
}

结合一开始的例子,能够这样实现继承的关系:

function constructorFn (state, data) {
    this.data = data;
    this.state = state;
}
constructorFn.prototype.isPlay = function () {
    return this.state + ' is ' + this.data;
}
constructorFn.prototype.isDoing = 'nonono!';
function subFn (state, data) {
    subFn.superObj.constructor.call(this, state, data);
    //从superFn.constructor中调用 
}
extend(subFn,  constructorFn ); // 获取构造函数原型上的属性和方法

javaScript的继承远不止这些,,只但愿可让新学js的小伙伴不那么盲目的去刻意记一些东西,固然学习最好的办法仍是要多写,最简单的就是直接打开浏览器的控制台,去验证本身各类奇奇怪怪的想法,动起来吧~

|-------赤裸裸的分割线-------|

彩蛋来啦:本周咱们的客户端app 5.6版本就要正式发版啦,新版本新增了小视频功能呢,你们能够经过小视频分享本身各类购物经验,也能够发挥本身的脑洞,展现本身的才华,快来给咱们的开发小哥哥打call吧~

快速入口💗

图片描述

相关文章
相关标签/搜索