这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
这篇文章主要详细介绍了 JavaScript 的原型继承的相关资料,十分详细, 需要的朋友可以参考下
JavaScript 是一门面向对象的语言。在 JavaScript 中有一句很经典的话,万物皆对象。既然是面向对象的,那就有面向对象的三大特征:封装、继承、多态。这里讲的是 JavaScript 的继承,其他两个容后再讲。
JavaScript 的继承和 C++ 的继承不大一样,C++ 的继承是基于类的,而 JavaScript 的继承是基于原型的。
现在问题来了。
原型是什么?原型我们可以参照 C++ 里的类,同样的保存了对象的属性和方法。例如我们写一个简单的对象
- function Animal(name) {
- this.name = name;
- }
- Animal.prototype.setName = function(name) {
- this.name = name;
- }
- var animal = new Animal("wangwang");
我们可以看到,这就是一个对象 Animal,该对象有个属性 name,有个方法 setName。要注意,一旦修改 prototype,比如增加某个方法,则该对象所有实例将同享这个方法。例如
- function Animal(name) {
- this.name = name;
- }
- var animal = new Animal("wangwang");
这时 animal 只有 name 属性。如果我们加上一句,
- Animal.prototype.setName = function(name) {
- this.name = name;
- }
这时 animal 也会有 setName 方法。
继承本复制——从空的对象开始我们知道,JS 的基本类型中,有一种叫做 object,而它的最基本实例就是空的对象,即直接调用 new Object()生成的实例,或者是用字面量 {} 来声明。空的对象是 "干净的对象",只有预定义的属性和方法,而其他所有对象都是继承自空对象,因此所有的对象都拥有这些预定义的 属性与方法。原型其实也是一个对象实例。原型的含义是指:如果构造器有一个原型对象 A,则由该构造器创建的实例都必然复制自 A。由于实例复制自对象 A,所以实例必然继承了 A 的所有属性、方法和其他性质。那么,复制又是怎么实现的呢?方法一:构造复制每构造一个实例,都从原型中复制出一个实例来,新的实例与原型占用了相同的内存空间。这虽然使得 obj1、obj2 与它们的原型 "完全一致",但也非常不经济——内存空间的消耗会急速增加。如图:
方法二:写时复制这种策略来自于一致欺骗系统的技术:写时复制。这种欺骗的典型示例就是操作系统中的动态链接库(DDL),它的内存区总是写时复制的。如图:
我们只要在系统中指明 obj1 和 obj2 等同于它们的原型,这样在读取的时候,只需要顺着指示去读原型即可。当需要写对象(例如 obj2)的属性时,我们就复制一个原型的映像出来,并使以后的操作指向该映像即可。如图:
这种方式的优点是我们在创建实例和读属性的时候不需要大量内存开销,只在第一次写的时候会用一些代码来分配内存,并带来一些代码和内存上的开销。但此后就不再有这种开销了,因为访问映像和访问原型的效率是一致的。不过,对于经常进行写操作的系统来说,这种方法并不比上一种方法经济。方法三:读遍历这种方法把复制的粒度从原型变成了成员。这种方法的特点是:仅当写某个实例的成员,将成员的信息复制到实例映像中。当写对象属性时,例如(obj2.value=10)时,会产生一个名为 value 的属性值,放在 obj2 对象的成员列表中。看图:
可以发现,obj2 仍然是一个指向原型的引用,在操作过程中也没有与原型相同大小的对象实例创建出来。这样,写操作并不导致大量的内存分配,因此内存的使用上就显得经济了。不同的是,obj2(以及所有的对象实例)需要维护一张成员列表。这个成员列表遵循两条规则:保证在读取时首先被访问到如果在对象中没有指定属性,则尝试遍历对象的整个原型链,直到原型为空或或找到该属性。原型链后面会讲。显然,三种方法中,读遍历是性能最优的。所以,JavaScript 的原型继承是读遍历的。constructor 熟悉 C++ 的人看完最上面的对象的代码,肯定会疑惑。没有 class 关键字还好理解,毕竟有 function 关键字,关键字不一样而已。但是,构造函数呢?实际上,JavaScript 也是有类似的构造函数的,只不过叫做构造器。在使用 new 运算符的时候,其实已经调用了构造器,并将 this 绑定为对象。例如,我们用以下的代码
- var animal = Animal("wangwang");
animal 将是 undefined。有人会说,没有返回值当然是 undefined。那如果将 Animal 的对象定义改一下:
- function Animal(name) {
- this.name = name;
- return this;
- }
猜猜现在 animal 是什么? 此时的 animal 变成 window 了,不同之处在于扩展了 window,使得 window 有了 name 属性。这是因为 this 在没有指定的情况下,默认指向 window,也即最顶层变量。只有调用 new 关键字,才能正确调用构造器。那么,如何避免用的人漏掉 new 关键字呢?我们可以做点小修改:
- function Animal(name) {
- if(!(this instanceof Animal)) {
- return new Animal(name);
- }
- this.name = name;
- }
这样就万无一失了。构造器还有一个用处,标明实例是属于哪个对象的。我们可以用 instanceof 来判断,但 instanceof 在继承的时候对祖先对象跟真正对象都会返回 true,所以不太适合。constructor 在 new 调用时,默认指向当前对象。
- console.log(Animal.prototype.constructor === Animal); // true
我们可以换种思维:prototype 在函数初始时根本是无值的,实现上可能是下面的逻辑
// 设定__proto__是函数内置的成员,get_prototyoe() 是它的方法
- var __proto__ = null;
- function get_prototype() {
- if(!__proto__) {
- __proto__ = new Object();
- __proto__.constructor = this;
- }
- return __proto__;
- }
这样的好处是避免了每声明一个函数都创建一个对象实例,节省了开销。constructor 是可以修改的,后面会讲到。基于原型的继承继承是什么相信大家都差不多知道,就不秀智商下限了。
JS 的继承有好几种,这里讲两种
1. 方法一这种方法最常用,安全性也比较好。我们先定义两个对象
- function Animal(name) {
- this.name = name;
- }
- function Dog(age) {
- this.age = age;
- }
- var dog = new Dog(2);
要构造继承很简单,将子对象的原型指向父对象的实例(注意是实例,不是对象)
- Dog.prototype = new Animal("wangwang");
这时,dog 就将有两个属性,name 和 age。而如果对 dog 使用 instanceof 操作符
- console.log(dog instanceof Animal); // true
- console.log(dog instanceof Dog); // false
这样就实现了继承,但是有个小问题
- console.log(Dog.prototype.constructor === Animal); // true
- console.log(Dog.prototype.constructor === Dog); // false
可以看到构造器指向的对象更改了,这样就不符合我们的目的了,我们无法判断我们 new 出来的实例属于谁。因此,我们可以加一句话:
- Dog.prototype.constructor = Dog;
再来看一下:
- console.log(dog instanceof Animal); // false
- console.log(dog instanceof Dog); // true
done。这种方法是属于原型链的维护中的一环,下文将详细阐述。2. 方法二这种方法有它的好处,也有它的弊端,但弊大于利。先看代码
- <pre name="code" class="javascript">function Animal(name) {
- this.name = name;
- }
- Animal.prototype.setName = function(name) {
- this.name = name;
- }
- function Dog(age) {
- this.age = age;
- }
- Dog.prototype = Animal.prototype;
这样就实现了 prototype 的拷贝。
这种方法的好处就是不需要实例化对象(和方法一相比),节省了资源。弊端也是明显,除了和上文一样的问题,即 constructor 指向了父对象,还只能复制父对象用 prototype 声明的属性和方法。也即是说,上述代码中,Animal 对象的 name 属性得不到复制,但能复制 setName 方法。最最致命的是,对子对象的 prototype 的任何修改,都会影响父对象的 prototype,也就是两个对象声明出来的实例都会受到影响。所以,不推荐这种方法。
原型链
写过继承的人都知道,继承可以多层继承。而在 JS 中,这种就构成了原型链。上文也多次提到了原型链,那么,原型链是什么?一个实例,至少应该拥有指向原型的 proto 属性,这是 JavaScript 中的对象系统的基础。不过这个属性是不可见的,我们称之为 "内部原型链",以便和构造器的 prototype 所组成的 "构造器原型链"(亦即我们通常所说的 "原型链")区分开。我们先按上述代码构造一个简单的继承关系:
- function Animal(name) {
- this.name = name;
- }
- function Dog(age) {
- this.age = age;
- }
- var animal = new Animal("wangwang");
- Dog.prototype = animal;
- var dog = new Dog(2);
提醒一下,前文说过,所有对象都是继承空的对象的。所以,我们就构造了一个原型链:
我们可以看到,子对象的 prototype 指向父对象的实例,构成了构造器原型链。子实例的内部 proto 对象也是指向父对象的实例,构成了内部原型链。当我们需要寻找某个属性的时候,代码类似于
- function getAttrFromObj(attr, obj) {
- if(typeof(obj) === "object") {
- var proto = obj;
- while(proto) {
- if(proto.hasOwnProperty(attr)) {
- return proto[attr];
- }
- proto = proto.__proto__;
- }
- }
- return undefined;
- }
在这个例子中,我们如果在 dog 中查找 name 属性,它将在 dog 中的成员列表中寻找,当然,会找不到,因为现在 dog 的成员列表只有 age 这一项。接着它会顺着原型链,即. proto 指向的实例继续寻找,即 animal 中,找到了 name 属性,并将之返回。假如寻找的是一个不存在的属性,在 animal 中寻找不到时,它会继续顺着. proto 寻找,找到了空的对象,找不到之后继续顺着. proto 寻找,而空的对象的. proto 指向 null,寻找退出。
原型链的维护我们在刚才讲原型继承的时候提出了一个问题,使用方法一构造继承时,子对象实例的 constructor 指向的是父对象。这样的好处是我们可以通过 constructor 属性来访问原型链,坏处也是显而易见的。一个对象,它产生的实例应该指向它本身,也即是
- (new obj()).prototype.constructor === obj;
然后,当我们重写了原型属性之后,子对象产生的实例的 constructor 不是指向本身!这样就和构造器的初衷背道而驰了。我们在上面提到了一个解决方案:
- Dog.prototype = new Animal("wangwang");
- Dog.prototype.constructor = Dog;
看起来没有什么问题了。但实际上,这又带来了一个新的问题,因为我们会发现,我们没法回溯原型链了,因为我们没法寻找到父对象,而内部原型链的. proto 属性是无法访问的。于是,SpiderMonkey 提供了一个改良方案:在任何创建的对象上添加了一个名为__proto__的属性,该属性总是指向构造器所用的原型。这样,对任何 constructor 的修改,都不会影响__proto__的值,就方便维护 constructor 了。
但是,这样又两个问题:
__proto__是可以重写的,这意味着使用它时仍然有风险
__proto__是 spiderMonkey 的特殊处理,在别的引擎(例如 JScript)中是无法使用的。
我们还有一种办法,那就是保持原型的构造器属性,而在子类构造器函数内初始化实例的构造器属性。
代码如下:改写子对象
- function Dog(age) {
- this.constructor = arguments.callee;
- this.age = age;
- }
- Dog.prototype = new Animal("wangwang");
这样,所有子对象的实例的 constructor 都正确的指向该对象,而原型的 constructor 则指向父对象。虽然这种方法的效率比较低,因为每次构造实例都要重写 constructor 属性,但毫无疑问这种方法能有效解决之前的矛盾。ES5 考虑到了这种情况,彻底的解决了这个问题:可以在任意时候使用 Object.getPrototypeOf() 来获得一个对象的真实原型,而无须访问构造器或维护外部的原型链。因此,像上一节所说的寻找对象属性,我们可以如下改写:
- function getAttrFromObj(attr, obj) {
- if(typeof(obj) === "object") {
- do {
- var proto = Object.getPrototypeOf(dog);
- if(proto[attr]) {
- return proto[attr];
- }
- }
- while(proto);
- }
- return undefined;
- }
当然,这种方法只能在支持 ES5 的浏览器中使用。为了向后兼容,我们还是需要考虑上一种方法的。更合适的方法是将这两种方法整合封装起来,这个相信读者们都非常擅长,这里就不献丑了。
来源: http://www.phperz.com/article/17/0412/272740.html