javascript学习-对象与原型

Javascript语言是符合面向对象思想的。一般来说,面向对象思想需要满足以下三个基本要求:

  1. 封装,Javascript的对象可以自由的扩充成员变量和方法,自然是满足该要求的
  2. 继承,Javascript采用了比较少见的原型继承机制,也满足该要求
  3. 多态,Javascript的原型继承机制也可以支持多态

这里的关键问题就是Javascript的原型继承机制到底是个啥玩意?

1.对象的原型

有很大的可能性,Javascript在设计之初根本就没有考虑那么复杂。啥玩意面向对象思想,跟我有半毛钱关系啊。公司就给我两周时间,连设计带编码,我当然是怎么简单怎么来了。如果说一门计算机语言一定要满足唯一的一个最最基本的设计思想,那一定不是面向对象,而应该是更简单的:语言应支持功能的复用。如果一个对象功能不够用了,那就再叫个帮手呗,于是Javascript硬性规定:任何对象都必须有一个原型对象。这下好了,所有的Javascript对象都是自带秘书的,自己搞不定的就交给秘书去搞,秘书再搞不定的,就交给秘书的秘书去搞,这样一路交接过去,直到彻底搞不定了,那就只好报错。这就是Javascript中的原型链检索机制,是不是超简单?

因为对象的原型是Javascript对象的基本构件,那么首要的问题就是如何得到对象的原型对象?大体来说有以下两种方法:

  • obj.__proto__,有点黑客的意思,但是现代几乎所有的浏览器都支持它,关键是最直接
  • Object.getPrototypeOf(obj),尽管是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__");
});

这段测试需要我们注意以下几点:

  • 对象的字面量{}其实就是new Object();的简写,一段语法糖而已
  • p的类型是Object,它的原型对象可以命名为Object.__proto__,后者是所有Object对象的原型对象,可以看做是全局唯一
  • Object.__proto__对象也有原型对象,可以命名为Object.__proto__.__proto__,但是这也就到头了,它就是一个null

既然函数也是对象,那么我们来测试一下函数的原型链:

            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__");
});

这段测试需要我们注意以下几点:

  • p的类型是Function,它的原型对象可以命名为Function.__proto__,后者是所有Functiont对象的原型对象,可以看做是全局唯一
  • Functiont.__proto__对象也有原型对象,可以命名为Functiont.__proto__.__proto__,最后我们发现其实它就是Object.__proto__,所有的__proto__最终都要推送到这里,因为它的__proto__就是null,代表彻底搞不定了

2.函数的prototype

总有一些对象是类似的,例如每个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是这样规定的:

  • 函数通过prototype属性得到函数描述对象
  • 函数描述对象中通过constructor属性反向得到函数对象
  • 也就是说,函数对象和函数描述对象是双向关联的

很多翻译把函数的描述对象翻译为原型对象,对象的原型对象,函数的原型对象,又一个函数的原型对象,这要把人绕晕吗?我建议就直接用__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__");
});

3.构造函数调用

还回到引入函数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只是调用语法上稍微有些不同,但结果其实是一模一样的。这下对构造函数的调用原理也不用纠结了吧,源码在那里,一共也没有几行,还不清楚的回去再多看几遍。

4.prototype的扩充

再回头看看那张经典的图,在不考虑继承的情况下,我们对类型的扩充其实就在prototype对象上

  • 对Object.prototype的扩充就是对所有Object类型的扩充
  • 对Function.prototype的扩充就是对所有函数的扩充
  • 对Foo.prototype的扩充就是对Foo类型的扩充

这里我们需要把所有内置对象类型的prototype显示出来,以验证我们的想法:

        <script type="text/javascript">
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);
});
</script>

代码没有多复杂,结果如下:

。。。

结果太多,就截一部分图吧,看看这些熟悉的方法,是不是有种点进去看看每个函数源码的冲动?

5.后记

关于Javascript的面向对象,这里对继承和多态没有涉及,那将是另一篇文档的事情。不过如果看懂了本篇文章的内容,我保证,那些继承和多态的东西还真没什么难度。

javascript学习-对象与原型的更多相关文章

  1. JavaScript学习笔记之原型对象

    本文是学习<JavaScript高级程序设计>第六章的笔记. JS中,便于批量创建对象的三种模式: 1.工厂模式:用一个函数封装创建对象的细节,传入必要的参数,在函数内部new一个对象并返 ...

  2. javascript学习随笔(二)原型prototype

    JavaScript三类方法: 1.类方法:2.对象方法:3.原型方法;注意三者异同 例: function People(name){ this.name=name; //对象方法 this.Int ...

  3. JavaScript学习3:原型和继承

    原型 我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个对象,它的用途是包括能够由特定类型的全部实例共享的属性和方法.逻辑上能够这么理解:prototype是通过调用构造函数而创 ...

  4. web前端学习(二) javascript对象和原型继承

    目录 1. JavaScrpt对象 2. 原型对象和继承 3. 对象的克隆 (1)javascript对象 在JS中,对象是属性的容器.对于单个对象来说,都由属性名和属性值构成:其中属性名需要是标识符 ...

  5. JavaScript学习总结(三)——闭包、IIFE、原型、函数与对象

    一.闭包(Closure) 1.1.闭包相关的问题 请在页面中放10个div,每个div中放入字母a-j,当点击每一个div时显示索引号,如第1个div显示0,第10个显示9:方法:找到所有的div, ...

  6. JavaScript学习(二)——深入学习js对象的原型与继承

    了解对象  什么是对象?   …… 这个就不说了 对象的声明的两种方式 var person = new Object(); person.name="linchen"; pers ...

  7. JavaScript学习系列博客_24_JavaScript 原型对象

    原型(prototype) - 创建一个函数(所有函数)以后,解析器都会默认在函数中添加一个属性prototype prototype属性指向的是一个对象,这个对象我们称为原型对象. 创建一个函数My ...

  8. JavaScript学习总结(二)数组和对象部分

    pt学习总结(二)数组和对象部分 2016-09-16    分类:WEB开发.编程开发.首页精华暂无人评论     来源:trigkit4 分享到:更多1 对象部分 Object类型 Object  ...

  9. JavaScript学习12 JS中定义对象的几种方式

    JavaScript学习12 JS中定义对象的几种方式 JavaScript中没有类的概念,只有对象. 在JavaScript中定义对象可以采用以下几种方式: 1.基于已有对象扩充其属性和方法 2.工 ...

随机推荐

  1. javascript基础教程学习总结(1)

    摘自javascript基础教程 开始: 1.将脚本放在哪里: 1.1 放在html和<html>之间 范例: <!DOCTYPE html PUBLIC "-//W3C/ ...

  2. LPC2478内存布局以及启动方式

    LPC2478 是NXP公司推出的一款基于APR7TDMI-S的工控型MCU,内置RAM与flash,同时提供外部扩展flash和ram接口,拥有LCD控制器,其内存布局如下所示 其中Flash高达5 ...

  3. SGU 194 Reactor Cooling ——网络流

    [题目分析] 无源汇上下界可行流. 上下界网络流的问题可以参考这里.↓ http://www.cnblogs.com/kane0526/archive/2013/04/05/3001108.html ...

  4. ecshop--标签数组

    $properties  商品属性 array(1) { ["商品属性"]=> array(1) { [178]=> array(2) { ["name&qu ...

  5. Tessnet2图片识别

    验证码识别据说可以用C#图像识别类库Tessnet2来实现,Tessnet2源于目前Google维护的开源项目Tesseract2.本文将对此传说进行验证,含验证结果与验证方法. 1. 验证结果 —— ...

  6. html css基础(一)

    1.HTML:做静态网页,是一种标签语言, HTML结构: 一个HTML文档由4个基本部分组成: ① 一个文档声明:<!DOCTYPE HTML> ② 一个html标签对:<html ...

  7. StackExchange.Redis 官方文档(二) Configuration

    配置 有多种方式可以配置redis,StackExchange.Redis提供了一个丰富的配置模型,在执行Connect (or ConnectAsync) 时被调用: var conn = Conn ...

  8. 【Xilinx-ZYNQ ucos-iii的移植与开发】-00-开始

    前一段时间,调试了一块ZYNQ的板子,上面用到了ucos-iii操作系统,最终在该板子上实现了操作系统的运行,并实现了一些外设模块的功能,主要包括PWM,I2C,GPIO,两级中断. 等有空了总结一下 ...

  9. python实现二叉树

    初学python,需要实现一个决策树,首先实践一下利用python实现一个二叉树数据结构.建树的时候做了处理,保证建立的二叉树是平衡二叉树. # -*- coding: utf-8 -*- from ...

  10. C++ STL算法系列1---unique , unique_copy函数

     一.unique函数 类属性算法unique的作用是从输入序列中“删除”所有相邻的重复元素. 该算法删除相邻的重复元素,然后重新排列输入范围内的元素,并且返回一个迭代器(容器的长度没变,只是元素顺序 ...