跟我学习javascript的prototype使用注意事项


Posted in Javascript onNovember 17, 2015

一、在prototype上保存方法

不使用prototype进行JavaScript的编码是完全可行的,例如:

function User(name, passwordHash) { 
 this.name = name; 
 this.passwordHash = passwordHash; 
 this.toString = function() { 
  return "[User " + this.name + "]"; 
 }; 
 this.checkPassword = function(password) { 
  return hash(password) === this.passwordHash; 
 }; 
} 

var u1 = new User(/* ... */); 
var u2 = new User(/* ... */); 
var u3 = new User(/* ... */);

当创建了多个User类型的实例时,就存在问题了:不仅是name和passwordHash属性在每个实例上都存在,toString和checkPassword方法在每个实例上都有一份拷贝。就像下图表示的那样:

跟我学习javascript的prototype使用注意事项

但是,当toString和checkPassword被定义在prototype上时,上图就变成下面这个样子了:

跟我学习javascript的prototype使用注意事项

toString和checkPassword方法现在定义在了User.prototype对象上,也就意味着这两个方法只存在一份拷贝,并被所有的User实例共享。

也许你会认为将方法作为拷贝放在每个实例上,会节省方法查询的时间。(当方法定义在prototype上时,首先会在实例本身上寻找方法,如果没有找到才会去prototype上继续找)

但是在现代的JavaScript执行引擎中,对方法的查询进行了大量优化,所以这个查询时间几乎是不需要考虑的,那么将方法放在prototype对象上就节省了很多内存。

二、使用闭包来保存私有数据

JavaScript的对象系统从其语法上而言并不鼓励使用信息隐藏(Information Hiding)。因为当使用诸如this.name,this.passwordHash的时候,这些属性默认的访问级别就是public的,在任何位置都能够通过obj.name,obj.passwordHash来对这些属性进行访问。

在ES5环境中,也提供了一些方法来更方便的访问一个对象上所有的属性,比如Object.keys(),Object.getOwnPropertyNames()。所以,一些开发人员使用一些规约来定义JavaScript对象的私有属性,比如最典型的是使用下划线作为属性的前缀来告诉其他开发人员和用户这个属性是不应该被直接访问的。

但是这样做,并不能从根本上解决问题。其他开发人员和用户还是能够对带有下划线的属性进行直接访问。对于确实需要私有属性的场合,可以使用闭包进行实现。

从某种意义而言,在JavaScript中,闭包对于变量的访问策略和对象的访问策略是两个极端。闭包中的任何变量默认都是私有的,只有在函数内部才能访问这些变量。比如,可以将User类型实现如下:

function User(name, passwordHash) { 
 this.toString = function() { 
  return "[User " + name + "]"; 
 }; 
 this.checkPassword = function(password) { 
  return hash(password) === passwordHash; 
 }; 
}

此时,name和passwordHash都没有被保存为实例的属性,而是通过局部变量进行保存。然后根据闭包的访问规则,实例上的方法可以对它们进行访问,而在其它地方则不能。

使用这种模式的一个缺点是,利用了局部变量的方法都需要被定义在实例本身上,不能讲这些方法定义在prototype对象上。正如在Item34中讨论的那样,这样做的问题是会增加内存的消耗。但是在某些特别的场合下,即使将方法定义在实例上也是可行的。

三、实例状态只保存在实例对象上

一个类型的prototype和该类型的实例之间是”一对多“的关系。那么,需要确保实例相关的数据不会被错误地保存在prototype之上。比如,对于一个实现了树结构的类型而言,将它的子节点保存在该类型的prototype上就是不正确的:

function Tree(x) { 
 this.value = x; 
} 
Tree.prototype = { 
 children: [], // should be instance state! 
 addChild: function(x) { 
  this.children.push(x); 
 } 
}; 

var left = new Tree(2); 
left.addChild(1); 
left.addChild(3); 

var right = new Tree(6); 
right.addChild(5); 
right.addChild(7); 

var top = new Tree(4); 
top.addChild(left); 
top.addChild(right); 

top.children; // [1, 3, 5, 7, left, right]

当状态被保存到了prototype上时,所有实例的状态都会被集中地保存,在上面这种场景中显然是不正确的:本来属于每个实例的状态被错误地共享了。如下图所示:

跟我学习javascript的prototype使用注意事项

正确的实现应该是这样的:

function Tree(x) { 
 this.value = x; 
 this.children = []; // instance state 
} 
Tree.prototype = { 
 addChild: function(x) { 
  this.children.push(x); 
 } 
};

此时,实例状态的存储如下所示:

跟我学习javascript的prototype使用注意事项

可见,当本属于实例的状态被共享到prototype上时,也许会产生问题。在需要在prototype上保存状态属性前,一定要确保该属性是能够被共享的。

总体而言,当一个属性是不可变(无状态)的属性时,就能将它保存在prototype对象上(比如方法能够被保存在prototype对象上就是因为这一点)。当然,有状态的属性也能够被放在prototype对象上,这要取决于具体的应用场景,典型的比如用来记录一个类型实例数量的变量。使用Java语言作为类比的话,这类能够存储在prototype对象上的变量就是Java中的类变量(使用static关键字修饰)。

四、避免继承标准类型

ECMAScript标准库不大,但是提供了一些重要的类型如Array,Function和Date。在一些场合下,你也许会考虑继承其中的某个类型来实现特定的功能,但是这种做法并不被鼓励。

比如为了操作一个目录,可以让目录类型继承Array类型如下:

function Dir(path, entries) { 
 this.path = path; 
 for (var i = 0, n = entries.length; i < n; i++) { 
  this[i] = entries[i]; 
 } 
} 
Dir.prototype = Object.create(Array.prototype); 
// extends Array 

var dir = new Dir("/tmp/mysite", ["index.html", "script.js", "style.css"]); 
dir.length; // 0

但是可以发现,dir.length的值是0,而不是期待中的3。

发生这种现象的原因在于:只有当对象是真正的Array类型时,length属性才会起作用。

在ECMAScript标准中,定义了一个不可见的内部属性被称为 [[class]]。该属性的值只是一个字符串,所以不要被误导认为JavaScript也实现了自己的类型系统。所以,对于Array类型,这个属性的值就是“Array”;对于Function类型,这个属性的值就是“Function”。下表是ECMAScript定义的所有[[class]] 值:

那么当对象的类型确实是Array时,length属性的特别之处就在于:length的值会和该对象中被索引的属性个数保持一致。比如对于一个数组对象arr,arr[0]和arr[1]就表示该对象有两个被索引的属性,那么length的值就是2。当添加了arr[2]的时候,length的值会被自动同步成3。同样地,当设置length值为2时,arr[2]会被自动设置成undefined。

但是当继承Array类型并创建实例时,该实例的 [[class]] 属性并不是Array,而是Object。因此length属性不能正确的工作。

在JavaScript中,也提供了用于查询 [[class]] 属性的方法,即使用Object.prototype.toString方法:

var dir = new Dir("/", []); 
Object.prototype.toString.call(dir); // "[object Object]" 
Object.prototype.toString.call([]); // "[object Array]"

因此,更好的实现方法是使用组合而不是继承:

function Dir(path, entries) { 
 this.path = path; 
 this.entries = entries; // array property 
} 
Dir.prototype.forEach = function(f, thisArg) { 
 if (typeof thisArg === "undefined") { 
  thisArg = this; 
 } 
 this.entries.forEach(f, thisArg); 
};

以上代码将不再使用继承,而是将一部分功能代理给内部的entries属性来实现,该属性的值是一个Array类型对象。

ECMAScript标准库中,大部分的构造函数都会依赖内部属性值如 [[class]] 来实现正确的行为。对于继承这些标准类型的子类型,无法保证它们的行为是正确的。因此,不要继承ECMAScript标准库中的类型如:
Array, Boolean, Date, Function, Number,RegExp,String

以上就是对使用prototype的几点注意事项进行总结,希望可以帮助大家正确的使用prototype。

Javascript 相关文章推荐
extjs 学习笔记 四 带分页的grid
Oct 20 Javascript
jquery 单击li防止重复加载的实现代码
Dec 24 Javascript
js读写cookie实现一个底部广告浮层效果的两种方法
Dec 29 Javascript
js分页代码分享
Apr 28 Javascript
JavaScript之Object类型介绍
Apr 01 Javascript
js实现仿MSN带关闭功能的右下角弹窗代码
Sep 04 Javascript
BootStrap Validator 版本差异问题导致的submitHandler失效问题的解决方法
Dec 01 Javascript
vue中将网页打印成pdf实例代码
Jun 15 Javascript
webpack 2的react开发配置实例代码
Jul 28 Javascript
webpack4之SplitChunksPlugin使用指南
Jun 12 Javascript
jQuery实现获取当前鼠标位置并输出功能示例
Jan 05 jQuery
JS实现点击下拉列表文本框中出现对应的网址,点击跳转按钮实现跳转
Nov 25 Javascript
js弹出对话框方式小结
Nov 17 #Javascript
跟我学习javascript的prototype,getPrototypeOf和__proto__
Nov 17 #Javascript
Jquery 垂直多级手风琴菜单附源码下载
Nov 17 #Javascript
JavaScript代码实现禁止右键、禁选择、禁粘贴、禁shift、禁ctrl、禁alt
Nov 17 #Javascript
跟我学习javascript的undefined与null
Nov 17 #Javascript
跟我学习javascript的arguments对象
Nov 16 #Javascript
JavaScript函数学习总结以及相关的编程习惯指南
Nov 16 #Javascript
You might like
德劲1102收音机的打理维修案例
2021/03/02 无线电
从php核心代码分析require和include的区别
2011/01/02 PHP
php微信公众平台开发之获取用户基本信息
2015/08/17 PHP
PHP常用工具类大全附全部代码下载
2015/12/07 PHP
javascript CSS画图之基础篇
2009/07/29 Javascript
javascript当onmousedown、onmouseup、onclick同时应用于同一个标签节点Element
2010/01/05 Javascript
几个比较实用的JavaScript 测试及效验工具
2010/04/18 Javascript
js简单实现让文本框内容逐个字的显示出来
2013/10/22 Javascript
JS+CSS实现可拖拽的漂亮圆角特效弹出层完整实例
2015/02/13 Javascript
js实现用户离开页面前提示是否离开此页面的方法(包括浏览器按钮事件)
2015/07/18 Javascript
微信JS-SDK坐标位置如何转换为百度地图坐标
2016/07/04 Javascript
Bootstrap Table使用心得总结
2016/11/29 Javascript
jQuery控制元素隐藏和显示
2017/03/03 Javascript
jQuery查找和过滤_动力节点节点Java学院整理
2017/07/04 jQuery
详解Node中导入模块require和import的区别
2017/08/11 Javascript
快速解决vue-cli不能初始化webpack模板的问题
2018/03/20 Javascript
[01:12]快闪回顾DOTA2亚洲邀请赛(DAC) 静候2018新征程开启
2018/03/11 DOTA
[02:19]DOTA选手解说齐贺岁
2018/02/11 DOTA
Python中的迭代器与生成器高级用法解析
2016/06/28 Python
Python微信企业号开发之回调模式接收微信端客户端发送消息及被动返回消息示例
2017/08/21 Python
Python2.7基于笛卡尔积算法实现N个数组的排列组合运算示例
2017/11/23 Python
Python使用functools实现注解同步方法
2018/02/06 Python
python版本的仿windows计划任务工具
2018/04/30 Python
Python实现随机生成任意数量车牌号
2020/01/21 Python
使用CSS3中的calc()属性来以算式表达尺寸数值
2016/06/06 HTML / CSS
GAP欧盟网上商店:GAP EU
2016/09/13 全球购物
Silk’n激光脱毛器官网:silkn.com
2016/10/06 全球购物
物流管理应届生求职信
2013/11/07 职场文书
销售简历自我评价
2014/01/24 职场文书
给面试官的感谢信
2014/02/01 职场文书
公司活动总结范文
2014/07/01 职场文书
学校2014年度工作总结
2014/12/06 职场文书
2015年社区矫正工作总结
2015/04/21 职场文书
老公写给老婆的检讨书
2015/05/06 职场文书
python实现腾讯滑块验证码识别
2021/04/27 Python
使用HBuilder制作一个简单的HTML5网页
2022/07/07 HTML / CSS