【面试必备】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; //true s.hasOwnProperty('charAt'); //false
s.__proto__.hasOwnProperty('charAt'); //true
要明白这个原型指针到底指向什么,就需要明白对象是如何创建出来的,所以接下来有必要了解一下构造器函数。
javascript中没有类,但可以把函数当类使,被用来当做类构造器的函数就叫构造器函数,一般把首字母大写来与普通函数进行区别,其实就是猪鼻子插根葱而已——装象。js中一切都是对象,所以函数也是对象,所以函数也有一个原型指针。与实例对象不同的是,函数这种特殊的对象,它的原型可以通过prototype属性显式的访问到,来看看String类的原型是啥样的:

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

这是什么原因呢?我们就要细究一下var s = new String('s');在执行的时候到底发生了什么,其实就是用new关键字调用函数String的时候发生了什么:
- 创建一个空对象obj,即Object的一个实例
- 把这个空对象obj绑定到函数的上下文环境中,相当于把this指向了obj
- 执行函数,这个过程就把函数中的属性、方法拷贝到了obj中
- 将obj的原型指向函数的prototype属性
- 返回这个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); //
Person.prototype.age = 11;
console.log(p1.age); //
这是因为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); //
可以看到Programmer的实例p1继承了Person的属性age。另外需要注意的就是constructor的修正。因为我们new一个Person对象出来,它的constructor指向自身的构造函数Person,所以在Programmer的原型中,这个constructor始终是Person,这与逻辑是不符的,所以必须显式的“纠正”一下这个副作用,让Programmer原型上的constructor指向自己。
以上代码实现了一个基本的继承。但其中还是有不少可以扩展的地方,如果面试的时候只答出上面的这些,只能算是及格吧。关于如何优化继承的代码,有位大牛的文章分析的十分详细,出于篇幅原因我在本篇就不再陈述。直接贴上链接地址:http://www.cnblogs.com/sanshi/archive/2009/07/08/1519036.html,共六篇系列博客,非常详细。
----------------补充于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); //
在People构造器中所有的属性和方法都用this关键字定义在了自身,而不是放在它的原型上。在子类Worker中,用call把People当作函数执行了一下,并传入this作为上下文对象。这样就相当于把People中的所有语句拿过来执行一次,所有属性的定义也都被复制过来了。同样可以实现继承。完全与原型无关。
那么这种方式与原型继承有何区别呢?最大的区别就在于原型是一个引用,所有实例都引用一个共享的对象,每次创建出一个实例时,并不会复制原型的内容,只是用一个指针指过去。而类复制的方法不存在共有的东西,每创建一个对象都把构造器中的代码执行一次,当构造器中的方法较多时,会消耗很多的内存。而原型继承就不会了,只需一个指针指过去就完了。
由这种工作方式产生的另一个区别就是动态修改,我们知道在原型继承中,只要修改了构造器原型中的值,实例对象也跟着变化。但是类复制就不能了,每个对象都有自己的一份数据,已创建出来的对象不会再受构造器的影响了。
另外还有一点,就是属性的访问速度。类复制的方式,对象的属性都在自身,所以在查找的时候可以立即找到,而原型继承在查找的时候还得顺着原型链向上查找,其访问速度肯定不如类复制的快。
总结
以上是我理解到的原型与继承的知识点,可能理解还是没有那么透彻,只是从比较浅的层次梳理了一下。与原型相关的知识还有很多有深度的,还有待于继续研究。这篇博客写完我也感觉到,写一篇基础知识分析的文章真是挺困难的,需要你对每一个细节都掌握清楚,生怕稍不注意就给别人误导。可能自己的水平也有待提高吧,本篇就先分析到这个程度,不知这个程度能否达到初级前端工程师的门槛。后续收集到了面试题,我会结合分析。
【面试必备】javascript的原型和继承的更多相关文章
- 深入浅出JavaScript之原型链&继承
Javascript语言的继承机制,它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instanc ...
- javascript的原型和继承(1)
原型与继承是javascript中基础,重要而相对比较晦涩难解的内容.在图灵的网上看到一篇翻译过的文章,有参考了一些知名博客.我自己总结了几篇.通过这次的总结,感觉自己对原型和继承的认识又增加了很多, ...
- JavaScript的原型链继承__propt__、prototype、constructor的理解、以及他们之间相互的关系。
回想自己已经工作了有一段时间了,但是自己对JavaScript的原型链.和继承的理解能力没有到位,最近他们彻底的整理并且复习了一遍. 本案例中部分文案来自网络和书籍,如有侵权请联系我,我只是把我的理解 ...
- JavaScript 面向对象 原型(prototype) 继承
1.对象的概念:无需属性的集合,属性可以为数值,对象或函数,ECMAscript中没有类的概念,这点是javascript与其他面向对象(OO)语言不同的地方. //创建一个自定义对象 var per ...
- JavaScript基于原型的继承
在一个纯粹的原型模式中,我们会摒弃类,转而专注于对象,基于原型的继承相比基于类的继承的概念上更为简单 if( typeof Object.beget !== 'function') { Object. ...
- javascript的原型与继承(2)
这是上一篇的后续. Javascript是一种基于对象的语言,遇到的所有东西几乎都是对象.如果我们想要把属性和方法封装成一个对象,应该怎么做呢: 假设我们把猫看成一个对象: var Cat = { n ...
- javascript高级:原型与继承
原型继承的本质就是一条原型链,对象会沿着这条链,访问链里的方法属性. 对象的__proto__属性就是用于访问它的原型链的上一层: 考虑以下对象: 1. 所有对象的原型: Object.prototy ...
- 🍓JavaScript 对象原型链继承的弊端 🍓
- javascript中继承(一)-----原型链继承的个人理解
[寒暄]好久没有更新博客了,说来话长,因为我下定决心要从一个后台程序员转为Front End,其间走过了一段漫长而艰辛的时光,今天跟大家分享下自己对javascript中原型链继承的理解. 总的说来, ...
随机推荐
- 123. Best Time to Buy and Sell Stock (三) leetcode解题笔记
123. Best Time to Buy and Sell Stock III Say you have an array for which the ith element is the pric ...
- 一起买beta版UI测试
一起买beta版UI测试 测试目的 保证代码质量,对各个单元进行测试,可以有效地保证代码的可靠性,让模块在与别的模块整合时出现更少的错误. UI测试 登录模块测试 登录模拟过程. 发帖模块测试 ...
- 一个URL链接到一个页面发生了什么?
最开始觉得这是一个很难理解的东西,后来看了很多人写的发现并没有那么难理解,本人只是一个学渣有什么说的不对的地方希望大家能够指出来! 一个URL从我们输入的那一刻起,到返回页面到底发生了什么呢? URL ...
- OVER 分析函数
over不能单独使用,要和分析函数:rank(),dense_rank(),row_number(),ntile ,sum(),avg()等一起使用. rank,dense_rank,row_numb ...
- 1075 PAT Judge (25)
排序题 #include <stdio.h> #include <string.h> #include <iostream> #include <algori ...
- DOM查找元素
1. 查找元素5种: 1. 按id查找1个元素对象: var elem=document.getElementById("id值"); 何时使用:1. 元素必须有id 2. 精确查 ...
- LeetCode(124) Binary Tree Maximum Path Sum
题目 Given a binary tree, find the maximum path sum. For this problem, a path is defined as any sequen ...
- Win7 64位 VS2015环境使用SDL2-2.0.4
之前在VS中使用SDL2,如果只链接SDL2.lib,会提示 error LNK2019: unresolved external symbol _main referenced in functio ...
- SIGGRAPH2016【转】
本文摘自:http://blog.selfshadow.com/ Open Access SIGGRAPH 2016 Conference Content (for a limited time) R ...
- Android 文章列表
Android --列表-- Android(1)-Handler Looper Message MessageQueuehttp://www.cnblogs.com/TS-qrt/articles ...