初看原型

JS的所有函数都有一个prototype属性,这个prototype属性本身又是一个object类型的对象。

prototype提供了一群同类对象共享属性和方法的机制。

将一个基类的实例作为子类的原型对象,原型继承

Employee.prototype = new Person();

JS通过简单的重写机制实现对象的“多态”性,与静态对象语言的虚函数和重载概念不谋而合。

可以通过给原型对象动态添加新的属性和方法,从而动态地扩展基类的功能特性。

在原型模型中,为了实现类继承,必须首先将子类构造函数的prototype设置为一个父类的对象实例。创建这个父类实例的目的就是为了构成原型链,以起到共享上层原型方法的作用。但创建这个实例对象时,上层构造函数也会给它设置对象成员,这些对象成员对于继承来说是没有意义的。虽然我们也没有给构造函数传递参数,但确实创建了若干没有用的成员,尽管其值是undefined,这也是一种浪费。

原型扩展

给内置的对象的构造函数添加新的方法和属性,从而增强JS的功能。

String.prototype.trim = function() { /* ... */ };

原型真谛

JS构造对象的过程可以分三步:第一步,创建一个空对象;第二步,将这对象内置的原型对象设置为构造函数prototype引用的原型对象;第三步,将该对象作为this参数调用构造函数,完成成员设置等初始化工作。

对象建立后,对象上的任何访问和操作都只与对象自身及其原型链上的那串对象相关,与构造函数基本扯不上关系了。换句话说,构造函数只是在创建对象时起到介绍原型对象和初始化对象两个作用。

思考:

那么我们能否自己定义一个对象来当做原型,并在这个原型上描述类,然后将这个原型设置给新创建的对象,将其当作对象的类呢?我们又能否将这个原型中的一个方法当作构造函数,去初始化新创建的对象呢?例如,我们定义这样一个原型对象:

var People = {                            // 定义一个对象当作原型
create: function(name, age) { // 这个当成构造函数
this.name = name;
this.age = age;
},
sayName: function() { // 定义方法
return this.name;
},
howOld: function() {
return this.age;
}
};

利用通用函数建立指定原型类的对象

/*
* 通过函数创建新对象并返回
* @param {Object} aClass 指定的原型类
* @param {Array} aParams 参数数组
*/
function New(aClass, aParams) { // 创建通用函数
function _New() { // 定义临时的中转函数壳
// 调用原型中定义的构造函数,中转构造逻辑和传递参数
aClass.create.apply(this, aParams);
}
_New.prototype = aClass; // 准备中转原型对象
return new _New();
}
修复版
/*
* 通过函数创建新对象并返回
* @param {Object} aClass 指定的原型类
* @param {Array} aParams 参数数组
*/
function New(aClass, aParams) {
function _New() { // 定义临时的中转函数壳
this.Type = aClass; // 给每个对象约定Type属性,据此可以访问到对象所属的类
// 调用原型中定义的构造函数,中转构造逻辑和传递参数
// create方法是约定的构造函数
if(aClass.create) aClass.create.apply(this, aParams);
}
_New.prototype = aClass; // 准备中转原型对象
return new _New();
}

通用函数New()就是一个语法甘露,不但中转原型对象,还中转了构造函数逻辑和构造参数。

有趣的是,每次创建完对象退出New函数作用域时,临时的_New函数对象会被自动释放。

我们还需要多一些语法甘露,实现类层次及其继承关系。

// 简洁而优雅地书写类层次及其继承关系
var object = { // 基本类,用于定义最基本的方法
isA: function(aBase) { // 判断类与类之间及类与对象的之间的关系
var self = this;
while(self) {
if(self ==aBase) return true;
else self = self.Type;
}
return false;
}
};
// 创建类的函数,用于声明类及继承关系
function Class(aBaseClass, aClassDefine) { // 创建类的临时函数壳
function _class() {
this.Type = aBaseClass; // 给每个类约定一个Type属性,引用其继承关系
// 复制类的全部定义到当前创建的类
for(var prop in aClassDefine) {
this[prop] = aClassDefine[prop];
}
}
_class.prototype = aBaseClass;
return new _class();
}

令人高兴的是,受这些甘露滋养的JS程序效率会更高效。因为其原型对象里既没有了毫无用处的那些对象级的成员,而且还不存在constructor属性体,少了与构造函数之间的牵连,但依旧保持了方法的共享性。

甘露模型

使用Class()甘露,我们已经可以用非常优雅的格式定义一个类。

// 定义Student类,继承People
// Student其实是一个对象,是模拟成的类
// 可以通过New函数创建Student对象
var Student = Class(People, { // 派生自People类
create: function(name, age, grade) {
// 调用超类的构造函数
People.create.call(this, name, age);
this.grade = grade;
},
getGrade: function() {
return '我的成绩是' + this.grade;
}
});

Class()语法甘露实际上是构造一个原型,并将这个原型挂在了相应的原型链上。它返回的是一个对象而不是函数。如果让Class()返回一个函数,不就可以用new Student()这种方式来创建对象吗?而且,我们可以为这个返回函数创建一个继承至相关原型链的原型对象,并设置到函数的prototype属性。这样,我们用new方式创建这个类函数的对象时,就自然地继承该类的原型了。

// 定义类的语法甘露:Class()
// 最后一个参数是JSON表示的类定义
// 如果参数的数量大于1,则第一个参数是基类
// 第一个参数和最后一个参数之间,可以表示类实现的接口
// 返回一个类,类是一个构造函数
function Class() {
var aDefind = arguments[arguments.length-1]; // 类定义
if( !aDefind ) return;
// 确定基类,默认是object基本类
// aBase 是一个构造函数,会继承它的原型对象
var aBase = arguments.length > 1? arguments[0] : object; function prototype_() {} // 临时函数, 用于挂接原型链
prototype_.prototype = aBase.prototype; // 准备传递prototype
aPrototype = new prototype_(); // 构造函数的prototype
// 复制类定义到当前类的原型prototype上
for(var member in aDefind) {
if(member != 'create') // 不复制构造函数create
aPrototype[member] = aDefind[member];
} var aType; // 存放构造函数的引用
if(aDefind.create) aType = aDefind.create; // 类型即为该构造函数
else aType = function() { // 如果未定义create(), 使用默认构造函数
this.base.apply(this, arguments); // 调用基类的构造函数
}
aType.prototype = aPrototype; // 给构造函数的原型属性赋值
aType.Base = aBase; // 设置类型关系(默认是object)
aType.prototype.Type = aType; // 为本类对象扩展一个Type属性,用于判断对象所属类
return aType; // 返回构造函数作为类
} // 根类object的定义
function object() { /* object 基本类 */ };
object.prototype.isA = function(aType) {
var self = this.type; // 调用isA()的当前类
while(self) {
if(self == aType) return true;
self = self.aBase; // 检查基类是否相等
}
return false;
};
object.prototype.base = function() { // 用于调用基类的构造函数
var Caller = object.prototype.base.caller; // 当前构造函数create()
Caller && Caller.Base && Caller.Base.apply(this, arguments);
};

使用Class()函数来定义一个类,实际上就是为创建对象准备了一个构造函数,而该构造函数的prototype已经初始化为方法表,并可继承上层类的方法表。这样当用new操作符创建一个对象时,也就很自然地将此构造函数的原型链传递给了新构造的对象。

Class()函数中的小问题

不支持不可枚举的内置方法如toString();object根类的base()方法用到了函数的caller属性,以此判断构造函数的层次。

1. 解决不能覆写不可枚举属性的问题并非难事。既然这些属性特殊,就可以对其进行特殊处理。我们在for-in复制完可枚举属性后,加上下面的处理语句:

if(aDefind.toString !== Object.prototype.toString) {

aPrototype['toString'] = aDefind.toString;

}

2. base()方法之所以要使用只身的caller属性,就是为了确定当前构造函数的层次,从而可以知道该调用上层的构造函数。

实际上,第一层构造函数调用this.base()时,我们可以通过this.Type属性知道一层构造函数,而this.Type.Base就是第二层构造函数。只是,第二层构造函数又会调用this.base(),其本来是想调用第三层的构造函数,但再次进入base()函数时,就无法知晓构造函数的层次了。

如果我们在第一层构造函数调用进入this.base()时,先改变this.base本身,让其在下次被调用时能掉到第3层构造函数。完成这个变身动作之后再调第2层构造函数,而第2层构造函数再调用this.base()时就能调用到第3层构造函数了。这样只要我们在每次的base()调用中都完成一个自我的变身动作,就可以按正确的顺序完成对构造函数的调用。

object.prototype.base = function() {    // 用于调用基类的构造函数
var Base = this.type.Base; // 第2层构造函数
if(!Base.Base) { // 如果基类没有基类
Base.apply(this, arguments); // 就直接调用基类的构造函数
}else { // 如果基类上面还有基类
this.base = makeBase_(Base); // 先覆盖this.base,返回一个函数
Base.apply(this, arguments); // 然后调用基类构造函数,其调用this.base
delete this.base;
}
function makeBase_(Type) { // 包装基类构造函数
var Base = Type.Base; // 第3层构造函数
if (!Base.Base) return Base; // 不存在基类,直接返回
return function() { // 包装为引用临时变量Base的闭包函数
this.base = makeBase_(Base);// 先覆写this.base
Base.apply(this, arguments);// 再调用基类的构造函数
};
}
};

makeBase_()函数,如果基类还有基类,它就返回一个闭包函数。下次this.base()被构造函数调用时,即调用的是这个闭包函数。但这个闭包函数又可能会调用makeBase_()形成另一个闭包函数,直到基类再无基类。

重写后的完美甘露模型代码

// 根类object的定义
function object() { /* object 基本类 */ };
object.prototype.isA = function(aType) {
var self = this.type; // 调用isA()的当前类
while(self) {
if(self == aType) return true;
self = self.aBase; // 检查基类是否相等
}
return false;
};
// object.prototype.base = function() { // 用于调用基类的构造函数
// var Caller = object.prototype.base.caller; // 当前构造函数create()
// Caller && Caller.Base && Caller.Base.apply(this, arguments);
// };
object.prototype.base = function() { // 用于调用基类的构造函数
var Base = this.type.Base; // 第2层构造函数
if(!Base.Base) { // 如果基类没有基类
Base.apply(this, arguments); // 就直接调用基类的构造函数
}else { // 如果基类上面还有基类
this.base = makeBase_(Base); // 先覆盖this.base,返回一个函数
Base.apply(this, arguments); // 然后调用基类构造函数,其调用this.base
delete this.base;
}
function makeBase_(Type) { // 包装基类构造函数
var Base = Type.Base; // 第3层构造函数
if (!Base.Base) return Base; // 不存在基类,直接返回
return function() { // 包装为引用临时变量Base的闭包函数
this.base = makeBase_(Base);// 先覆写this.base
Base.apply(this, arguments);// 再调用基类的构造函数
};
}
}; // 定义类的语法甘露:Class()
// 最后一个参数是JSON表示的类定义
// 如果参数的数量大于1,则第一个参数是基类
// 第一个参数和最后一个参数之间,可以表示类实现的接口
// 返回一个类,类是一个构造函数
function Class() {
var aDefind = arguments[arguments.length-1]; // 类定义
if( !aDefind ) return;
// 确定基类,默认是object基本类
// aBase 是一个构造函数,会继承它的原型对象
var aBase = arguments.length > 1? arguments[0] : object; function prototype_() {} // 临时函数, 用于挂接原型链
prototype_.prototype = aBase.prototype; // 准备传递prototype
aPrototype = new prototype_(); // 建立类要用的prototype
// 复制类定义到当前类的原型prototype上
for(var member in aDefind) {
if(member != 'create') // 不复制构造函数create
aPrototype[member] = aDefind[member];
}
// 处理不可枚举的内置方法
if(aDefind.toString !== Object.prototype.toString) {
aPrototype['toString'] = aDefind.toString;
}
if(aDefind.valueOf !== Object.prototype.valueOf) {
aPrototype['valueOf'] = aDefind.valueOf;
}
if(aDefind.toJSON !== Object.prototype.toJSON) {
aPrototype['toJSON'] = aDefind.toJSON;
}
var aType; // 存放构造函数的引用
if(aDefind.create) aType = aDefind.create; // 类型即为该构造函数
else aType = function() { // 如果未定义create(), 使用默认构造函数
this.base.apply(this, arguments); // 调用基类的构造函数
}
aType.prototype = aPrototype; // 设置类的prototype
aType.Base = aBase; // 设置类型关系
aType.prototype.Type = aType; // 为本类对象扩展一个Type属性
return aType; // 返回构造函数作为类
}

来源:悟透Javascript

JS原型-语法甘露的更多相关文章

  1. JS 原型链图形详解

    JS原型链 这篇文章是「深入ECMA-262-3」系列的一个概览和摘要.每个部分都包含了对应章节的链接,所以你可以阅读它们以便对其有更深的理解. 对象 ECMAScript做为一个高度抽象的面向对象语 ...

  2. js 原型链和继承(转)

    在理解继承之前,需要知道 js 的三个东西: 什么是 JS 原型链 this 的值到底是什么 JS 的 new 到底是干什么的 1. 什么是 JS 原型链? 我们知道 JS 有对象,比如 var ob ...

  3. JS(原型和原型链)

    (学习自慕课网<前端JavaScript 面试技巧> JS(原型和原型链) 题目1.如何准确判断一个变量是数组类型 使用 instanceof 方法 题目2.写一个原型链继承的例子 实例: ...

  4. 【学习笔记】深入理解js原型和闭包(8)——简述【执行上下文】上

    什么是“执行上下文”(也叫做“执行上下文环境”)?暂且不下定义,先看一段代码: 第一句报错,a未定义,很正常.第二句.第三句输出都是undefined,说明浏览器在执行console.log(a)时, ...

  5. 【学习笔记】深入理解js原型和闭包(2)——函数和对象的关系

    上文(深入理解jS原型和闭包(1)——一切都是对象)已经提到,函数就是对象的一种,因为通过instanceof函数可以判断. var fn = function () { }; console.log ...

  6. 【学习笔记】深入理解js原型和闭包(0)——目录

    文章转载:https://www.cnblogs.com/wangfupeng1988/p/4001284.html 说明: 本篇文章一共16篇章,外加两篇后补的和一篇自己后来添加的学习笔记,一共19 ...

  7. JS原型链

    JS作为发展了多年了对象语言,支持继承,和完全面向对象语言不同的是,JS依赖原型链来实现对象的继承. 首先JS的对象分两大类,函数对象和普通对象,每个对象均内置__proto__属性,在不人为赋值__ ...

  8. 深入分析JS原型链以及为什么不能在原型链上使用对象

    在刚刚接触JS原型链的时候都会接触到一个熟悉的名词:prototype:如果你曾经深入过prototype,你会接触到另一个名词:__proto__(注意:两边各有两条下划线,不是一条).以下将会围绕 ...

  9. 【09-23】js原型继承学习笔记

    js原型继承学习笔记 function funcA(){ this.a="prototype a"; } var b=new funcA(); b.a="object a ...

随机推荐

  1. GCD中的dispatch_apply的用法及作用

    GCD中的dispatch_apply的用法及作用 (一)dispatch_apply的基本用法 dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联A ...

  2. 百度地图开发的学习(一)——配置环境&基础地图

    由于项目需求缘故,最近在学习Android地图的开发,所以就记录一下学习过程.最近都会陆续更新啦.目前使用百度地图API的挺多的,所以就先以它为基础学习一些地图的调用. 一.AK的申请 与web开发不 ...

  3. OC语言-07-OC语言-Foundation框架

    结构体 NSRange/CGRange 用来表示一个元素在另一个元素中的范围,NSRange等价于CGRange 包含两个属性: NSUInteger location:表示一个元素在另一个元素中的位 ...

  4. IOS的UI基础02

    *:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...

  5. animation of android (3)

    视图动画,只有view可以使用. 在android3.0以后,属性动画. ValueAnimation 可以记录属性变化的过程,所以他的对象是任何object. 所以ValueAnimation 的真 ...

  6. C#获取HTML文件指定DIV内容

    最近自己找了一个开源的博客网站,放到阿里云上,方便自己发布博客. 我一般把文章发布到博客园和QQ空间,家了这个网站后又要多发布一次,为了省事就做了一个从博客园读取文章的功能: 输入链接URL地址点击提 ...

  7. 学习随笔—Redis常用命令

    info 服务器基本信息 monitor 实时转储收到的请求 flushdb 清空当前数据库 flushall 清空所有数据库 quit 关闭连接 save 将数据同步保持到磁盘 bgsave     ...

  8. 关于Redis中交互的过程

    一.Redis启动 加载配置(命令行或者配置文件) 启动TCP监听,客户端的列表保存在redisserver的clients中 启动AE Event Loop事件,异步处理客户请求 事件处理器的主循环 ...

  9. html页面识别当前系统和语言

    项目中需要一个下载功能,根据系统跳转到不同的页面,如iphone跳转到IOS页面,android跳转到android页面. 下面为页面判断页面: <!DOCTYPE HTML> <h ...

  10. 如何解决mysql stop fail的问题

    最近在学习mysql,碰到了一个mysql stop fail的问题,在这里把碰到的问题以及解决的过程写出来,不是这个问题有多难,而是我在解决此问题的过程中没有发现一个行之有效的解决问题的中文网页,搞 ...