开场白

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

设计思想

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

prototype对象

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

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对象上,实例便有了本地属性方法和引用属性方法,如下:

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对象上的属性和方法,别急,没多久就会结合其他问题综合解答。

同时,你可能会问,如果修改实例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的值就会影响到构造函数的原型对象的属性值?

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

  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新增了构造函数本地的属性和方法。

理解了上面三行代码的含义,那么三个问题也就迎刃而解了。

问题1:实例在新建的时候,本身的__ptoto__指向了构造函数的原型。

问题2:实例1和实例2 在新建后,有了各自的this,修改实例1的isDoing属性,只是修改了当前对象的isDoing的属性值,并没有影响到构造函数。

问题3:修改实例1的__proto__,即修改了构造函数的原型对象的共享属性

到此处,涉及到的内容大家可以再回头捋一遍,理解了就会觉得醍醐灌顶。

__proto__

同时,你可能又会问,__proto__是什么?

简单来说,__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

用图形表示的话,如下:

可以看出,constructor.prototype.__proto__的指向是Object的原型对象。

那么,Object.prototype.__proto__的指向呢?


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

用图形表示的话,如下:

可以发现,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吧~

快速入口

JS继承的从入门到理解的更多相关文章

  1. js继承的几种方法理解和代码演示

    1.属性继承 :call .apply:不建议使用浪费内存. function Person(name,age,sex){ this.name = name; this.age = age; this ...

  2. JS继承(简单理解版)

    童鞋们,我们今天聊聊js的继承,关于继承,平时开发基本用不到,但是面试没有不考的,我就想问,这是人干的事吗? 好吧,迫于社会主义核心价值观,我们今天就来简单说一说js的继承,谁让它是面向对象编程很重要 ...

  3. D3.js的v5版本入门教程(第四章)—— 理解Update、Enter、Exit

    D3.js的v5版本入门教程(第四章) Update.Enter.Exit是D3.js中很重要的概念,下面来讲一下它们到底是什么?(当你看完后.你就会知道如果数据集个数和选择集个数不匹配的情况下使用d ...

  4. 一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合/Class extends

    说实在话,以前我只需要知道"寄生组合继承"是最好的,有个祖传代码模版用就行.最近因为一些事情,几个星期以来一直心心念念想整理出来.本文以<JavaScript高级程序设计&g ...

  5. JS继承的一些见解

    JS继承的一些见解 js在es6之前的继承是五花八门的.而且要在项目中灵活运用面向对象写法也是有点别扭,更多的时候还是觉得面向过程的写法更为简单,效率也高.久而久之对js的继承每隔一段时间就会理解出现 ...

  6. js继承之call,apply和prototype随谈

    在js中,call,apply和prototype都可以实现对象的继承,下面我们看一个例子: function FatherObj1() { this.sayhello = "I am jo ...

  7. js继承实现

    JS实现继承可以分为:对象冒充和原型链继承 其中对象冒充又包括:临时变量,call 和 apply 临时变量方法: function Person(name,sex){ this.name = nam ...

  8. js继承之借用构造函数继承

    我的上一篇文章介绍了,原型链继承模式.但是单纯的原型链模式并不能很好地实现继承. 一.原型链的缺点 1.1 单纯的原型链继承最大的一个缺点,来自于原型中包含引用类型的值. 本来,我们没有通过原型链实现 ...

  9. js继承之原型链继承

    面向对象编程都会涉及到继承这个概念,JS中实现继承的方式主要是通过原型链的方法. 一.构造函数.原型与实例之间的关系 每创建一个函数,该函数就会自动带有一个 prototype 属性.该属性是个指针, ...

随机推荐

  1. python中打印一个字符串中有多少个单词

    1.for实现: str1 = str.strip(input("please input a str:"))n = 1i = 0for i in range(len(str1)- ...

  2. JAVA实训第三次作业

    编写"学生"类及其测试类. 5.1 "学生"类: 类名:Student 属性:姓名.性别.年龄.学号.5门课程的成绩 方法1:在控制台输出各个属性的值. 方法2 ...

  3. asp.net core部署到iis中出现 HTTP Error 502.5 - Process Failure的问题

    环境是windows Server2012  问题的原因是缺少文件:api-ms-win-crt-runtimel1-1-0.dll, dotnet 启动程序失败. 解决方案1: 安装系统补丁: 20 ...

  4. robotframework+selenium搭配chrome浏览器,web测试案例(搭建篇)

    这两天发布版本 做的事情有点多,都没有时间努力学习了,先给自己个差评,今天折腾了一天, 把robotframework 和 selenium 还有appnium 都研究了一下 ,大概有个谱,先说说we ...

  5. C++标准库第二版笔记 3 和异常的理解 1

    C++标准库第二版笔记 3 和异常的理解 1 差错和异常(error and exception)的处理 标准异常类(exception class) 定义于 分为: 1.语言本身支持的异常 2.标准 ...

  6. 2019浙江省赛B zoj4101 Element Swapping(推公式)

    http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=6003 题意 \(数组a通过交换一对数字,得到了b数组,给出x=\sum^n_{ ...

  7. Codeforces Round #485 (Div. 2) F. AND Graph

    Codeforces Round #485 (Div. 2) F. AND Graph 题目连接: http://codeforces.com/contest/987/problem/F Descri ...

  8. SVN客户端操作

    版权声明:本文为博主原创文章,转载请注明原文出处. https://blog.csdn.net/zzfenglin/article/details/50937119   下面我们来了解一下SVN客户端 ...

  9. ES6学习:两个面试题目--关于模板字符串

    号称看完就能“让开发飞起来”,不过文中的两个面试题目的知识点并没包括在文中. https://www.jianshu.com/p/287e0bb867ae 文中并没有完整的知识点去完成上面的两道题,这 ...

  10. dva-counter

    dva实例Counter. import dva, { connect } from 'dva'; import { Router, Route } from 'dva/router'; import ...