JS原型-语法甘露
初看原型
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原型-语法甘露的更多相关文章
- JS 原型链图形详解
JS原型链 这篇文章是「深入ECMA-262-3」系列的一个概览和摘要.每个部分都包含了对应章节的链接,所以你可以阅读它们以便对其有更深的理解. 对象 ECMAScript做为一个高度抽象的面向对象语 ...
- js 原型链和继承(转)
在理解继承之前,需要知道 js 的三个东西: 什么是 JS 原型链 this 的值到底是什么 JS 的 new 到底是干什么的 1. 什么是 JS 原型链? 我们知道 JS 有对象,比如 var ob ...
- JS(原型和原型链)
(学习自慕课网<前端JavaScript 面试技巧> JS(原型和原型链) 题目1.如何准确判断一个变量是数组类型 使用 instanceof 方法 题目2.写一个原型链继承的例子 实例: ...
- 【学习笔记】深入理解js原型和闭包(8)——简述【执行上下文】上
什么是“执行上下文”(也叫做“执行上下文环境”)?暂且不下定义,先看一段代码: 第一句报错,a未定义,很正常.第二句.第三句输出都是undefined,说明浏览器在执行console.log(a)时, ...
- 【学习笔记】深入理解js原型和闭包(2)——函数和对象的关系
上文(深入理解jS原型和闭包(1)——一切都是对象)已经提到,函数就是对象的一种,因为通过instanceof函数可以判断. var fn = function () { }; console.log ...
- 【学习笔记】深入理解js原型和闭包(0)——目录
文章转载:https://www.cnblogs.com/wangfupeng1988/p/4001284.html 说明: 本篇文章一共16篇章,外加两篇后补的和一篇自己后来添加的学习笔记,一共19 ...
- JS原型链
JS作为发展了多年了对象语言,支持继承,和完全面向对象语言不同的是,JS依赖原型链来实现对象的继承. 首先JS的对象分两大类,函数对象和普通对象,每个对象均内置__proto__属性,在不人为赋值__ ...
- 深入分析JS原型链以及为什么不能在原型链上使用对象
在刚刚接触JS原型链的时候都会接触到一个熟悉的名词:prototype:如果你曾经深入过prototype,你会接触到另一个名词:__proto__(注意:两边各有两条下划线,不是一条).以下将会围绕 ...
- 【09-23】js原型继承学习笔记
js原型继承学习笔记 function funcA(){ this.a="prototype a"; } var b=new funcA(); b.a="object a ...
随机推荐
- UITabBarController 微信
AppDelegate.m #import "AppDelegate.h" #import "FirstViewController.h" #import &q ...
- Java 网络编程----基本概念
网络现在是一个非常普遍的概念. 以下是维基百科上的解释: 网络一词有多种意义,可解作: 网络流也简称为网络(network).一般用于管道系统.交通系统.通讯系统建模. 有时特指计算机网络. 或特指其 ...
- QA:Failed to deploy artifacts from/to snapshots XX Failed to transfer file Return code is: 405, ReasonPhrase:Method Not Allowed.
QA: Failed to deploy artifacts from/to snapshots XX Failed to transfer file Return code is: 405, Rea ...
- Effective Java 55 Optimize judiciously
Principle Strive to write good programs rather than fast ones. Strive to avoid design decisions that ...
- netty-socketio使用namespace
一.简介 netty-socketio中的namespace可以用于区别在相同连接地址下的不同用户,当两个不同的用户打开同一个页面的时候,可以使用namespace用来标记不同用户.例如我们可以在用户 ...
- ehcache入门
一.简介 ehcache是一个开源的,纯java进程内的缓存框架.它具有快速,简单,具有多种缓存策略等特点. Hibernate中默认就是用了ehcache.在我们的应用中使用ehcache可以快速地 ...
- 如何解决mysql stop fail的问题
最近在学习mysql,碰到了一个mysql stop fail的问题,在这里把碰到的问题以及解决的过程写出来,不是这个问题有多难,而是我在解决此问题的过程中没有发现一个行之有效的解决问题的中文网页,搞 ...
- try catch finally 用法
trycatchfinally 1.将预见可能引发异常的代码包含在try语句块中.2.如果发生了异常,则转入catch的执行.catch有几种写法:catch这将捕获任何发生的异常.catch(Exc ...
- EasyUI DataGrid单选如何取消选中
EasyUI DataGrid在多选时,选中某行,可以取消:而在单选时,并不能取消选中某一行. 可以通过修改源码来完成. 在其源码中找到 opts.singleSelect==true 将代码做如下修 ...
- phpcms v9 下拉菜单 二级 三级子栏目调用方法
很多网站的导航栏可以实现下拉二级菜单,三级菜单等效果,今天我们就来分享phpcms v9 支持下拉菜单的方法,可以支持无限子栏目调用,具体写法如下: <ul> {pc:content ac ...