为了说明 JavaScript 是一门彻底的面向对象的语言, 首先有必要从面向对象的概念着手 , 探讨一下面向对象中的几个概念:
1. 一切事物皆对象
2. 对象具有封装和继承特性
3. 对象与对象之间使用消息通信,各自存在信息隐藏
JavaScript 语言是通过一种叫做 原型(prototype) 的方式来实现面向对象编程的。
基于类的面向对象和基于原型的面向对象方式比较
在基于类的面向对象方式中,对象(object)依靠 类(class)来产生。 而在基于原型的面向对象方式中,对象(object)则是依靠 构造器(constructor)利用 原型(prototype)构造出来的。
举个客观世界的例子来说明二种方式认知的差异。例如工厂造一辆车,一方面,工人必须参照一张工程图纸,设计规定这辆车应该如何制造。这里的工程图纸就好比是语言中的 类 (class),而车就是按照这个 类(class)制造出来的;另一方面,工人和机器 ( 相当于 constructor) 利用各种零部件如发动机,轮胎,方向盘 ( 相当于 prototype 的各个属性 ) 将汽车构造出来。
对象的上下文依赖
var str = '我是一个 String 对象 , 我声明在这里 , 但我不是独立存在的!'
var obj
= {
des: '我是一个 Object 对象 , 我声明在这里,我也不是独立存在的。'
};
var fun = function () {
console.log('我是一个Function 对象!谁调用我,我属于谁:', this);
};
obj.fun = fun;
console.log(this === window); // 打印true
console.log(window.str === str); // 打印 true
console.log(window.obj === obj); // 打印true
console.log(window.fun === fun); // 打印 true
fun(); // 打印 我是一个 Function对象!谁调用我,我属于谁:window
obj.fun(); // 打印 我是一个 Function 对象!谁调用我,我属于谁:obj
fun.apply(str); // 打印我是一个 Function 对象!谁调用我,我属于谁:str
最基本的面向对象
ECMAScript 是一门彻底的面向对象的编程语言(参考资源),JavaScript 是其中的一个变种 (variant)。它提供了 6 种基本数据类型 ,即 Boolean、Number、String、Null、Undefined、Object。为了实现面向对象,ECMAScript设计出了一种非常成功的数据结构 - JSON(JavaScript Object Notation) , 这一经典结构已经可以脱离语言而成为一种广泛应用的数据交互格式 (参考资源)。
应该说,具有基本数据类型和 JSON 构造语法的 ECMAScript 已经基本可以实现面向对象的编程了。开发者可以随意地用 字面式声明(literal notation)方式来构造一个对象,并对其不存在的属性直接赋值,或者用 delete 将属性删除
字面式 (literal notation) 对象声明
var person = {
name: '张三',
age: 26,
gender: '男',
eat: function (stuff) {
alert('我在吃' + stuff);
}
};
person.height = 176;
delete person['age'];
然而,这样的代码复用性非常弱,与其他实现了继承、派生、多态等等的类式面向对象的强类型语言比较起来显得有些干瘪,不能满足复杂的 JS 应用开发。所以 ECMAScript 引入原型来解决对象继承问题。
使用函数构造器构造对象
除了 字面式声明(literal notation)方式之外,ECMAScript 允许通过 构造器(constructor)创建对象。每个构造器实际上是一个 函数(function) 对象, 该函数对象含有一个”prototype”属性用于实现 基于原型的继承(prototype-based inheritance)和 共享属性(shared properties)。 对象可以由”new 关键字 + 构造器调用”的方式来创建,
// 构造器 Person 本身是一个函数对象
function Person() { // 此处可做一些初始化工作 }
// 它有一个名叫 prototype 的属性
Person.prototype = {
name: '张三',
age: 26,
gender: '男',
eat:
function (stuff) {
alert('我在吃' + stuff);
}
}
}
// 使用 new 关键字构造对象
var p = new Person();
note: 由于早期 JavaScript 的发明者为了使这门语言与大名鼎鼎的 Java 拉上关系 ( 虽然现在大家知道二者是雷锋和雷锋塔的关系 ),使用了 new 关键字来限定构造器调用并创建对象,以使其在语法上跟 Java 创建对象的方式看上去类似。但需要指出的是,这两门语言的 new含义毫无关系,因为其对象构造的机理完全不同。
彻底理解原型链 (prototype chain)
在 ECMAScript 中,每个由构造器创建的对象 拥有一个指向构造器 prototype 属性值的 隐式引用(implicit reference),这个引用称之为 原型(prototype)。进一步,每个原型可以拥有指向自己原型的 隐式引用(即该原型的原型) ,如此下去,这就是所谓的 原型链(prototype chain) (参考资源)。 在具体的语言实现中,每个对象都有一个 __proto__
属性来实现对原型的 隐式引用。
对象的 proto 属性和隐式引用
function Person(name) {
this.name = name;
}
var p = new Person();
// 对象的隐式引用指向了构造器的 prototype 属性,所以此处打印 true
console.log(p.__proto__ === Person.prototype);
//原型本身是一个 Object 对象,所以他的隐式引用指向了
// Object 构造器的 prototype 属性 , 故而打印 true
console.log(Person.prototype.__proto__ === Object.prototype);
// 构造器 Person 本身是一个函数对象,所以此处打印 true
console.log(Person.__proto__ === Function.prototype);
有了 原型链,便可以定义一种所谓的 属性隐藏机制 ,并通过这种机制实现继承。ECMAScript 规定,当要给某个对象的属性赋值时,解释器会查找该对象原型链中第一个含有该属性的对象(注:原型本身就是一个对象,那么原型链即为一组对象的链。对象的原型链中的第一个对象是该对象本身)进行赋值。反之,如果要获取某个对象属性的值,解释器自然是返回该对象原型链中首先具有该属性的对象属性值。
图 1. 原型链中的属性隐藏机制
在图 1 中,object1->prototype1->prototype2 构成了 对象 object1 的原型链,根据上述属性隐藏机制,可以清楚地看到 prototype1 对象中的 property4 属性和 prototype2 对象中的 property3 属性皆被隐藏。理解了原型链,那么将非常容易理解 JS 中基于原型的继承实现原理,下面的程序清单是利用原型链实现继承的简单例子。
利用原型链 Horse->Mammal->Animal 实现继承
/**
* Created by shan on 2016/4/26.
*/
// 声明 Animal 对象构造器
function Animal() {
}
// 将Animal 的 prototype 属性指向一个对象,
// 亦可直接理解为指定 Animal 对象的原型
Animal.prototype = {
name: 'animal',
weight: 0,
eat: function () {
alert('Animal is eating!');
}
}
// 声明 Mammal 对象构造器
function Mammal() {
this.name = 'mammal';
}
// 指定 Mammal 对象的原型为一个 Animal 对象。
// 实际上此处便是在创建 Mammal 对象和Animal对象之间的原型链
Mammal.prototype = new Animal();
// 声明 Horse 对象构造器
function Horse(height, weight) {
this.name = 'horse';
this.height = height;
this.weight = weight;
}
// 将 Horse对象的原型指定为一个 Mamal 对象,继续构建 Horse 与 Mammal 之间的原型链
Horse.prototype = new Mammal();
// 重新指定 eat方法 , 此方法将覆盖从 Animal 原型继承过来的 eat 方法
Horse.prototype.eat = function () {
alert('Horse is eating grass!');
}
// 验证并理解原型链
var horse = new Horse(100, 300);
console.log(horse.__proto__ === Horse.prototype);
console.log(Horse.prototype.__proto__ ===
Mammal.prototype);
console.log(Mammal.prototype.__proto__ === Animal.prototype);
对象原型继承逻辑实现的关键在于 Horse.prototype = new Mammal() 和 Mammal.prototype = new Animal() 这两句代码。首先,等式右边的结果是构造出一个临时对象,然后将这个对象赋值给等式左边对象的 prototype 属性。也就是说将右边新建的对象作为左边对象的原型。
JavaScript 类式继承的实现方法
基于原型的继承方式,虽然实现了代码复用,但其行文松散且不够流畅,可阅读性差,不利于实现扩展和对源代码进行有效地组织管理。不得不承认,类式继承方式在语言实现上更具健壮性,且在构建可复用代码和组织架构程序方面具有明显的优势。 目前一些主流的 JS 框架都提供了这种转换机制,也即类式声明方法,比如 Dojo.declare()、Ext.entend() 等等。用户使用这些框架,可以轻易而友好地组织自己的 JS 代码。
jQuery 之父 John Resig 在搏众家之长之后,用不到 30 行代码便实现了自己的 Simple Inheritance 。使用其提供的 extend 方法声明类非常简单。程序清单 是使用了 Simple Inheritance库实现类的声明的例子。其中最后一句打印输出语句是对 Simple Inheritance实现类式继承的最好说明。
使用 Simple Inheritance 实现类式继承
/**
* Created by shan on 2016/4/26.
*/
// 声明 Person 类
var Person = Class.extend({
_issleeping: true,
init: function (name) {
this._name = name;
},
isSleeping: function () {
return this._issleeping;
}
});
// 声明 Programmer 类,并继承 Person
var Programmer = Person.extend({
init: function (name, issleeping) {
// 调用父类构造函数
this._super(name);
// 设置自己的状态
this._issleeping = issleeping;
}
});
var person = new Person("张三");
var diors = new Programmer("张江男", false);
// 打印 true
console.log(person.isSleeping());
// 打印 false
console.log(diors.isSleeping());
// 此处全为 true,故打印 true
console.log(person instanceof Person && person instanceof Class &&
diors instanceof Programmer && diors instanceof Person && diors instanceof Class);
Simple Inheritance 源码
/**
* Created by shan on 2016/4/26.
*/
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*/
// Inspired by base2 and Prototype
(function () {
var initializing = false, fnTest = /xyz/.test(function () {
xyz;
}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
this.Class = function () {
};
// Create a new Class that inherits from this class
Class.extend = function (prop) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don’t run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we’re overwriting an existing function
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" &&
fnTest.test(prop[name]) ?
(function (name, fn) {
return function () {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we’re done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if (!initializing && this.init)
this.init.apply(this, arguments);
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.prototype.constructor = Class;
// And make this class extendable
Class.extend = arguments.callee;
return Class;
};
})();
封装
封装 和 Prototype模式
Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype.type = "猫科动物";
Cat.prototype.eat = function(){alert("吃老鼠")};
var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.type); // 猫科动物
cat1.eat(); // 吃老鼠
这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。
alert(cat1.eat == cat2.eat); //true
Prototype模式的验证方法
为了配合prototype属性,Javascript定义了一些辅助方法,帮助我们使用它。,
isPrototypeOf()
这个方法用来判断,某个proptotype对象和某个实例之间的关系。
alert(Cat.prototype.isPrototypeOf(cat1)); //true
alert(Cat.prototype.isPrototypeOf(cat2)); //true
hasOwnProperty()
每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。
alert(cat1.hasOwnProperty("name")); // true
alert(cat1.hasOwnProperty("type")); // false
in运算符
in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。
alert("name" in cat1); // true
alert("type" in cat1); // true
in运算符还可以用来遍历某个对象的所有属性。
for(var prop in cat1) { alert("cat1["+prop+"]="+cat1[prop]); }
JavaScript 私有成员实现
使用闭包实现信息隐藏
/**
* Created by shan on 2016/4/26.
*/
// 声明 User 构造器
function User(pwd) {
// 定义私有属性
var password = pwd;
// 定义私有方法
function getPassword() {
// 返回了闭包中的 password
return password;
}
//特权函数声明,用于该对象其他公有方法能通过该特权方法访问到私有成员
this.passwordService = function () {
return getPassword();
}
}
// 公有成员声明
User.prototype.checkPassword = function (pwd) {
return this.passwordService() === pwd;
};
// 验证隐藏性
var u = new User("123456");
// 打印 true
console.log(u.checkPassword("123456"));
// 打印 undefined
console.log(u.password);
// 打印 true
console.log(typeof u.getPassword === "undefined");
更多
封装
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_encapsulation.html
构造函数的继承
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance.html
非构造函数的继承
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance_continued.html
全面理解面向对象的 JavaScript
http://www.ibm.com/developerworks/cn/web/1304_zengyz_jsoo/#resources