摘要:

 原型、闭包、作用域等知识可以说是js中面试必考的东西,通过你理解的深度也就能衡量出你基本功是否扎实。今天来复习一下javascript的原型和继承,虽说是老生常谈的话题,但对于这些知识,自己亲手写一遍能更加透彻的理解,能用自己的话说明白了,也就真正理解了。

 原型、闭包、作用域等知识可以说是js中面试必考的东西,通过你理解的深度也就能衡量出你基本功是否扎实。今天来复习一下javascript的原型和继承,虽说是老生常谈的话题,但对于这些知识,自己亲手写一遍能更加透彻的理解,能用自己的话说明白了,也就真正理解了。

原型是什么?

  在javascript中,通过关键字new调用构造器函数或者使用字面量声明,我们可以得到一个对象实例。每个对象实例内部都持有一个指针,指向一个普通的对象,这个普通的对象就是原型,这是天生的。为什么说它是普通的对象呢?因为它确实没什么特别的地方,同样也是某个构造器函数的一个实例,这个构造器可以是Object,可以是Array,也可以是其他你自己定义的构造器函数。在js中,对象实例的原型是不可访问的,不过在chrome和Firefox浏览器中,我们可以用一个名为__proto__的属性来访问到,来看一下所谓的原型长什么样:

  我用string的包装类来创建了一个对象s,可以看到s的原型是一个对象,该对象上包含了一系列方法,比如我们熟悉的charAt。这里也就很明显了,我们平时调用s.charAt(0),其实调用的是s的原型上的方法,也就是说,原型上的属性可以被对象访问到,就像是在访问自身的属性一样。可以认为原型就像孕妇肚子里的孩子一样,孩子的胳膊也可以算是孕妇的胳膊,都在自己身上嘛。不过区别是这里的原型只是一个引用,并不是真正的包含这个对象。注意不要被__proto__后面的那个String迷惑到,s的原型是一个Object的实例,而不是String的实例。下面的代码可以证明:

s.__proto__ instanceOf String; //false
s.__proto__ instanceOf Object; //trues.hasOwnProperty('charAt'); //false
s.__proto__.hasOwnProperty('charAt'); //true复制代码

  要明白这个原型指针到底指向什么,就需要明白对象是如何创建出来的,所以接下来有必要了解一下构造器函数。

  javascript中没有类,但可以把函数当类使,被用来当做类构造器的函数就叫构造器函数,一般把首字母大写来与普通函数进行区别,其实就是猪鼻子插根葱而已——装象。js中一切都是对象,所以函数也是对象,所以函数也有一个原型指针。与实例对象不同的是,函数这种特殊的对象,它的原型可以通过prototype属性显式的访问到,来看看String类的原型是啥样的:

  好像跟我们上面看到的s的原型是一模一样的。。。是这样吗?验证一下:

  这是什么原因呢?我们就要细究一下var s = new String('s');在执行的时候到底发生了什么,其实就是用new关键字调用函数String的时候发生了什么:

  1. 创建一个空对象obj,即Object的一个实例
  2. 把这个空对象obj绑定到函数的上下文环境中,相当于把this指向了obj
  3. 执行函数,这个过程就把函数中的属性、方法拷贝到了obj中
  4. 将obj的原型指向函数的prototype属性
  5. 返回这个obj对象,s作为它的引用。

  到这里就可以得出结论了:对象实例与它的构造器函数拥有同一个原型,这个原型指向的是构造器的父类的一个实例。

  我第一次提到了“父类”,在面向对象的语言中,如果B继承自A,我们说A是B的父类。javascript是通过原型实现继承的,所以我也可以说,我的原型指向谁,谁就是我的父类。通过上面的代码我们可以得出:

String.prototype === s.__proto__ //true
String.prototype instanceOf Object //true复制代码

  可以用面向对象语言的话说,Object就是String的父类。之所以这么说是因为这样容易记住,再来重复一遍结论:对象实例与它的构造器函数拥有同一个原型,这个原型指向的是构造器的父类的一个实例。这个结论是非常有用的,由于对象实例的原型是不可访问的(__proto__只是浏览器提供的能力),我们可以通过constructor属性得到它的构造器,然后用构造器的prototype属性来访问到原型,像这样:

s.constructor.prototype复制代码

  理解的过程像是在做一道道证明题一样。尽管有大师推荐在js中用构造器函数这个称呼来代替类,但为了便于理解和记忆,我还是这么叫吧~

原型的一些特性

  明白是原型是什么东西,来看看原型都有哪些特性。其实也不能说是原型的特性,而是javascript语言的特性。

  首先要看的就是所谓的原型链。每个对象都有原型,而对象的原型也是一个普通对象,那么就可以形成一个链,例如String对象的原型是Object类的一个实例,而Object对象的原型是一个空对象,空对象的原型是null。除去null不看的话,原型链的顶端是一个空对象{}

  当我们访问对象的一个属性时,会先从对象自身找,如过自身没有,就会顺着原型链一直往上找,直到找到为止。如果最后也没找到,则返回undefined。这样对象的内容就会很“丰富”,我的是我的,原型的也是我的。通过修改原型的指向,对象可以获得相应原型上的属性,js就是通过这种方式实现了继承。

  有一点需要注意的是,属性的读操作会顺着原型链来查找,而写操作却不是。如果一个对象没有属性a,为该对象的a属性赋值会直接写在该对象上,而不是先在原型上找到该属性然后修改值。举个例子:

var s = new String('string');
s.charAt(0); //返回s
s.hasOwnProperty('charAt'); //返回false  说明charAt不是自身的方法,而是原型上的
s.charAt = function(){return 1;} //为s的charAt赋值
s.hasOwnProperty('charAt'); //返回true   说明自身有了charAt方法
s.charAt(0); //返回1   这时候调用charAt找到了自身的方法
s.constructor.prototype.charAt.call(s,0); //返回s  调用原型上的charAt方法结果与原来一样复制代码

  上面的例子说明,为对象的属性赋值是不会影响到原型的。这也是合理的,因为创建出来的对象s,它的原型是一个指针,指向了构造器的原型。如果原型被修改,那么该类的其他实例也会跟着改变,这显然是不愿意看到的。

  我们愿意看到的是,修改了一个构造器的原型,由它构造出的实例也跟着动态变化,这是符合逻辑的。比如我们创建一个Person类,然后修改其原型上的属性,观察它的实例的变化:

function Person(name){this.name = name;
}
Person.prototype.age = 10;
var p1 = new Person('p1');
console.log(p1.age); //10
Person.prototype.age = 11;
console.log(p1.age); //11复制代码

  这是因为age存在于原型上,p1只是拥有一个指针指向原型,原型发生改变后,用p1.age访问该属性必然也跟着变化。

用原型实现继承

  用原型实现继承的思路非常简单,令构造函数的原型指向其父类的一个实例,这样父类中的属性和方法也就相当于被引用到了,调用起来和调用自己的一样。比如定义一个Programmer类继承自Person:

function Person(name){this.name = name;
}
Person.prototype.age = 10;function Programmer(name){this.name = name;
}
Programmer.prototype = new Person();
Programmer.prototype.constructor = Programmer;
var p1 = new Programmer('p1');
console.log(p1.age); //10复制代码

  可以看到Programmer的实例p1继承了Person的属性age。另外需要注意的就是constructor的修正。因为我们new一个Person对象出来,它的constructor指向自身的构造函数Person,所以在Programmer的原型中,这个constructor始终是Person,这与逻辑是不符的,所以必须显式的“纠正”一下这个副作用,让Programmer原型上的constructor指向自己。

  以上代码实现了一个基本的继承。但其中还是有不少可以扩展的地方,如果面试的时候只答出上面的这些,只能算是及格吧。关于如何优化继承的代码,有位大牛的文章分析的十分详细,出于篇幅原因我在本篇就不再陈述。直接贴上链接地址:www.cnblogs.com/sanshi/arch…,共六篇系列博客,非常详细。

----------------补充于2014.01.07---------------------

  在上面的继承实现方式中,有一个消耗内存的地方,就是为子类指定原型时需要new一个父类的对象,有人做了比较好的处理,今天看到了代码,据说是coffeescript中的,抄在这里:

var _hasProp = {}.hasOwnProperty;
var extends = function(child,parent){for(var key in parent){if(_hasProp.call(parent,key)){child[key] = parent[key];}}function ctor(){this.constructor = child;}ctor.prototype = parent.prototype;child.prototype = new ctor();child._super_ = parnet.prototype;return child;
}复制代码

  是一个完整的实现继承的方法。在内部创建了一个最小化的对象,减少内存消耗。

继承的另一种实现方式

  除了用原型,还有一种方式也可以实现继承,叫做类复制。怎么个复制法呢,看下面的代码:

function People(name){this.name = name;this.age = 11;this.getName = function(){return this.name;}
}function Worker(name){People.call(this,name);
}var w1 = new Worker('w1');
console.log(w1.getName()); //w1
console.log(w1.age); //11复制代码

  在People构造器中所有的属性和方法都用this关键字定义在了自身,而不是放在它的原型上。在子类Worker中,用call把People当作函数执行了一下,并传入this作为上下文对象。这样就相当于把People中的所有语句拿过来执行一次,所有属性的定义也都被复制过来了。同样可以实现继承。完全与原型无关。

  那么这种方式与原型继承有何区别呢?最大的区别就在于原型是一个引用,所有实例都引用一个共享的对象,每次创建出一个实例时,并不会复制原型的内容,只是用一个指针指过去。而类复制的方法不存在共有的东西,每创建一个对象都把构造器中的代码执行一次,当构造器中的方法较多时,会消耗很多的内存。而原型继承就不会了,只需一个指针指过去就完了。

  由这种工作方式产生的另一个区别就是动态修改,我们知道在原型继承中,只要修改了构造器原型中的值,实例对象也跟着变化。但是类复制就不能了,每个对象都有自己的一份数据,已创建出来的对象不会再受构造器的影响了。

  另外还有一点,就是属性的访问速度。类复制的方式,对象的属性都在自身,所以在查找的时候可以立即找到,而原型继承在查找的时候还得顺着原型链向上查找,其访问速度肯定不如类复制的快。

总结

  以上是我理解到的原型与继承的知识点,可能理解还是没有那么透彻,只是从比较浅的层次梳理了一下。与原型相关的知识还有很多有深度的,还有待于继续研究。这篇博客写完我也感觉到,写一篇基础知识分析的文章真是挺困难的,需要你对每一个细节都掌握清楚,生怕稍不注意就给别人误导。可能自己的水平也有待提高吧,本篇就先分析到这个程度,不知这个程度能否达到初级前端工程师的门槛。后续收集到了面试题,我会结合分析。

分类: javascript相关,前端面试题
本文转自吕大豹博客园博客,原文链接:http://www.cnblogs.com/lvdabao/p/3502944.html。

【面试必备】javascript的原型和继承相关推荐

  1. JavaScript的原型和继承

    目录 一.原型 1.原型的特点 2.原型链查找 原型链查找概念(重要) a)属性的遮蔽现象 b) 三种属性总结 c) 判定属性来自对象本身还是原型链 (d) 原型中的方法及其它类型的方法 3.原型链的 ...

  2. JavaScript原型与继承的秘密

    原文发布在dreamapplehappy/blog,本文如若有更新,都会在我的博客进行更新. 我们最想夸耀的事物,就是我们所未拥有的事物 <罗生门>- 芥川龙之介 JavaScript的原 ...

  3. Javascript 原型和继承(Prototypes and Inheritance)

    Javascript 原型和继承(Prototypes and Inheritance) 收藏  前面我们看到了如何使用 constructor 来初始化对象.如果这样做,那么每一个创建的新对象都会对 ...

  4. 【JS面试向】深入原型链之class的继承

    class 是如何实现继承的? 我相信时至今日,大部分同学看完题目都能很快的写出答案. 使用 ES 6 提供的类,能够很快的实现继承. class Parent {constructor() {thi ...

  5. JavaScript中的原型和继承

    请在此暂时忘记之前学到的面向对象的一切知识.这里只需要考虑赛车的情况.是的,就是赛车. 最近我正在观看 24 Hours of Le Mans ,这是法国流行的一项赛事.最快的车被称为 Le Mans ...

  6. JavaScript进阶学习(二)—— 基于原型链继承的js工具库的实现方法

    文章来源:小青年原创 发布时间:2016-07-03 关键词:JavaScript,原型链,jQuery类库 转载需标注本文原始地址: http://zhaomenghuan.github.io... ...

  7. JavaScript:原型链、继承

    1.理解原型对象 我们先使用构造函数创建一个对象: function Person() { } var person = new Person(); person.name = 'Kevin'; co ...

  8. JavaScript简餐——继承之原型链继承

    文章目录 前言 一.实现方式 二.继承实例 三.问题所在 1.引用值误修改 2.子类型实例化时无法给父类构造函数传参 四.总结 前言 写本<JavaScript简餐>系列文章的目的是记录在 ...

  9. javascript 原型链继承

    JavaScript中没有类的概念,只有一个构造函数来创建对象. 但是JavaScript也可以实现继承. 首先要说的是,JavaScript中的对象分为函数对象和普通对象. 何为函数对象?? 就是 ...

最新文章

  1. EIGRP 实验2: 邻居关系
  2. 代码质量与规范,那些年你欠下的技术债
  3. Touch Bar 废物利用系列 | 在触控栏上显示 Dock 应用图标
  4. 在线用户管理--ESFramework 4.0 进阶(05)
  5. .net core 使用Redis的发布订阅
  6. 国内maven仓库地址资源汇总
  7. php的完整代码块,超级实用的9个PHP代码片段
  8. VS2013编译最简单的PPAPI插件
  9. 汇编语言典型例子详解_汇编语言及编程实例(电子教案).pdf
  10. 企业微信在线客服机器人系统开发
  11. 汇编语言--寄存器间接寻址
  12. 【AutoCAD】04.直线类命令
  13. 【BZOJ4972】小Q的方格纸 前缀和
  14. 一个img文件-实验吧
  15. android 实现 bilili 动画播放效果
  16. Linux SD卡/SDIO驱动开发-dw_mci_probe
  17. Java虚拟机部分知识点
  18. 鼎捷t100架构_鼎捷T100 管理软件
  19. 冲激函数与冲激函数相乘与冲激函数对冲激函数卷积之间的区别
  20. 最简单的共享列表服务器KissLists

热门文章

  1. pdf屏幕取词 android,===C#屏幕取词Demo热键版鼠标划词版【含语音版】【附源码】===...
  2. ASP.NET 2.0 正式版中无刷新页面(客户端回调)的开发
  3. Nginx深入了解-基础(一)
  4. 全新 Hexo Material Design 主题 Mellow
  5. 如何附加被分离的质疑数据库?
  6. 迈向成功的关键在于执行(摘自李开复博士的《做最好的自己》)
  7. Xcode制作动态及静态Framework
  8. SQL语句练习(三)
  9. GridView 72 般绝技
  10. 【转载】Python 深入浅出支持向量机(SVM)算法