你所在位置:首页Javascript开发 → JavaScript继承详解(二)

JavaScript继承详解(二)

发布时间:2019-06-12

重点介绍JavaScript中几个重要的属性(this、constructor、prototype),这些属性对于我们理解如何实现JavaScript中的类和继承起着至关重要的作用。
this
this表示当前对象,如果在全局作用范围内使用this,则指代当前页面对象windows; 如果在函数中使用this,则this指代什么是根据运行时此函数在什么对象上被调用。 我们还可以使用apply和call两个全局方法来改变函数中this的具体指向。
先看一个在全局作用范围内使用this的例子:

<scripttype="text/javascript"> console.log(this === window);
// trueconsole.log(window.alert === this.alert);
// true console.log(this.parseInt("021",
10));
//
10 </script>
函数中的this是在运行时决定的,而不是函数定义时,如下:

// 定义一个全局函数 function foo() { console.log(this.fruit);
} //定义一个全局变量,等价于window.fruit = "apple";
var fruit = "apple";
//此时函数foo中this指向window对象
// 这种调用方式和window.foo();是完全等价的 foo();
// "apple"
//自定义一个对象,并将此对象的属性foo指向全局函数foo var pack = { fruit: "orange", foo: foo };
//此时函数foo中this指向window.pack对象pack.foo();
// "orange"
全局函数apply和call可以用来改变函数中this的指向,如下:

// 定义一个全局函数 function foo() { console.log(this.fruit);
} //定义一个全局变量 var fruit = "apple";
//自定义一个对象 var pack = { fruit: "orange" };
//等价于window.foo(); foo.apply(window);
// "apple"
//此时foo中的this ===pack foo.apply(pack);
 // "orange"
注:apply和call两个函数的作用相同,唯一的区别是两个函数的参数定义不同。
因为在JavaScript中函数也是对象,所以我们可以看到如下有趣的例子:

// 定义一个全局函数 function foo() { if (this === window) { console.log("this iswindow.");
}
} //函数foo也是对象,所以可以定义foo的属性boo为一个函数foo.boo = function() { if (this === foo) {console.log("this is foo.");
} else if (this === window) {console.log("this is window.");
} };
 //等价于window.foo();
foo();
// this is window.
//可以看到函数中this的指向调用函数的对象 foo.boo();
// this is foo.
//使用apply改变函数中this的指向 foo.boo.apply(window);
// this is window.
prototype
我们已经在第一章中使用prototype模拟类和继承的实现。 prototype本质上还是一个JavaScript对象。 并且每个函数都有一个默认的prototype属性。
如果这个函数被用在创建自定义对象的场景中,我们称这个函数为构造函数。 比如下面一个简单的场景:

// 构造函数 function Person(name) { this.name = name; }
//定义Person的原型,原型中的属性可以被自定义对象引用 Person.prototype = { getName: function() { returnthis.name;
} }
var zhang = new Person("ZhangSan");console.log(zhang.getName());
// "ZhangSan"
作为类比,我们考虑下JavaScript中的数据类型 - 字符串(String)、数字(Number)、数组(Array)、对象(Object)、日期(Date)等。
我们有理由相信,在JavaScript内部这些类型都是作为构造函数来实现的,比如:

// 定义数组的构造函数,作为JavaScript的一种预定义类型 function Array() {
// ... }
//初始化数组的实例
var arr1 = new Array(1, 56, 34, 12);
//但是,我们更倾向于如下的语法定义:
var arr2 = [1, 56, 34, 12];
同时对数组操作的很多方法(比如concat、join、push)应该也是在prototype属性中定义的。
实际上,JavaScript所有的固有数据类型都具有只读的prototype属性(这是可以理解的:因为如果修改了这些类型的prototype属性,则哪些预定义的方法就消失了), 但是我们可以向其中添加自己的扩展方法。

// 向JavaScript固有类型Array扩展一个获取最小值的方法
Array.prototype.min = function() { var min = this[0];
for (var i = 1;
i< this.length;
i++) { if (this[i] < min) { min = this[i];
} } return min;};
//在任意Array的实例上调用min方法console.log([1, 56, 34, 12].min());
// 1
注意:这里有一个陷阱,向Array的原型中添加扩展方法后,当使用for-in循环数组时,这个扩展方法也会被循环出来。
下面的代码说明这一点(假设已经向Array的原型中扩展了min方法):
var arr =[1, 56, 34, 12];
var total = 0; for (var i in arr) { total += parseInt(arr[i],10);
} console.log(total);
// NaN
解决方法也很简单:
var arr =[1, 56, 34, 12]; var total = 0; for (var i in arr) { if (arr.hasOwnProperty(i)){ total += parseInt(arr[i], 10); } } console.log(total); // 103
constructor
constructor始终指向创建当前对象的构造函数。比如下面例子:
// 等价于
var foo = new Array(1, 56, 34, 12);
var arr = [1, 56, 34, 12];console.log(arr.constructor === Array);
// true //等价于 var foo = new Function();
var Foo = function() { };
console.log(Foo.constructor === Function);
// true
//由构造函数实例化一个obj对象 var obj =new Foo();
console.log(obj.constructor === Foo);
// true
//将上面两段代码合起来,就得到下面的结论 console.log(obj.constructor.constructor === Function);
// true
但是当constructor遇到prototype时,有趣的事情就发生了。
我们知道每个函数都有一个默认的属性prototype,而这个prototype的constructor默认指向这个函数。如下例所示:
functionPerson(name) { this.name = name;
};
Person.prototype.getName = function() {return this.name; };
var p = new Person("ZhangSan");
console.log(p.constructor === Person);
// trueconsole.log(Person.prototype.constructor === Person);
// true
// 将上两行代码合并就得到如下结果 console.log(p.constructor.prototype.constructor === Person);
// true
当时当我们重新定义函数的prototype时(注意:和上例的区别,这里不是修改而是覆盖),constructor的行为就有点奇怪了,如下示例:
functionPerson(name) { this.name = name;
};
 Person.prototype = { getName: function() {return this.name;
}
};
var p = new Person("ZhangSan");
console.log(p.constructor === Person);
// falseconsole.log(Person.prototype.constructor === Person);
// falseconsole.log(p.constructor.prototype.constructor === Person);
// false
为什么呢?
原来是因为覆盖Person.prototype时,等价于进行如下代码操作:
Person.prototype= new Object({ getName: function() { return this.name;
} });
而constructor始终指向创建自身的构造函数,所以此时Person.prototype.constructor=== Object,即是:
functionPerson(name) { this.name = name;
};
Person.prototype = { getName: function() {return this.name;
} };
var p = new Person("ZhangSan");
console.log(p.constructor === Object);
// trueconsole.log(Person.prototype.constructor === Object);
// trueconsole.log(p.constructor.prototype.constructor === Object);
// true
怎么修正这种问题呢?方法也很简单,重新覆盖Person.prototype.constructor即可:
functionPerson(name) { this.name = name;
};
Person.prototype = new Object({ getName:function() { return this.name;
} });
Person.prototype.constructor = Person;
varp = new Person("ZhangSan"); console.log(p.constructor === Person);
//true console.log(Person.prototype.constructor === Person);
// trueconsole.log(p.constructor.prototype.constructor === Person);
// true
下一章我们将会对第一章提到的Person-Employee类和继承的实现进行完善。
JavaScript中的对象、函数和继承
前一段时间在看Extjs的源代码,起初打算从他的Widget开始看起,因为想借鉴一下 并应用到自己的代码中,但是看了一段时间发现很难进去,主要因为对他的整体如何组织对象和事件的方式不是很清楚。所以还是耐下性子从最基础的开伎雌穑裁词荅xtjs的基础,可以说是他的Ext.extend函数,因为之后的各个wdiget的扩展都用它来实现的。但是起初发现他的内容并不是那么 容易就可以看明白的,有的时候觉得自己看明白了,但是再多问自己一个为什么,可能又答不上来了。
这个时候正好碰到一本很不错的讲JS原理书,周爱民的《Javascript语 言精粹与实践》,如获至宝,赶紧买来阅读。在阅读的过程中又在我面前蹭蹭蹭,出现了几本很不错的js书籍,在这里也向大家推荐一下,一本是有 Yahoo的js大牛Douglas Crockeord(后文称之为老道)所著,由淘宝UED的和秦歌翻译的 《Javascript语言精粹》,一本是《Javascript设计模式》。最早我读的这两本书都是英文版的,所以还被第二本书中的一些观点给误导了, 但是幸好看到第二本书的译者很负责任,在批注中都已经修正过来了。最后一本,其实不是书,而是《ECMAScript Language Specification》,俗称ECMA-262,有两个值得看的版本,一个是第三版,一个是第五版;现在的大部分js实现都是基于第三版的规范,但 是在有些问题上,第五版描述的更加清晰一些。废话少说,进入正题。
1、 Javascript中的对象
JavaScript可以说是一个基于对象的编程语言,为什么说是基于对象而不是面向对象,因为JavaScript自身只实现了封装,而没有实现继承和多态。既然他是基于对象的,那么我们就来说说js中的对象。有人说js中所有的都是对 象,这句话不完全正确。正确的一方是他强调了对象在js中的重要性,对象在js中无处不在,包括可以构造对象的函数本身也是对象。但是另一方面,js中也 有一些简单的数据类型,包括数字、字符串和布尔值、null值和undefined值,而这些不是对象。那为什么这些类型的值不是对象呢,毕竟他们也有方 法。那让我们来看一下,JavaScript中对于对象的定义,有两种定义。
(1)JavaScript中的对象是可变的键控集合(keyed collections) (此定义来自老道的那本书的第三章)
(2)JavaScript 中的对象是无序(unordered)的属性集合,这些属性可以含有简单的数据类型、对象、函数;保存在一个对象属性中的函数也被称为这个对象的方法。(来自ECMA-262 的4.3.3)(注:这里所说的属性是可以在js脚本中创建和访问的(我们称之为显性属性),不包括为对象自动分配的内部属性)
那为什么那个简单的数据类型不是对象呢,主要是因为这些数据类型的值中拥有的方法是不可变的,而一个对象的属性是应当可以被改变的。
2、 对象中的原型链[[proto]]
JavaScript中的每个对象创建的时候系统都会自动为其分配一个原型属性 [[proto]],用来连接到他的原型对象。在JavaScript中就是通过每个对象中的[[proto]]来实现对象的继承关系的。但是对象的 [[proto]]属性在JavaScript是不能访问和修改的,他是作为一个内部的属性存在的,而且是在对象被创建的同时由系统自动设定的。
当访问一个对象的某一属性,如果这个属性在此对象中不存在,就在他的[[proto]]所指的原型对象的属性中寻找,如果找到则返回,否则继续沿着[[proto]]链一直找下去,直到[[proto]]的连接为null的时候停止。
3、 函数也是对象
JavaScript中的函数本身就是一个对象(所以我们经常称之为函数对象),而且可以说他是js中最重要的对象。之所以称之为最重要的对象,一方面他可以扮演像其他语言中的函数同样的角色,可以被调用,可以被传入参数;另一方面他还被作为对象的构造器(constructor)来使用,可以结合new操作符来创建对象。
既然函数就是对象,所以必然含有对象拥有的全部性质,包括对象在创建时设定的原型链[[proto]]属性。
让我们来看看函数对象和普通对象有什么区别。我们前面说过,对象就是无序的属性集合,那么 函数的属性和普通对象的属性有什么不同呢。根据ECMA-262中的13.2节所述,在函数对象创建时,系统会默认为其创建两个属性[[call]]和 [[constructor]],当函数对象被当做一个普通函数调用的时候(例如myFunc()),“()”操作符指明函数对象的[[call]]属性 就被执行,当他被当做一个构造器被调用的时候(例如new myConst()),他的[[constructor]]属性就被执行,[[cosntructor]]的执行过程我们将在下一节中介绍。除此之外,当 一个函数被创建时,系统会默认的再为其创建一个显示属性prototype,并为其赋值为
this.prototype = {constructor:this}
具体内容可以参加老道的那本书的第五章。这个函数对象的prototype属性也是为了 js把函数当做构造器来实现继承是准备的,但是这个属性是可以在js脚本中访问和修改的。在这里要强调的一点是,大家一定要区分对象中的 [[proto]]属性和函数对象中的prototype属性,我在刚开始学习的时候就是因为没有很好的区分这两个东西,走了很多的弯路。
4、 对象的创建
在js中有两种创建对象的方法,一种是通过字面量来实现,如
[javascript]
1. var Person = { 
2. “first_name”:’liang’, 
3. ‘last_name’:’yang’ 
4. } 
另一种方法是通过构造器来创建
[javascript]
1. var my = new Person(‘liang’,’yang’); 
其实第一种方式的创建过程相当于调用Object构造器来实现,如下。
var Person = new Object();
Person.first_name = ‘liang’;
Person.last_name = ‘yang’
所以我们可以把js中所有对象的创建都合并到使用构造器来实现,下面我么来详细说明构造器创建对象的过程:
第一步,先创建一个空的对象(既没有任何属性),并将这个对象的[[proto]]指向这个构造器函数的prototype属性对象
第二步,将这个空的对象作为this指针传给构造器函数并执行
第三步,如果上面的函数返回一个对象,则返回这个对象,否则返回第一步创建的对象
第四步,把函数当做一个类来使用
由上面的步骤我们可以看出,一般来说函数对象的prototype指向的是一个普通对象,而不是一个函数对象,这个普通对象中的属在由此函数构造器创建的对象中也可以访问。由此可以如此设计我们的代码,假设一个函数就可以代表一个类,这个构造 器函数生成的对象就是这个类的实例对象,那么实例对象中应有的属性和方法应该放在这个构造器函数的prototype中,这个类的静态方法就可以直接放到 这个函数作为对象的属性中,最后这个函数体就是我们平时在面向对象语言中所说的构造函数(在这里我们要区分连个词“构造函数”和“构造器函数”,所谓构造 函数是指普通的面向对象语言中的类的构造函数,而构造器函数是指javascript中的一个函数被当做构造器使用)。
在第3节我们说过每个函数的prototype对象中总是含有一个constructor 属性,这个属性就是连接到我们的这个函数本身。再加之,有这个函数生成的每个对象的[[proto]]属性都是指向构造器函数的prototype对象, 所以通过[[proto]]链,每个由构造器函数生成的对象,都有一个constructor属性,指向生成他的构造器函数,因此我们可以通过这个属性来判断这个对象是有哪个构造器函数生成的。
5、 函数继承(类继承)
说了这么多,终于到了我们可以在javascript中讨论继承的时候了,我们先来考虑一下要实现类的继承我们都要做些什么,假设我们要从superClass继承到子类subClass
为了使得由subClass生成的对象中能够访问superClass生成的对象中的属性,那么可以使subClass.prototype为一个superClass构造函数生成的对象。
subclass.prototye = new superClass();
但 是问题来了,根据我们在第4节说的new superClass()不仅复制了superClass.prototype中的所有方法,而且还运行了superClass()这个函数,这个函数起 到的作用是类中的构造函数。我们知道应该在子类的构造函数中调用父类的构造函数来实现初始化。为此我们可以创建一个构造函数为空的,但是原型和 superClass原型一致的函数,并使subClass.prototype指向这个函数生成的对象。
var F = function() {};
F.prototype = superClass.prototype;
subClass.protptype = new F();
这样我们就可以再不调用构造函数的同时完成属性复制的工作。但是还有一个问题,那就是我们修改了subClass的prototype属性,从而也就删除了其中的constructor属性,这样我们就无法知道他是哪个构造器函数生成的对象了。我们可以再次给他赋值
subClass.prototype.constructor = subClass;
这样复制属性的问题就迎刃而解了。但是新的问题又出现了,在subClass中我们无法知道他的父类是哪个构造器函数,所以就无法在构造函数中调用父类的构造函数,为此我们可以为subClass添加一个属性,指明他的父类
subClass.superClass = superClass.prototype;
这样我么就可以在子类的构造函数中使用subClass.superClass.constructor来访问父类的构造函数了。最后我们把以上的思路写成一个函数
[javascript]
1. myPro.extend = function(subClass, superClass) { 
2.     var F =function() {}; 
3. F.prototype = superClass.prototype; 
4. subClass.protptype = new F(); 
5. subClass.prototype.constructor = subClass; 
6. subClass.superClass = superClass.prototype; 
7. superClass.prototype.constructor = superClass; 
8. } 

 

 

摘自 hanzhou4519


上一篇:js面向对象的属性和方法
下一篇:JavaScript继承详解(一)