Javascript 语言是符合面向对象思想的。一般来说,面向对象思想需要满足以下三个基本要求:
这里的关键问题就是 Javascript 的原型继承机制到底是个啥玩意?
有很大的可能性,Javascript 在设计之初根本就没有考虑那么复杂。啥玩意面向对象思想,跟我有半毛钱关系啊。公司就给我两周时间,连设计带编码,我当然是怎么简单怎么来了。如果说一门计算机语言一定要满足唯一的一个最最基本的设计思想,那一定不是面向对象,而应该是更简单的:语言应支持功能的复用。如果一个对象功能不够用了,那就再叫个帮手呗,于是 Javascript 硬性规定:任何对象都必须有一个原型对象。这下好了,所有的 Javascript 对象都是自带秘书的,自己搞不定的就交给秘书去搞,秘书再搞不定的,就交给秘书的秘书去搞,这样一路交接过去,直到彻底搞不定了,那就只好报错。这就是 Javascript 中的原型链检索机制,是不是超简单?
因为对象的原型是 Javascript 对象的基本构件,那么首要的问题就是如何得到对象的原型对象?大体来说有以下两种方法:
说了这么多,来一段测试代码吧:
- test("得到对象的原型", function() {
- var empty = {};
- assert(empty.__proto__ === Object.getPrototypeOf(empty),
- "__proto__和Object.getPrototypeOf的等效性");
- });
这个测试是说那两种得到对象原型对象的方法是一致的,为了简单,以下采取第一种方式。
再测试一下对象的原型链:
- test("对象的原型链", function() {
- var p = {};
- var pp = p.__proto__;
- var ppp = pp.__proto__;
- var q = new Object();
- var qp = q.__proto__;
- var qpp = qp.__proto__;
- assert(pp === qp, "Object.__proto__");
- assert(ppp === qpp && ppp === null,
- "Object.__proto__.__proto__");
- });
这段测试需要我们注意以下几点:
既然函数也是对象,那么我们来测试一下函数的原型链:
- test("函数的原型链", function() {
- function p() {};
- var pp = p.__proto__;
- var ppp = pp.__proto__;
- function q() {};
- var qp = q.__proto__;
- var qpp = qp.__proto__;
- assert(pp === qp, "Function.__proto__");
- assert(ppp === qpp, "Function.__proto__.__proto__");
- assert(ppp === {}.__proto__,
- "Function.__proto__.__proto__ === Object.__proto__");
- });
这段测试需要我们注意以下几点:
总有一些对象是类似的,例如每个 person 对象中都有 name 和 age 属性,我们可以创建很多的类似的 person1,person2 等等,我们希望能够把这种类似的对象的创建过程复用起来,直接封装为一个函数无疑是最简单直接的:
- test("构造函数的引入", function() {
- function Person(name, age) {
- this.name = name;
- this.age = age;
- this.say = function() {
- return this.name + " " + this.age;
- };
- }
- var person1 = {};
- Person.call(person1, "wgs", 18);
- assert(person1.name === "wgs"
- && person1.age === 18
- && person1.say() === "wgs 18",
- "person1");
- var person2 = {};
- Person.call(person2, "www", 28);
- assert(person2.name === "www"
- && person2.age === 28
- && person2.say() === "www 28",
- "person2");
- });
利用函数的知识可以很容易的解决这个问题。类似的我们还可以搞出 Animal 函数,Student 函数等等。但是这里的问题是这样创建的任何对象都是 Object 类型的,我们的需求是最好 Person 函数创建的对象是 Person 类型的,Animal 函数创建的对象是 Animal 类型的,Student 函数创建的对象是 Student 类型的等等。这就要求每个函数对象必须有个函数描述对象。于是 Javascript 硬性规定:任何函数都必须有一个描述对象。这下就热闹了,当你你定义一个函数的时候,你以为你只搞出了一个函数对象,实际上却是两个对象,一个函数对象,一个函数描述对象。后者是强制附送的,这情形就好比是说本来公司只打算招收了 10 位员工的,没想到国家法律规定,必须夫妻俩一起招,于是于是一下子来了 20 位员工。这里的好消息是我们不用自己手动创建函数的描述对象,每当 Javascript 引擎解析完一个函数后,它会在创建函数对象的同时,自动创建好函数的描述对象。
那么最直接的问题来了,怎么得到函数的描述对象?Javascript 是这样规定的:
很多翻译把函数的描述对象翻译为原型对象,对象的原型对象,函数的原型对象,又一个函数的原型对象,这要把人绕晕吗?我建议就直接用__proto__对象和 prototype 对象区分一下吧,两者根本不是一回事,尽管有时候,两者可以是同一个对象。
所有的 prototype 对象也是有__proto__对象的,很简单,那就是前面反复提到过的 Object.__proto__对象。
有人注意到一个有趣的现象没有?Javascript 这么搞实际上是把函数和类型合二为一了,有一个函数就有一个类型,有一个类型就有一个函数。这下子玩得有点大啊,想想在一般面向对象语言中,我们定义一个类(在 Javascript 中其实它也是一个函数),我们为这个类定义几个方法(在 Javascript 中其实它们也是几个类型)。Javascript 的内存使用率和运行效率肯定不如 C++,C#,Java 这些语言,但是毫无疑问,Javascript 的灵活性,那是相当的高。
于是我们熟知的 Javascript 中的那几个内置对象类型:Object、Array,Function,Regexp,Date,Error 其实都是函数。那几个包装函数 Number,String,Boolean 其实也可以看做是内置对象类型。这样我们就很容易理解 Object.__proto__其实和 Object 函数是一对,Function.__proto__其实和 Function 函数是一对。
到现在为止,我们就没有必要再纠结 Object.__proto__,Function.__proto__这种奇怪的命名了,其实它们就是 Object.prototype 和 Function.prototype
综上所述,我们得到了下面的图:
这就是那张号称把无数 Javascript 初学者绕晕的经典图。现在看起来是不是很清晰了呢?
再补点简单的测试吧:
- test("函数的prototype", function() {
- function Person() {};
- assert(Person.prototype.constructor === Person,
- "Person.prototype.constructor === Person");
- assert(Person.prototype.__proto__ === {}.__proto__,
- "Person.prototype.__proto__ === Object.__proto__");
- });
还回到引入函数 prototype 对象的最初问题上来,如何实现 Person 函数创建的对象是 Person 类型的,Animal 函数创建的对象是 Animal 类型的,Student 函数创建的对象是 Student 类型的等等?道理上基本都懂了,现在欠缺只是一点编码实践,于是我们来一个 new 关键字的模拟:
- test("模拟构造函数", function() {
- function _new(Fun) {
- // 创建一个新对象
- var this_ = {};
- // 除了类型参数以外,我们需要打包构造函数的参数
- var args = Array.prototype.slice.call(arguments, 1);
- // 调用构造函数
- Fun.apply(this_, args);
- // 将函数的prototype赋值给对象对的__proto__
- this_.__proto__ = Fun.prototype;
- // 返回创建的新对象
- return this_;
- }
- function Person(name, age) {
- this.name = name;
- this.age = age;
- }
- var person1 = new Person("wgs", 18);
- var person2 = _new(Person, "wgs", 18);
- assert(JSON.stringify(person1.__proto__) === "{}",
- "person1.__proto__ is {}");
- assert(person1.__proto__.constructor === Person,
- "person1.__proto__.constructor is Person");
- assert(person1.constructor === Person,
- "person1.constructor is Person");
- assert(person1.name === "wgs" && person1.age === 18,
- "person1 name and age");
- assert(JSON.stringify(person2.__proto__) === "{}",
- "person2.__proto__ is {}");
- assert(person2.__proto__.constructor === Person,
- "person2.__proto__.constructor is Person");
- assert(person2.constructor === Person,
- "person2.constructor is Person");
- assert(person2.name === "wgs" && person2.age === 18,
- "person2 name and age");
- });
new 和_new 只是调用语法上稍微有些不同,但结果其实是一模一样的。这下对构造函数的调用原理也不用纠结了吧,源码在那里,一共也没有几行,还不清楚的回去再多看几遍。
再回头看看那张经典的图,在不考虑继承的情况下,我们对类型的扩充其实就在 prototype 对象上
这里我们需要把所有内置对象类型的 prototype 显示出来,以验证我们的想法:
- asserts();
- function testCase(desc, proto) {
- // 得到prototype的所有属性名称
- var names = Object.getOwnPropertyNames(proto);
- for (var i = 0; i < names.length; i++) {
- var name = names[i];
- // 因为Function的arguments和caller不可访问,
- // 所以需要过滤掉这两个属性
- if (name == "arguments" || name == "caller") {
- continue;
- }
- // 根据名称得到属性的值
- var value = proto[name];
- // 显示属性及其内容
- assert(true, "{0} : {1}".fmt(name, _varDesc(value)));
- }
- log(desc, proto);
- }
- test("Object.prototype",
- function() {
- testCase("Object.prototype:\n", Object.prototype);
- });
- test("Array.prototype",
- function() {
- testCase("Array.prototype:\n", Array.prototype);
- });
- test("Function.prototype",
- function() {
- testCase("Function.prototype:\n", Function.prototype);
- });
- test("RegExp.prototype",
- function() {
- testCase("RegExp.prototype:\n", RegExp.prototype);
- });
- test("Date.prototype",
- function() {
- testCase("Date.prototype:\n", Date.prototype);
- });
- test("Error.prototype",
- function() {
- testCase("Error.prototype:\n", Error.prototype);
- });
- test("Number.prototype",
- function() {
- testCase("Number.prototype:\n", Number.prototype);
- });
- test("String.prototype",
- function() {
- testCase("String.prototype:\n", String.prototype);
- });
- test("Boolean.prototype",
- function() {
- testCase("Boolean.prototype:\n", Boolean.prototype);
- });
代码没有多复杂,结果如下:
。。。
结果太多,就截一部分图吧,看看这些熟悉的方法,是不是有种点进去看看每个函数源码的冲动?
关于 Javascript 的面向对象,这里对继承和多态没有涉及,那将是另一篇文档的事情。不过如果看懂了本篇文章的内容,我保证,那些继承和多态的东西还真没什么难度。
来源: http://www.cnblogs.com/oowgsoo/p/6407115.html