最近工作一直在用nodejs做开发,有了nodejs,前端、后端、脚本全都可以用javascript搞定,很是方便。但是javascript的很多语法,比如对象,就和我们常用的面向对象的编程语言不同;看某个javascript开源项目,也经常会看到使用this关键字,而这个this关键字在javascript中因上下文不同而意义不同;还有让人奇怪的原型链。这些零零碎碎的东西加起来就很容易让人不知所措,所以,有必要对javascript这门语言进行一下深入了解。
我这篇文章主要想说说如何在javascript中进行面向对象的编程,同时会讲一些javascript这门语言在设计之初的理念。下面让我们开始吧。
其中值得一提的是,为什么继承借鉴了self语言的原型机制而不是java的类机制?首先我们要知道:
- self的原型机制是靠运行时的语义
- java的类机制是靠编译时的类语法
javascript1.0的功能相对简单,为了在今后不断丰富javascript本身功能的同时保持旧代码的兼容性,javascript通过改变运行时的支持来增加新功能,而不是通过修改javascript的语法,这就保证了旧代码的兼容性。这也就是javascript选择基于运行时的原型机制的原因。
在javascript中,除了数字、字符串、布尔值(true/false)、undefined这几个简单类型外,其他的都是对象。
数字、字符串、布尔值这些简单类型都是不可变量,对象是可变的键值对的集合(mutable keyed conllections),对象包括数组<code>array</code>、正则表达式<code>regexp</code>、函数<code>function</code>,当然对象<code>object</code>也是对象。
对象在javascript中说白了就是<code>一系列的键值对</code>。键可以是任何字符串,包括空串;值可以是除了undefined以外的任何值。在javascript中是没有类的概念(class-free)的,但是它有一个原型链(prototype linkage)。javascript对象通过这个链来实现继承关系。
javascript中的每种类型的对象都可以采用<code>字面量(literal)</code>的方式创建。
对于object对象,可以使用<code>对象字面量(object literal)</code>来创建,例如:
当然,也可以用<code>new object()</code>或<code>object.create()</code>的方式来创建对象。
对于<code>function</code>、<code>array</code>对象都有其相应的字面量形式,后面会讲到,这里不再赘述。
javascript中的每个对象都隐式含有一个<code>[[prototype]]</code>属性,这是ecmascript中的记法,目前各大浏览器厂商在实现自己的javascript解释器时,采用的记法是<code>__proto__</code>,也就是说每个对象都隐式包含一个<code>__proto__</code>属性。举个例子:
foo这个对象在内存中的存储结构大致是这样的:
当有多个对象时,通过<code>__proto__</code>属性就能够形成一条原型链。看下面的例子:
上面的代码在声明对象b、c时,指明了它们的原型为对象a(a的原型默认指向object.prototye,object.prototype这个对象的原型指向null),这几个对象在内存中的结构大致是这样的:
除了我们这里说的<code>__proto__</code>属性,相信大家平常更常见的是<code>prototype</code>属性。比如,date对象中没有加几天的函数,那么我们可以这么做:
那么以后所有的date对象都拥有<code>adddays</code>方法了(后面讲解继承是会解释为什么)。那么<code>__proto__</code>属性与<code>prototype</code>属性有什么区别呢?
javascript的每个对象都有<code>__proto__</code>属性,但是只有<code>函数对象</code>有<code>prototype</code>属性。
那么在函数对象中, 这两个属性的有什么区别呢?
1. <code>__proto__</code>表示该函数对象的原型
2. <code>prototype</code>表示使用new来执行该函数时(这种函数一般成为构造函数,后面会讲解),新创建的对象的原型。例如:
var d = new date();
d.proto === date.prototype; //这里为true
看到这里,希望大家能够理解这两个属性的区别了。
在javascript,原型和函数是最重要的两个概念,上面说完了原型,下面说说函数对象。
首先,函数在javascript中无非也是个对象,可以作为value赋值给某个变量,唯一不同的是函数能够被执行。
使用对象字面量方式创建的对象的<code>__proto__</code>属性指向<code>object.prototype</code>(<code>object.prototype</code>的<code>__proto__</code>属性指向<code>null</code>);使用函数字面量创建的对象的<code>__proto__</code>属性指向<code>function.prototype</code>(<code>function.prototype</code>对象的<code>__proto__</code>属性指向<code>object.prototype</code>)。
函数对象除了<code>__proto__</code>这个隐式属性外,还有两个隐式的属性:
1. 函数的上下文(function’s context)
2. 实现函数的代码(the code that implements the function’s behavior)
和对象字面量一样,我们可以使用<code>函数字面量(function literal)</code>来创建函数。类似于下面的方式:
一个函数字面量有四个部分:
1. function关键字,必选项。
2. 函数名,可选项。上面的示例中就省略了函数名。
3. 由圆括号括起来的一系列参数,必选项。
4. 由花括号括起来的一系列语句,必选项。该函数执行时将会执行这些语句。
一个函数在被调用时,除了声明的参数外,还会隐式传递两个额外的参数:<code>this</code>与<code>arguments</code>。
this在oop中很重要,this的值随着调用方式的不同而不同。javascript中共有四种调用方式:
1. method invocation pattern。当函数作为某对象一个属性调用时,this指向这个对象。this赋值过程发生在函数调用时(也就是运行时),这叫做late binding
2. function invocation pattern。当函数不作为属性调用时,this指向全局对象,这是个设计上的错误,正确的话,内部函数的this应该指向外部函数。可以通过在函数中定义一个变量来解决这个问题。
construct invocation pattern。javascript是一门原型继承语言,这也就意味着对象可以直接从其他对象中继承属性,没有类的概念。这和java中的继承不一样。但是javascript提供了一种类似与java创建对象的语法。当一个函数用new来调用时,this指向新创建的对象。这时的函数通常称为构造函数。
需要注意的是,这里的arguments不是一个数组,它只是一个有length属性的类数组对象(array-like),它并不拥有数组的其他方法。
关于对象,最后说一下数组,javascript中的数组和平常编程中的数组不大一样。
数组是一种在内存中线性分配的数据结构,通过下标计算出元素偏移量,从而取出元素。数组应该是一个快速存取的数据结构,但是在javascript中,数组不具备这种特性。
数组在javascript中一个具有传统数组特性的对象,这种对象能够把数组下标转为字符串,然后把这个字符串作为对象的key,最后对取出对应该key的value(这又一次说明了对象在javascript中就是一系列键值对)。
javascript也为数组提供了很方便的<code>字面量(array literal)</code>定义方式:
通过数组字面量创建的数组对象的<code>__proto__</code>指向array.prototype。
在java中,对象是某个类的实例,一个类可以从另一个类中继承。但是在基于原型链的javascript中,对象可以直接从另一个对象创建。
在上面讲解对象时,我们知道了在创建一个对象时,该对象会自动赋予一个<code>__proto__</code>属性,使用各种类型的<code>字面量(literal)</code>时,javascript解释器自动为<code>__proto__</code>进行了赋值。当我们在javascript执行使用new操作符创建对象时,javascript解释器在构造函数时,同时会执行类似于下面的语句
新创建的对象都会有一个<code>__proto__</code>属性,这个属性有一个<code>constructor</code>属性,并且这个属性指向这个新对象。举个例子:
如果new不是一个操作符,而是一个函数的话,它的实现类似于下面的代码:
之前也说了,基于原型的继承机制是根据运行时的语义决定的,这就给我们提供了很大的便利。比如,我们想为所有的array添加一个map函数,那么我们可以这么做:
因为所有的数组对象的<code>__proto__</code>都指向array.prototype对象,所以我们为这个对象增加方法,那么所有的数组对象就都拥有了这个方法。
javascript解释器会顺着原型链查看某个方法或属性。如果想查看某对象的是否有某个属性,可以使用<code>object.prototype.hasownproperty</code>方法。
<a href="http://dmitrysoshnikov.com/ecmascript/javascript-the-core/">javascript. the core</a>