- 〇、前言
- 一、JavaScript和Java在面向对象机制上的区别
- 1、面向对象编程的特征
- 2、机制差异简述
- 二、面向对象机制差异举例
- 1、Java版的继承机制例子
- 2、JavaScript版的派生例子
- 三、JavaScript的面向对象的基础知识点
- 四、结合代码和对象图解读理解原型链
- 五、总结
〇、前言
JavaScript是一种面向对象的语言,但它并没有采用Java那种基于类
(class-based)的方式来实现,而是采用基于原型
(prototype-based)方式----这种方式能提供更灵活的机制,但没有基于类
方式那么简洁,因而苦涩难懂。
本文尝试通过一段例子代码和其对应的UML对象图来阐述JavaScript的面向对象机制,以让读者能快速、深入地理解。
本文前面部分有一些理论知识,略显枯燥,你可以先浏览一下文中的图、代码例子,以便先有个感性认识,这里先提前贴一下下文中会出现的图:
注1:本文假定读者有一定的JavaScript基础,以及对Java的语法有初步的了解。
注2:ES6(ES2015)引入了class,但只是语法糖(Syntactic sugar),
所以还是有必要理解JavaScript的基于原型的机制。
注3:本文有时会用JS代替JavaScript,OO代替Object Oriented(面向对象)。
一、JavaScript和Java在面向对象机制上的区别
ES6(ES2015)引入了class,但只是语法糖
(Syntactic sugar),即是说,核心机制还是基于原型
的,所以还是有必要理解JavaScript的基于原型的OO机制。
下文主要以ES5为基准,所以以“JS中没有class这样东西”为论调。
一.1、面向对象编程的特征
JS也是一门面向对象编程
的语言,所以也具备这有3个OO特征,这里简单概述一下。(跟本文关系最密切的是继承Inheritance)
- 封装 Encapsulation:把跟一种对象(一个class)相关的属性、操作放在一起,便于理解、编程,同时可以指定这些属性、操作的访问权限;
- 继承 Inheritance:允许一个类继承另外一个类的属性、操作,必要时改写这些属性和行为----被继承的类被称之为
父类
(superclass),继承父类的类被称为子类
(subclass); - 多态 Polymorphism:允许在某个API或某段代码中指定使用某个类的instances,而在运行态时,可以传递这个类的子类的instances给这个API或这段代码。
一.2、机制差异简述
要快速、准确地理解原型链,最好的办法是通过Java的OO机制对比着来理解。两者主要差别有:
- Java的OO机制是
基于类
(class-based)的,而JS是基于原型
(prototype-based)的; - 具体来说,就是Java中分别有
类
(class)和实例
(instance)的区分;而JS中没有class这个概念,与class类似的是prototype(原型),而与instance类似的是object(对象); - Java通过class与class之间的继承来实现继承(Inheritance),比较简洁、直观;而JS是通过
构造函数
(constructor)和其对应的原型
(prototype),以及原型链
(prototype chain)来实现的,比较复杂。(下文有图为证)
二、面向对象机制差异举例
这部分先给出Java的例子,然后用JS实现同样的功能,以此展示JS的OO机制的特点、复杂性。
Java的OO机制在语法上太简单了,以至于哪怕你不懂Java,但只要你对JavaScript的OO机制有初步的了解,也能读懂下面的Java例子。
二.1、Java版的继承机制例子
直接上代码吧,请留意代码中的注释,以及注释中通过“->”标识的输出结果。
class InheritanceDemo {
class Clock {
protected String mTimezone; // 实例属性
public Clock(String timezone) { // 构造函数
mTimezone = timezone;
}
public void greeting() { // 实例方法
System.out.println("A clock - " + mTimezone);
}
}
class PrettyClock extends Clock { // 类的派生
protected String mColor;
public PrettyClock(String timezone, String color) {
super(timezone); // 调用父类的构造函数
mColor = color;
}
@Override
public void greeting() { // 重写(override)父类的一个方法
System.out.println("A clock - " + mTimezone + " + "+ mColor);
}
}
public void demo() {
Clock clock1 = new Clock("London");
Clock clock2 = new Clock("Shanghai");
Clock clock3 = new PrettyClock("Chongqing", "RED");
clock1.greeting(); // -> "A clock - London"
clock2.greeting(); // -> "A clock - Shanghai"
clock3.greeting(); // -> "A clock - Chongqing + RED"
}
public static void main(String[] args) {
(new InheritanceDemo()).demo();
}
}
上述的Clock和PrettyClock的继承关系,用UML类图方式表达如下:
注:Clock类没有指明父类,默认派生于Object这个class。
二.2、JavaScript版的派生例子
在ES6/ES2015添加class这个语法糖之前,要实现继承则比较复杂,下面是其中一种方法(这段JS代码实现跟上面的Java代码段一样的功能).
注:这段代码在Node.js或者Chrome的console中运行通过。
function Clock(timezone) { // 构造函数
this.timezone = timezone; // 添加实例属性
}
// JS默认就为构造函数添加了一个prototype属性
// 为Clock类(通过其prototype属性)添加实例方法
Clock.prototype.greeting = function() {
console.log("A clock - " + this.timezone);
}
// 定义子类 - 此为子类的构造函数
function PrettyClock(timezone, color) {
Clock.call(this, timezone); // 调用父类的constructor
this.color = color; // 添加实例属性
}
// 通过操作子类构造函数的prototype来使其成为子类
PrettyClock.prototype = Object.create(Clock.prototype);
PrettyClock.prototype.constructor = PrettyClock; // 手工添加
PrettyClock.prototype.greeting = function() {
console.log("A clock - " + this.timezone + " + " + this.color);
}
function demo() {
var clock1 = new Clock("London");
var clock2 = new Clock("Shanghai");
var clock3 = new PrettyClock("Chongqing", "RED");
clock1.greeting(); // -> "A clock - London"
clock2.greeting(); // -> "A clock - Shanghai"
clock3.greeting(); // -> "A clock - Chongqing + RED"
}
demo();
对比Java和JS两个版本的继承例子代码,可以看出JS的OO语法远没有Java的那么简洁、直观 ---- 这点差异其实不算什么了,难点在于后面要介绍的原型链
机制,这难点可以在这个例子中的JS的OO机制(原型链)的对象图
(object diagram)中看出来:
一时看不懂这张图?没关系,因为不是理解能力的问题,而是JS的OO机制实在太不直观了。下面需要先补充一些JS知识,然后解释这幅图。
三、JavaScript的面向对象的基础知识点
这里的几点知识点很重要,建议先好好理解一下,必要时多看几遍再读下一部分。
因为它们之间关系比较紧密,所以就按这种方式来排版了,请理解一下。
- JS中,对象(object)是指一组
属性
(property,每个属性都是一对key-value)的集合,例如{x: 11, y: 22}
;实际上,- 一个函数也是一个object,所以
-
(function() {}) instanceof Object // -> true
;
-
- 连Object、Function、String等这些关键字对应的东西都是objects:
Object instanceof Object // -> true
Function instanceof Object // -> true
String instanceof Object // -> true
- 除了Boolean, Number, String, Null, Undefined, Symbol(ES6/ES2015中加入)类型的值外,其它的都是Object类型的值(即都是objects);所以:
-
123 instanceof Object // -> false
-- 数值123是原始值(Primitive value)而不是对象; -
"hello" instanceof Object // -> false
-- 字符串也是原始值(Primitive value); -
"hello".length // -> 5
-- 字符串用起来像对象,原因是JS隐性地使用了包裹对象
(Wrapper objects,详情)来封装; -
"hello".toUpperCase() // -> "HELLO"
-- 同上;
-
- 一个函数也是一个object,所以
- JS的objects可以被看作是Java中的Map的instances,并且JS支持用
对象直接量
(Object literal)来创建对象。这一点需要记住,因为下文很多例子用到这个语法:-
var o1 = {}; o1.age = 18;
- 创建一个object,并添加一个名为age的属性; -
var o2 = {name: "Alan", age: 18}
- 创建了一个object,里面有两个属性; -
({name:"Alan", age:18}).age === 18 // -> true
- 创建了一个object并访问其中一个属性;
-
- JS的OO机制中没有Java的class这东西(ES6/ES2015的class只是语法糖),JS的OO机制是通过
原型
(Prototype)来实现的,而你看到的Object, Function, String等,其实并不是class(虽然它们是大写字母开头的,而Java要求类名以大写字母开头),而是构造函数
(Constructor),因为函数也是对象;typeof Object // -> "function"
typeof String // -> "function"
- JS为每个function都默认添加了一个prototype属性;
- 这个属性的值是个对象:
typeof (function() {}).prototype // -> "object"
- 默认情况下,每个function的prototype值都是不一样的:
(function() {}).prototype == (function() {}).prototype // -> false
- 所以,语法上每个function都可以被当作构造函数来使用,例如:
-
new (function(x) {this.v = x*2})(3) // -> {v: 6}
- 上一句定义了一个匿名函数,用它作为构造函数,参数是3,创建了一个object,里面有一个属性(key="v", value=6);
-
new (function(){}) // -> {}
-- 创建了一个空object -
new (function(x) {x = 2})(3) // -> {}
-- 空object,因为没有通过this往object里添加属性;
-
- 这个属性的值是个对象:
- 除了Object这个object等少数几个objects外,JS里每个object都有它的原型,你可以用
Object.getPrototypeOf()
这种标准方式来获取,或者使用__proto__
这个非标准属性来获取(IE的JS引擎中的objects就没有__proto__
这个属性):Object.getPrototypeOf({}) === Object.prototype // -> true
({}).__proto__ === Object.prototype // -> true
Object.getPrototypeOf(function (){}) === Function.prototype // -> true
- 当一个function跟着new操作符时,它就被用作
构造函数
来使用了,例如:var clock3 = new PrettyClock("Chongqing", "RED")
- 上面一句的new操作符实际上做了4件事情:
- A. 创建一个空的临时object,类似于
var tmp = {};
; - B. 记录临时object与PrettyClock的关系(原型关系),类似于
tmp.__proto__ = PrettyClock.prototype
;- 注:这一点是浏览器、JS引擎的内部机制,Chrome和Node.js中有
__proto__
,但IE中没有;
- 注:这一点是浏览器、JS引擎的内部机制,Chrome和Node.js中有
- C. 以
"Chongqing", "RED"
为参数,调用PrettyClock这个构造函数(Constructor),在PrettyClock的body内部时this
指向那个新创建的临时空对象(那个tmp);- 这里值得强调的是:
构造函数
中的this.timezone = timezone;
一类赋值语句,其实是在那个临时object中添加或者修改实例属性
-- 这跟Java的机制不一样,Java是在class内部定义了实例属性(必要时可以同时赋初始值),而在(Java的)构造函数中,最多是修改实例属性值,无法添加属性或者修改属性的名字或者类型。
- 这里值得强调的是:
- D. PrettyClock函数结束执行后,返回临时object,上述语句中就赋给了clock3。
- A. 创建一个空的临时object,类似于
- 当访问一个object的属性(普通属性或者方法)时,步骤如下:
- 如果本object中有这个属性,就返回它的值,访问结束;
- 如果本object中没有这个属性,就往该object的
__proto__
属性指向的object(prototype object)里找,如果找到,就返回它的值,访问结束; - 如果还没找到,就不断重复上一步骤的动作,直到没有上级prototype,此时返回undefined这个值;
- 实际运行中,是一直找到Object.prototype这个object的,因为它的
__proto__
等于null,所以不会再继续找了。相关信息如下:typeof Object.prototype // -> "object"
Object.prototype.__proto__ === null // -> true
- 实际运行中,是一直找到Object.prototype这个object的,因为它的
- 上一点提到的寻找object的某个属性的过程,就是
原型链
的工作机制,它的运行效果跟Java中“在类继承树上一直往上找,直到Object类中都找不到为止”差不多。
四、结合代码和对象图解读理解原型链
要理解JS的OO机制,需要先理解原型链
(prototype chain);要理解原型链,则需要理解构造函数
(constructor)、原型对象
(prototype object)、普通对象(object)之间的关系,说明如下。
为了避免你来回滚动网页,这里把上面的对象图再贴一次:
- 图中每个“大框”(2到4个小方框的集合)都是一个object,大框中的每个小框(除了最上面的一个)都是一个
属性
(Property),图中列举的这些属性,它们的值就是箭头指向的那个object。其中,- 左边一列(浅蓝色)的objects都是
构造函数
(Constructor),它们都是functions(概念上,可以认为Function是Object的子类),从图中它们的__proto__
属性都指向Function.prototype
(红色的线)就可以看出来; - 中间一列(浅绿色)的objects充当
原型
(Prototype)的角色,它们本身是objects,这从它们的__proto__
属性指向Object.prototype
(青色的线)可以看得出来;- 充当prototype的objects都需要有一个
constructor
属性(留意绿色的线),指向其对应的构造函数,这个属性在你手工构造prototype时,则需要自行显式添加;
- 充当prototype的objects都需要有一个
- 右边一列(浅橙色)的objects就是普通对象了,它们本身没有
constructor
或者prototype
属性,但有JS引擎内部为它们添加的__proto__
属性来指向对应的prototype对象;- 在
var clock3 = new PrettyClock("Chongqing", "RED")
时,new
操作符先创建一个临时对象,然后把PrettyClock的prototype
属性的值(就是图中浅绿色的PrettyClock.prototype
这个object的引用)添加到这个临时对象中(作为一个内部属性,名为__proto__
),最后将临时对象赋给变量clock3。
- 在
- 左边一列(浅蓝色)的objects都是
- JS的
原型链
就是由图中青色的线+蓝色的线所构成的,其运作机制需要用几个例子来说明:-
clock3.greeting(); // -> "A clock - Chongqing + RED"
- clock3对象本身没有名为"greeting"的属性,所以到其
__proto__
指向的object即PrettyClock.prototype
里找,结果有,所以就调用;clock3.hasOwnProperty("greeting") // -> false
clock3.__proto__.hasOwnProperty("greeting") // -> true
- 调用时这个greeting函数时,关键字
this
指向的是clock3这个对象,这个对象有timezone
和color
两个属性值,所以打印结果"A clock - Chongqing + RED";- clock3的这两个属性值,是在用
new
调用PrettyClock这个构造函数(图中浅蓝色的PrettyClock)时被添加进去的,其中timezone
这个属性是在(PrettyClock直接地)调用Clock这个构造函数添加的 ---- 这一点很重要,请好好理解一下;
- clock3的这两个属性值,是在用
- clock3对象本身没有名为"greeting"的属性,所以到其
-
clock3.toString(); // -> "[object Object]"
- clock3本身没有"toString"这个属性(例子中没有添加这个属性),估往
PrettyClock.prototype
找(蓝线),也没,再往Clock.prototype
找(青色虚线),还是没有,再往Object.prototype
找(青色线),就找到了。 - 这里需要指出的是,你Chrome的console中能打印出一个object的一个属性值,并不代表这个这个属性值在这个object中,这需要用
hasOwnProperty()
来检查,例子如下:-
clock3.toString != null // -> true
- 能读到这个属性 -
clock3.hasOwnProperty("toString") // -> false
- 不直接拥有 -
clock3.__proto__.__proto__.__proto__.hasOwnProperty('toString') // -> true
- 终于找到了
-
clock3.__proto__.__proto__.__proto__ === Object.prototype // -> true
- 这一点,结合图中的线来理解一下吧;
-
clock3.__proto__.__proto__ === Clock.prototype // -> true
- 如果上面一点理解了,这一点就不在话下了。
-
- clock3本身没有"toString"这个属性(例子中没有添加这个属性),估往
-
Object.prototype.__proto__ === null // -> true
-
Object.prototype
这个object是原型链的最顶端了(就如Java中Object这个class是最顶端的class一样),所以它没有原型了,所以它的__proto__
这个属性的值为null(注意:不是undefined,至于null与undefined的区别,请自行搜索一下); - 这一点的严谨写法是:
Object.getPrototypeOf(Object.prototype) === null // -> true
- 再重复一遍:Chrome、Node.js中每个object都有
__proto__
这个属性,但IE中就没有,因为它不是JavaScript的规范的中定义的,Object.getPrototypeOf()
才是标准用法。
-
-
写到这里就差不多了。😊
五、总结
JS的OO机制、原型链不容易理解,原因有这几点:
- JS的OO机制本来就复杂(没有Java的那么简洁);
- “对象”、“属性”、“原型”这些词的含义比较模糊,带来了理解上的困难;
- Object、Function、String等关键字其实是函数,它们充当着“某个class的门面”的角色,但它们并不直接是原型链的一部分,因为那些
实例方法
(instance method)不是放在它们里面,而是放在它们对应的原型对象里面。