原型模式和基于原型继承的JavaScript对象系统
在以类为中心的面向对象编程语言中,类和对象的关系可以想象成铸模和铸件的关系,对象总是从类中创建而来。
而在原型编程的思想中,类并不是必需的,对象未必需要从类中创建而来,一个对象是通过另一个对象所得到的。
就像电影《第六日》一样,通过克隆可以创造另外一个一模一样的人,而本体和克隆体看不出任何区别。
原型模式不单是一种设计模式,也被称为一种编程泛型。
使用克隆的原型模式:
let Plane = function () {
this.blood = 100;
this.attackLevel = 1;
this.defenseLevel = 1;
}
let plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;
let clonePlane = Object.create(plane);
// clonePlane --> {[[Prototype]]: Plane, attackLevel: 10, blood: 500, defenseLevel: 7}
// clonePlane.attackLevel: 10
// 在不支持Object.create()方法的浏览器中,则可以使用以下代码:
Object.create = Object.create || function (obj) {
let F = function() {};
F.prototype = obj;
return new F();
}
原型编程范型的一些规则:
JavaScript原型编程基本原则:
- 所有的数据都是对象。
- 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
- 对象会记住它的原型。
- 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。
JavaScript中的原型继承:
1. 所有的数据都是对象
/**
* JavaScript在设计的时候,模仿Java引入了两套类型机制:基本类型和对象类型。
* 基本类型包括:undefined,number,boolean,string,function,object,从现在看来,这并不是一个好的想法。
* 按照 JavaScript设计者的本意,除了undefined之外。一切都应是对象。
* 为了实现这一目标,number,boolean,string这几种基本类型数据也可以通过 "包装类"的方式编程对象类型数据来处理。
* 我们不能说在JavaScript中所有的数据都是对象。但可以说绝大部分数据都是对象。那么相信JavaScript中也一定会有根对象存在。
* 这些对象追根溯源都来源于这个根对象。
*
* 事实上,JavaScript中的根对象是Object.prototype对象。Object.prototype对象是一个空的对象。
* 我们在JavaScript遇到的每个对象,实际上都是从Object.prototype对象克隆而来的。
* Object.prototype对象就是它们的原型。
*/
let obj1 = new Object();
let obj2 = {};
// 来查看这两个对象的原型
Reflect.getPrototypeOf(obj1) === Object.prototype // true
Reflect.getPrototypeOf(obj2) === Object.prototype // true
2. 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
/**
* 在JavaScript语言里,我们并不需要关心克隆的细节,因为这个引擎内部负责实现的。
* 我们所需要做的只是显示调用 var obj1 = new Object(); 或者 var obj2 = {};
* 引擎内部会从 Object.prototype上面克隆一个对象出来。我们最终得到的就是这个对象。
*/
// 再来看看如何用new运算符从构造器中得到一个对象。
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
}
let person = new Person('sven');
console.log(person.name); // sven
console.log(person.getName()); // sven
console.log(Reflect.getPrototypeOf(person) === Person.prototype); // true
/**
* JavaScript中没有类的概念,这句话我们已经重复过很多次了,但是刚才不是明明调用了new Person()吗?
*
* 在这里Person并不是类,而是函数构造器,JavaScript的函数可以作为普通函数被调用,也可以作为构造器被调用。
* 当使用 new 运算符来调用函数时,此时的函数就是一个构造器。
* 用 new 运算符来创建对象的过程,实际上也只是先克隆 Object.prototype对象。再进行一些其他额外操作的过程。
*/
/**
* 在 Chrome 和 Firefox等向外暴露了对象__proto__属性的浏览器下,我们通过这段代码来理解 new 运算符的过程:
*
*/
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
}
// 模拟 new操作符的过程
let objectFactory = function () {
let obj = new Object(), // 从 Object.prototype上克隆一个空的对象
Constructor = [].shift.call(arguments); // 取得外部传入的构造器, 此列是Person
obj.__proto__ = Constructor.prototype; // 指向正确的原型
let ret = Constructor.apply(obj, arguments); // 借用外部构传入的构造器给 obj设置属性
return typeof ret === 'object' ? ret : obj; // 确保构造器总数会返回一个对象
}
let a = objectFactory(Person, 'sven');
console.log("a.name", a.name); // 'sven'
console.log("a.getName()", a.getName()); // 'sven'
console.log(Reflect.getPrototypeOf(a) === Person.prototype); // true
// 我们看到,分别调用下面两句代码产生了一样的结果:
// let a = objectFactory(Person, 'sven');
// let a = new Person('sven');
3.对象会记住它的原型
/**
* JavaScript给对象提供了一个名为:__proto__的隐藏属性,某个对象的__proto__属性默认会指向它的构造器的原型对象,{Constructor}.prototype.
* 在一些浏览器中,__proto__被公开出来,我们可以在Chrome或者Firefox上用这段代码来验证:
*
*/
let a = new Object()
console.log("a.__proto__ === Object.prototype", a.__proto__ === Object.prototype); // true
/**
* 实际上,__proto__就是对象跟 "对象构造器的原型" 联系起来的纽带。正因为对象要通过 __proto__ 属性来记住它的构造器的原型。
*/
4.如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型
虽然JavaScript的对象最初都是由 Object.prototype对象克隆而来的,但是对象构造器的原型并不仅限于 Object.prototype上,而是可以动态指向其他对象。 这样一来,当对象 a 需要借用 b 的能力时 可以选择性地把对象 a 的构造器的原型指向 对象 b 从而达到继续的效果。
// 原型继承
let obj = { name: 'sven' }
let A = function () {}
A.prototype = obj;
let a = new A(); // a对象构造器的原型 指向 对象b
console.log(a.name) // 'sven'
// 执行过程
// 首先, 尝试遍历 对象a 中的所有属性,但是没找到 name 这个属性。
// 查找 name 属性的这个请求被委托个 对象a的构造器的原型,它被 a.__proto__记录着并且指向 A.prototype,而 A.prototype被设置为对象 obj
// 在对象obj中找到了 name 属性,并返回它的值。
console.log("当我们期望得到一个 '类' 继承自另外一个 '类' 的效果时,往往会用下面的代码来模拟实现:")
let A = function () {}
A.prototype = { name: 'sven'}
let B = function () {}
B.prototype = new A()
let b = new B()
console.log(b.name) // sven
// 执行过程:
// 首先, 尝试遍历对象b中的所有属性,但没找到name这个属性。
// 查找 name 属性的请求会被委托给 对象b的构造器的原型,它被 b.__proto__ 记录着并且指向 B.prototype,而 B.prototype被设置为一个通过 new A()创建出来的对象。
// 在该对象中依然没找到 name属性,于是请求被继续委托给这个对象的构造器的原型 A.prototype.
// 在A.prototype中找到了name属性,并放回它的值。
// 如果我们尝试访问 对象a 的 address属性,那么它的请求会最终传递到哪里呢?
// 请求会被传递到 A.prototype的构造器原型 Object.prototype, 显然 Object.prototype中也没有address属性,但是 Object.prototype的原型是 null,所以请求就此打住,返回 undefined